diff --git a/.agent/plans/qdmi-target-environment-adapter.md b/.agent/plans/qdmi-target-environment-adapter.md new file mode 100644 index 0000000000..0812cce3fe --- /dev/null +++ b/.agent/plans/qdmi-target-environment-adapter.md @@ -0,0 +1,46 @@ +# QDMI-to-compiler program-capability adapter + +Status: locally validated, design-gated prototype. + +## Scope and decisions + +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. + +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. + +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. + +## Design and release boundary + +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. + +## Validation + +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. + +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. + +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 9fd99c1acb..2d78637123 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 @@ -913,6 +913,7 @@ for previous changelogs._ [#2240]: https://github.com/munich-quantum-toolkit/core/pull/2240 [#2232]: https://github.com/munich-quantum-toolkit/core/pull/2232 [#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 18b71781f0..d90dda619c 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -190,6 +191,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 +944,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 +1028,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..92b8d1f148 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.ddsim.default", + 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.ddsim.default", 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..a169098851 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 @@ -24,34 +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 List the stable IDs of registered QDMI devices. - * - * @details This adapter contains exceptions from QDMI registry discovery and - * returns them as LLVM errors. - */ +/// 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); + +/// 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); + +/// 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 98f495c135..fdeb6d13ab 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,143 @@ 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) { + 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"); + } + + 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, + }; + + 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 +650,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..1f68cf9032 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,16 +20,168 @@ #include #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) { + 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 +288,157 @@ 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_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 = "", + }; + 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")); @@ -145,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) { @@ -152,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/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..9997bb9ee7 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,63 @@ 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_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)