Skip to content

Add QIR Program Format Support to the QDMI DDSim Device#1766

Open
rturrado wants to merge 17 commits into
munich-quantum-toolkit:mainfrom
rturrado:1695
Open

Add QIR Program Format Support to the QDMI DDSim Device#1766
rturrado wants to merge 17 commits into
munich-quantum-toolkit:mainfrom
rturrado:1695

Conversation

@rturrado
Copy link
Copy Markdown

@rturrado rturrado commented Jun 5, 2026

Description

First contribution to UnitaryHACK for me! 🎉 And man, I've enjoyed working on this one! 😀

Note

The current PR still does not cover all the functionality proposed in the #1695 issue. In particular, the support for QIR adaptive base modules and strings is not yet implemented. But the current solution is ready for review.

There are two main pieces of work:

  1. The introduction of an MQT Core QIR JIT library (MQT::CoreQIRJIT, src/qir/jit). This library contains almost all of the previous Runner.cpp code: LLVM JIT initialization, QIR runtime symbols registration, and program execution. Compared to that previous Runner code, it also lets you parse an IR directly from a string, and not only from a file. The main purpose of the QIR JIT library is to encapsulate the LLVM JIT code so that it can also be used from the QDMI device. The interface is a qir::jit::Session type that can be constructed from an input file or from a byte string (text for LLVM assembly or binary for LLVM bitcode), and then run with or without arguments (e.g., the Runner CLI can run a JIT program with arguments, and the QDMI device without arguments).
  2. The job submission in Device.cpp. By using the MQT Core QIR JIT library, the device can just run a JIT program, get the results, and process them (filter zero and one addresses, sort the addresses, and transform the measurements into a bit string).

The new functionality is tested via two tests in results_sampling_test.cpp, one that exercises the QIR base module path, and the other that exercises the QIR base string path.

For this PR I have used Claude Opus 4.7, mainly from a CLion terminal, and for different purposes: from understanding existing code, to drafting code snippets, catching bugs and typos, running clang-format and clang-tidy and proposing fixes for clang-tidy warnings, reviewing wording in commit messages and docs...

Fixes #1695

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

rturrado added 6 commits June 5, 2026 14:05
Add qir::jit::Session to encapsulate the LLJIT initialization, QIR runtime symbols registration, and program execution. The idea is to avoid future duplication of JIT setup both in the Runner and in the Device.
Update Runner to use qir::jit::Session.
circuits.hpp, error_handling_test.cpp: add QIR_BELL_PAIR_STATIC with an LLVM assembly string of a Bell pair program, and rename MALFORMED_PROGRAM to QASM3_MALFORMED (to be consistent with the other QASM3_ variables).

src/qdmi/devices/dd/CMakeLists.txt: add MQT::CoreQIRJIT library dependency.

src/qir/jit/CMakeLists.txt: link libraries privately.

test/qdmi/devices/dd/CMakeLists.txt: add LLVM native libs dependencies.

Device.hpp: add submitQASMProgram and submitQIRProgram APIs.

Device.cpp: handle job submissions of a QASM program and of a QIR base module/string separately. Implement submitQIRProgram: for numShots_, reset the runtime, run the program, process results into a bit string, update measurement counts.

Device.cpp, test_utils.cpp: the device stores the program into a std::string of a given size, and submitQIRProgram reads exactly that size. No passing +1 bytes or sometimes retrieving -1 bytes is needed in any case.

job_parameters_test.cpp: set QDMI_PROGRAM_FORMAT_QIRBASEMODULE and QDMI_PROGRAM_FORMAT_QIRBASESTRING as supported formats.

results_sampling_test.cpp: change HistogramKeysAndValuesSumToShots into a test class, and implement two new test fixtures for QIR base modules and strings. These two new tests exercise the submission of a QIR program for a DD device job.

Runtime: add getResults API.

Session: make run methods const.
This option enables the QIR format support for QDMI. It is defined in the top level CMakeLists.txt, and it is disabled by default.

The QDMI DDSim Device library only links to QIR JIT and QIR Runtime libraries if the option is ON. The same applies to the corresponding test library.
Add a 'QIR Support in the QDMI Device' entry.

Assisted-by: Claude Opus 4.7 via Claude Code
Leave just one function where both arguments have default values.
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 5, 2026

Codecov Report

❌ Patch coverage is 75.23810% with 52 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/qir/jit/Session.cpp 75.0% 49 Missing ⚠️
src/qir/runtime/Runtime.cpp 0.0% 2 Missing ⚠️
src/qdmi/devices/dd/Device.cpp 91.6% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

rturrado added 3 commits June 5, 2026 23:43
When BUILD_MQT_CORE_MLIR=OFF , find_package(MLIR) is not called, so LLVM CMake macros like llvm_map_components_to_libnames are undefined. The new qir/jit subdirectory used that macro unconditionally and failed to configure.

Make BUILD_MQT_CORE_QDMI_WITH_QIR a cmake_dependent_option on BUILD_MQT_CORE_MLIR (forced OFF when MLIR is off).
Add the qir/jit subdirectory only when at least one consumer (Runner CLI or QDMI-with-QIR) is enabled.

