From 15dc8b2bacfb97ba6cd1ddfc96c9e4bea4dc1bd8 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 24 Aug 2026 14:21:27 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Build=20target=20environments?= =?UTF-8?q?=20from=20QDMI=20payloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Snapshot one exact device-supported descriptor, its grouped optional features, and its normative baseline into an owning TargetEnvironment. Expose matching C++ and Python factories. Assisted-by: GPT-5.6 Sol via Codex --- .../plans/qdmi-target-environment-adapter.md | 120 ++++++++++++ CHANGELOG.md | 3 +- bindings/mlir/register_mlir.cpp | 94 ++++++++-- docs/mlir/target_compilation.md | 52 ++--- docs/qdmi/ddsim_device.md | 9 +- mlir/include/mlir/Compiler/QDMIAdapter.h | 24 +++ mlir/lib/Compiler/QDMIAdapter.cpp | 177 ++++++++++++++++++ .../Compiler/test_compiler_qdmi_adapter.cpp | 74 ++++++++ python/mqt/core/mlir.pyi | 26 +++ test/python/test_mlir.py | 36 ++++ 10 files changed, 557 insertions(+), 58 deletions(-) create mode 100644 .agent/plans/qdmi-target-environment-adapter.md diff --git a/.agent/plans/qdmi-target-environment-adapter.md b/.agent/plans/qdmi-target-environment-adapter.md new file mode 100644 index 0000000000..3c4125d918 --- /dev/null +++ b/.agent/plans/qdmi-target-environment-adapter.md @@ -0,0 +1,120 @@ +# Build a target environment from one QDMI payload + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept up to date as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +Compiler users must not reconstruct payload facts from a format name. After this +change, one C++ or Python factory snapshots a QDMI device and an exact accepted +descriptor into a detached `TargetEnvironment`. The result contains both the +hardware target and the selected payload capabilities. + +## Progress + +- [x] (2026-08-24 06:25Z) Added C++ conversion for an open device and a stable + device ID. +- [x] (2026-08-24 06:25Z) Added Python `TargetEnvironment.from_device` and + `from_device_id` factories with session overrides. +- [x] (2026-08-24 06:25Z) Added exact-format, grouped-feature, QIR baseline, and + rejection tests. +- [x] (2026-08-24 14:30Z) Passed all 10 focused C++ adapter tests and both + focused Python factory tests on the final stack. +- [x] (2026-08-24 14:30Z) Passed the release build, all 4,073 configured CTest + cases, and stub generation on the final stack. + +## Surprises & Discoveries + +- Observation: QDMI reports one constrained feature group as several records + with the same feature ID and value. Evidence: each record contains one + `constraint_id` and `constraint_value` pair. +- Observation: standard descriptor baselines are implicit. Evidence: QIR 2.1 + Adaptive guarantees five control-flow features even when the optional list is + empty. + +## Decision Log + +- Decision: Require the selected descriptor to equal one device-supported value. + Rationale: compilation must not claim a payload the device rejects. + Date/Author: 2026-08-24 / GPT-5.6 Sol via Codex. +- Decision: Group records by feature ID and value and preserve constraints. + Rationale: different values are alternatives, while constraints in one group + are conjunctive. Date/Author: 2026-08-24 / GPT-5.6 Sol via Codex. +- Decision: Add the QIR Adaptive baseline in the adapter and keep optional-set + completeness separate. Rationale: QDMI makes baseline facts implicit. + Date/Author: 2026-08-24 / GPT-5.6 Sol via Codex. + +## Outcomes & Retrospective + +The implementation is complete and remains detached from a live QDMI session. +All 10 focused C++ adapter tests and both focused Python factory tests pass. The +complete stack passes the release build, all 4,073 configured CTest cases, stub +generation, documentation, and lint. The preceding atomic QDMI migration layer +also passes its complete validation set. + +## Context and Orientation + +`mlir::TargetEnvironment` combines a `CompilerTarget` with a validated +`PayloadSpecification`. Its definitions live in +`mlir/include/mlir/Compiler/TargetEnvironment.h`. The QDMI compatibility +boundary is `mlir/include/mlir/Compiler/QDMIAdapter.h` with its implementation +in `mlir/lib/Compiler/QDMIAdapter.cpp`. Python bindings are in +`bindings/mlir/register_mlir.cpp`. + +## Plan of Work + +Extend the existing QDMI adapter with factories for an open `qdmi::Device` and a +registered device ID. Validate the exact descriptor, convert its version and +encoding, group optional feature records, add the normative baseline, and +preserve whether optional metadata is complete. Snapshot the existing compiler +target and payload into one owning value. Bind both factories in Python, +regenerate `python/mqt/core/mlir.pyi`, and update compiler and DDSIM examples. + +## Concrete Steps + +Run from the repository root: + + cmake --build --preset release --target mqt-core-mlir-unittests-compiler + ./build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler \ + --gtest_filter='CompilerQDMIAdapterTest.*' + uvx nox -s stubs + uvx nox -s tests-3.12 -- test/python/test_mlir.py \ + -k target_environment_from_device + uvx nox --non-interactive -s docs + uvx nox -s lint + +## Validation and Acceptance + +The factories reject a canonical descriptor that the device does not accept. +They preserve ID, canonical version, profile, and encoding. Records with the +same feature ID and value become one capability with all constraints. QIR 2.1 +Adaptive contains each baseline capability once. A successful empty optional +query is known; `NOTSUPPORTED` remains unknown. The returned environment works +after the originating device is destroyed. + +## Idempotence and Recovery + +All commands are repeatable. Stub generation is the only authorized way to +change `python/mqt/core/mlir.pyi`. No step changes remote state. + +## Artifacts and Notes + +The adapter is stacked above the low-level QDMI 1.4 contract. It does not add a +second loader, registry, or payload inference path. + +## Interfaces and Dependencies + +The final C++ functions are +`targetEnvironmentFromDevice(const qdmi::Device&, const QDMI_Program_Format&)` +and +`targetEnvironmentFromDeviceId(std::string_view, const QDMI_Program_Format&)`. +Python exposes matching static factories. This layer requires the exact +descriptors and feature query from the preceding QDMI layer and +`TargetEnvironment` from the selected-payload compiler layer. + +Plan update, 2026-08-24: Split this compiler adapter from the atomic QDMI +producer-and-consumer migration. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fd99c1acb..dfba587f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ releases may include breaking changes. - ✨ Add immutable MLIR compiler targets, selected payload specifications, QDMI device integration, ordered operation applicability, directional native synthesis, and target compilation through C++, Python, and `mqt-cc` ([#2285], - [#2219], [#2049], [#1999], [#1993], [#1687]) ([**@MatthiasReumann**], + [#2227], [#2219], [#2049], [#1999], [#1993], [#1687]) ([**@MatthiasReumann**], [**@simon1hofmann**], [**@burgholzer**]) #### Import and export @@ -912,6 +912,7 @@ for previous changelogs._ [#2246]: https://github.com/munich-quantum-toolkit/core/pull/2246 [#2240]: https://github.com/munich-quantum-toolkit/core/pull/2240 [#2232]: https://github.com/munich-quantum-toolkit/core/pull/2232 +[#2227]: https://github.com/munich-quantum-toolkit/core/pull/2227 [#2228]: https://github.com/munich-quantum-toolkit/core/pull/2228 [#2224]: https://github.com/munich-quantum-toolkit/core/pull/2224 [#2220]: https://github.com/munich-quantum-toolkit/core/pull/2220 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 18b71781f0..5f47c18c9c 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -20,6 +20,7 @@ #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/bench/Generate.h" #include "qdmi/Client.hpp" +#include "qdmi/ProgramFormat.hpp" #include "qdmi/driver/SessionConfig.hpp" #include "qiskit/Qiskit.h" @@ -39,6 +40,7 @@ #include #include #include +#include #include #include @@ -190,6 +192,31 @@ template baseUrl, + std::optional token, + std::optional authFile, + std::optional authUrl, std::optional username, + std::optional password, + std::optional deviceConfig, + std::optional deviceConfigFile, + std::optional custom1, std::optional custom2, + std::optional custom3, std::optional custom4, + std::optional custom5) { + /// Validate before crossing the extension boundary for consistent ValueError. + if (deviceConfig && deviceConfigFile) { + throw nb::value_error( + "device_config and device_config_file are mutually exclusive"); + } + const auto overrides = qdmi::makeDeviceSessionConfig( + std::move(baseUrl), std::move(token), std::move(authFile), + std::move(authUrl), std::move(username), std::move(password), + std::move(deviceConfig), std::move(deviceConfigFile), std::move(custom1), + std::move(custom2), std::move(custom3), std::move(custom4), + std::move(custom5)); + return qdmi::Session::openDevice(deviceId, overrides); +} + template [[nodiscard]] static ProgramType copiedOrConsumed(ProgramType& program, const bool copy) { @@ -918,21 +945,13 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::optional custom3, std::optional custom4, std::optional custom5) { - // Keep this preflight at the Python boundary so the public - // ValueError does not depend on cross-extension exception - // translation. - if (deviceConfig && deviceConfigFile) { - throw nb::value_error( - "device_config and device_config_file are mutually " - "exclusive"); - } - const auto overrides = qdmi::makeDeviceSessionConfig( - std::move(baseUrl), std::move(token), std::move(authFile), - std::move(authUrl), std::move(username), std::move(password), - std::move(deviceConfig), std::move(deviceConfigFile), - std::move(custom1), std::move(custom2), std::move(custom3), - std::move(custom4), std::move(custom5)); - auto device = qdmi::Session::openDevice(deviceId, overrides); + auto device = openQDMIDevice( + deviceId, std::move(baseUrl), std::move(token), + std::move(authFile), std::move(authUrl), std::move(username), + std::move(password), std::move(deviceConfig), + std::move(deviceConfigFile), std::move(custom1), + std::move(custom2), std::move(custom3), std::move(custom4), + std::move(custom5)); return takeResult(mlir::compilerTargetFromDevice(device)); }, "device_id"_a, nb::kw_only(), "base_url"_a = std::nullopt, @@ -1010,6 +1029,51 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); "A compiler target and its selected payload specification.") .def(nb::init(), "target"_a, "payload_specification"_a) + .def_static( + "from_device", + [](const qdmi::Device& device, + const QDMI_Program_Format& programFormat) { + return takeResult( + mlir::targetEnvironmentFromDevice(device, programFormat)); + }, + "device"_a, "program_format"_a, + "Snapshot a QDMI device and one accepted payload.") + .def_static( + "from_device_id", + [](const std::string& deviceId, + const QDMI_Program_Format& programFormat, + std::optional baseUrl, + std::optional token, + std::optional authFile, + std::optional authUrl, + std::optional username, + std::optional password, + std::optional deviceConfig, + std::optional deviceConfigFile, + std::optional custom1, + std::optional custom2, + std::optional custom3, + std::optional custom4, + std::optional custom5) { + auto device = openQDMIDevice( + deviceId, std::move(baseUrl), std::move(token), + std::move(authFile), std::move(authUrl), std::move(username), + std::move(password), std::move(deviceConfig), + std::move(deviceConfigFile), std::move(custom1), + std::move(custom2), std::move(custom3), std::move(custom4), + std::move(custom5)); + return takeResult( + mlir::targetEnvironmentFromDevice(device, programFormat)); + }, + "device_id"_a, "program_format"_a, nb::kw_only(), + "base_url"_a = std::nullopt, "token"_a = std::nullopt, + "auth_file"_a = std::nullopt, "auth_url"_a = std::nullopt, + "username"_a = std::nullopt, "password"_a = std::nullopt, + "device_config"_a = std::nullopt, + "device_config_file"_a = std::nullopt, "custom1"_a = std::nullopt, + "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, + "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, + "Open a registered device and snapshot one accepted payload.") .def_prop_ro("target", &mlir::TargetEnvironment::target, "The compiler target.") .def_prop_ro("payload_specification", diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 66c3b79f73..9d710a4aeb 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -15,34 +15,27 @@ Open a configured QDMI device and snapshot it as a compiler target: ```python from mqt.core.mlir import ( - CompilerTarget, - PayloadFormat, - PayloadEncoding, - PayloadSpecification, TargetEnvironment, compile_program, ) +from mqt.core.qdmi import ProgramFormat -target = CompilerTarget.from_device_id("mqt.sc.iqm.garnet") -payload = PayloadSpecification(PayloadFormat("qir", "2.1.0", "base", PayloadEncoding.BINARY)) -environment = TargetEnvironment(target, payload) +environment = TargetEnvironment.from_device_id( + "mqt.sc.iqm.garnet", + ProgramFormat.QIR21_BASE_BINARY, +) compiled = compile_program( "bell.qasm", target_environment=environment, ) ``` -The payload specification identifies the exact representation selected for the -device. MQT Core derives the compiler output from that specification and uses +The QDMI adapter checks that the device accepts the exact program format. It +groups program-feature records by ID and value, adds the selected format's +normative baseline, and preserves whether the optional feature list is known. +MQT Core derives the compiler output from this payload specification and uses the canonical QCO pipeline. The targeted overload therefore accepts one -`TargetEnvironment` and no independent output or custom pipeline. MQT Core's -QDMI adapter does not yet translate QDMI program-format and feature metadata, so -callers must construct the payload specification from the device documentation. - -The example has no reported execution capabilities. A producer must add every -effective capability, including the selected format's baseline. Set -`optional_capabilities_known=True` only when the producer also knows that the -list contains every optional device capability. +`TargetEnvironment` and no independent output or custom pipeline. The target can also be constructed directly. Connectivity and native-operation support are required: @@ -157,35 +150,24 @@ device ID and the compiler-owned target: ```cpp #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Programs.h" -#include "mlir/Compiler/TargetEnvironment.h" +#include "qdmi/ProgramFormat.hpp" #include #include -auto target = mlir::compilerTargetFromDeviceId("mqt.sc.iqm.garnet"); -if (!target) { - llvm::errs() << "Failed to create compiler target: " - << llvm::toString(target.takeError()) << '\n'; - return 1; -} - -auto payload = mlir::PayloadSpecification::create({ - .id = "qir", - .version = "2.1.0", - .profile = "base", - .encoding = mlir::PayloadEncoding::Binary, -}); -if (!payload) { - llvm::errs() << llvm::toString(payload.takeError()) << '\n'; +auto environment = mlir::targetEnvironmentFromDeviceId( + "mqt.sc.iqm.garnet", qdmi::QIR21_BASE_BINARY); +if (!environment) { + llvm::errs() << "Failed to create target environment: " + << llvm::toString(environment.takeError()) << '\n'; return 1; } -mlir::TargetEnvironment environment(*target, *payload); auto qc = mlir::QCProgram::fromQASMFile("input.qasm"); if (!qc) { return 1; } auto qco = std::move(*qc).intoQCO(); -if (!qco || !qco->compileForTarget(environment)) { +if (!qco || !qco->compileForTarget(*environment)) { return 1; } ``` diff --git a/docs/qdmi/ddsim_device.md b/docs/qdmi/ddsim_device.md index 03bf2b9199..14c92e0065 100644 --- a/docs/qdmi/ddsim_device.md +++ b/docs/qdmi/ddsim_device.md @@ -59,10 +59,6 @@ program to QIR, and submit the resulting bitcode to the same device: ```python from mqt.core.mlir import ( - CompilerTarget, - PayloadFormat, - PayloadEncoding, - PayloadSpecification, TargetEnvironment, compile_program, ) @@ -70,11 +66,10 @@ from mqt.core.qdmi import ProgramFormat from mqt.core.qdmi.driver import open_device device = open_device("mqt.ddsim.default") -target = CompilerTarget.from_device(device) -payload = PayloadSpecification(PayloadFormat("qir", "2.1.0", "base", PayloadEncoding.BINARY)) +environment = TargetEnvironment.from_device(device, ProgramFormat.QIR21_BASE_BINARY) program = compile_program( "bell.qasm", - target_environment=TargetEnvironment(target, payload), + target_environment=environment, ) job = device.submit_job( diff --git a/mlir/include/mlir/Compiler/QDMIAdapter.h b/mlir/include/mlir/Compiler/QDMIAdapter.h index ab3577e6b0..661f238c89 100644 --- a/mlir/include/mlir/Compiler/QDMIAdapter.h +++ b/mlir/include/mlir/Compiler/QDMIAdapter.h @@ -11,8 +11,10 @@ #pragma once #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.h" #include +#include #include #include @@ -46,6 +48,28 @@ compilerTargetFromDevice(const qdmi::Device& device); [[nodiscard]] llvm::Expected compilerTargetFromDeviceId(std::string_view deviceId); +/** + * @brief Snapshot a QDMI device and one accepted payload as a target + * environment. + * + * @details The adapter preserves the exact program format, groups feature + * records with the same ID and value, and adds the normative baseline of a + * standard payload. Unknown optional feature metadata remains unknown. + */ +[[nodiscard]] llvm::Expected +targetEnvironmentFromDevice(const qdmi::Device& device, + const QDMI_Program_Format& format); + +/** + * @brief Open a registered QDMI device and snapshot one accepted payload. + * + * @details This adapter contains exceptions from the QDMI C++ API and returns + * them as LLVM errors. The returned environment owns all queried metadata. + */ +[[nodiscard]] llvm::Expected +targetEnvironmentFromDeviceId(std::string_view deviceId, + const QDMI_Program_Format& format); + /** * @brief List the stable IDs of registered QDMI devices. * diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index 98f495c135..6698591bb3 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -11,19 +11,24 @@ #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "qdmi/Client.hpp" +#include "qdmi/ProgramFormat.hpp" #include "qdmi/driver/Driver.hpp" #include #include #include #include +#include #include #include #include #include +#include #include +#include #include #include #include @@ -471,6 +476,154 @@ snapshotCompilerTarget(const qdmi::Device& device) { std::move(*durationUnit)); } +[[nodiscard]] static llvm::Error +invalidFeatureGroup(const std::string_view id, const uint64_t value, + const llvm::StringRef detail) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + llvm::Twine("Invalid QDMI program feature group '") + id + "' value " + + llvm::Twine(value) + ": " + detail); +} + +[[nodiscard]] static llvm::Expected> +groupProgramFeatures(const std::vector& features) { + struct FeatureGroup { + ProgramCapability capability; + bool unrestricted = false; + }; + + std::vector groups; + std::map, size_t> groupIndices; + groups.reserve(features.size()); + + for (const auto& feature : features) { + if (!qdmi::isValidProgramFeature(feature)) { + return invalidFeatureGroup("", feature.value, + "record fields are not canonical"); + } + const std::string id{std::data(feature.id)}; + const auto key = std::pair{id, feature.value}; + const auto [position, inserted] = + groupIndices.try_emplace(key, groups.size()); + if (inserted) { + groups.push_back({.capability = {.id = id, .value = feature.value}}); + } + auto& group = groups[position->second]; + const std::string constraintId{std::data(feature.constraint_id)}; + if (constraintId.empty()) { + if (!inserted || !group.capability.constraints.empty()) { + return invalidFeatureGroup( + id, feature.value, + "an unrestricted group must contain exactly one record"); + } + group.unrestricted = true; + continue; + } + if (group.unrestricted) { + return invalidFeatureGroup( + id, feature.value, + "an unrestricted record cannot have constrained siblings"); + } + if (std::ranges::any_of(group.capability.constraints, + [&](const ProgramConstraint& constraint) { + return constraint.id == constraintId; + })) { + return invalidFeatureGroup(id, feature.value, + "constraint IDs must be unique"); + } + group.capability.constraints.push_back( + {.id = constraintId, .value = feature.constraint_value}); + } + + std::vector capabilities; + capabilities.reserve(groups.size()); + std::ranges::transform( + groups, std::back_inserter(capabilities), + [](FeatureGroup& group) { return std::move(group.capability); }); + return capabilities; +} + +[[nodiscard]] static std::vector +payloadBaseline(const PayloadFormat& format) { + if (format.id != "qir" || format.version != "2.1.0" || + format.profile != "adaptive") { + return {}; + } + return {{.id = QDMI_PROGRAM_FEATURE_MID_CIRCUIT_MEASUREMENT}, + {.id = QDMI_PROGRAM_FEATURE_MEASURED_QUBIT_REUSE}, + {.id = QDMI_PROGRAM_FEATURE_MEASUREMENT_RESULT_USE}, + {.id = QDMI_PROGRAM_FEATURE_BOOLEAN_COMPUTATION}, + {.id = QDMI_PROGRAM_FEATURE_FORWARD_BRANCHING}}; +} + +[[nodiscard]] static llvm::Expected +snapshotPayloadSpecification(const qdmi::Device& device, + const QDMI_Program_Format& format) { + if (!qdmi::isValidProgramFormat(format)) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "Invalid QDMI program format: fields are not canonical"); + } + const auto supported = device.getSupportedProgramFormats(); + if (std::ranges::none_of(supported, [&](const auto& candidate) { + return qdmi::equal(candidate, format); + })) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "QDMI device does not accept the selected program format"); + } + + auto encoding = PayloadEncoding::Text; + switch (format.encoding) { + case QDMI_PROGRAM_ENCODING_TEXT: + encoding = PayloadEncoding::Text; + break; + case QDMI_PROGRAM_ENCODING_BINARY: + encoding = PayloadEncoding::Binary; + break; + default: + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "Invalid QDMI program format encoding"); + } + PayloadFormat payloadFormat{ + .id = std::data(format.id), + .version = std::to_string(QDMI_VERSION_MAJOR(format.version)) + "." + + std::to_string(QDMI_VERSION_MINOR(format.version)) + "." + + std::to_string(QDMI_VERSION_PATCH(format.version)), + .profile = std::data(format.profile), + .encoding = encoding}; + + auto capabilities = payloadBaseline(payloadFormat); + const auto optionalFeatures = device.tryGetProgramFeatures(format); + if (optionalFeatures) { + auto optionalCapabilities = groupProgramFeatures(*optionalFeatures); + if (!optionalCapabilities) { + return optionalCapabilities.takeError(); + } + capabilities.insert(capabilities.end(), + std::make_move_iterator(optionalCapabilities->begin()), + std::make_move_iterator(optionalCapabilities->end())); + } + return PayloadSpecification::create(std::move(payloadFormat), + std::move(capabilities), + optionalFeatures.has_value()); +} + +[[nodiscard]] static llvm::Expected +snapshotTargetEnvironment(const qdmi::Device& device, + const QDMI_Program_Format& format) { + auto target = snapshotCompilerTarget(device); + if (!target) { + return target.takeError(); + } + auto payload = snapshotPayloadSpecification(device, format); + if (!payload) { + return payload.takeError(); + } + return TargetEnvironment(*target, std::move(*payload)); +} + [[nodiscard]] static llvm::Error qdmiError(const llvm::Twine& action, const char* const detail) { return llvm::createStringError(std::make_error_code(std::errc::io_error), @@ -508,6 +661,30 @@ compilerTargetFromDeviceId(const std::string_view deviceId) { } } +llvm::Expected +targetEnvironmentFromDevice(const qdmi::Device& device, + const QDMI_Program_Format& format) { + try { + return snapshotTargetEnvironment(device, format); + } catch (...) { + return qdmiError("Failed to query QDMI device and payload", + std::current_exception()); + } +} + +llvm::Expected +targetEnvironmentFromDeviceId(const std::string_view deviceId, + const QDMI_Program_Format& format) { + const auto action = std::string("Failed to open or query QDMI device '") + + std::string(deviceId) + "' and payload"; + try { + return snapshotTargetEnvironment(qdmi::Session::openDevice(deviceId), + format); + } catch (...) { + return qdmiError(action, std::current_exception()); + } +} + llvm::Expected> registeredQDMIDeviceIds() { try { return qdmi::Driver::get().registeredDeviceIds(); diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 73ea2af25f..7e505412d7 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -10,7 +10,9 @@ #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "qdmi/Client.hpp" +#include "qdmi/ProgramFormat.hpp" #include "qdmi/driver/Driver.hpp" #include @@ -18,6 +20,7 @@ #include #include #include +#include #include #include @@ -28,6 +31,18 @@ using mlir::CompilerTarget; +[[nodiscard]] static const mlir::ProgramCapability& +findCapability(const mlir::PayloadSpecification& payload, + const llvm::StringRef id) { + const auto* const found = + llvm::find_if(payload.capabilities(), [&](const auto& capability) { + return capability.id == id; + }); + assert(found != payload.capabilities().end() && + "Payload capability not found"); + return *found; +} + [[nodiscard]] static const CompilerTarget::Operation& findOperation(const CompilerTarget& target, const llvm::StringRef name) { const auto* const found = @@ -134,6 +149,65 @@ TEST(CompilerQDMIAdapterTest, InfersDDSIMTargetFacts) { EXPECT_EQ(target.supportsOperation("barrier", 0, 0), false); } +TEST(CompilerQDMIAdapterTest, SnapshotsExactPayloadAndFeatureGroups) { + const auto device = qdmi::Session::openDevice("mqt.ddsim.default"); + const auto environment = llvm::cantFail( + mlir::targetEnvironmentFromDevice(device, qdmi::OPENQASM3)); + const auto& payload = environment.payloadSpecification(); + + EXPECT_EQ(payload.format(), + (mlir::PayloadFormat{.id = "openqasm", + .version = "3.0.0", + .profile = "", + .encoding = mlir::PayloadEncoding::Text})); + EXPECT_TRUE(payload.optionalCapabilitiesKnown()); + ASSERT_EQ(payload.capabilities().size(), 5); + for (const llvm::StringRef id : {QDMI_PROGRAM_FEATURE_MID_CIRCUIT_MEASUREMENT, + QDMI_PROGRAM_FEATURE_MEASURED_QUBIT_REUSE, + QDMI_PROGRAM_FEATURE_MEASUREMENT_RESULT_USE, + QDMI_PROGRAM_FEATURE_BOOLEAN_COMPUTATION, + QDMI_PROGRAM_FEATURE_FORWARD_BRANCHING}) { + const auto& capability = findCapability(payload, id); + EXPECT_EQ(capability.value, 0); + EXPECT_TRUE(capability.constraints.empty()); + } +} + +TEST(CompilerQDMIAdapterTest, AddsQIRAdaptiveBaselineOnce) { + const auto environment = llvm::cantFail(mlir::targetEnvironmentFromDeviceId( + "mqt.ddsim.default", qdmi::QIR21_ADAPTIVE_BINARY)); + const auto& payload = environment.payloadSpecification(); + + EXPECT_EQ(payload.format(), + (mlir::PayloadFormat{.id = "qir", + .version = "2.1.0", + .profile = "adaptive", + .encoding = mlir::PayloadEncoding::Binary})); + EXPECT_TRUE(payload.optionalCapabilitiesKnown()); + ASSERT_EQ(payload.capabilities().size(), 5); + for (const llvm::StringRef id : {QDMI_PROGRAM_FEATURE_MID_CIRCUIT_MEASUREMENT, + QDMI_PROGRAM_FEATURE_MEASURED_QUBIT_REUSE, + QDMI_PROGRAM_FEATURE_MEASUREMENT_RESULT_USE, + QDMI_PROGRAM_FEATURE_BOOLEAN_COMPUTATION, + QDMI_PROGRAM_FEATURE_FORWARD_BRANCHING}) { + EXPECT_TRUE(findCapability(payload, id).constraints.empty()); + } +} + +TEST(CompilerQDMIAdapterTest, RejectsPayloadNotAcceptedByDevice) { + constexpr QDMI_Program_Format unsupported{ + .version = QDMI_MAKE_VERSION(3, 1, 0), + .encoding = QDMI_PROGRAM_ENCODING_TEXT, + .id = "openqasm", + .profile = ""}; + const auto device = qdmi::Session::openDevice("mqt.ddsim.default"); + auto environment = mlir::targetEnvironmentFromDevice(device, unsupported); + + ASSERT_FALSE(environment); + EXPECT_NE(llvm::toString(environment.takeError()).find("does not accept"), + std::string::npos); +} + TEST(CompilerQDMIAdapterTest, ListsRegisteredDeviceIds) { const auto deviceIds = llvm::cantFail(mlir::registeredQDMIDeviceIds()); EXPECT_TRUE(llvm::is_contained(deviceIds, "mqt.ddsim.default")); diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 4141280c5a..254bc58f2d 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -17,6 +17,7 @@ import numpy as np import qiskit.circuit import mqt.core.dd +import mqt.core.qdmi from mqt.core.qdmi import Device from mqt.core.typing import QDMISessionParameters @@ -475,6 +476,31 @@ class TargetEnvironment: """A compiler target and its selected payload specification.""" def __init__(self, target: CompilerTarget, payload_specification: PayloadSpecification) -> None: ... + @staticmethod + def from_device(device: mqt.core.qdmi.Device, program_format: mqt.core.qdmi.ProgramFormat) -> TargetEnvironment: + """Snapshot a QDMI device and one accepted payload.""" + + @staticmethod + def from_device_id( + device_id: str, + program_format: mqt.core.qdmi.ProgramFormat, + *, + base_url: str | None = None, + token: str | None = None, + auth_file: str | os.PathLike | None = None, + auth_url: str | None = None, + username: str | None = None, + password: str | None = None, + device_config: str | None = None, + device_config_file: str | os.PathLike | None = None, + custom1: str | None = None, + custom2: str | None = None, + custom3: str | None = None, + custom4: str | None = None, + custom5: str | None = None, + ) -> TargetEnvironment: + """Open a registered device and snapshot one accepted payload.""" + @property def target(self) -> CompilerTarget: """The compiler target.""" diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index d43d955755..fe2eed2f1e 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -39,6 +39,7 @@ TargetEnvironment, compile_program, ) +from mqt.core.qdmi import ProgramFormat from mqt.core.qdmi.driver import open_device requires_qiskit_translation = pytest.mark.skipif( @@ -722,6 +723,41 @@ def test_compiler_target_from_device_id_preserves_open_and_conversion_errors() - ) +def test_target_environment_from_device_groups_exact_payload_features() -> None: + """QDMI conversion preserves the exact format and complete feature groups.""" + environment = TargetEnvironment.from_device(open_device("mqt.ddsim.default"), ProgramFormat.OPENQASM3) + payload = environment.payload_specification + + assert payload.format.format_id == "openqasm" + assert payload.format.version == "3.0.0" + assert not payload.format.profile + assert payload.format.encoding == PayloadEncoding.TEXT + assert payload.optional_capabilities_known + assert {capability.capability_id for capability in payload.capabilities} == { + "mid-circuit-measurement", + "measured-qubit-reuse", + "measurement-result-use", + "boolean-computation", + "forward-branching", + } + assert all(not capability.constraints for capability in payload.capabilities) + + +def test_target_environment_from_device_id_adds_adaptive_baseline() -> None: + """Stable-ID conversion adds the QIR Adaptive normative baseline once.""" + environment = TargetEnvironment.from_device_id( + "mqt.ddsim.default", ProgramFormat.QIR21_ADAPTIVE_BINARY, custom1="value" + ) + payload = environment.payload_specification + + assert payload.format.format_id == "qir" + assert payload.format.version == "2.1.0" + assert payload.format.profile == "adaptive" + assert payload.format.encoding == PayloadEncoding.BINARY + assert payload.optional_capabilities_known + assert len(payload.capabilities) == 5 + + def test_qco_program_runs_textual_pipeline() -> None: """Run registered QCO passes through MLIR textual pipeline syntax.""" qco = compile_program(QASM_STRING, output=OutputFormat.QCO) From f109042d88cbbe4c374fe863d9b0c47205fd70cd Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 24 Aug 2026 17:25:47 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20Cover=20QDMI=20target=20adapter?= =?UTF-8?q?=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercise constrained and malformed feature groups, invalid descriptors, snapshot propagation, and exception containment through the public adapter entry points. Remove duplicate validation branches that cannot run after the QDMI C++ client validates provider records and encodings. Assisted-by: GPT-5.6 Sol via Codex --- .../plans/qdmi-target-environment-adapter.md | 144 +++------- CHANGELOG.md | 2 +- bindings/mlir/register_mlir.cpp | 1 - docs/mlir/target_compilation.md | 4 +- mlir/include/mlir/Compiler/QDMIAdapter.h | 62 ++-- mlir/lib/Compiler/QDMIAdapter.cpp | 35 +-- .../Compiler/test_compiler_qdmi_adapter.cpp | 267 +++++++++++++++++- test/python/test_mlir.py | 22 ++ 8 files changed, 351 insertions(+), 186 deletions(-) diff --git a/.agent/plans/qdmi-target-environment-adapter.md b/.agent/plans/qdmi-target-environment-adapter.md index 3c4125d918..0812cce3fe 100644 --- a/.agent/plans/qdmi-target-environment-adapter.md +++ b/.agent/plans/qdmi-target-environment-adapter.md @@ -1,120 +1,46 @@ -# Build a target environment from one QDMI payload +# QDMI-to-compiler program-capability adapter -This ExecPlan is a living document. The sections `Progress`, -`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must -be kept up to date as work proceeds. +Status: locally validated, design-gated prototype. -This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the -repository root. +## Scope and decisions -## Purpose / Big Picture +Core PR `#2227` snapshots an open QDMI device and an exact accepted program +format into an owning compiler target environment. The runtime descriptors and +feature query belong to Core PR `#2226`; the detached compiler model belongs to +Core PR `#2219`. Neither foundation depends on this adapter or the other +foundation. A temporary integration base combines them only for testing. -Compiler users must not reconstruct payload facts from a format name. After this -change, one C++ or Python factory snapshots a QDMI device and an exact accepted -descriptor into a detached `TargetEnvironment`. The result contains both the -hardware target and the selected payload capabilities. +The adapter preserves format identity and encoding, grouped optional features, +and the prototype's standard-format baseline. Unknown optional feature metadata +remains distinct from an empty complete list. Unknown topology or operation +support still fails during target inference; existing simulator control families +and zero-arity global phase are unchanged. -## Progress +Bindings reuse one session-opening helper with configuration validation at the +Python boundary. The resulting environment remains valid after the device +session closes. Tests use explicit known topology in their fake provider. -- [x] (2026-08-24 06:25Z) Added C++ conversion for an open device and a stable - device ID. -- [x] (2026-08-24 06:25Z) Added Python `TargetEnvironment.from_device` and - `from_device_id` factories with session overrides. -- [x] (2026-08-24 06:25Z) Added exact-format, grouped-feature, QIR baseline, and - rejection tests. -- [x] (2026-08-24 14:30Z) Passed all 10 focused C++ adapter tests and both - focused Python factory tests on the final stack. -- [x] (2026-08-24 14:30Z) Passed the release build, all 4,073 configured CTest - cases, and stub generation on the final stack. +## Design and release boundary -## Surprises & Discoveries +QDMI issue `#523` and Core issue `#2365` must settle program-capability +semantics before this prototype is merge-ready. It is a non-blocking Core 4.1 +candidate, never a Core 4.0 dependency. Native multi-program jobs, driver +replacement, metadata removal, and compiler control-flow passes remain +independent. Retarget the adapter to the normal development branch after both +foundations land. Release artifacts require released dependencies. -- Observation: QDMI reports one constrained feature group as several records - with the same feature ID and value. Evidence: each record contains one - `constraint_id` and `constraint_value` pair. -- Observation: standard descriptor baselines are implicit. Evidence: QIR 2.1 - Adaptive guarantees five control-flow features even when the optional list is - empty. +## Validation -## Decision Log +Run the release CTest suite, Python compiler/QDMI/SDK tests, generated stubs, +repository lint and C++ lint. Cover exact-format rejection, malformed grouped +features, optional metadata, QIR baselines, snapshot lifetime and error +translation. Exercise the documented DDSIM compilation/submission path. -- Decision: Require the selected descriptor to equal one device-supported value. - Rationale: compilation must not claim a payload the device rejects. - Date/Author: 2026-08-24 / GPT-5.6 Sol via Codex. -- Decision: Group records by feature ID and value and preserve constraints. - Rationale: different values are alternatives, while constraints in one group - are conjunctive. Date/Author: 2026-08-24 / GPT-5.6 Sol via Codex. -- Decision: Add the QIR Adaptive baseline in the adapter and keep optional-set - completeness separate. Rationale: QDMI makes baseline facts implicit. - Date/Author: 2026-08-24 / GPT-5.6 Sol via Codex. +Local validation passed 3,891 native tests with one existing skip and 454 Python +compiler/QDMI/SDK tests, including DDSIM bitcode submission. Generated stubs, +repository lint and C++ lint passed. Hosted CI and contract review remain +separate gates. -## Outcomes & Retrospective - -The implementation is complete and remains detached from a live QDMI session. -All 10 focused C++ adapter tests and both focused Python factory tests pass. The -complete stack passes the release build, all 4,073 configured CTest cases, stub -generation, documentation, and lint. The preceding atomic QDMI migration layer -also passes its complete validation set. - -## Context and Orientation - -`mlir::TargetEnvironment` combines a `CompilerTarget` with a validated -`PayloadSpecification`. Its definitions live in -`mlir/include/mlir/Compiler/TargetEnvironment.h`. The QDMI compatibility -boundary is `mlir/include/mlir/Compiler/QDMIAdapter.h` with its implementation -in `mlir/lib/Compiler/QDMIAdapter.cpp`. Python bindings are in -`bindings/mlir/register_mlir.cpp`. - -## Plan of Work - -Extend the existing QDMI adapter with factories for an open `qdmi::Device` and a -registered device ID. Validate the exact descriptor, convert its version and -encoding, group optional feature records, add the normative baseline, and -preserve whether optional metadata is complete. Snapshot the existing compiler -target and payload into one owning value. Bind both factories in Python, -regenerate `python/mqt/core/mlir.pyi`, and update compiler and DDSIM examples. - -## Concrete Steps - -Run from the repository root: - - cmake --build --preset release --target mqt-core-mlir-unittests-compiler - ./build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler \ - --gtest_filter='CompilerQDMIAdapterTest.*' - uvx nox -s stubs - uvx nox -s tests-3.12 -- test/python/test_mlir.py \ - -k target_environment_from_device - uvx nox --non-interactive -s docs - uvx nox -s lint - -## Validation and Acceptance - -The factories reject a canonical descriptor that the device does not accept. -They preserve ID, canonical version, profile, and encoding. Records with the -same feature ID and value become one capability with all constraints. QIR 2.1 -Adaptive contains each baseline capability once. A successful empty optional -query is known; `NOTSUPPORTED` remains unknown. The returned environment works -after the originating device is destroyed. - -## Idempotence and Recovery - -All commands are repeatable. Stub generation is the only authorized way to -change `python/mqt/core/mlir.pyi`. No step changes remote state. - -## Artifacts and Notes - -The adapter is stacked above the low-level QDMI 1.4 contract. It does not add a -second loader, registry, or payload inference path. - -## Interfaces and Dependencies - -The final C++ functions are -`targetEnvironmentFromDevice(const qdmi::Device&, const QDMI_Program_Format&)` -and -`targetEnvironmentFromDeviceId(std::string_view, const QDMI_Program_Format&)`. -Python exposes matching static factories. This layer requires the exact -descriptors and feature query from the preceding QDMI layer and -`TargetEnvironment` from the selected-payload compiler layer. - -Plan update, 2026-08-24: Split this compiler adapter from the atomic QDMI -producer-and-consumer migration. +The source is in `mlir/Compiler/QDMIAdapter`, its unit tests and +`bindings/mlir/register_mlir.cpp`; canonical usage is documented in +`docs/mlir/target_compilation.md`. diff --git a/CHANGELOG.md b/CHANGELOG.md index dfba587f91..2d78637123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -912,8 +912,8 @@ for previous changelogs._ [#2246]: https://github.com/munich-quantum-toolkit/core/pull/2246 [#2240]: https://github.com/munich-quantum-toolkit/core/pull/2240 [#2232]: https://github.com/munich-quantum-toolkit/core/pull/2232 -[#2227]: https://github.com/munich-quantum-toolkit/core/pull/2227 [#2228]: https://github.com/munich-quantum-toolkit/core/pull/2228 +[#2227]: https://github.com/munich-quantum-toolkit/core/pull/2227 [#2224]: https://github.com/munich-quantum-toolkit/core/pull/2224 [#2220]: https://github.com/munich-quantum-toolkit/core/pull/2220 [#2219]: https://github.com/munich-quantum-toolkit/core/pull/2219 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 5f47c18c9c..d90dda619c 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -20,7 +20,6 @@ #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/bench/Generate.h" #include "qdmi/Client.hpp" -#include "qdmi/ProgramFormat.hpp" #include "qdmi/driver/SessionConfig.hpp" #include "qiskit/Qiskit.h" diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 9d710a4aeb..92b8d1f148 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -21,7 +21,7 @@ from mqt.core.mlir import ( from mqt.core.qdmi import ProgramFormat environment = TargetEnvironment.from_device_id( - "mqt.sc.iqm.garnet", + "mqt.ddsim.default", ProgramFormat.QIR21_BASE_BINARY, ) compiled = compile_program( @@ -155,7 +155,7 @@ device ID and the compiler-owned target: #include auto environment = mlir::targetEnvironmentFromDeviceId( - "mqt.sc.iqm.garnet", qdmi::QIR21_BASE_BINARY); + "mqt.ddsim.default", qdmi::QIR21_BASE_BINARY); if (!environment) { llvm::errs() << "Failed to create target environment: " << llvm::toString(environment.takeError()) << '\n'; diff --git a/mlir/include/mlir/Compiler/QDMIAdapter.h b/mlir/include/mlir/Compiler/QDMIAdapter.h index 661f238c89..a169098851 100644 --- a/mlir/include/mlir/Compiler/QDMIAdapter.h +++ b/mlir/include/mlir/Compiler/QDMIAdapter.h @@ -26,56 +26,46 @@ class Device; namespace mlir { -/** - * @brief Snapshot a circuit-model QDMI device as an MLIR compiler target. - * - * @details The returned target owns all queried metadata and remains valid - * after the originating device and session have been destroyed. Neutral-atom - * zone models are not supported. Explicit QDMI site lists must cover every - * site for one-qubit operations, every undirected topology edge for two-qubit - * operations, and every ordered tuple of distinct sites for higher arities. - * Each supported ordered placement carries optional calibration data. - */ +/// Snapshot a circuit-model QDMI device as an MLIR compiler target. +/// +/// The returned target owns all queried metadata and remains valid +/// after the originating device and session have been destroyed. Neutral-atom +/// zone models are not supported. Explicit QDMI site lists must cover every +/// site for one-qubit operations, every undirected topology edge for two-qubit +/// operations, and every ordered tuple of distinct sites for higher arities. +/// Each supported ordered placement carries optional calibration data. [[nodiscard]] llvm::Expected compilerTargetFromDevice(const qdmi::Device& device); -/** - * @brief Open a registered QDMI device and snapshot it as a compiler target. - * - * @details This adapter contains exceptions from the QDMI C++ API and returns - * them as LLVM errors. The returned target owns all queried metadata. - */ +/// Open a registered QDMI device and snapshot it as a compiler target. +/// +/// This adapter contains exceptions from the QDMI C++ API and returns +/// them as LLVM errors. The returned target owns all queried metadata. [[nodiscard]] llvm::Expected compilerTargetFromDeviceId(std::string_view deviceId); -/** - * @brief Snapshot a QDMI device and one accepted payload as a target - * environment. - * - * @details The adapter preserves the exact program format, groups feature - * records with the same ID and value, and adds the normative baseline of a - * standard payload. Unknown optional feature metadata remains unknown. - */ +/// Snapshot a QDMI device and one accepted payload as a target +/// environment. +/// +/// The adapter preserves the exact program format, groups feature +/// records with the same ID and value, and adds the normative baseline of a +/// standard payload. Unknown optional feature metadata remains unknown. [[nodiscard]] llvm::Expected targetEnvironmentFromDevice(const qdmi::Device& device, const QDMI_Program_Format& format); -/** - * @brief Open a registered QDMI device and snapshot one accepted payload. - * - * @details This adapter contains exceptions from the QDMI C++ API and returns - * them as LLVM errors. The returned environment owns all queried metadata. - */ +/// Open a registered QDMI device and snapshot one accepted payload. +/// +/// This adapter contains exceptions from the QDMI C++ API and returns +/// them as LLVM errors. The returned environment owns all queried metadata. [[nodiscard]] llvm::Expected targetEnvironmentFromDeviceId(std::string_view deviceId, const QDMI_Program_Format& format); -/** - * @brief List the stable IDs of registered QDMI devices. - * - * @details This adapter contains exceptions from QDMI registry discovery and - * returns them as LLVM errors. - */ +/// List the stable IDs of registered QDMI devices. +/// +/// This adapter contains exceptions from QDMI registry discovery and +/// returns them as LLVM errors. [[nodiscard]] llvm::Expected> registeredQDMIDeviceIds(); diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index 6698591bb3..fdeb6d13ab 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -497,10 +497,6 @@ groupProgramFeatures(const std::vector& features) { groups.reserve(features.size()); for (const auto& feature : features) { - if (!qdmi::isValidProgramFeature(feature)) { - return invalidFeatureGroup("", feature.value, - "record fields are not canonical"); - } const std::string id{std::data(feature.id)}; const auto key = std::pair{id, feature.value}; const auto [position, inserted] = @@ -549,11 +545,13 @@ payloadBaseline(const PayloadFormat& format) { format.profile != "adaptive") { return {}; } - return {{.id = QDMI_PROGRAM_FEATURE_MID_CIRCUIT_MEASUREMENT}, - {.id = QDMI_PROGRAM_FEATURE_MEASURED_QUBIT_REUSE}, - {.id = QDMI_PROGRAM_FEATURE_MEASUREMENT_RESULT_USE}, - {.id = QDMI_PROGRAM_FEATURE_BOOLEAN_COMPUTATION}, - {.id = QDMI_PROGRAM_FEATURE_FORWARD_BRANCHING}}; + return { + {.id = QDMI_PROGRAM_FEATURE_MID_CIRCUIT_MEASUREMENT}, + {.id = QDMI_PROGRAM_FEATURE_MEASURED_QUBIT_REUSE}, + {.id = QDMI_PROGRAM_FEATURE_MEASUREMENT_RESULT_USE}, + {.id = QDMI_PROGRAM_FEATURE_BOOLEAN_COMPUTATION}, + {.id = QDMI_PROGRAM_FEATURE_FORWARD_BRANCHING}, + }; } [[nodiscard]] static llvm::Expected @@ -573,26 +571,17 @@ snapshotPayloadSpecification(const qdmi::Device& device, "QDMI device does not accept the selected program format"); } - auto encoding = PayloadEncoding::Text; - switch (format.encoding) { - case QDMI_PROGRAM_ENCODING_TEXT: - encoding = PayloadEncoding::Text; - break; - case QDMI_PROGRAM_ENCODING_BINARY: - encoding = PayloadEncoding::Binary; - break; - default: - return llvm::createStringError( - std::make_error_code(std::errc::invalid_argument), - "Invalid QDMI program format encoding"); - } + const auto encoding = format.encoding == QDMI_PROGRAM_ENCODING_TEXT + ? PayloadEncoding::Text + : PayloadEncoding::Binary; PayloadFormat payloadFormat{ .id = std::data(format.id), .version = std::to_string(QDMI_VERSION_MAJOR(format.version)) + "." + std::to_string(QDMI_VERSION_MINOR(format.version)) + "." + std::to_string(QDMI_VERSION_PATCH(format.version)), .profile = std::data(format.profile), - .encoding = encoding}; + .encoding = encoding, + }; auto capabilities = payloadBaseline(payloadFormat); const auto optionalFeatures = device.tryGetProgramFeatures(format); diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 7e505412d7..1f68cf9032 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -21,16 +21,155 @@ #include #include #include +#include #include +#include +#include #include +#include #include +#include #include +#include #include #include +namespace { using mlir::CompilerTarget; +class AdapterDeviceLibrary final : public qdmi::DeviceLibrary { + static inline AdapterDeviceLibrary* activeLibrary = nullptr; + + [[nodiscard]] static AdapterDeviceLibrary& + fromSession(QDMI_Device_Session session) { + return *reinterpret_cast(session); + } + + static auto copyValue(const void* source, const size_t requiredSize, + const size_t size, void* value, size_t* sizeRet) + -> int { + if (value != nullptr && size < requiredSize) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (value != nullptr && requiredSize != 0U) { + std::memcpy(value, source, requiredSize); + } + if (sizeRet != nullptr) { + *sizeRet = requiredSize; + } + return QDMI_SUCCESS; + } + + static auto alloc(QDMI_Device_Session* session) -> int { + if (session == nullptr || activeLibrary == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + *session = reinterpret_cast(activeLibrary); + return QDMI_SUCCESS; + } + + static auto init(QDMI_Device_Session session) -> int { + return session == nullptr ? QDMI_ERROR_INVALIDARGUMENT : QDMI_SUCCESS; + } + + static void free([[maybe_unused]] QDMI_Device_Session session) {} + + static auto queryDeviceProperty(QDMI_Device_Session session, + const QDMI_Device_Property property, + const size_t size, void* value, + size_t* sizeRet) -> int { + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + auto& library = fromSession(session); + switch (property) { + case QDMI_DEVICE_PROPERTY_NAME: { + static constexpr std::string_view DEVICE_NAME{"adapter-test"}; + return copyValue(DEVICE_NAME.data(), DEVICE_NAME.size() + 1U, size, value, + sizeRet); + } + case QDMI_DEVICE_PROPERTY_QUBITSNUM: { + constexpr size_t numQubits = 1U; + return copyValue(&numQubits, sizeof(numQubits), size, value, sizeRet); + } + case QDMI_DEVICE_PROPERTY_SITES: { + auto* const site = reinterpret_cast(&library); + return copyValue(static_cast(&site), sizeof(QDMI_Site), size, + value, sizeRet); + } + case QDMI_DEVICE_PROPERTY_COUPLINGMAP: + case QDMI_DEVICE_PROPERTY_OPERATIONS: + return copyValue(nullptr, 0U, size, value, sizeRet); + case QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS: + return copyValue(library.formats.data(), + library.formats.size() * sizeof(QDMI_Program_Format), + size, value, sizeRet); + default: + return QDMI_ERROR_NOTSUPPORTED; + } + } + + static auto + queryProgramFeatures(QDMI_Device_Session session, + [[maybe_unused]] const QDMI_Program_Format* format, + const size_t size, QDMI_Program_Feature* value, + size_t* sizeRet) -> int { + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + const auto& features = fromSession(session).features; + return copyValue(features.data(), + features.size() * sizeof(QDMI_Program_Feature), size, + value, sizeRet); + } + + static auto querySiteProperty(QDMI_Device_Session session, + [[maybe_unused]] QDMI_Site site, + const QDMI_Site_Property property, + const size_t size, void* value, size_t* sizeRet) + -> int { + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (property != QDMI_SITE_PROPERTY_INDEX) { + return QDMI_ERROR_NOTSUPPORTED; + } + constexpr size_t index = 0U; + return copyValue(&index, sizeof(index), size, value, sizeRet); + } + +public: + std::vector formats{qdmi::OPENQASM3}; + std::vector features; + + AdapterDeviceLibrary() { + assert(activeLibrary == nullptr); + activeLibrary = this; + device_session_alloc = alloc; + device_session_init = init; + device_session_free = free; + device_session_query_device_property = queryDeviceProperty; + device_session_query_program_features = queryProgramFeatures; + device_session_query_site_property = querySiteProperty; + } + + ~AdapterDeviceLibrary() override { + assert(activeLibrary == this); + activeLibrary = nullptr; + } +}; + +class CompilerQDMIPayloadAdapterTest : public testing::Test { +protected: + std::shared_ptr library_ = + std::make_shared(); + QDMI_Device_impl_d handle_{library_}; + qdmi::Device device_ = qdmi::Session::createSessionlessDevice(&handle_); +}; + +} // namespace + [[nodiscard]] static const mlir::ProgramCapability& findCapability(const mlir::PayloadSpecification& payload, const llvm::StringRef id) { @@ -162,11 +301,13 @@ TEST(CompilerQDMIAdapterTest, SnapshotsExactPayloadAndFeatureGroups) { .encoding = mlir::PayloadEncoding::Text})); EXPECT_TRUE(payload.optionalCapabilitiesKnown()); ASSERT_EQ(payload.capabilities().size(), 5); - for (const llvm::StringRef id : {QDMI_PROGRAM_FEATURE_MID_CIRCUIT_MEASUREMENT, - QDMI_PROGRAM_FEATURE_MEASURED_QUBIT_REUSE, - QDMI_PROGRAM_FEATURE_MEASUREMENT_RESULT_USE, - QDMI_PROGRAM_FEATURE_BOOLEAN_COMPUTATION, - QDMI_PROGRAM_FEATURE_FORWARD_BRANCHING}) { + for (const llvm::StringRef id : { + QDMI_PROGRAM_FEATURE_MID_CIRCUIT_MEASUREMENT, + QDMI_PROGRAM_FEATURE_MEASURED_QUBIT_REUSE, + QDMI_PROGRAM_FEATURE_MEASUREMENT_RESULT_USE, + QDMI_PROGRAM_FEATURE_BOOLEAN_COMPUTATION, + QDMI_PROGRAM_FEATURE_FORWARD_BRANCHING, + }) { const auto& capability = findCapability(payload, id); EXPECT_EQ(capability.value, 0); EXPECT_TRUE(capability.constraints.empty()); @@ -185,21 +326,111 @@ TEST(CompilerQDMIAdapterTest, AddsQIRAdaptiveBaselineOnce) { .encoding = mlir::PayloadEncoding::Binary})); EXPECT_TRUE(payload.optionalCapabilitiesKnown()); ASSERT_EQ(payload.capabilities().size(), 5); - for (const llvm::StringRef id : {QDMI_PROGRAM_FEATURE_MID_CIRCUIT_MEASUREMENT, - QDMI_PROGRAM_FEATURE_MEASURED_QUBIT_REUSE, - QDMI_PROGRAM_FEATURE_MEASUREMENT_RESULT_USE, - QDMI_PROGRAM_FEATURE_BOOLEAN_COMPUTATION, - QDMI_PROGRAM_FEATURE_FORWARD_BRANCHING}) { + for (const llvm::StringRef id : { + QDMI_PROGRAM_FEATURE_MID_CIRCUIT_MEASUREMENT, + QDMI_PROGRAM_FEATURE_MEASURED_QUBIT_REUSE, + QDMI_PROGRAM_FEATURE_MEASUREMENT_RESULT_USE, + QDMI_PROGRAM_FEATURE_BOOLEAN_COMPUTATION, + QDMI_PROGRAM_FEATURE_FORWARD_BRANCHING, + }) { EXPECT_TRUE(findCapability(payload, id).constraints.empty()); } } +TEST_F(CompilerQDMIPayloadAdapterTest, GroupsConstrainedFeatures) { + library_->features = { + { + .id = QDMI_PROGRAM_FEATURE_COUNTED_ITERATION, + .value = 0U, + .constraint_id = + QDMI_PROGRAM_CONSTRAINT_MAX_CONTROL_FLOW_NESTING_DEPTH, + .constraint_value = 3U, + }, + { + .id = QDMI_PROGRAM_FEATURE_COUNTED_ITERATION, + .value = 0U, + .constraint_id = QDMI_PROGRAM_CONSTRAINT_MAX_ITERATION_COUNT, + .constraint_value = 100U, + }, + }; + + const auto environment = llvm::cantFail( + mlir::targetEnvironmentFromDevice(device_, qdmi::OPENQASM3)); + const auto& capability = + findCapability(environment.payloadSpecification(), + QDMI_PROGRAM_FEATURE_COUNTED_ITERATION); + + EXPECT_EQ(capability.value, 0U); + EXPECT_EQ( + capability.constraints, + (std::vector{ + {.id = QDMI_PROGRAM_CONSTRAINT_MAX_CONTROL_FLOW_NESTING_DEPTH, + .value = 3U}, + {.id = QDMI_PROGRAM_CONSTRAINT_MAX_ITERATION_COUNT, .value = 100U}})); +} + +TEST_F(CompilerQDMIPayloadAdapterTest, RejectsInvalidFeatureGroups) { + const auto expectError = [&](std::vector features) { + library_->features = std::move(features); + auto environment = + mlir::targetEnvironmentFromDevice(device_, qdmi::OPENQASM3); + ASSERT_FALSE(environment); + llvm::consumeError(environment.takeError()); + }; + + expectError({ + QDMI_PROGRAM_FEATURE_UNCONSTRAINED(QDMI_PROGRAM_FEATURE_COUNTED_ITERATION, + 0U), + QDMI_PROGRAM_FEATURE_UNCONSTRAINED(QDMI_PROGRAM_FEATURE_COUNTED_ITERATION, + 0U), + }); + expectError({ + QDMI_PROGRAM_FEATURE_UNCONSTRAINED(QDMI_PROGRAM_FEATURE_COUNTED_ITERATION, + 0U), + { + .id = QDMI_PROGRAM_FEATURE_COUNTED_ITERATION, + .value = 0U, + .constraint_id = QDMI_PROGRAM_CONSTRAINT_MAX_ITERATION_COUNT, + .constraint_value = 10U, + }, + }); + expectError({ + { + .id = QDMI_PROGRAM_FEATURE_COUNTED_ITERATION, + .value = 0U, + .constraint_id = QDMI_PROGRAM_CONSTRAINT_MAX_ITERATION_COUNT, + .constraint_value = 10U, + }, + { + .id = QDMI_PROGRAM_FEATURE_COUNTED_ITERATION, + .value = 0U, + .constraint_id = QDMI_PROGRAM_CONSTRAINT_MAX_ITERATION_COUNT, + .constraint_value = 20U, + }, + }); +} + +TEST_F(CompilerQDMIPayloadAdapterTest, RejectsInvalidProgramFormats) { + auto missingVersion = qdmi::OPENQASM3; + missingVersion.version = 0U; + auto invalidEncoding = qdmi::OPENQASM3; + invalidEncoding.encoding = 3U; + + for (const auto& format : {missingVersion, invalidEncoding}) { + auto environment = mlir::targetEnvironmentFromDevice(device_, format); + ASSERT_FALSE(environment); + EXPECT_NE(llvm::toString(environment.takeError()).find("not canonical"), + std::string::npos); + } +} + TEST(CompilerQDMIAdapterTest, RejectsPayloadNotAcceptedByDevice) { constexpr QDMI_Program_Format unsupported{ .version = QDMI_MAKE_VERSION(3, 1, 0), .encoding = QDMI_PROGRAM_ENCODING_TEXT, .id = "openqasm", - .profile = ""}; + .profile = "", + }; const auto device = qdmi::Session::openDevice("mqt.ddsim.default"); auto environment = mlir::targetEnvironmentFromDevice(device, unsupported); @@ -219,6 +450,14 @@ TEST(CompilerQDMIAdapterTest, ConvertsUnknownDeviceFailureToError) { const auto message = llvm::toString(target.takeError()); EXPECT_NE(message.find("mqt.unknown.device"), std::string::npos); EXPECT_NE(message.find("Unknown QDMI device ID"), std::string::npos); + + auto environment = mlir::targetEnvironmentFromDeviceId("mqt.unknown.device", + qdmi::OPENQASM3); + ASSERT_FALSE(environment); + const auto environmentMessage = llvm::toString(environment.takeError()); + EXPECT_NE(environmentMessage.find("mqt.unknown.device"), std::string::npos); + EXPECT_NE(environmentMessage.find("Unknown QDMI device ID"), + std::string::npos); } TEST(CompilerQDMIAdapterTest, RejectsNonhomogeneousOperationSupport) { @@ -226,9 +465,9 @@ TEST(CompilerQDMIAdapterTest, RejectsNonhomogeneousOperationSupport) { overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG}; const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); - auto target = mlir::compilerTargetFromDevice(device); - ASSERT_FALSE(target); - const auto message = llvm::toString(target.takeError()); + auto environment = mlir::targetEnvironmentFromDevice(device, qdmi::OPENQASM3); + ASSERT_FALSE(environment); + const auto message = llvm::toString(environment.takeError()); EXPECT_NE(message.find("homogeneous"), std::string::npos); EXPECT_NE(message.find("all topology edges"), std::string::npos); } diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index fe2eed2f1e..9997bb9ee7 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -758,6 +758,28 @@ def test_target_environment_from_device_id_adds_adaptive_baseline() -> None: assert len(payload.capabilities) == 5 +def test_target_environment_rejects_conflicting_configuration() -> None: + """Both device factories validate configuration at the Python boundary.""" + with pytest.raises(ValueError, match="mutually exclusive"): + TargetEnvironment.from_device_id( + "mqt.ddsim.default", + ProgramFormat.OPENQASM3, + device_config="{}", + device_config_file=Path("device.json"), + ) + + +def test_target_environment_compiles_and_submits_ddsim_bitcode() -> None: + """Exercise the documented exact-format compilation and submission path.""" + device = open_device("mqt.ddsim.default") + environment = TargetEnvironment.from_device(device, ProgramFormat.QIR21_BASE_BINARY) + program = compile_program(QASM_STRING, target_environment=environment) + assert isinstance(program, QIRProgram) + job = device.submit_job(program.to_bitcode(), ProgramFormat.QIR21_BASE_BINARY, num_shots=32, custom1=7) + job.wait() + assert sum(job.get_counts().values()) == 32 + + def test_qco_program_runs_textual_pipeline() -> None: """Run registered QCO passes through MLIR textual pipeline syntax.""" qco = compile_program(QASM_STRING, output=OutputFormat.QCO)