Skip to content
Closed
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
366 changes: 366 additions & 0 deletions .agent/plans/qdmi-integration-redesign.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ releases may include breaking changes.

### Changed

- 💥 Replace the singleton QDMI driver and client layers with public device
registries, immutable managers, process-default convenience functions, and
lifetime-safe direct device objects ([#1901]) ([**@burgholzer**])
- 💥 Prune dead and misleading CoreIR APIs, including renaming the non-garbage
logical output count to `getNoutputQubits()` and `num_output_qubits` ([#2112])
([**@simon1hofmann**])
Expand Down Expand Up @@ -910,6 +913,7 @@ for previous changelogs._
[#1911]: https://github.com/munich-quantum-toolkit/core/pull/1911
[#1910]: https://github.com/munich-quantum-toolkit/core/pull/1910
[#1904]: https://github.com/munich-quantum-toolkit/core/pull/1904
[#1901]: https://github.com/munich-quantum-toolkit/core/pull/1901
[#1897]: https://github.com/munich-quantum-toolkit/core/pull/1897
[#1895]: https://github.com/munich-quantum-toolkit/core/pull/1895
[#1887]: https://github.com/munich-quantum-toolkit/core/pull/1887
Expand Down
4 changes: 0 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ option(BUILD_MQT_CORE_QDMI_SC_DEVICE "Build the MQT Core superconducting QDMI de
${PROJECT_IS_TOP_LEVEL})
option(BUILD_MQT_CORE_DOCUMENTATION "Generate documentation artifacts as part of the CMake build"
OFF)
if(BUILD_MQT_CORE_TESTS AND (NOT BUILD_MQT_CORE_QDMI_DDSIM_DEVICE OR NOT
BUILD_MQT_CORE_QDMI_SC_DEVICE))
message(FATAL_ERROR "BUILD_MQT_CORE_TESTS requires all bundled QDMI devices")
endif()
if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
message(
FATAL_ERROR
Expand Down
31 changes: 19 additions & 12 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,22 @@ replacements:
- `mlir/Compiler/FoMaCAdapter.h` and `MQTCompilerFoMaCAdapter` become
`mlir/Compiler/QDMIAdapter.h` and `MQTCompilerQDMIAdapter`.

The class and function names do not change. For example:
MQT Core 4 removes the singleton `qdmi::Driver`, the `qdmi::Session` factory,
the `qdmi/driver/Driver.hpp` header, and the `MQT::CoreQDMIDriver` CMake target.
Use the free process-default functions for the shortest migration:

```cpp
#include "qdmi/Client.hpp"
#include "qdmi/DeviceManager.hpp"

auto device = qdmi::Session::openDevice("mqt.ddsim.default");
auto device = qdmi::openDevice("mqt.ddsim.default");
```

Use `qdmi::DeviceRegistry` and `qdmi::DeviceManager` when code needs an isolated
registry snapshot. Each open creates a fresh session. Replacing a definition
affects future opens but does not change live devices. The `qdmi/Client.hpp`
header remains as a forwarding include for the device object model; new code
should include the specific QDMI headers that it uses.

### QDMI Qiskit primitive options

`QDMISampler` and `QDMIEstimator` no longer accept the MQT-specific `options`
Expand Down Expand Up @@ -456,7 +464,7 @@ stable device ID followed by an explicit open. In Python, replace
`add_dynamic_device_library(library_path, prefix, ...)` with:

```python
from mqt.core.fomac import DeviceDefinition, open_device, register_device
from mqt.core.qdmi.driver import DeviceDefinition, open_device, register_device

definition = DeviceDefinition("my.device", library_path, prefix, base_url="https://device.example")
register_device(definition)
Expand All @@ -476,19 +484,18 @@ The equivalent C++ flow is:
qdmi::DeviceDefinition definition{.id = "my.device",
.library = libraryPath,
.prefix = prefix};
auto& driver = qdmi::Driver::get();
driver.registerDevice(definition);
auto device = fomac::Session::openDevice("my.device");
qdmi::registerDevice(definition);
auto device = qdmi::openDevice("my.device");
```

Registration validates and stores metadata without loading native code. Opening
an unknown or disabled ID fails. `fomac::Session::openDevice` creates a fresh
owned session on every call. `qdmi::Driver::open(id)` retains its cached-device
behavior for client callers.
an unknown or disabled ID fails. `qdmi::openDevice` creates a fresh owned
session on every call. An explicit `qdmi::DeviceManager` opens from an isolated,
immutable snapshot instead of the process-default registry.

See the {doc}`QDMI device configuration guide <qdmi/configuration>` for the
versioned JSON and TOML formats, configuration precedence, and relocatable
device manifests.
versioned JSON format, configuration precedence, and relocatable device
manifests.

### FoMaC program payload handling

Expand Down
7 changes: 4 additions & 3 deletions bindings/mlir/register_mlir.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
#include "mlir/Compiler/Programs.h"
#include "mlir/Compiler/QDMIAdapter.h"
#include "mlir/Compiler/Target.h"
#include "qdmi/Client.hpp" // NOLINT(misc-include-cleaner)
#include "qdmi/driver/SessionConfig.hpp"
#include "qdmi/Device.hpp"
#include "qdmi/DeviceManager.hpp"
#include "qdmi/SessionConfig.hpp"
#include "qiskit/Qiskit.h"

#include <llvm/Support/Error.h>
Expand Down Expand Up @@ -552,7 +553,7 @@ means every operation is native.)pb");
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 = qdmi::openDevice(deviceId, overrides);
return takeResult(mlir::compilerTargetFromDevice(device));
},
"device_id"_a, nb::kw_only(), "base_url"_a = std::nullopt,
Expand Down
131 changes: 119 additions & 12 deletions bindings/qdmi/qdmi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
* Licensed under the MIT License
*/

#include "qdmi/Client.hpp"
#include "qdmi/driver/Driver.hpp"
#include "qdmi/driver/SessionConfig.hpp"
#include "qdmi/Device.hpp"
#include "qdmi/DeviceManager.hpp"
#include "qdmi/DeviceRegistry.hpp"
#include "qdmi/SessionConfig.hpp"

#include <nanobind/nanobind.h>
#include <nanobind/operators.h>
Expand Down Expand Up @@ -674,27 +675,134 @@ when the custom slot is unsupported.)pb");
.def_ro("prefix", &qdmi::DeviceDefinition::prefix,
R"pb(Prefix used for the QDMI device interface functions.)pb");

nb::class_<qdmi::DeviceRegistry>(
driver, "DeviceRegistry",
R"pb(Discover or explicitly register QDMI device definitions.)pb")
.def(
nb::init<>(),
R"pb(Discover definitions from the standard configuration sources.)pb")
.def(nb::init<std::vector<qdmi::DeviceDefinition>>(), "definitions"_a,
R"pb(Create an isolated registry from explicit definitions.)pb")
.def_prop_ro(
"definitions",
[](const qdmi::DeviceRegistry& self) { return self.definitions(); },
R"pb(Enabled definitions in stable registration order.)pb")
.def_prop_ro("device_ids", &qdmi::DeviceRegistry::deviceIds,
R"pb(Enabled stable device IDs.)pb")
.def("register_device", &qdmi::DeviceRegistry::registerDevice,
"definition"_a, nb::kw_only(), "replace"_a = false,
R"pb(Register a definition, optionally replacing the same ID.)pb")
.def("register_device_if_absent",
&qdmi::DeviceRegistry::registerDeviceIfAbsent, "definition"_a,
R"pb(Register a fallback unless its ID exists or is disabled.)pb");

nb::class_<qdmi::OpenAllResult>(
driver, "OpenAllResult",
R"pb(Per-ID successes and failures from opening all registered devices.)pb")
.def_ro("devices", &qdmi::OpenAllResult::devices,
R"pb(Successfully opened devices keyed by stable ID.)pb")
.def_ro("errors", &qdmi::OpenAllResult::errors,
R"pb(Error messages for devices that could not be opened.)pb");

auto manager = nb::class_<qdmi::DeviceManager>(
driver, "DeviceManager",
R"pb(An immutable registry snapshot that opens fresh device sessions.)pb");
manager
.def(nb::init<>(),
R"pb(Snapshot the current process default registry.)pb")
.def(nb::init<qdmi::DeviceRegistry>(), "registry"_a,
R"pb(Snapshot an explicit registry.)pb")
.def_prop_ro(
"definitions",
[](const qdmi::DeviceManager& self) { return self.definitions(); },
R"pb(Definitions in this immutable snapshot.)pb")
.def_prop_ro("device_ids", &qdmi::DeviceManager::deviceIds,
R"pb(Stable IDs in this immutable snapshot.)pb")
.def(
"open",
[](const qdmi::DeviceManager& self, 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) {
return self.open(
deviceId,
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)));
},
"device_id"_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,
R"pb(Open a fresh session for one stable device ID.)pb")
.def(
"open_all",
[](const qdmi::DeviceManager& self,
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) {
return self.openAll(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)));
},
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,
R"pb(Open all devices and isolate failures by stable ID.)pb");

driver.def(
"register_device",
[](qdmi::DeviceDefinition definition, const bool replace) {
qdmi::Driver::get().registerDevice(std::move(definition), replace);
qdmi::registerDevice(std::move(definition), replace);
},
"definition"_a, nb::kw_only(), "replace"_a = false,
R"pb(Register a QDMI device definition without loading its library.

Args:
definition: Definition to validate and store.
replace: Replace an existing definition if it has not been opened.
replace: Replace an existing definition for future opens.

Raises:
ValueError: If the definition is invalid or its ID is already registered.
RuntimeError: If replacing an already opened ID.)pb");
ValueError: If the definition is invalid or its ID is already registered.)pb");

driver.def(
"register_device_if_absent",
[](qdmi::DeviceDefinition definition) {
return qdmi::Driver::get().registerDeviceIfAbsent(
std::move(definition));
return qdmi::registerDeviceIfAbsent(std::move(definition));
},
"definition"_a,
R"pb(Register a valid QDMI device definition if its ID is absent.
Expand All @@ -712,8 +820,7 @@ still raise.
ValueError: If the definition is invalid.)pb");

driver.def(
"registered_device_ids",
[] { return qdmi::Driver::get().registeredDeviceIds(); },
"registered_device_ids", &qdmi::registeredDeviceIds,
R"pb(Return registered, enabled QDMI device IDs in registration order.

This includes devices registered at runtime and does not load native device
Expand All @@ -738,7 +845,7 @@ libraries or expose their definitions.)pb");
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);
return qdmi::openDevice(deviceId, overrides);
},
"device_id"_a, nb::kw_only(), "base_url"_a = std::nullopt,
"token"_a = std::nullopt, "auth_file"_a = std::nullopt,
Expand Down
56 changes: 30 additions & 26 deletions docs/qdmi/configuration.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# QDMI device configuration

MQT Core discovers QDMI device definitions from versioned JSON configuration.
Discovery only parses definitions. When the QDMI driver initializes a client
session, it opens the configured native libraries. The stable-ID API opens only
the requested device.
Discovery only parses definitions. A manager or process-default open loads the
requested native library and creates a fresh client session. Stable-ID opening
does not load unrelated devices.

:::{warning}
QDMI configuration is a native-code loading trust boundary. Use configuration
Expand Down Expand Up @@ -63,10 +63,10 @@ or:
The inline value must be a JSON object. A relative file path is resolved against
the registry file that declares it. The complete source is one merge field:
changing from `inline` to `file` at a higher-precedence layer replaces the
inherited inline JSON. The Driver adapts inline JSON to QDMI v1 CUSTOM1 and a
file path to CUSTOM2 when opening the native session. Consequently,
`device-config` cannot be combined with raw `custom1` or `custom2`; CUSTOM3
through CUSTOM5 remain available to providers.
inherited inline JSON. MQT Core adapts inline JSON to QDMI v1 CUSTOM1 and a file
path to CUSTOM2 when opening the native session. Consequently, `device-config`
cannot be combined with raw `custom1` or `custom2`; CUSTOM3 through CUSTOM5
remain available to providers.

Relative library and authentication-file paths are resolved against the file
that declared them. For `MQT_CORE_QDMI_CONFIG_JSON`, they resolve against the
Expand All @@ -75,13 +75,13 @@ current working directory.
Unknown keys, invalid types, duplicate IDs within one source, unsupported schema
versions, and incomplete enabled definitions are hard errors. Diagnostics name
the source and configuration path. Credentials and session values are not
included in Driver warnings.
included in diagnostic warnings.

## Discovery and precedence

Definitions are merged field by field by ID, from lowest to highest precedence:

1. generated `*.qdmi.json` fragments packaged beside the MQT Core Driver;
1. generated `*.qdmi.json` fragments packaged beside MQT Core's QDMI library;
2. the system `qdmi.json`;
3. the user or XDG `qdmi.json`;
4. the nearest project `qdmi.json`;
Expand All @@ -103,9 +103,9 @@ retaining packaged built-ins.

## Using configured devices

When the QDMI driver initializes a client session, it opens the configured
definitions. A failure to load one definition does not hide the remaining
devices. Stable-ID registration does not initialize device libraries.
Stable-ID registration does not initialize device libraries. Opening one ID does
not initialize other definitions. Use a manager's `open_all()` operation when an
application wants per-ID successes and failures from the full snapshot.

```python
from mqt.core.qdmi.driver import open_device, registered_device_ids
Expand Down Expand Up @@ -156,14 +156,18 @@ enabled stable IDs in deterministic registration order. This includes runtime
registrations without loading native device libraries or exposing their paths,
prefixes, or session configuration.

The equivalent C++ registration operation is
{cpp-api:func}`qdmi::Driver::registerDevice`. Duplicate IDs are rejected unless
`replace` is true, and an opened definition cannot be replaced.
{cpp-api:func}`qdmi::Driver::registeredDeviceIds` provides the same load-free
enumeration, and {cpp-api:func}`qdmi::Driver::open` returns the cached device.
{cpp-api:func}`qdmi::Session::openDevice` returns a fresh device session and
does not add it to the QDMI client catalog. Runtime registrations and explicit
opens are not added to that catalog.
The equivalent process-default C++ functions are
{cpp-api:func}`qdmi::registerDevice`, {cpp-api:func}`qdmi::registeredDeviceIds`,
and {cpp-api:func}`qdmi::openDevice`. Duplicate IDs are rejected unless
`replace` is true. Replacement affects future opens. Live devices retain the
session and library state created from the prior definition.

For isolated state, construct {cpp-api:class}`qdmi::DeviceRegistry`, register
definitions on that object, and pass it to {cpp-api:class}`qdmi::DeviceManager`.
A manager takes an immutable snapshot. Changes to the source registry or the
process-default registry do not change that snapshot. `DeviceManager::openAll`
continues after an individual failure and returns successes and error messages
by stable ID.

Multiple definitions may refer to the same library and prefix. MQT Core reuses
the initialized library while creating a fresh QDMI device session, with its own
Expand Down Expand Up @@ -216,15 +220,15 @@ availability check, or a provider queue length.
Built-in targets generate manifests beside their runtime libraries in both build
and install trees. Library paths in those fragments contain only the target
filename, so moving an installed tree or Python wheel preserves discovery.
Automatic discovery searches relative to the MQT Core Driver, not every library
loaded by the process. An application using a separately installed device
implementation therefore copies its manifest beside the Driver or registers its
definition by stable ID.
Automatic discovery searches relative to the MQT Core QDMI library, not every
library loaded by the process. An application using a separately installed
device implementation therefore copies its manifest beside that library or
registers its definition by stable ID.

A fully static executable has no portable shared-module location. Place the
fragments beside the executable, point `MQT_CORE_QDMI_CONFIG_FILE` at a complete
configuration, or use {cpp-api:func}`qdmi::Driver::registerDevice` and
{cpp-api:func}`qdmi::Driver::open`. No install prefix is compiled into the
configuration, or use {cpp-api:func}`qdmi::registerDevice` and
{cpp-api:func}`qdmi::openDevice`. No install prefix is compiled into the
manifests.

An installed MQT Core CMake package provides a helper that colocates selected
Expand Down
Loading
Loading