Assisted-by: Claude Opus 4.7 via Claude Code
The HistogramKeysAndValuesSumToShots fixture class in results_sampling_test.cpp was at file scope, triggering clang-tidy's misc-use-anonymous-namespace check in CI (clang-tidy 22). Wrap it in an anonymous namespace to enforce internal linkage, matching the convention used by the ErrorHandling fixture in error_handling_test.cpp.

Assisted-by: Claude Opus 4.7 via Claude Code
@mergify mergify Bot added the conflict label Jun 5, 2026
Signed-off-by: Roberto Turrado Camblor <rturrado@gmail.com>
@mergify mergify Bot removed the conflict label Jun 5, 2026
rturrado and others added 2 commits June 6, 2026 00:17
Copy link
Copy Markdown
Collaborator

@ystade ystade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @rturrado,
Your contribution already looks great in this state. Thanks for taking part in the unitaryHACK. I am a big fan of the restructuring, i.e., having the JIT interpreter as its own library. This makes the code a lot better structured. It is actually also cool to see that this refactoring comprises the most part of this PR, and the remaining changes are quite local. The biggest action item that remains in my mind is the documentation of the Session class. In particular, I reviewed all files in detail except the session class because the documentation eases the review here a lot. So, in summary, my biggest request are comprehensive docstrings for the Session class. Overall, this contribution looks already very good and is very valuable.

Comment thread docs/qir/index.md

### QIR Support in the QDMI Device

The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR base module (LLVM bitcode), and QIR base string (LLVM assembly).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, it is not immediately clear that "base" refers to QIR's Base Profile. Perhaps this sentence can be made more precise, similar to the sentence referring to the Base Profile in the paragraph above.

/// Helper function to submit a QASM 2 or QASM 3 program
auto submitQASMProgram() -> QDMI_STATUS;

#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have other QDMI Device implementations without a simulator backend in MQT Core. I am wondering whether this option is confusing and should rather refer to the DDSim QDMI Device explicitly by renaming this option to BUILD_MQT_CORE_DDSIM_QDMI_WITH_QIR. However, this option is exceptionally long then.
I would be interested in @burgholzer opinion whether we could even drop this option and always build the DDSim QDMI Device with QIR support whenever the MLIR build is enabled, i.e., BUILD_MQT_CORE_MLIR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be in favor of building the DDSIM device with QIR support by default (as long as building MLIR is enabled).
Since we will soon (™️) also enable that flag for Python, MLIR will be available anywhere the next time we make a release.

So overall, I think it would be best to not add an extra option here.

target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreDD MQT::CoreQASM MQT::CoreCircuitOptimizer
MQT::CoreQDMICommon spdlog::spdlog)
if(BUILD_MQT_CORE_QDMI_WITH_QIR)
target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRJIT MQT::CoreQIRRuntime)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to link against both, MQT::CoreQIRJIT and MQT::CoreQIRRuntime? I would expect that one is included in the other.

case QDMI_DEVICE_JOB_PARAMETER_PROGRAM:
if (value != nullptr) {
program_ = std::string(static_cast<const char*>(value), size - 1);
program_ = std::string(static_cast<const char*>(value), size);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? As far as I can see, the '-1' strips off the terminating null-character at the end.

return submitQIRProgram();
}
#endif
return QDMI_ERROR_NOTSUPPORTED;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should not be reachable after all because I would expect the NOTSUPPORTED already when trying to set an unsupported program format. However, it feel slightly wrong to see NOTSUPPORTED here, QDMI_ERROR_FATAL feel more natural to me. A very nitpick...

Comment on lines 109 to +114
QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING,
QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE,
#ifndef BUILD_MQT_CORE_QDMI_WITH_QIR
QDMI_PROGRAM_FORMAT_QIRBASEMODULE,
QDMI_PROGRAM_FORMAT_QIRBASESTRING,
#endif
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING,
QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE,
#ifndef BUILD_MQT_CORE_QDMI_WITH_QIR
QDMI_PROGRAM_FORMAT_QIRBASEMODULE,
QDMI_PROGRAM_FORMAT_QIRBASESTRING,
#endif
#ifndef BUILD_MQT_CORE_QDMI_WITH_QIR
QDMI_PROGRAM_FORMAT_QIRBASEMODULE,
QDMI_PROGRAM_FORMAT_QIRBASESTRING,
#endif
QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING,
QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE,

Very minor nitpick. I would keep the ordering nonetheless.

