Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .agent/plans/qdmi-target-environment-adapter.md
Original file line number Diff line number Diff line change
@@ -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`.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ releases may include breaking changes.
[**@burgholzer**])
- ✨ Add immutable MLIR compiler targets, selected payload specifications, QDMI
device integration, and target compilation through C++, Python, and `mqt-cc`
([#1687], [#1993], [#1999], [#2049], [#2219]) ([**@MatthiasReumann**],
[**@simon1hofmann**], [**@burgholzer**])
([#1687], [#1993], [#1999], [#2049], [#2219], [#2227])
([**@MatthiasReumann**], [**@simon1hofmann**], [**@burgholzer**])

#### Import and export

Expand Down Expand Up @@ -894,6 +894,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
Expand Down
93 changes: 78 additions & 15 deletions bindings/mlir/register_mlir.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
#include <nanobind/stl/string_view.h>
#include <nanobind/stl/variant.h>
#include <nanobind/stl/vector.h>
#include <qdmi/constants.h>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We still need to streamline our includes when we have fewer PRs open, but this would comply with the current style:

Suggested change
#include <qdmi/constants.h>
#include "qdmi/constants.h"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah. You are right. Still have this wired wrongly in my brain. We did device at some point to only treat stdlib includes with angle brackets, right?
Because this is still an external header pulled in as a dependency.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, but then we changed our minds again, following the discussion in FullStaQD/qcc#37. I still have it on my list to apply this across the MQT. Maybe just before the v4 release is a good time, since we hopefully won't have too many open PRs at that point. 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah. Before the release makes sense.
We may even want to consider that as a point where we may use an "mqt" namespace for our code to differentiate it a tad bit more from plain mlir. Similar to how this is done in FullStaQD with qcc. I was initially opposed to that but I am starting to see the potential benefits.


#include <cctype>
#include <complex>
Expand Down Expand Up @@ -186,6 +187,31 @@ template <typename Fn>
return *std::move(result);
}

[[nodiscard]] static qdmi::Device openQDMIDevice(
const std::string& deviceId, std::optional<std::string> baseUrl,
std::optional<std::string> token,
std::optional<std::filesystem::path> authFile,
std::optional<std::string> authUrl, std::optional<std::string> username,
std::optional<std::string> password,
std::optional<std::string> deviceConfig,
std::optional<std::filesystem::path> deviceConfigFile,
std::optional<std::string> custom1, std::optional<std::string> custom2,
std::optional<std::string> custom3, std::optional<std::string> custom4,
std::optional<std::string> 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 <class ProgramType>
[[nodiscard]] static ProgramType copiedOrConsumed(ProgramType& program,
const bool copy) {
Expand Down Expand Up @@ -909,21 +935,13 @@ either unrestricted or explicitly enumerated native-operation support.)pb");
std::optional<std::string> custom3,
std::optional<std::string> custom4,
std::optional<std::string> 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,
Expand Down Expand Up @@ -995,6 +1013,51 @@ either unrestricted or explicitly enumerated native-operation support.)pb");
"A compiler target and its selected payload specification.")
.def(nb::init<mlir::CompilerTarget, mlir::PayloadSpecification>(),
"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<std::string> baseUrl,
std::optional<std::string> token,
std::optional<std::filesystem::path> authFile,
std::optional<std::string> authUrl,
std::optional<std::string> username,
std::optional<std::string> password,
std::optional<std::string> deviceConfig,
std::optional<std::filesystem::path> deviceConfigFile,
std::optional<std::string> custom1,
std::optional<std::string> custom2,
std::optional<std::string> custom3,
std::optional<std::string> custom4,
std::optional<std::string> 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",
Expand Down
52 changes: 17 additions & 35 deletions docs/mlir/target_compilation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The SC provider does not advertise any program formats right?
Same for the example in lines 111-112.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It does not. But maybe it should.
Maybe the configuration for the device should include which formats the device claims to accept/support. Would allow a more faithful model of an IQM machine.

)
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:
Expand Down Expand Up @@ -136,35 +129,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 <llvm/Support/Error.h>
#include <llvm/Support/raw_ostream.h>

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;
}
```
Expand Down
9 changes: 2 additions & 7 deletions docs/qdmi/ddsim_device.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,17 @@ 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,
)
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(
Expand Down
54 changes: 34 additions & 20 deletions mlir/include/mlir/Compiler/QDMIAdapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
#pragma once

#include "mlir/Compiler/Target.h"
#include "mlir/Compiler/TargetEnvironment.h"

#include <llvm/Support/Error.h>
#include <qdmi/constants.h>

#include <string>
#include <string_view>
Expand All @@ -24,32 +26,44 @@ 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 and site-dependent operation support are not supported by the
* circuit-model compiler pipeline.
*/
/// 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 and site-dependent operation support are not supported by the
/// circuit-model compiler pipeline.
[[nodiscard]] llvm::Expected<CompilerTarget>
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<CompilerTarget>
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<TargetEnvironment>
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<TargetEnvironment>
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<std::vector<std::string>>
registeredQDMIDeviceIds();

Expand Down
Loading
Loading