Add QIR Program Format Support to the QDMI DDSim Device#1766
Conversation
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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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
Signed-off-by: Roberto Turrado Camblor <rturrado@gmail.com>
Remove merge conflict strings.
ystade
left a comment
There was a problem hiding this comment.
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.
|
|
||
| ### 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). |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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...
| QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, | ||
| QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, | ||
| #ifndef BUILD_MQT_CORE_QDMI_WITH_QIR | ||
| QDMI_PROGRAM_FORMAT_QIRBASEMODULE, | ||
| QDMI_PROGRAM_FORMAT_QIRBASESTRING, | ||
| #endif |
There was a problem hiding this comment.
| 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.
| 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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
| 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(); | ||
| }; |
There was a problem hiding this comment.
This class is missing docstrings. Especially, because its name is quite generic, these would improve the understanding a lot.
|
@rturrado Just came to say this is a great initial draft! Keep it up 💪🏻 |
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
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:
|
Assisted-by: Claude Opus 4.7 via Claude Code
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:
MQT::CoreQIRJIT,src/qir/jit). This library contains almost all of the previousRunner.cppcode: 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 aqir::jit::Sessiontype 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).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
If PR contains AI-assisted content:
Assisted-by: [Model Name] via [Tool Name]footer.