Comment on lines +38 to +54
class HistogramKeysAndValuesSumToShots : public ::testing::Test {
protected:
static void Run(QDMI_Program_Format format, std::string_view program) {
const qdmi_test::SessionGuard s{};
const qdmi_test::JobGuard j{s.session};
ASSERT_EQ(qdmi_test::setProgram(j.job, format, program), QDMI_SUCCESS);
constexpr size_t shots = 1024;
ASSERT_EQ(qdmi_test::setShots(j.job, shots), QDMI_SUCCESS);
ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS);

auto [keys, vals] = qdmi_test::getHistogram(j.job);
ASSERT_EQ(keys.size(), vals.size());
size_t sum = 0U;
for (const auto& v : vals) {
sum += v;
}
EXPECT_EQ(sum, shots);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why this cannot be a simple function in the anonymous namespace? I would have a slight tendency not to intermingle the class and the function if there is no need for it, but I might overlook some dependency of GTest here.

const std::string_view program = qdmi_test::QIR_BELL_PAIR_STATIC;
llvm::LLVMContext context;
llvm::SMDiagnostic err;
auto module = llvm::parseAssemblyString(program, err, context);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am actually surprised this works with module as a variable name here. Since C++20, module is a reserved keyword. Since, we are using a C++20 in the project we should avoid it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for context: I believe it is a reserved keyword only in certain contexts. Still best to avoid it altogether. Triggers weird syntax highlighting on GitHub 😅

rc = MQT_DDSIM_QDMI_device_job_set_parameter(
job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, program.size() + 1,
program.data());
job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, program.size(), program.data());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a string, also the terminating null-character must be given as a parameter, hence the +1. However, I think, there was a bug in the previous implementation, and it should actually be program.c_str() to ensure null-char termination.

Comment on lines +25 to +49
class Session {
public:
using MainFn = int(int, char**);

explicit Session(llvm::StringRef inputFile);
Session(llvm::StringRef irBytes, llvm::StringRef bufferName);
~Session();
int run(llvm::ArrayRef<std::string> args = {},
llvm::StringRef progName = "") const;

private:
llvm::orc::ThreadSafeContext tsCtx_{std::make_unique<llvm::LLVMContext>()};
llvm::orc::ThreadSafeModule module_;
std::unique_ptr<llvm::orc::LLJIT> jit_;
MainFn* mainFn_ = nullptr;

static void registerRuntimeSymbols();
static void initNativeTargets();
llvm::Expected<llvm::orc::ThreadSafeModule>
loadModuleFromFile(llvm::StringRef irPath);
llvm::Expected<llvm::orc::ThreadSafeModule>
loadModuleFromMemory(llvm::StringRef irBytes, llvm::StringRef bufferName);
void initialize();
void deinitialize();
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is missing docstrings. Especially, because its name is quite generic, these would improve the understanding a lot.

@burgholzer
Copy link
Copy Markdown
Member

@rturrado Just came to say this is a great initial draft! Keep it up 💪🏻

rturrado added 4 commits June 6, 2026 22:16
Also move comment from Runner's main into Session::run.
Move filtering and sorting of Result pointers to getResults.
Add qir::toBitString to convert a map<Result*, bool> to a bit string.

Assisted-by: Claude Opus 4.7 via Claude Code
Add an injectable std::ostream pointer member to Runtime, defaulting to std::cout, with getOstream/setOstream/resetOstream accessors.
Update __quantum__rt__result_record_output (QIR.cpp) to write through runtime.getOstream() rather than directly to std::cout.

Migrate the test fixtures in test_qir_runtime.cpp (QIRRuntimeTest) and results_sampling_test.cpp (HistogramKeysAndValuesSumToShots).
Inject a std::ostringstream sink via setOstream in SetUp and restore the default via resetOstream in TearDown.
QIRRuntimeTest's previous std::cout.rdbuf swap is gone.
The QDMI sampling tests no longer spew thousands of result-record lines to stdout per run.

Link MQT::CoreQIRRuntime to the dd-device test target when BUILD_MQT_CORE_QDMI_WITH_QIR is ON, since results_sampling_test.cpp now includes Runtime.hpp directly.

Assisted-by: Claude Opus 4.7 via Claude Code
@rturrado
Copy link
Copy Markdown
Author

rturrado commented Jun 6, 2026

Hi @rturrado, Your contribution already looks great in this state. Thanks for taking part in the unitaryHACK. I am a big fan of the restructuring, i.e., having the JIT interpreter as its own library. This makes the code a lot better structured. It is actually also cool to see that this refactoring comprises the most part of this PR, and the remaining changes are quite local. The biggest action item that remains in my mind is the documentation of the Session class. In particular, I reviewed all files in detail except the session class because the documentation eases the review here a lot. So, in summary, my biggest request are comprehensive docstrings for the Session class. Overall, this contribution looks already very good and is very valuable.

Hi @ystade ! Many thanks for the comments and for the thorough review. I'll go over each of the items in the following days.

There are still a few things I want to tackle as part of this PR, on top of whatever you consider necessary:

  • Runner is not try/catching the JIT session. ✔
  • submitQIRProgram calls Runtime's getResults but then does a lot of post processing (filter zero and one addresses, order addresses, and convert to bit string). I would prefer part of that functionality to be done by the Runtime itself (filter and, optionally, order) and by a utility function (toBitString). ✔
  • Outputting to an ostream, not just to standard output. ✔
  • Dynamic modules. This is not tested in the current implementation. TODO.
  • Adaptive program formats. Same. Currently untested. TODO.
  • Code coverage errors.

Assisted-by: Claude Opus 4.7 via Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Add QIR Program Format Support to the QDMI DDSim Device

3 participants