diff --git a/CHANGELOG.md b/CHANGELOG.md index 941d811227..0214758b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Added +- ✨ Add QIR program format support to the DDSIM QDMI Device ([#1766]) ([**@rturrado**]) - 🚸 Add [CMake presets] to provide a standardized and reproducible way to configure builds ([#1660]) ([**@denialhaag**]) - ✨ Add a `quantum-loop-unroll` pass for unrolling for-loop operations containing quantum operations ([#1718]) ([**@MatthiasReumann**]) - ✨ Add a `hadamard-lifting` pass for lifting Hadamard gates above Pauli gates ([#1605]) ([**@lirem101**], [**@burgholzer**]) @@ -405,6 +406,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [#1781]: https://github.com/munich-quantum-toolkit/core/pull/1781 [#1776]: https://github.com/munich-quantum-toolkit/core/pull/1776 [#1774]: https://github.com/munich-quantum-toolkit/core/pull/1774 +[#1766]: https://github.com/munich-quantum-toolkit/core/pull/1766 [#1765]: https://github.com/munich-quantum-toolkit/core/pull/1765 [#1762]: https://github.com/munich-quantum-toolkit/core/pull/1762 [#1751]: https://github.com/munich-quantum-toolkit/core/pull/1751 @@ -656,6 +658,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [**@simon1hofmann**]: https://github.com/simon1hofmann [**@keefehuang**]: https://github.com/keefehuang [**@J4MMlE**]: https://github.com/J4MMlE +[**@rturrado**]: https://github.com/rturrado diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c1a0eee53..017d91b5f0 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,10 @@ endif() cmake_dependent_option(BUILD_MQT_CORE_QIR_RUNNER "Build the QIR runner of the MQT Core project" ON "BUILD_MQT_CORE_MLIR" OFF) +cmake_dependent_option( + BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR "Enable QIR program format support for the DDSIM QDMI Device" + ON "BUILD_MQT_CORE_MLIR" OFF) + # add main library code add_subdirectory(src) diff --git a/docs/qir/index.md b/docs/qir/index.md index 4e09d4acdb..c794af124c 100644 --- a/docs/qir/index.md +++ b/docs/qir/index.md @@ -16,7 +16,8 @@ See {cite:p}`stadeTowardsSupportingQIR2025` for more details. ### Building the Runner -To build this tool, the CMake option `BUILD_MQT_CORE_QIR_RUNNER` has to be enabled (which depends on `BUILD_MQT_CORE_MLIR` being set). +To build this tool, the CMake option `BUILD_MQT_CORE_QIR_RUNNER` has to be enabled. +It is enabled by default, but depends on `BUILD_MQT_CORE_MLIR` being set. From the root of the repository, you can build the runner as follows: ```bash @@ -36,3 +37,9 @@ The `mqt-core-qir-runner` can be used to execute a QIR file (typically with a `. This will simulate the circuit and print the measurement results to the console. The runner supports the QIR Base Profile. + +### QIR Support in the DDSIM QDMI Device + +The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR Base/Adaptive Profile Module (LLVM bitcode), and QIR Base/Adaptive Profile String (LLVM assembly). +These QIR formats are only supported when the `BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR` CMake option is enabled. +It is enabled by default, but depends on `BUILD_MQT_CORE_MLIR` being set. diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index d6e4350f13..38b7c5e203 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,8 @@ #include #include #include +#include +#include namespace qdmi::dd { class Device final : public Singleton { @@ -189,8 +192,11 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { /// The program format QDMI_Program_Format format_ = QDMI_PROGRAM_FORMAT_QASM3; - /// The quantum program associated with the job - std::string program_; + /// The quantum program associated with the job. + /// Text formats (QASM2/3, QIR Base/Adaptive String) are stored as + /// @c std::string; binary formats (QIR Base/Adaptive Module) are stored as + /// @c std::vector. + std::variant> program_; /// The number of shots for the job size_t numShots_ = 1024U; @@ -234,6 +240,41 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { auto getProbabilities(size_t size, void* data, size_t* sizeRet) -> QDMI_STATUS; + /// Run @p body on a worker thread with the standard job lifecycle: + /// - increase the running-job count, + /// - set status to RUNNING, + /// - run @p body, + /// - set status to DONE or FAILED, and + /// - decrease the running-job count. + /// Typically, @p body will: + /// - parse the program, + /// - run or simulate it, and + /// - store the results in the job's output fields. + /// @returns @c QDMI_SUCCESS once the worker has been spawned. + /// Failures inside @p body are reported through the job status (FAILED), + /// not through the return value. + auto submitProgramAsync(std::function body) -> QDMI_STATUS; + + /// Submit a QASM 2 or QASM 3 program. + /// Dispatches to the sampling or the state-extraction helper depending on + /// @c numShots_. + auto submitQASMProgram() -> QDMI_STATUS; + /// Sampling path for a QASM program (@c numShots_ > 0). + auto submitQASMProgramSampling() -> QDMI_STATUS; + /// State-extraction path for a QASM program (@c numShots_ == 0). + auto submitQASMProgramStateExtraction() -> QDMI_STATUS; + +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR + /// Submit a QIR Base/Adaptive Module or String program. + /// Dispatches to the sampling or the state-extraction helper depending on + /// @c numShots_. + auto submitQIRProgram() -> QDMI_STATUS; + /// Sampling path for a QIR program (@c numShots_ > 0). + auto submitQIRProgramSampling() -> QDMI_STATUS; + /// State-extraction path for a QIR Base Profile program (@c numShots_ == 0). + auto submitQIRProgramStateExtraction() -> QDMI_STATUS; +#endif + public: /// Constructor for the MQT_DDSIM_QDMI_Device_Job_impl_d. explicit MQT_DDSIM_QDMI_Device_Job_impl_d( @@ -251,6 +292,15 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { /** * @brief Sets a parameter for the job. + * @note When setting @c QDMI_DEVICE_JOB_PARAMETER_PROGRAM, the device uses + * the current @c QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT to decide whether + * the payload's wire @p size: + * - includes a trailing @c '\0' (text formats: QASM2, QASM3, + * QIR Base/Adaptive String) or + * - is the exact byte count (binary formats: QIR Base/Adaptive Module). + * Callers should therefore set @c PROGRAMFORMAT before @c PROGRAM. + * The default of @c QDMI_PROGRAM_FORMAT_QASM3 is assumed if @c PROGRAMFORMAT + * is not set. * @see MQT_DDSIM_QDMI_device_job_set_parameter */ auto setParameter(QDMI_Device_Job_Parameter param, size_t size, diff --git a/include/mqt-core/qir/jit/IRRewriter.hpp b/include/mqt-core/qir/jit/IRRewriter.hpp new file mode 100644 index 0000000000..d144f0fe45 --- /dev/null +++ b/include/mqt-core/qir/jit/IRRewriter.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include + +#include +#include + +namespace qir { + +/// The set of call targets that @ref stripMeasurementRelatedCalls erases. +inline constexpr std::array STRIP_TARGETS = { + "__quantum__qis__mz__body", + "__quantum__qis__m__body", + "__quantum__qis__measure__body", + "__quantum__rt__result_record_output", + "__quantum__rt__result_update_reference_count", +}; + +/** + * @brief Strips QIR measurement-related calls from @p m in place. + * @details Erases calls to the QIR measurement intrinsics, to the + * result-recording intrinsic, and to the result reference-count update + * intrinsic (whose Result operands would otherwise reference the null + * pointers left by the stripped measurements). + * Intended for QIR Base Profile programs only: in Adaptive Profile programs, + * measurement results feed classical control flow, so removing them silently + * changes observable behavior. + * + * The typical use is to prepare a Base Profile module for state-vector + * extraction: after this transform the JIT'd @c main can be run once and + * the resulting state remains in the @ref qir::Runtime DD instead of being + * collapsed by measurement. + * + * @param m Module to rewrite in place. + * @return Number of instructions erased. + */ +std::size_t stripMeasurementRelatedCalls(llvm::Module& m); + +} // namespace qir diff --git a/include/mqt-core/qir/jit/Session.hpp b/include/mqt-core/qir/jit/Session.hpp new file mode 100644 index 0000000000..abeb416fc0 --- /dev/null +++ b/include/mqt-core/qir/jit/Session.hpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace qir { + +/** + * @brief Whether the JIT'd program runs to produce measurement samples or + * to leave the final quantum state in @ref qir::Runtime for external + * extraction. + * @details In @c StateExtraction mode the session strips QIR measurement + * and result-management calls from the IR before JIT-compiling, so the + * runtime's quantum state remains intact after @c main returns. Intended + * for QIR Base Profile programs only. + */ +enum class Execution { Sampling, StateExtraction }; + +/** + * @brief In-process JIT executor for QIR programs. + * @details The session does the following, in order: + * - Loads an LLVM module from either an IR file (text or bitcode) or + * an in-memory buffer, + * - JIT-compiles it via LLVM's OrcJIT with lazy compilation. + * - wires up the QIR runtime symbols, and + * - runs the module's @c main function. + * A session owns a single LLJIT instance and is not meant to be reused across + * modules; create a new @ref JitSession for each program. + */ +class JitSession { +public: + /// Signature of the @c main function produced by QIR-compiled modules. + using MainFn = int(int, char**); + + /** + * @brief Build a session by loading IR from a file on disk. + * @param inputFile Path to a textual IR or bitcode file. + * @param mode Execution mode; see @ref Execution. + * @throws std::runtime_error if the file cannot be parsed or the JIT fails + * to initialize. + */ + explicit JitSession(llvm::StringRef inputFile, + Execution mode = Execution::Sampling); + + /** + * @brief Build a session by loading IR from a memory buffer. + * @details Accepts either textual IR or bitcode. The buffer does not have + * to be null-terminated. + * @param irBytes Byte view of the IR. + * @param bufferName Identifier used in diagnostics. + * @param mode Execution mode; see @ref Execution. + * @throws std::runtime_error if the IR cannot be parsed or the JIT fails + * to initialize. + */ + JitSession(llvm::StringRef irBytes, llvm::StringRef bufferName, + Execution mode = Execution::Sampling); + + /// Tears down the LLJIT and any JIT'd resources owned by the session. + ~JitSession(); + + /** + * @brief Executes the JIT'd @c main function. + * @param args Argument strings passed as @c argv (excluding @c argv[0]). + * @param progName Value used as @c argv[0]. + * @return The integer returned by the JIT'd @c main. + */ + int run(llvm::ArrayRef args = {}, + llvm::StringRef progName = "") const; + +private: + llvm::orc::ThreadSafeContext tsCtx_{std::make_unique()}; + llvm::orc::ThreadSafeModule module_; + std::unique_ptr jit_; + MainFn* mainFn_ = nullptr; + + /// Registers the QIR runtime symbols with @c llvm::sys::DynamicLibrary so the + /// JIT can resolve them at link time. + /// Safe to call multiple times; the work runs only on the first call. + static void registerRuntimeSymbols(); + + /// Initializes the native target, asm printer and asm parser. + /// Safe to call multiple times; the work runs only on the first call. + static void initNativeTargets(); + + /// Parses LLVM IR from @p irPath using the session's thread-safe context. + llvm::Expected + loadModuleFromFile(llvm::StringRef irPath); + + /// Parses LLVM IR (textual or bitcode) from @p irBytes using the session's + /// thread-safe context. @p bufferName is used in diagnostics. + llvm::Expected + loadModuleFromMemory(llvm::StringRef irBytes, llvm::StringRef bufferName); + + /// Prepares the session to run the program: + /// - Validates the loaded module. + /// - Optionally strips measurement and result management calls + /// (for @ref Execution::StateExtraction). + /// - Builds the @c LLJIT instance + /// - Registers QIR runtime symbols + /// - Resolves @c main. + /// @throws std::runtime_error if loading failed or the JIT cannot start. + void initialize(llvm::Expected llvmModule, + Execution mode); + + /// Tears down the @c LLJIT. + void deinitialize() const; +}; + +} // namespace qir diff --git a/include/mqt-core/qir/runtime/Runtime.hpp b/include/mqt-core/qir/runtime/Runtime.hpp index 009b3d66dd..8459225d90 100644 --- a/include/mqt-core/qir/runtime/Runtime.hpp +++ b/include/mqt-core/qir/runtime/Runtime.hpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,7 @@ struct ArrayImpl { }; namespace qir { + // Primary template template static constexpr bool IS_STD_ARRAY_V = false; // Specialization for std::array @@ -183,6 +185,7 @@ class Utils { return array; } }; + /** * @note This class is implemented following the design pattern Singleton in * order to access an instance of this class from the C function without having @@ -193,6 +196,37 @@ class Runtime { static constexpr uintptr_t RESULT_ZERO_ADDRESS = 0x10000; static constexpr uintptr_t RESULT_ONE_ADDRESS = 0x10001; + /// The quantum state held by the runtime: + /// - a DD package, + /// - the root edge into that package, and + /// - the number of qubits the state spans. + struct QState { + std::unique_ptr dd; + dd::vEdge edge; + dd::Qubit numQubits; + + QState() + : dd(std::make_unique()), edge(dd::vEdge::one()), + numQubits(0) {} + + /// Reset to a fresh empty state. + /// If @c dd is currently populated, the existing package's `decRef` plus + /// `garbageCollect` path is used so the package (and its internal caches) + /// is kept warm. + /// If @c dd was moved out (e.g. by @ref Runtime::takeState), a new package + /// is allocated. + auto reset() -> void { + if (dd) { + dd->decRef(edge); + dd->garbageCollect(); + } else { + dd = std::make_unique(); + } + edge = dd::vEdge::one(); + numQubits = 0; + } + }; + private: static constexpr uintptr_t MIN_DYN_QUBIT_ADDRESS = 0x10000; enum class AddressMode : uint8_t { UNKNOWN, DYNAMIC, STATIC }; @@ -203,13 +237,13 @@ class Runtime { std::vector qubitPermutation; static constexpr uintptr_t MIN_DYN_RESULT_ADDRESS = 0x10000; std::unordered_map rRegister; + std::string recordedOutputs; uintptr_t currentMaxQubitAddress; qc::Qubit currentMaxQubitId; uintptr_t currentMaxResultAddress; - dd::Qubit numQubitsInQState; - std::unique_ptr dd; - dd::vEdge qState; + QState qState; std::mt19937_64 mt; + std::ostream* os = &std::cout; Runtime(); explicit Runtime(uint64_t randomSeed); @@ -269,7 +303,7 @@ class Runtime { auto apply(Args&&... args) -> void { const qc::StandardOperation& operation = createOperation(std::forward(args)...); - qState = applyUnitaryOperation(operation, qState, *dd); + qState.edge = applyUnitaryOperation(operation, qState.edge, *qState.dd); } template auto measure(Args... args) -> void { const auto& qubits = Utils::packOfType(args...); @@ -293,8 +327,8 @@ class Runtime { // measure qubits Utils::apply2( [&](const auto q, auto& r) { - const auto& result = - dd->measureOneCollapsing(qState, static_cast(q), mt); + const auto& result = qState.dd->measureOneCollapsing( + qState.edge, static_cast(q), mt); deref(r).r = result == '1'; }, targets, results); @@ -306,7 +340,7 @@ class Runtime { } const qc::NonUnitaryOperation resetOp( {targets.data(), targets.data() + SIZE}, qc::Reset); - qState = applyReset(resetOp, qState, *dd, mt); + qState.edge = applyReset(resetOp, qState.edge, *qState.dd, mt); } auto swap(Qubit* qubit1, Qubit* qubit2) -> void; auto qAlloc() -> Qubit*; @@ -356,5 +390,25 @@ class Runtime { auto deref(Result* result) -> ResultStruct&; auto rFree(Result* result) -> void; auto equal(Result* result1, Result* result2) -> bool; + + /// Append the value referenced by `result` to the recorded outputs bit + /// string in record order. + auto recordOutput(Result* result) -> void; + + /// @returns the outputs declared by the program as a bit string in record + /// order. + auto getRecordedOutputs() const -> const std::string&; + + /// Move the quantum state out of the runtime. + /// Then reset the runtime to a clean state ready for the next job. + /// Intended for use after a @c JitSession constructed with + /// @c Execution::StateExtraction has finished running. + /// @returns the moved @c QState from the runtime. + auto takeState() -> QState; + + auto getOstream() -> std::ostream&; + auto setOstream(std::ostream& other) -> void; + auto resetOstream() -> void; }; + } // namespace qir diff --git a/src/qdmi/devices/dd/CMakeLists.txt b/src/qdmi/devices/dd/CMakeLists.txt index bfebf8a92a..ee4a2a3458 100644 --- a/src/qdmi/devices/dd/CMakeLists.txt +++ b/src/qdmi/devices/dd/CMakeLists.txt @@ -40,6 +40,10 @@ if(NOT TARGET ${TARGET_NAME}) # Add link libraries target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreDD MQT::CoreQASM MQT::CoreCircuitOptimizer MQT::CoreQDMICommon spdlog::spdlog) + if(BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRJIT MQT::CoreQIRRuntime) + target_compile_definitions(${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) + endif() # Make QDMI version available and ensure symbols are exported when building the library target_compile_definitions(${TARGET_NAME} PRIVATE QDMI_VERSION="${QDMI_VERSION}" diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 4fc828b3e4..d20e0202d4 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -41,8 +42,21 @@ #include #include #include +#include #include #include +#include +#include + +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +#include "qir/jit/Session.hpp" +#include "qir/runtime/Runtime.hpp" + +#include +#include + +#include +#endif namespace { constexpr uintptr_t OFFSET = 0x10000U; @@ -346,7 +360,14 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( return QDMI_ERROR_INVALIDARGUMENT; } if (format != QDMI_PROGRAM_FORMAT_QASM2 && - format != QDMI_PROGRAM_FORMAT_QASM3) { + format != QDMI_PROGRAM_FORMAT_QASM3 +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR + && format != QDMI_PROGRAM_FORMAT_QIRBASEMODULE && + format != QDMI_PROGRAM_FORMAT_QIRBASESTRING && + format != QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE && + format != QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING +#endif + ) { return QDMI_ERROR_NOTSUPPORTED; } format_ = format; @@ -354,7 +375,21 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_PROGRAM: if (value != nullptr) { - program_ = std::string(static_cast(value), size - 1); + const bool isTextProgramFormat = + format_ == QDMI_PROGRAM_FORMAT_QASM2 || + format_ == QDMI_PROGRAM_FORMAT_QASM3 || + format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING || + format_ == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; + if (isTextProgramFormat) { + // Text payloads include the trailing '\0' in `size`. + // Strip it so it is not counted in the stored string's size. + const auto* text = static_cast(value); + program_ = std::string(text, size - 1); + } else { + // Binary payloads are stored exactly as received. + const std::span bytes(static_cast(value), size); + program_ = std::vector(bytes.begin(), bytes.end()); + } } return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM: @@ -380,50 +415,135 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::queryProperty( ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT, QDMI_Program_Format, format_, prop, size, value, sizeRet) - ADD_STRING_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAM, program_.c_str(), prop, + if (std::holds_alternative(program_)) { + const auto& text = std::get(program_); + ADD_STRING_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAM, text.c_str(), prop, + size, value, sizeRet) + } else { + const auto& bytes = std::get>(program_); + ADD_LIST_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAM, std::byte, bytes, prop, size, value, sizeRet) + } ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM, size_t, numShots_, prop, size, value, sizeRet) return QDMI_ERROR_NOTSUPPORTED; } +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitProgramAsync( + std::function body) -> QDMI_STATUS { + jobHandle_ = std::async(std::launch::async, [this, body = std::move(body)]() { + qdmi::dd::Device::get().increaseRunningJobs(); + status_.store(QDMI_JOB_STATUS_RUNNING); + try { + body(); + status_.store(QDMI_JOB_STATUS_DONE); + } catch (const std::exception& e) { + status_.store(QDMI_JOB_STATUS_FAILED); + std::cerr << "Error: " << e.what() << '\n'; + } + qdmi::dd::Device::get().decreaseRunningJobs(); + }); + return QDMI_SUCCESS; +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgram() -> QDMI_STATUS { + return numShots_ > 0 ? submitQASMProgramSampling() + : submitQASMProgramStateExtraction(); +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgramSampling() + -> QDMI_STATUS { + return submitProgramAsync([this]() { + const auto& text = std::get(program_); + const auto qc = qasm3::Importer::imports(text); + counts_ = dd::sample(qc, numShots_); + }); +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgramStateExtraction() + -> QDMI_STATUS { + return submitProgramAsync([this]() { + const auto& text = std::get(program_); + auto qc = qasm3::Importer::imports(text); + qc::CircuitOptimizer::removeFinalMeasurements(qc); + const auto nQubits = qc.getNqubits(); + dd_ = std::make_unique(nQubits); + stateVecDD_ = dd::simulate(qc, dd::makeZeroState(nQubits, *dd_), *dd_); + }); +} +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { + return numShots_ > 0 ? submitQIRProgramSampling() + : submitQIRProgramStateExtraction(); +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgramSampling() + -> QDMI_STATUS { + return submitProgramAsync([this]() { + auto& runtime = qir::Runtime::getInstance(); + auto irBytes = std::visit( + [](const auto& p) { + return llvm::StringRef(reinterpret_cast(p.data()), + p.size()); + }, + program_); + auto jitSession = qir::JitSession(irBytes, "QDMI job"); + for (size_t i = 0; i < numShots_; ++i) { + runtime.reset(); + if (const auto rc = jitSession.run(); rc != 0) { + throw std::runtime_error( + llvm::formatv("QIR program failed with error: {}", rc)); + } + // Update the measurement counts. + ++counts_[runtime.getRecordedOutputs()]; + } + }); +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgramStateExtraction() + -> QDMI_STATUS { + // State extraction strips measurement calls from the IR, which only + // preserves semantics for QIR Base Profile (measurements are terminal there). + // Adaptive Profile has measurement-dependent control flow, so stripping would + // silently change the program's meaning. + if (format_ != QDMI_PROGRAM_FORMAT_QIRBASEMODULE && + format_ != QDMI_PROGRAM_FORMAT_QIRBASESTRING) { + return QDMI_ERROR_NOTSUPPORTED; + } + return submitProgramAsync([this]() { + auto& runtime = qir::Runtime::getInstance(); + runtime.reset(); + auto irBytes = std::visit( + [](const auto& p) { + return llvm::StringRef(reinterpret_cast(p.data()), + p.size()); + }, + program_); + auto jitSession = + qir::JitSession(irBytes, "QDMI job", qir::Execution::StateExtraction); + if (const auto rc = jitSession.run(); rc != 0) { + throw std::runtime_error( + llvm::formatv("QIR program failed with error: {}", rc)); + } + auto state = runtime.takeState(); + dd_ = std::move(state.dd); + stateVecDD_ = state.edge; + }); +} +#endif auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { if (status_.load() != QDMI_JOB_STATUS_CREATED) { return QDMI_ERROR_BADSTATE; } status_.store(QDMI_JOB_STATUS_SUBMITTED); - if (numShots_ > 0) { - jobHandle_ = std::async(std::launch::async, [this]() { - qdmi::dd::Device::get().increaseRunningJobs(); - status_.store(QDMI_JOB_STATUS_RUNNING); - try { - const auto qc = qasm3::Importer::imports(program_); - counts_ = dd::sample(qc, numShots_); - status_.store(QDMI_JOB_STATUS_DONE); - } catch (const std::exception& e) { - status_.store(QDMI_JOB_STATUS_FAILED); - std::cerr << "Error: " << e.what() << '\n'; - } - qdmi::dd::Device::get().decreaseRunningJobs(); - }); - } else { - jobHandle_ = std::async(std::launch::async, [this]() { - try { - qdmi::dd::Device::get().increaseRunningJobs(); - status_.store(QDMI_JOB_STATUS_RUNNING); - auto qc = qasm3::Importer::imports(program_); - qc::CircuitOptimizer::removeFinalMeasurements(qc); - const auto nqubits = qc.getNqubits(); - dd_ = std::make_unique(nqubits); - stateVecDD_ = dd::simulate(qc, dd::makeZeroState(nqubits, *dd_), *dd_); - status_.store(QDMI_JOB_STATUS_DONE); - } catch (const std::exception& e) { - status_.store(QDMI_JOB_STATUS_FAILED); - std::cerr << "Error: " << e.what() << '\n'; - } - qdmi::dd::Device::get().decreaseRunningJobs(); - }); - } - return QDMI_SUCCESS; + if (format_ == QDMI_PROGRAM_FORMAT_QASM2 || + format_ == QDMI_PROGRAM_FORMAT_QASM3) { + return submitQASMProgram(); + } +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR + if (format_ == QDMI_PROGRAM_FORMAT_QIRBASEMODULE || + format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING || + format_ == QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE || + format_ == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING) { + return submitQIRProgram(); + } +#endif + // Format is validated against the allowed set at setParameter time. + qdmi::unreachable(); } auto MQT_DDSIM_QDMI_Device_Job_impl_d::cancel() -> QDMI_STATUS { const auto s = status_.load(); diff --git a/src/qir/CMakeLists.txt b/src/qir/CMakeLists.txt index 4a8b8ba413..2c60f4191d 100644 --- a/src/qir/CMakeLists.txt +++ b/src/qir/CMakeLists.txt @@ -6,6 +6,10 @@ # # Licensed under the MIT License +if(BUILD_MQT_CORE_QIR_RUNNER OR BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) + add_subdirectory(jit) +endif() + add_subdirectory(runtime) if(BUILD_MQT_CORE_QIR_RUNNER) diff --git a/src/qir/jit/CMakeLists.txt b/src/qir/jit/CMakeLists.txt new file mode 100644 index 0000000000..6795fc58c1 --- /dev/null +++ b/src/qir/jit/CMakeLists.txt @@ -0,0 +1,45 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qir-jit) + +if(NOT TARGET ${TARGET_NAME}) + # Add QIRJIT library + add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QIRJIT) + + # Add sources to target + target_sources(${TARGET_NAME} PRIVATE Session.cpp IRRewriter.cpp) + + # Add headers using file sets + target_sources( + ${TARGET_NAME} + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_CORE_INCLUDE_BUILD_DIR} + FILES + ${MQT_CORE_INCLUDE_BUILD_DIR}/qir/jit/Session.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qir/jit/IRRewriter.hpp) + + # Get the LLVM native target libraries + llvm_map_components_to_libnames( + llvm_native_libs + codegen + core + executionengine + irreader + native + orcdebugging + orcjit + orctargetprocess + support + targetparser) + + # Add link libraries + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime ${llvm_native_libs}) +endif() diff --git a/src/qir/jit/IRRewriter.cpp b/src/qir/jit/IRRewriter.cpp new file mode 100644 index 0000000000..e7a98cb1e3 --- /dev/null +++ b/src/qir/jit/IRRewriter.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "qir/jit/IRRewriter.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace qir { + +static bool isStripTarget(const llvm::CallInst& call) { + const auto* callee = call.getCalledFunction(); + if (callee == nullptr) { + return false; + } + const auto name = callee->getName(); + return std::ranges::any_of( + STRIP_TARGETS, [&](llvm::StringRef target) { return name == target; }); +} + +std::size_t stripMeasurementRelatedCalls(llvm::Module& m) { + std::size_t erased = 0; + for (auto& fn : m) { + for (auto& bb : fn) { + for (auto& inst : llvm::make_early_inc_range(bb)) { + auto* call = llvm::dyn_cast(&inst); + if (call == nullptr || !isStripTarget(*call)) { + continue; + } + // `m` and `measure` return a `Result*`. + // Replace uses of `m` and `measure` return values with a null before + // erasure so they do not dangle. + if (!call->getType()->isVoidTy() && !call->use_empty()) { + call->replaceAllUsesWith( + llvm::Constant::getNullValue(call->getType())); + } + call->eraseFromParent(); + ++erased; + } + } + } + return erased; +} + +} // namespace qir diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp new file mode 100644 index 0000000000..11c86ee446 --- /dev/null +++ b/src/qir/jit/Session.cpp @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "qir/jit/Session.hpp" + +#include "qir/jit/IRRewriter.hpp" +#include "qir/runtime/QIR.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "mqt-core-qir-jit" + +namespace qir { + +static void exitOnLazyCallThroughFailure() { exit(1); } + +static int mingwNoopMain() { + // Cygwin and MinGW insert calls from the main function to the runtime + // function __main. The __main function is responsible for setting up main's + // environment (e.g. running static constructors), however this is not needed + // when running under lli: the executor process will have run non-JIT ctors, + // and ORC will take care of running JIT'd ctors. To avoid a missing symbol + // error we just implement __main as a no-op. + return 0; +} + +// Try to enable debugger support for the given instance. +// This always returns success, but prints a warning if it's not able to enable +// debugger support. +static llvm::Error tryEnableDebugSupport(llvm::orc::LLJIT& jit) { + if (auto err = enableDebuggerSupport(jit)) { + [[maybe_unused]] const std::string errMsg = toString(std::move(err)); + // NOLINTNEXTLINE(cppcoreguidelines-avoid-do-while) + LLVM_DEBUG(llvm::dbgs() << DEBUG_TYPE ": " << errMsg << "\n"); + } + return llvm::Error::success(); +} + +static llvm::Expected +getThreadSafeModuleOrError(std::unique_ptr llvmModule, + const llvm::SMDiagnostic& err, + llvm::orc::ThreadSafeContext tsCtx) { + if (!llvmModule) { + std::string errMsg; + { + llvm::raw_string_ostream errMsgStream(errMsg); + err.print(DEBUG_TYPE, errMsgStream); + } + return llvm::make_error(std::move(errMsg), + llvm::inconvertibleErrorCode()); + } + return llvm::orc::ThreadSafeModule(std::move(llvmModule), std::move(tsCtx)); +} + +llvm::Expected +JitSession::loadModuleFromFile(const llvm::StringRef irPath) { + llvm::SMDiagnostic err; + auto m = tsCtx_.withContextDo( + [&](llvm::LLVMContext* ctx) { return parseIRFile(irPath, err, *ctx); }); + return getThreadSafeModuleOrError(std::move(m), err, tsCtx_); +} + +llvm::Expected +JitSession::loadModuleFromMemory(const llvm::StringRef irBytes, + const llvm::StringRef bufferName) { + llvm::SMDiagnostic err; + auto buffer = llvm::MemoryBuffer::getMemBuffer( + irBytes, bufferName, + /*RequiresNullTerminator=*/false); // bitcode isn't null-terminated + auto m = tsCtx_.withContextDo([&](llvm::LLVMContext* ctx) { + return parseIR(buffer->getMemBufferRef(), err, *ctx); + }); + return getThreadSafeModuleOrError(std::move(m), err, tsCtx_); +} + +JitSession::JitSession(const llvm::StringRef inputFile, const Execution mode) { + initialize(loadModuleFromFile(inputFile), mode); +} + +JitSession::JitSession(const llvm::StringRef irBytes, + const llvm::StringRef bufferName, const Execution mode) { + initialize(loadModuleFromMemory(irBytes, bufferName), mode); +} + +JitSession::~JitSession() { deinitialize(); } + +int JitSession::run(llvm::ArrayRef args, + llvm::StringRef progName) const { + // Manual in-process execution with RuntimeDyld. + return llvm::orc::runAsMain(mainFn_, args, progName); +} + +namespace { +std::vector> manualSymbols; +} // namespace + +#define REGISTER_SYMBOL(name) \ + llvm::sys::DynamicLibrary::AddSymbol(#name, \ + reinterpret_cast(&(name))); \ + manualSymbols.emplace_back(#name, reinterpret_cast(&(name))); + +void JitSession::registerRuntimeSymbols() { + static std::once_flag flag; + std::call_once(flag, []() { + REGISTER_SYMBOL(__quantum__rt__result_get_zero); + REGISTER_SYMBOL(__quantum__rt__result_get_one); + REGISTER_SYMBOL(__quantum__rt__result_equal); + REGISTER_SYMBOL(__quantum__rt__result_update_reference_count); + REGISTER_SYMBOL(__quantum__rt__array_create_1d); + REGISTER_SYMBOL(__quantum__rt__array_get_size_1d); + REGISTER_SYMBOL(__quantum__rt__array_get_element_ptr_1d); + REGISTER_SYMBOL(__quantum__rt__array_update_reference_count); + REGISTER_SYMBOL(__quantum__rt__qubit_allocate); + REGISTER_SYMBOL(__quantum__rt__qubit_allocate_array); + REGISTER_SYMBOL(__quantum__rt__qubit_release); + REGISTER_SYMBOL(__quantum__rt__qubit_release_array); + REGISTER_SYMBOL(__quantum__qis__x__body); + REGISTER_SYMBOL(__quantum__qis__y__body); + REGISTER_SYMBOL(__quantum__qis__z__body); + REGISTER_SYMBOL(__quantum__qis__h__body); + REGISTER_SYMBOL(__quantum__qis__s__body); + REGISTER_SYMBOL(__quantum__qis__sdg__body); + REGISTER_SYMBOL(__quantum__qis__sx__body); + REGISTER_SYMBOL(__quantum__qis__sxdg__body); + REGISTER_SYMBOL(__quantum__qis__sqrtx__body); + REGISTER_SYMBOL(__quantum__qis__sqrtxdg__body); + REGISTER_SYMBOL(__quantum__qis__t__body); + REGISTER_SYMBOL(__quantum__qis__tdg__body); + REGISTER_SYMBOL(__quantum__qis__r__body); + REGISTER_SYMBOL(__quantum__qis__prx__body); + REGISTER_SYMBOL(__quantum__qis__rx__body); + REGISTER_SYMBOL(__quantum__qis__ry__body); + REGISTER_SYMBOL(__quantum__qis__rz__body); + REGISTER_SYMBOL(__quantum__qis__p__body); + REGISTER_SYMBOL(__quantum__qis__rxx__body); + REGISTER_SYMBOL(__quantum__qis__ryy__body); + REGISTER_SYMBOL(__quantum__qis__rzz__body); + REGISTER_SYMBOL(__quantum__qis__rzx__body); + REGISTER_SYMBOL(__quantum__qis__u__body); + REGISTER_SYMBOL(__quantum__qis__u3__body); + REGISTER_SYMBOL(__quantum__qis__u2__body); + REGISTER_SYMBOL(__quantum__qis__u1__body); + REGISTER_SYMBOL(__quantum__qis__cu1__body); + REGISTER_SYMBOL(__quantum__qis__cu3__body); + REGISTER_SYMBOL(__quantum__qis__cnot__body); + REGISTER_SYMBOL(__quantum__qis__cx__body); + REGISTER_SYMBOL(__quantum__qis__cy__body); + REGISTER_SYMBOL(__quantum__qis__cz__body); + REGISTER_SYMBOL(__quantum__qis__ch__body); + REGISTER_SYMBOL(__quantum__qis__swap__body); + REGISTER_SYMBOL(__quantum__qis__cswap__body); + REGISTER_SYMBOL(__quantum__qis__crz__body); + REGISTER_SYMBOL(__quantum__qis__cry__body); + REGISTER_SYMBOL(__quantum__qis__crx__body); + REGISTER_SYMBOL(__quantum__qis__cp__body); + REGISTER_SYMBOL(__quantum__qis__ccx__body); + REGISTER_SYMBOL(__quantum__qis__ccy__body); + REGISTER_SYMBOL(__quantum__qis__ccz__body); + REGISTER_SYMBOL(__quantum__qis__m__body); + REGISTER_SYMBOL(__quantum__qis__measure__body); + REGISTER_SYMBOL(__quantum__qis__mz__body); + REGISTER_SYMBOL(__quantum__qis__reset__body); + REGISTER_SYMBOL(__quantum__rt__initialize); + REGISTER_SYMBOL(__quantum__rt__read_result); + REGISTER_SYMBOL(__quantum__rt__result_record_output); + }); +} + +#undef REGISTER_SYMBOL + +void JitSession::initNativeTargets() { + static std::once_flag flag; + std::call_once(flag, []() { + static const llvm::codegen::RegisterCodeGenFlags CGF; + + // If we have a native target, initialize it to ensure it is linked in and + // usable by the JIT. + llvm::InitializeNativeTarget(); + llvm::InitializeNativeTargetAsmPrinter(); + llvm::InitializeNativeTargetAsmParser(); + }); +} + +void JitSession::initialize( + llvm::Expected llvmModule, + const Execution mode) { + if (!llvmModule) { + throw std::runtime_error(llvm::toString(llvmModule.takeError())); + } + module_ = std::move(*llvmModule); + + // In StateExtraction mode, strip QIR measurement and result-management calls + // so the runtime's quantum state remains intact after main returns. + if (mode == Execution::StateExtraction) { + module_.withModuleDo( + [](llvm::Module& m) { stripMeasurementRelatedCalls(m); }); + } + + registerRuntimeSymbols(); + initNativeTargets(); + + // Get TargetTriple and DataLayout from the main module if they're explicitly + // set. + std::optional tt; + std::optional dl; + module_.withModuleDo([&](llvm::Module& m) { + if (!m.getTargetTriple().empty()) { + tt = m.getTargetTriple(); + } + if (!m.getDataLayout().isDefault()) { + dl = m.getDataLayout(); + } + }); + + // Configure the lazy JIT builder. + llvm::orc::LLLazyJITBuilder builder; + + // Use the module's target triple if set, otherwise detect the host's. + auto host = llvm::orc::JITTargetMachineBuilder::detectHost(); + if (!host) { + throw std::runtime_error(llvm::toString(host.takeError())); + } + builder.setJITTargetMachineBuilder( + tt ? llvm::orc::JITTargetMachineBuilder(*tt) : *host); + + // Cache the resolved triple; apply the module's explicit data layout if any. + tt = builder.getJITTargetMachineBuilder()->getTargetTriple(); + if (dl) { + builder.setDataLayout(dl); + } + + // Optional architecture override from the -march codegen flag. + if (!llvm::codegen::getMArch().empty()) { + builder.getJITTargetMachineBuilder()->getTargetTriple().setArchName( + llvm::codegen::getMArch()); + } + + // Apply CPU, features, relocation model, and code model from codegen flags. + builder.getJITTargetMachineBuilder() + ->setCPU(llvm::codegen::getCPUStr()) + .addFeatures(llvm::codegen::getFeatureList()) + .setRelocationModel(llvm::codegen::getExplicitRelocModel()) + .setCodeModel(llvm::codegen::getExplicitCodeModel()); + + // Link process symbols. + builder.setLinkProcessSymbolsByDefault(true); + + // Set up the in-process execution session and lazy call-through manager. + auto pc = llvm::orc::SelfExecutorProcessControl::Create(); + if (!pc) { + throw std::runtime_error(llvm::toString(pc.takeError())); + } + auto es = std::make_unique(std::move(*pc)); + builder.setLazyCallthroughManager( + std::make_unique( + *es, llvm::orc::ExecutorAddr(), nullptr)); + builder.setExecutionSession(std::move(es)); + + // Abort on lazy compilation failure. + builder.setLazyCompileFailureAddr( + llvm::orc::ExecutorAddr::fromPtr(exitOnLazyCallThroughFailure)); + + // Enable debugging of JIT'd code (only works on JITLink for ELF and MachO). + builder.setPrePlatformSetup(tryEnableDebugSupport); + + // Build the JIT. + auto expectedJit = builder.create(); + if (!expectedJit) { + throw std::runtime_error(llvm::toString(expectedJit.takeError())); + } + jit_ = std::move(*expectedJit); + + // Register QIR runtime symbols. + auto& jd = jit_->getMainJITDylib(); + llvm::orc::SymbolMap hostSymbols; + for (const auto& [name, ptr] : manualSymbols) { + hostSymbols[jit_->mangleAndIntern(name)] = { + llvm::orc::ExecutorAddr::fromPtr(ptr), llvm::JITSymbolFlags::Exported}; + } + if (auto err = jd.define(llvm::orc::absoluteSymbols(hostSymbols))) { + throw std::runtime_error(llvm::toString(std::move(err))); + } + + // DynamicLibrarySearchGenerator + auto gen = llvm::orc::DynamicLibrarySearchGenerator::GetForCurrentProcess( + jit_->getDataLayout().getGlobalPrefix()); + if (!gen) { + throw std::runtime_error(llvm::toString(gen.takeError())); + } + jit_->getMainJITDylib().addGenerator(std::move(*gen)); + + // GDB listener (no error path) + auto* objLayer = &jit_->getObjLinkingLayer(); + if (auto* rtDyldObjLayer = + dyn_cast(objLayer)) { + rtDyldObjLayer->registerJITEventListener( + *llvm::JITEventListener::createGDBRegistrationListener()); + } + + // If this is a Mingw or Cygwin executor then we need to alias __main to + // orc_rt_int_void_return_0. + if (jit_->getTargetTriple().isOSCygMing()) { + auto& workaroundJD = jit_->getProcessSymbolsJITDylib() + ? *jit_->getProcessSymbolsJITDylib() + : jit_->getMainJITDylib(); + if (auto err = workaroundJD.define(llvm::orc::absoluteSymbols( + {{jit_->mangleAndIntern("__main"), + {llvm::orc::ExecutorAddr::fromPtr(mingwNoopMain), + llvm::JITSymbolFlags::Exported}}}))) { + throw std::runtime_error(llvm::toString(std::move(err))); + } + } + + // Regular modules are greedy: They materialize as a whole and trigger + // materialization for all required symbols recursively. Lazy modules go + // through partitioning, and they replace outgoing calls with reexport stubs + // that resolve on call-through. + auto addModule = [&](llvm::orc::JITDylib& jdlib, + llvm::orc::ThreadSafeModule m) { + return jit_->addIRModule(jdlib, std::move(m)); + }; + + // Add the main module. + if (auto err = addModule(jit_->getMainJITDylib(), std::move(module_))) { + throw std::runtime_error(llvm::toString(std::move(err))); + } + + // Run any static constructors. + if (auto err = jit_->initialize(jit_->getMainJITDylib())) { + throw std::runtime_error(llvm::toString(std::move(err))); + } + + // Resolve the main function. + auto mainAddr = jit_->lookup("main"); + if (!mainAddr) { + throw std::runtime_error(llvm::toString(mainAddr.takeError())); + } + mainFn_ = mainAddr->toPtr(); +} + +void JitSession::deinitialize() const { + if (!jit_) { + return; + } + if (auto err = jit_->deinitialize(jit_->getMainJITDylib())) { + llvm::errs() << "JitSession deinitialize failed: " + << llvm::toString(std::move(err)) << "\n"; + } +} + +} // namespace qir diff --git a/src/qir/runner/CMakeLists.txt b/src/qir/runner/CMakeLists.txt index ea9564b7fa..4577bbfa0a 100644 --- a/src/qir/runner/CMakeLists.txt +++ b/src/qir/runner/CMakeLists.txt @@ -11,12 +11,8 @@ set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qir-runner) if(NOT TARGET ${TARGET_NAME}) add_llvm_tool(${TARGET_NAME} Runner.cpp DEPENDS intrinsics_gen EXPORT_SYMBOLS) - # Get the native target libraries - llvm_map_components_to_libnames(llvm_native_libs native) - # Add link libraries - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime LLVMOrcDebugging - ${llvm_native_libs}) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRJIT) # Set versioning information set_target_properties(${TARGET_NAME} PROPERTIES VERSION ${PROJECT_VERSION} EXPORT_NAME diff --git a/src/qir/runner/Runner.cpp b/src/qir/runner/Runner.cpp index 9335c0f590..4ef8fc200e 100644 --- a/src/qir/runner/Runner.cpp +++ b/src/qir/runner/Runner.cpp @@ -8,48 +8,16 @@ * Licensed under the MIT License */ -#include "qir/runtime/QIR.h" +#include "qir/jit/Session.hpp" -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include #include #include -#include -#include -#include -#include -#include -#include -#include +#include #include #include -#include -#include #define DEBUG_TYPE "mqt-core-qir-runner" @@ -64,255 +32,18 @@ static llvm::cl::list static llvm::ExitOnError ExitOnError; -static void exitOnLazyCallThroughFailure() { exit(1); } - -static llvm::Expected -loadModule(const llvm::StringRef path, llvm::orc::ThreadSafeContext tsCtx) { - llvm::SMDiagnostic err; - auto m = tsCtx.withContextDo( - [&](llvm::LLVMContext* ctx) { return parseIRFile(path, err, *ctx); }); - if (!m) { - std::string errMsg; - { - llvm::raw_string_ostream errMsgStream(errMsg); - err.print(DEBUG_TYPE, errMsgStream); - } - return llvm::make_error(std::move(errMsg), - llvm::inconvertibleErrorCode()); - } - - return llvm::orc::ThreadSafeModule(std::move(m), std::move(tsCtx)); -} - -static int mingwNoopMain() { - // Cygwin and MinGW insert calls from the main function to the runtime - // function __main. The __main function is responsible for setting up main's - // environment (e.g. running static constructors), however this is not needed - // when running under lli: the executor process will have run non-JIT ctors, - // and ORC will take care of running JIT'd ctors. To avoid a missing symbol - // error we just implement __main as a no-op. - return 0; -} - -// Try to enable debugger support for the given instance. -// This always returns success, but prints a warning if it's not able to enable -// debugger support. -static llvm::Error tryEnableDebugSupport(llvm::orc::LLJIT& jit) { - if (auto err = enableDebuggerSupport(jit)) { - [[maybe_unused]] const std::string errMsg = toString(std::move(err)); - // NOLINTNEXTLINE(cppcoreguidelines-avoid-do-while) - LLVM_DEBUG(llvm::dbgs() << DEBUG_TYPE ": " << errMsg << "\n"); - } - return llvm::Error::success(); -} - -static std::vector> manualSymbols; - -static int runOrcJIT() { - // Start setting up the JIT environment. - - // Parse the main module. - const llvm::orc::ThreadSafeContext tsCtx( - std::make_unique()); - auto mainModule = ExitOnError(loadModule(InputFile, tsCtx)); - - // Get TargetTriple and DataLayout from the main module if they're explicitly - // set. - std::optional tt; - std::optional dl; - mainModule.withModuleDo([&](llvm::Module& m) { - if (!m.getTargetTriple().empty()) { - tt = m.getTargetTriple(); - } - if (!m.getDataLayout().isDefault()) { - dl = m.getDataLayout(); - } - }); - - llvm::orc::LLLazyJITBuilder builder; - - builder.setJITTargetMachineBuilder( - tt ? llvm::orc::JITTargetMachineBuilder(*tt) - : ExitOnError(llvm::orc::JITTargetMachineBuilder::detectHost())); - - tt = builder.getJITTargetMachineBuilder()->getTargetTriple(); - if (dl) { - builder.setDataLayout(dl); - } - - if (!llvm::codegen::getMArch().empty()) { - builder.getJITTargetMachineBuilder()->getTargetTriple().setArchName( - llvm::codegen::getMArch()); - } - - builder.getJITTargetMachineBuilder() - ->setCPU(llvm::codegen::getCPUStr()) - .addFeatures(llvm::codegen::getFeatureList()) - .setRelocationModel(llvm::codegen::getExplicitRelocModel()) - .setCodeModel(llvm::codegen::getExplicitCodeModel()); - - // Link process symbols. - builder.setLinkProcessSymbolsByDefault(true); - - auto es = std::make_unique( - ExitOnError(llvm::orc::SelfExecutorProcessControl::Create())); - builder.setLazyCallthroughManager( - std::make_unique( - *es, llvm::orc::ExecutorAddr(), nullptr)); - builder.setExecutionSession(std::move(es)); - - builder.setLazyCompileFailureAddr( - llvm::orc::ExecutorAddr::fromPtr(exitOnLazyCallThroughFailure)); - - // Enable debugging of JIT'd code (only works on JITLink for ELF and MachO). - builder.setPrePlatformSetup(tryEnableDebugSupport); - - const auto jit = ExitOnError(builder.create()); - - auto& jd = jit->getMainJITDylib(); - llvm::orc::SymbolMap hostSymbols; - for (const auto& [name, ptr] : manualSymbols) { - hostSymbols[jit->mangleAndIntern(name)] = { - llvm::orc::ExecutorAddr::fromPtr(ptr), llvm::JITSymbolFlags::Exported}; - } - ExitOnError(jd.define(llvm::orc::absoluteSymbols(hostSymbols))); - - jit->getMainJITDylib().addGenerator(ExitOnError( - llvm::orc::DynamicLibrarySearchGenerator::GetForCurrentProcess( - jit->getDataLayout().getGlobalPrefix()))); - - auto* objLayer = &jit->getObjLinkingLayer(); - if (auto* rtDyldObjLayer = - dyn_cast(objLayer)) { - rtDyldObjLayer->registerJITEventListener( - *llvm::JITEventListener::createGDBRegistrationListener()); - } - - // If this is a Mingw or Cygwin executor then we need to alias __main to - // orc_rt_int_void_return_0. - if (jit->getTargetTriple().isOSCygMing()) { - auto& workaroundJD = jit->getProcessSymbolsJITDylib() - ? *jit->getProcessSymbolsJITDylib() - : jit->getMainJITDylib(); - ExitOnError(workaroundJD.define(llvm::orc::absoluteSymbols( - {{jit->mangleAndIntern("__main"), - {llvm::orc::ExecutorAddr::fromPtr(mingwNoopMain), - llvm::JITSymbolFlags::Exported}}}))); - } - - // Regular modules are greedy: They materialize as a whole and trigger - // materialization for all required symbols recursively. Lazy modules go - // through partitioning and they replace outgoing calls with reexport stubs - // that resolve on call-through. - auto addModule = [&](llvm::orc::JITDylib& jd, llvm::orc::ThreadSafeModule m) { - return jit->addIRModule(jd, std::move(m)); - }; - - // Add the main module. - ExitOnError(addModule(jit->getMainJITDylib(), std::move(mainModule))); - - // Run any static constructors. - ExitOnError(jit->initialize(jit->getMainJITDylib())); - - // Resolve and run the main function. - const auto mainAddr = ExitOnError(jit->lookup("main")); - - // Manual in-process execution with RuntimeDyld. - using mainFnTy = int(int, char**); - auto mainFn = mainAddr.toPtr(); - const int result = - llvm::orc::runAsMain(mainFn, InputArgv, llvm::StringRef(InputFile)); - - // Run destructors. - ExitOnError(jit->deinitialize(jit->getMainJITDylib())); - - return result; -} - -#define REGISTER_SYMBOL(name) \ - llvm::sys::DynamicLibrary::AddSymbol(#name, \ - reinterpret_cast(&(name))); \ - manualSymbols.emplace_back(#name, reinterpret_cast(&(name))); - auto main(int argc, char* argv[]) -> int { const llvm::InitLLVM session(argc, argv); - if (const std::span args(argv, argc); args.size() > 1) { ExitOnError.setBanner(std::string(args[0]) + ": "); } - - // If we have a native target, initialize it to ensure it is linked in and - // usable by the JIT. - llvm::InitializeNativeTarget(); - llvm::InitializeNativeTargetAsmPrinter(); - llvm::InitializeNativeTargetAsmParser(); - llvm::cl::ParseCommandLineOptions(argc, argv, "qir interpreter & dynamic compiler\n"); - REGISTER_SYMBOL(__quantum__rt__result_get_zero); - REGISTER_SYMBOL(__quantum__rt__result_get_one); - REGISTER_SYMBOL(__quantum__rt__result_equal); - REGISTER_SYMBOL(__quantum__rt__result_update_reference_count); - REGISTER_SYMBOL(__quantum__rt__array_create_1d); - REGISTER_SYMBOL(__quantum__rt__array_get_size_1d); - REGISTER_SYMBOL(__quantum__rt__array_get_element_ptr_1d); - REGISTER_SYMBOL(__quantum__rt__array_update_reference_count); - REGISTER_SYMBOL(__quantum__rt__qubit_allocate); - REGISTER_SYMBOL(__quantum__rt__qubit_allocate_array); - REGISTER_SYMBOL(__quantum__rt__qubit_release); - REGISTER_SYMBOL(__quantum__rt__qubit_release_array); - REGISTER_SYMBOL(__quantum__qis__x__body); - REGISTER_SYMBOL(__quantum__qis__y__body); - REGISTER_SYMBOL(__quantum__qis__z__body); - REGISTER_SYMBOL(__quantum__qis__h__body); - REGISTER_SYMBOL(__quantum__qis__s__body); - REGISTER_SYMBOL(__quantum__qis__sdg__body); - REGISTER_SYMBOL(__quantum__qis__sx__body); - REGISTER_SYMBOL(__quantum__qis__sxdg__body); - REGISTER_SYMBOL(__quantum__qis__sqrtx__body); - REGISTER_SYMBOL(__quantum__qis__sqrtxdg__body); - REGISTER_SYMBOL(__quantum__qis__t__body); - REGISTER_SYMBOL(__quantum__qis__tdg__body); - REGISTER_SYMBOL(__quantum__qis__r__body); - REGISTER_SYMBOL(__quantum__qis__prx__body); - REGISTER_SYMBOL(__quantum__qis__rx__body); - REGISTER_SYMBOL(__quantum__qis__ry__body); - REGISTER_SYMBOL(__quantum__qis__rz__body); - REGISTER_SYMBOL(__quantum__qis__p__body); - REGISTER_SYMBOL(__quantum__qis__rxx__body); - REGISTER_SYMBOL(__quantum__qis__ryy__body); - REGISTER_SYMBOL(__quantum__qis__rzz__body); - REGISTER_SYMBOL(__quantum__qis__rzx__body); - REGISTER_SYMBOL(__quantum__qis__u__body); - REGISTER_SYMBOL(__quantum__qis__u3__body); - REGISTER_SYMBOL(__quantum__qis__u2__body); - REGISTER_SYMBOL(__quantum__qis__u1__body); - REGISTER_SYMBOL(__quantum__qis__cu1__body); - REGISTER_SYMBOL(__quantum__qis__cu3__body); - REGISTER_SYMBOL(__quantum__qis__cnot__body); - REGISTER_SYMBOL(__quantum__qis__cx__body); - REGISTER_SYMBOL(__quantum__qis__cy__body); - REGISTER_SYMBOL(__quantum__qis__cz__body); - REGISTER_SYMBOL(__quantum__qis__ch__body); - REGISTER_SYMBOL(__quantum__qis__swap__body); - REGISTER_SYMBOL(__quantum__qis__cswap__body); - REGISTER_SYMBOL(__quantum__qis__crz__body); - REGISTER_SYMBOL(__quantum__qis__cry__body); - REGISTER_SYMBOL(__quantum__qis__crx__body); - REGISTER_SYMBOL(__quantum__qis__cp__body); - REGISTER_SYMBOL(__quantum__qis__ccx__body); - REGISTER_SYMBOL(__quantum__qis__ccy__body); - REGISTER_SYMBOL(__quantum__qis__ccz__body); - REGISTER_SYMBOL(__quantum__qis__m__body); - REGISTER_SYMBOL(__quantum__qis__measure__body); - REGISTER_SYMBOL(__quantum__qis__mz__body); - REGISTER_SYMBOL(__quantum__qis__reset__body); - REGISTER_SYMBOL(__quantum__rt__initialize); - REGISTER_SYMBOL(__quantum__rt__read_result); - REGISTER_SYMBOL(__quantum__rt__result_record_output); - - return runOrcJIT(); + try { + auto jitSession = qir::JitSession(llvm::StringRef(InputFile)); + return jitSession.run(InputArgv, InputFile); + } catch (const std::exception& e) { + ExitOnError(llvm::createStringError(e.what())); + } } - -#undef REGISTER_SYMBOL diff --git a/src/qir/runtime/CMakeLists.txt b/src/qir/runtime/CMakeLists.txt index aef86c8b01..84f65ae266 100644 --- a/src/qir/runtime/CMakeLists.txt +++ b/src/qir/runtime/CMakeLists.txt @@ -9,7 +9,7 @@ set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qir-runtime) if(NOT TARGET ${TARGET_NAME}) - # Add QIRBackend library + # Add QIRRuntime library add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QIRRuntime) # Add sources to target diff --git a/src/qir/runtime/QIR.cpp b/src/qir/runtime/QIR.cpp index dc7eef132e..a52c5c1cb5 100644 --- a/src/qir/runtime/QIR.cpp +++ b/src/qir/runtime/QIR.cpp @@ -381,8 +381,10 @@ bool __quantum__rt__read_result(Result* result) { } void __quantum__rt__result_record_output(Result* result, const char* label) { - std::cout << label << ": " << (__quantum__rt__read_result(result) ? 1 : 0) - << "\n"; + auto& runtime = qir::Runtime::getInstance(); + runtime.recordOutput(result); + runtime.getOstream() << label << ": " + << (__quantum__rt__read_result(result) ? 1 : 0) << "\n"; } } // extern "C" diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index 0ca6a70c6b..16f53c1d88 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -21,11 +21,14 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include #include #include @@ -47,14 +50,6 @@ Runtime& Runtime::getInstance() { } auto Runtime::reset() -> void { addressMode = AddressMode::UNKNOWN; - currentMaxQubitAddress = MIN_DYN_QUBIT_ADDRESS; - currentMaxQubitId = 0; - currentMaxResultAddress = MIN_DYN_RESULT_ADDRESS; - numQubitsInQState = 0; - dd->decRef(qState); - dd->garbageCollect(); - qState = dd::vEdge::one(); - mt.seed(generateRandomSeed()); qRegister.clear(); rRegister.clear(); // NOLINTBEGIN(performance-no-int-to-ptr) @@ -63,6 +58,12 @@ auto Runtime::reset() -> void { rRegister.emplace(reinterpret_cast(RESULT_ONE_ADDRESS), ResultStruct{.refcount = 0, .r = true}); // NOLINTEND(performance-no-int-to-ptr) + recordedOutputs.clear(); + currentMaxQubitAddress = MIN_DYN_QUBIT_ADDRESS; + currentMaxQubitId = 0; + currentMaxResultAddress = MIN_DYN_RESULT_ADDRESS; + qState.reset(); + mt.seed(generateRandomSeed()); } Runtime::Runtime() : Runtime(generateRandomSeed()) {} @@ -70,9 +71,7 @@ Runtime::Runtime() : Runtime(generateRandomSeed()) {} Runtime::Runtime(const uint64_t randomSeed) : addressMode(AddressMode::UNKNOWN), currentMaxQubitAddress(MIN_DYN_QUBIT_ADDRESS), currentMaxQubitId(0), - currentMaxResultAddress(MIN_DYN_RESULT_ADDRESS), numQubitsInQState(0), - dd(std::make_unique()), qState(dd::vEdge::one()), - mt(randomSeed) { + currentMaxResultAddress(MIN_DYN_RESULT_ADDRESS), mt(randomSeed) { qRegister = std::unordered_map(); rRegister = std::unordered_map(); // NOLINTBEGIN(performance-no-int-to-ptr) @@ -84,32 +83,33 @@ Runtime::Runtime(const uint64_t randomSeed) } auto Runtime::enlargeState(const std::uint64_t maxQubit) -> void { - if (maxQubit >= numQubitsInQState) { - const auto d = maxQubit - numQubitsInQState + 1; - qubitPermutation.resize(numQubitsInQState + d); - std::iota(qubitPermutation.begin() + - static_cast::difference_type>( - numQubitsInQState), - qubitPermutation.end(), numQubitsInQState); - numQubitsInQState += static_cast(d); - - // resize the DD package only if necessary - if (dd->qubits() < numQubitsInQState) { - dd->resize(numQubitsInQState); + if (maxQubit >= qState.numQubits) { + const auto d = maxQubit - qState.numQubits + 1; + qubitPermutation.resize(qState.numQubits + d); + std::iota(qubitPermutation.begin() + qState.numQubits, + qubitPermutation.end(), qState.numQubits); + qState.numQubits += static_cast(d); + + // Resize the DD package only if necessary. + if (qState.dd->qubits() < qState.numQubits) { + qState.dd->resize(qState.numQubits); } - // if the state is terminal, we need to create a new node - if (qState.isTerminal()) { - qState = makeZeroState(d, *dd); + // If the state is terminal, we need to create a new node. + if (qState.edge.isTerminal()) { + qState.edge = makeZeroState(d, *qState.dd); return; } - // enlarge state - for (auto q = qState.p->v; q < numQubitsInQState; ++q) { - auto old = qState; - qState = dd->makeDDNode(q + 1U, std::array{qState, dd::vEdge::zero()}); - dd->incRef(qState); - dd->decRef(old); + // Enlarge state. + // Each iteration adds one level above the current root, raising root.v by + // one. After the loop, root.v == numQubits - 1. + for (auto q = qState.edge.p->v; q + 1 < qState.numQubits; ++q) { + auto old = qState.edge; + qState.edge = qState.dd->makeDDNode( + q + 1U, std::array{qState.edge, dd::vEdge::zero()}); + qState.dd->incRef(qState.edge); + qState.dd->decRef(old); } } } @@ -164,4 +164,24 @@ auto Runtime::equal(Result* result1, Result* result2) -> bool { return deref(result1).r == deref(result2).r; } +auto Runtime::recordOutput(Result* result) -> void { + recordedOutputs.push_back(deref(result).r ? '1' : '0'); +} + +auto Runtime::getRecordedOutputs() const -> const std::string& { + return recordedOutputs; +} + +auto Runtime::takeState() -> QState { + QState ret = std::move(qState); + reset(); + return ret; +} + +auto Runtime::getOstream() -> std::ostream& { return *os; } + +auto Runtime::setOstream(std::ostream& other) -> void { os = &other; } + +auto Runtime::resetOstream() -> void { os = &std::cout; } + } // namespace qir diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e58442a617..6203a14e01 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,9 +13,9 @@ add_subdirectory(dd) add_subdirectory(ir) add_subdirectory(na) add_subdirectory(zx) +add_subdirectory(qir) add_subdirectory(qdmi) add_subdirectory(fomac) -add_subdirectory(qir) # copy test circuits to build directory file(COPY ${PROJECT_SOURCE_DIR}/test/circuits DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/test/circuits/BellPairAdaptive.ll b/test/circuits/BellPairAdaptive.ll new file mode 100644 index 0000000000..441546ebce --- /dev/null +++ b/test/circuits/BellPairAdaptive.ll @@ -0,0 +1,61 @@ +; ModuleID = 'Adaptive module implementing Bell-pair correlation via classical correction' +source_filename = "BellPairAdaptive.ll" + +%Qubit = type opaque +%Result = type opaque + +@0 = internal constant [3 x i8] c"r0\00" +@1 = internal constant [3 x i8] c"r1\00" + +define i32 @main() #0 { +entry: + call void @__quantum__rt__initialize(i8* null) + %q0 = call %Qubit* @__quantum__rt__qubit_allocate() + %q1 = call %Qubit* @__quantum__rt__qubit_allocate() + call void @__quantum__qis__h__body(%Qubit* %q0) + %r0 = call %Result* @__quantum__qis__m__body(%Qubit* %q0) + %b = call i1 @__quantum__rt__read_result(%Result* %r0) + br i1 %b, label %correct, label %record + +correct: + call void @__quantum__qis__x__body(%Qubit* %q1) + br label %record + +record: + %r1 = call %Result* @__quantum__qis__m__body(%Qubit* %q1) + call void @__quantum__rt__qubit_release(%Qubit* %q0) + call void @__quantum__rt__qubit_release(%Qubit* %q1) + call void @__quantum__rt__result_record_output(%Result* %r0, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @0, i32 0, i32 0)) + call void @__quantum__rt__result_record_output(%Result* %r1, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @1, i32 0, i32 0)) + call void @__quantum__rt__result_update_reference_count(%Result* %r0, i32 -1) + call void @__quantum__rt__result_update_reference_count(%Result* %r1, i32 -1) + ret i32 0 +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__x__body(%Qubit*) + +declare %Result* @__quantum__qis__m__body(%Qubit*) #1 + +declare i1 @__quantum__rt__read_result(%Result*) + +declare void @__quantum__rt__initialize(i8*) + +declare %Qubit* @__quantum__rt__qubit_allocate() + +declare void @__quantum__rt__qubit_release(%Qubit*) + +declare void @__quantum__rt__result_record_output(%Result*, i8*) + +declare void @__quantum__rt__result_update_reference_count(%Result*, i32) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/qir/BellPairDynamic.ll b/test/circuits/BellPairDynamic.ll similarity index 85% rename from test/qir/BellPairDynamic.ll rename to test/circuits/BellPairDynamic.ll index 15696c4816..1d80f9e034 100644 --- a/test/qir/BellPairDynamic.ll +++ b/test/circuits/BellPairDynamic.ll @@ -1,5 +1,5 @@ -; ModuleID = 'bell' -source_filename = "bell" +; ModuleID = 'Dynamic module implementing Bell pair' +source_filename = "BellPairDynamic.ll" %Qubit = type opaque %Result = type opaque @@ -10,8 +10,8 @@ source_filename = "bell" define i32 @main() #0 { entry: call void @__quantum__rt__initialize(i8* null) - %q0 = call %Qubit* @__quantum__rt__qubit_allocate(); - %q1 = call %Qubit* @__quantum__rt__qubit_allocate(); + %q0 = call %Qubit* @__quantum__rt__qubit_allocate() + %q1 = call %Qubit* @__quantum__rt__qubit_allocate() call void @__quantum__qis__h__body(%Qubit* %q0) call void @__quantum__qis__cnot__body(%Qubit* %q0, %Qubit* %q1) %r0 = call %Result* @__quantum__qis__m__body(%Qubit* %q0) @@ -48,5 +48,5 @@ attributes #1 = { "irreversible" } !0 = !{i32 1, !"qir_major_version", i32 1} !1 = !{i32 7, !"qir_minor_version", i32 0} -!2 = !{i32 1, !"dynamic_qubit_management", i1 false} -!3 = !{i32 1, !"dynamic_result_management", i1 false} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/qir/BellPairStatic.ll b/test/circuits/BellPairStatic.ll similarity index 94% rename from test/qir/BellPairStatic.ll rename to test/circuits/BellPairStatic.ll index eb98b571ce..972ab7261a 100644 --- a/test/qir/BellPairStatic.ll +++ b/test/circuits/BellPairStatic.ll @@ -1,5 +1,5 @@ -; ModuleID = 'bell' -source_filename = "bell" +; ModuleID = 'Static module implementing Bell pair' +source_filename = "BellPairStatic.ll" %Qubit = type opaque %Result = type opaque diff --git a/test/qir/GHZ4Dynamic.ll b/test/circuits/GHZ4Dynamic.ll similarity index 100% rename from test/qir/GHZ4Dynamic.ll rename to test/circuits/GHZ4Dynamic.ll diff --git a/test/qdmi/devices/dd/CMakeLists.txt b/test/qdmi/devices/dd/CMakeLists.txt index a2e19861d1..321b105718 100644 --- a/test/qdmi/devices/dd/CMakeLists.txt +++ b/test/qdmi/devices/dd/CMakeLists.txt @@ -33,6 +33,15 @@ if(TARGET MQT::CoreQDMI_DDSIM_Device) target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_VERSION="${MQT_CORE_VERSION}") target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + if(BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) + llvm_map_components_to_libnames(llvm_native_libs asmparser bitwriter core support) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime mqt-core-qir-test-utils + ${llvm_native_libs}) + target_compile_definitions( + ${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR + QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") + endif() + # On Windows, we need to copy the DLL to the test executable directory if(WIN32) add_custom_command( diff --git a/test/qdmi/devices/dd/error_handling_test.cpp b/test/qdmi/devices/dd/error_handling_test.cpp index 199d42234f..b2766c051a 100644 --- a/test/qdmi/devices/dd/error_handling_test.cpp +++ b/test/qdmi/devices/dd/error_handling_test.cpp @@ -294,7 +294,7 @@ TEST_F(ErrorHandling, MalformedProgramFailsForBothModes) { { const qdmi_test::JobGuard j{s.session}; ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::MALFORMED_PROGRAM), + qdmi_test::QASM3_MALFORMED), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::setShots(j.job, 128), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); @@ -306,7 +306,7 @@ TEST_F(ErrorHandling, MalformedProgramFailsForBothModes) { { const qdmi_test::JobGuard j{s.session}; ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::MALFORMED_PROGRAM), + qdmi_test::QASM3_MALFORMED), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::setShots(j.job, 0), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); diff --git a/test/qdmi/devices/dd/helpers/circuits.hpp b/test/qdmi/devices/dd/helpers/circuits.hpp index 16a4d2af63..156dd1de94 100644 --- a/test/qdmi/devices/dd/helpers/circuits.hpp +++ b/test/qdmi/devices/dd/helpers/circuits.hpp @@ -30,7 +30,7 @@ h q[0]; cx q[0], q[1]; )"; -inline constexpr const char* MALFORMED_PROGRAM = "Definitely not OpenQASM"; +inline constexpr const char* QASM3_MALFORMED = "Definitely not OpenQASM"; // A slightly heavier 5-qubit sampling circuit to prolong runtime slightly while // remaining fast diff --git a/test/qdmi/devices/dd/helpers/test_utils.cpp b/test/qdmi/devices/dd/helpers/test_utils.cpp index f17e6b63df..9290821378 100644 --- a/test/qdmi/devices/dd/helpers/test_utils.cpp +++ b/test/qdmi/devices/dd/helpers/test_utils.cpp @@ -97,9 +97,19 @@ int setProgram(MQT_DDSIM_QDMI_Device_Job job, const QDMI_Program_Format fmt, if (rc != QDMI_SUCCESS && rc != QDMI_ERROR_NOTSUPPORTED) { return rc; } + // Text payloads include the trailing '\0' per the QDMI wire convention. + // Binary payloads ship the exact byte count. + // The `+1` is safe here because every existing call to `setProgram` with a + // text format passes a `program` with a string literal or `std::string`, both + // of which guarantee `'\0'` at `data()[size()]`. + const bool isTextProgramFormat = fmt == QDMI_PROGRAM_FORMAT_QASM2 || + fmt == QDMI_PROGRAM_FORMAT_QASM3 || + fmt == QDMI_PROGRAM_FORMAT_QIRBASESTRING || + fmt == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; + const auto bytesToSend = + isTextProgramFormat ? program.size() + 1 : program.size(); 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, bytesToSend, program.data()); return rc; } diff --git a/test/qdmi/devices/dd/job_parameters_test.cpp b/test/qdmi/devices/dd/job_parameters_test.cpp index 6cfe4b1214..044b13ec1e 100644 --- a/test/qdmi/devices/dd/job_parameters_test.cpp +++ b/test/qdmi/devices/dd/job_parameters_test.cpp @@ -90,8 +90,16 @@ TEST(JobParameters, ProgramFormatSupport) { const qdmi_test::JobGuard j{s.session}; // Supported - for (QDMI_Program_Format fmt : - {QDMI_PROGRAM_FORMAT_QASM2, QDMI_PROGRAM_FORMAT_QASM3}) { + for (QDMI_Program_Format fmt : { + QDMI_PROGRAM_FORMAT_QASM2, + QDMI_PROGRAM_FORMAT_QASM3, +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR + QDMI_PROGRAM_FORMAT_QIRBASESTRING, + QDMI_PROGRAM_FORMAT_QIRBASEMODULE, + QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, + QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, +#endif + }) { EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, sizeof(QDMI_Program_Format), &fmt), @@ -100,10 +108,12 @@ TEST(JobParameters, ProgramFormatSupport) { // Unsupported → NOTSUPPORTED for (QDMI_Program_Format fmt : { +#ifndef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR QDMI_PROGRAM_FORMAT_QIRBASESTRING, QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, +#endif QDMI_PROGRAM_FORMAT_CALIBRATION, QDMI_PROGRAM_FORMAT_QPY, QDMI_PROGRAM_FORMAT_IQMJSON, diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index 8d329f6c16..622e14b83b 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -18,27 +18,133 @@ #include +#include #include +#include +#include +#include +#include +#include #include -TEST(ResultsSampling, HistogramKeysAndValuesSumToShots) { - const qdmi_test::SessionGuard s{}; - const qdmi_test::JobGuard j{s.session}; - ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::QASM3_BELL_SAMPLING), - 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); +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +#include "qir/helpers/test_utils.hpp" +#include "qir/runtime/Runtime.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#endif + +namespace { + +class HistogramTest : public ::testing::Test { +protected: +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR + std::ostringstream sink; + void SetUp() override { qir::Runtime::getInstance().setOstream(sink); } + void TearDown() override { qir::Runtime::getInstance().resetOstream(); } +#endif + + using Histogram = std::pair, std::vector>; + static constexpr size_t SHOTS = 1024; + + static Histogram runProgram(const QDMI_Program_Format format, + const std::string_view program) { + const qdmi_test::SessionGuard s{}; + const qdmi_test::JobGuard j{s.session}; + EXPECT_EQ(qdmi_test::setProgram(j.job, format, program), QDMI_SUCCESS); + EXPECT_EQ(qdmi_test::setShots(j.job, SHOTS), QDMI_SUCCESS); + EXPECT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); + return qdmi_test::getHistogram(j.job); + } + + static void checkHistogram(const Histogram& hist) { + const auto& [keys, vals] = hist; + // Keys and values come from two independent device queries. + // Check both vectors have the same size. + ASSERT_EQ(keys.size(), vals.size()); + // Values should sum up to the number of SHOTS. + const auto sum = std::accumulate(vals.cbegin(), vals.cend(), size_t{0}); + EXPECT_EQ(sum, SHOTS); + // Both keys '00' and '11' should be expected. + ASSERT_EQ(keys.size(), 2U); + // And no other keys should be expected. + EXPECT_TRUE(std::ranges::all_of( + keys, [](const auto& k) { return k == "00" || k == "11"; })); + } +}; - 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; +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +class QIRHistogramTestModule : public HistogramTest { +protected: + static std::string getProgram(const std::string_view file) { + const std::string text = qir_test::getProgram(file); + llvm::LLVMContext context; + llvm::SMDiagnostic err; + auto llvmModule = llvm::parseAssemblyString(text, err, context); + EXPECT_NE(llvmModule, nullptr) + << "parseAssemblyString failed: " << err.getMessage().str(); + if (llvmModule == nullptr) { + return {}; + } + std::string bitcodeBuffer; + llvm::raw_string_ostream os(bitcodeBuffer); + llvm::WriteBitcodeToFile(*llvmModule, os); + os.flush(); + return bitcodeBuffer; } - EXPECT_EQ(sum, shots); +}; + +class QIRHistogramTestString : public HistogramTest {}; +#endif + +} // namespace + +TEST_F(HistogramTest, QASM3Program) { + constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QASM3; + constexpr std::string_view program = qdmi_test::QASM3_BELL_SAMPLING; + checkHistogram(runProgram(format, program)); +} + +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +TEST_F(QIRHistogramTestModule, BaseStatic) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + checkHistogram(runProgram(format, getProgram("BellPairStatic.ll"))); +} + +TEST_F(QIRHistogramTestString, BaseStatic) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; + checkHistogram(runProgram(format, qir_test::getProgram("BellPairStatic.ll"))); +} + +TEST_F(QIRHistogramTestModule, BaseDynamic) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + checkHistogram(runProgram(format, getProgram("BellPairDynamic.ll"))); +} + +TEST_F(QIRHistogramTestString, BaseDynamic) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; + checkHistogram( + runProgram(format, qir_test::getProgram("BellPairDynamic.ll"))); +} + +TEST_F(QIRHistogramTestModule, Adaptive) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE; + checkHistogram(runProgram(format, getProgram("BellPairAdaptive.ll"))); +} + +TEST_F(QIRHistogramTestString, Adaptive) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; + checkHistogram( + runProgram(format, qir_test::getProgram("BellPairAdaptive.ll"))); } +#endif TEST(ResultsSampling, BufferTooSmallErrors) { const qdmi_test::SessionGuard s{}; diff --git a/test/qdmi/devices/dd/results_statevector_test.cpp b/test/qdmi/devices/dd/results_statevector_test.cpp index 5f9b78a033..eb05ee429a 100644 --- a/test/qdmi/devices/dd/results_statevector_test.cpp +++ b/test/qdmi/devices/dd/results_statevector_test.cpp @@ -22,6 +22,15 @@ #include #include +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +#include "qir/helpers/test_utils.hpp" +#include "qir/runtime/Runtime.hpp" + +#include +#include +#include +#endif + TEST(ResultsStatevector, DenseNormalizedAndBufferTooSmall) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; @@ -103,3 +112,37 @@ TEST(ResultsStatevector, HistogramRequestsInvalidWithShotsZero) { j.job, QDMI_JOB_RESULT_HIST_VALUES, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); } + +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +namespace { + +class QIRStateExtractionTest : public testing::Test { +protected: + std::ostringstream sink; + void SetUp() override { qir::Runtime::getInstance().setOstream(sink); } + void TearDown() override { qir::Runtime::getInstance().resetOstream(); } +}; + +TEST_F(QIRStateExtractionTest, BellPairStaticBaseStringYieldsBellState) { + const qdmi_test::SessionGuard s{}; + const qdmi_test::JobGuard j{s.session}; + const auto program = qir_test::getProgram("BellPairStatic.ll"); + ASSERT_EQ( + qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QIRBASESTRING, program), + QDMI_SUCCESS); + ASSERT_EQ(qdmi_test::setShots(j.job, 0), QDMI_SUCCESS); + ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); + + const auto vec = qdmi_test::getDenseState(j.job); + ASSERT_EQ(vec.size(), 4U); + + // Bell pair: amplitudes at |00> and |11> are 1/sqrt(2), |01> and |10> are 0. + constexpr double invSqrt2 = 1.0 / std::numbers::sqrt2; + EXPECT_NEAR(std::abs(vec[0]), invSqrt2, 1e-6); + EXPECT_NEAR(std::abs(vec[1]), 0.0, 1e-6); + EXPECT_NEAR(std::abs(vec[2]), 0.0, 1e-6); + EXPECT_NEAR(std::abs(vec[3]), invSqrt2, 1e-6); +} + +} // namespace +#endif diff --git a/test/qir/CMakeLists.txt b/test/qir/CMakeLists.txt index d5fce427ab..434588601e 100644 --- a/test/qir/CMakeLists.txt +++ b/test/qir/CMakeLists.txt @@ -6,5 +6,7 @@ # # Licensed under the MIT License +add_subdirectory(helpers) +add_subdirectory(jit) add_subdirectory(runtime) add_subdirectory(runner) diff --git a/test/qir/helpers/CMakeLists.txt b/test/qir/helpers/CMakeLists.txt new file mode 100644 index 0000000000..bd57bd8850 --- /dev/null +++ b/test/qir/helpers/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +add_library(mqt-core-qir-test-utils STATIC test_utils.cpp) +target_link_libraries(mqt-core-qir-test-utils PUBLIC gtest) +target_include_directories(mqt-core-qir-test-utils PUBLIC ${PROJECT_SOURCE_DIR}/test) +target_compile_definitions(mqt-core-qir-test-utils + PRIVATE QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") diff --git a/test/qir/helpers/test_utils.cpp b/test/qir/helpers/test_utils.cpp new file mode 100644 index 0000000000..f6c6fa7da8 --- /dev/null +++ b/test/qir/helpers/test_utils.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "qir/helpers/test_utils.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace qir_test { + +std::string getProgram(const std::string_view file) { + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / file; + std::ifstream ifs(path); + EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + return {std::istreambuf_iterator{ifs}, {}}; +} + +} // namespace qir_test diff --git a/test/qir/helpers/test_utils.hpp b/test/qir/helpers/test_utils.hpp new file mode 100644 index 0000000000..e6d5c0777a --- /dev/null +++ b/test/qir/helpers/test_utils.hpp @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +/* + * Test utilities for QIR-based tests. + */ +#pragma once + +#include +#include + +namespace qir_test { + +/// Read a QIR source file from the test circuits directory and +/// return its contents as a string. +std::string getProgram(std::string_view file); + +} // namespace qir_test diff --git a/test/qir/jit/CMakeLists.txt b/test/qir/jit/CMakeLists.txt new file mode 100644 index 0000000000..8552a167d0 --- /dev/null +++ b/test/qir/jit/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +if(TARGET MQT::CoreQIRJIT) + set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qir-jit-test) + + # Get the LLVM native target libraries + llvm_map_components_to_libnames(llvm_native_libs_for_test asmparser core irreader support) + + package_add_test(${TARGET_NAME} MQT::CoreQIRJIT test_ir_rewriter.cpp test_jit_session.cpp) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime mqt-core-qir-test-utils + ${llvm_native_libs_for_test}) + + target_compile_definitions(${TARGET_NAME} + PRIVATE QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") +endif() diff --git a/test/qir/jit/test_ir_rewriter.cpp b/test/qir/jit/test_ir_rewriter.cpp new file mode 100644 index 0000000000..7c1c7ba698 --- /dev/null +++ b/test/qir/jit/test_ir_rewriter.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "qir/jit/IRRewriter.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::size_t countCallsTo(const llvm::Module& m, llvm::StringRef name) { + std::size_t count = 0; + for (const auto& fn : m) { + for (const auto& bb : fn) { + for (const auto& inst : bb) { + const auto* call = llvm::dyn_cast(&inst); + if (call == nullptr) { + continue; + } + const auto* callee = call->getCalledFunction(); + if (callee != nullptr && callee->getName() == name) { + ++count; + } + } + } + } + return count; +} + +std::size_t countCallsToStripTarget(const llvm::Module& m) { + return std::accumulate(qir::STRIP_TARGETS.begin(), qir::STRIP_TARGETS.end(), + std::size_t{}, + [&m](std::size_t total, const auto& target) { + return total + countCallsTo(m, target); + }); +} + +std::unique_ptr loadIRFile(const std::filesystem::path& path, + llvm::LLVMContext& ctx) { + llvm::SMDiagnostic err; + auto llvmModule = llvm::parseIRFile(path.string(), err, ctx); + if (!llvmModule) { + std::string errStr; + llvm::raw_string_ostream s(errStr); + err.print("test_ir_rewriter", s); + throw std::runtime_error("Failed to parse IR file " + path.string() + ": " + + errStr); + } + return llvmModule; +} + +class IRRewriterTest : public testing::TestWithParam { +protected: + llvm::LLVMContext ctx_; +}; + +TEST_P(IRRewriterTest, StripMeasurementRelatedCalls) { + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / GetParam(); + auto llvmModule = loadIRFile(path, ctx_); + + const auto numStripCalls = countCallsToStripTarget(*llvmModule); + ASSERT_GT(numStripCalls, 0U) << "Module has no calls to strip targets"; + + const auto numErased = qir::stripMeasurementRelatedCalls(*llvmModule); + + EXPECT_EQ(numErased, numStripCalls); + EXPECT_EQ(countCallsToStripTarget(*llvmModule), 0U); +} + +INSTANTIATE_TEST_SUITE_P(BellPair, IRRewriterTest, + testing::Values("BellPairStatic.ll", + "BellPairDynamic.ll")); + +} // namespace diff --git a/test/qir/jit/test_jit_session.cpp b/test/qir/jit/test_jit_session.cpp new file mode 100644 index 0000000000..5f1ff5feaf --- /dev/null +++ b/test/qir/jit/test_jit_session.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "qir/helpers/test_utils.hpp" +#include "qir/jit/Session.hpp" +#include "qir/runtime/Runtime.hpp" + +#include + +#include +#include +#include +#include + +namespace { + +class JitSessionTest : public testing::Test { +protected: + std::ostringstream sink; + + void SetUp() override { + auto& runtime = qir::Runtime::getInstance(); + runtime.reset(); + runtime.setOstream(sink); + } + void TearDown() override { qir::Runtime::getInstance().resetOstream(); } +}; + +TEST_F(JitSessionTest, LoadModuleFromMemory) { + const auto program = qir_test::getProgram("BellPairStatic.ll"); + const qir::JitSession session(program, "BellPairStatic.ll"); + ASSERT_EQ(session.run(), 0); + EXPECT_FALSE(qir::Runtime::getInstance().getRecordedOutputs().empty()); +} + +TEST_F(JitSessionTest, SamplingRecordsOutputs) { + const auto path = std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; + // qir::Execution::Sampling is the default Execution mode + const qir::JitSession session(path.string()); + ASSERT_EQ(session.run(), 0); + EXPECT_FALSE(qir::Runtime::getInstance().getRecordedOutputs().empty()); +} + +TEST_F(JitSessionTest, StateExtractionLeavesNoRecordedOutputs) { + const auto path = std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; + const qir::JitSession session(path.string(), qir::Execution::StateExtraction); + ASSERT_EQ(session.run(), 0); + EXPECT_TRUE(qir::Runtime::getInstance().getRecordedOutputs().empty()); +} + +TEST(JitSessionErrors, MalformedIRThrows) { + constexpr std::string_view ir = R"(define i32 @main() {})"; + EXPECT_THROW(qir::JitSession(ir, "MalformedIR.ll"), std::runtime_error); +} + +} // namespace diff --git a/test/qir/runner/CMakeLists.txt b/test/qir/runner/CMakeLists.txt index f79e3cf62d..6fd179f5be 100644 --- a/test/qir/runner/CMakeLists.txt +++ b/test/qir/runner/CMakeLists.txt @@ -13,7 +13,7 @@ if(TARGET MQT::CoreQIRRunner) package_add_test(${TARGET_NAME} "" test_qir_runner.cpp) # Collect QIR files set(QIR_FILES "") - set(QIR_FILES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) + set(QIR_FILES_DIR ${PROJECT_SOURCE_DIR}/test/circuits) file(REAL_PATH "${QIR_FILES_DIR}" QIR_FILES_DIR) file(GLOB QIR_FILES "${QIR_FILES_DIR}/*.ll") # transform QIR_FILES to comma separated list of string ("...") diff --git a/test/qir/runtime/CMakeLists.txt b/test/qir/runtime/CMakeLists.txt index 782b801182..ba21107f7d 100644 --- a/test/qir/runtime/CMakeLists.txt +++ b/test/qir/runtime/CMakeLists.txt @@ -53,7 +53,7 @@ if(TARGET MQT::CoreQIRRuntime) # add tests for QIR files set(QIR_EXECUTABLES "") - set(QIR_FILES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) + set(QIR_FILES_DIR ${PROJECT_SOURCE_DIR}/test/circuits) file(GLOB QIR_FILES "${QIR_FILES_DIR}/*.ll") foreach(QIR_EXAMPLE ${QIR_FILES}) diff --git a/test/qir/runtime/test_qir_runtime.cpp b/test/qir/runtime/test_qir_runtime.cpp index cc4e733f7f..7486517916 100644 --- a/test/qir/runtime/test_qir_runtime.cpp +++ b/test/qir/runtime/test_qir_runtime.cpp @@ -10,6 +10,7 @@ #include "ir/Definitions.hpp" #include "qir/runtime/QIR.h" +#include "qir/runtime/Runtime.hpp" #include #include @@ -18,9 +19,7 @@ #include #include #include -#include #include -#include #ifdef _WIN32 #define SYSTEM _wsystem @@ -34,10 +33,9 @@ namespace { class QIRRuntimeTest : public testing::Test { protected: - std::stringstream buffer; - std::streambuf* old = nullptr; - void SetUp() override { old = std::cout.rdbuf(buffer.rdbuf()); } - void TearDown() override { std::cout.rdbuf(old); } + std::ostringstream sink; + void SetUp() override { Runtime::getInstance().setOstream(sink); } + void TearDown() override { Runtime::getInstance().resetOstream(); } }; } // namespace @@ -263,7 +261,7 @@ TEST_F(QIRRuntimeTest, SwapGate) { __quantum__qis__mz__body(q1, r1); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_EQ(buffer.str(), "r0: 0\nr1: 1\n"); + EXPECT_EQ(sink.str(), "r0: 0\nr1: 1\n"); } TEST_F(QIRRuntimeTest, CSwapGate) { @@ -366,7 +364,7 @@ TEST_F(QIRRuntimeTest, BellPairStatic) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(buffer.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); } TEST_F(QIRRuntimeTest, BellPairDynamic) { @@ -384,7 +382,7 @@ TEST_F(QIRRuntimeTest, BellPairDynamic) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(buffer.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); __quantum__rt__result_update_reference_count(r0, -1); __quantum__rt__result_update_reference_count(r1, -1); } @@ -404,7 +402,7 @@ TEST_F(QIRRuntimeTest, BellPairStaticReverse) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(buffer.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); } TEST_F(QIRRuntimeTest, BellPairDynamicReverse) { @@ -422,7 +420,7 @@ TEST_F(QIRRuntimeTest, BellPairDynamicReverse) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(buffer.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); __quantum__rt__result_update_reference_count(r0, -1); __quantum__rt__result_update_reference_count(r1, -1); } @@ -505,6 +503,30 @@ TEST_F(QIRRuntimeTest, GHZ4Dynamic) { __quantum__rt__array_update_reference_count(rArr, -1); } +TEST_F(QIRRuntimeTest, PackageResizeWhenEnlargingState) { + // dd::Package starts at 32 qubits. + // Acting on qubit 32 forces qState.dd->resize. + auto* q32 = reinterpret_cast(32UL); + __quantum__rt__initialize(nullptr); + __quantum__qis__h__body(q32); +} + +TEST_F(QIRRuntimeTest, TakeStateReturnsStateAndResetsRuntime) { + // Drive a small program through the runtime: H on q0. + auto* q0 = reinterpret_cast(0UL); + __quantum__rt__initialize(nullptr); + __quantum__qis__h__body(q0); + + auto state = Runtime::getInstance().takeState(); + EXPECT_NE(state.dd, nullptr); + EXPECT_FALSE(state.edge.isTerminal()); + EXPECT_EQ(state.numQubits, 1); + + // After takeState the runtime is reset and usable again. + EXPECT_NO_THROW(__quantum__rt__initialize(nullptr)); + EXPECT_NO_THROW(__quantum__qis__h__body(q0)); +} + namespace { class QIRFilesTest : public ::testing::TestWithParam {};