Skip to content

Commit 73c364f

Browse files
✨ Add support for QDMI child devices to the driver and FoMaC libraries (backport #1897) (#1898)
## Description This PR adds support for the newly introduced child device feature of QDMI v1.3.2 to the MQT Core Driver and the FoMaC libraries. Assisted-by: gpt-5.6-sol via Codex CLI @ystade FYI ## Checklist - [x] The pull request only contains commits that are focused and relevant to this change. - [x] I have added appropriate tests that cover the new/changed functionality. - [x] I have updated the documentation to reflect these changes. - [x] I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals. - [x] I have added migration instructions to the upgrade guide (if needed). - [x] The changes follow the project's style guidelines and introduce no new warnings. - [x] The changes are fully tested and pass the CI checks. - [x] I have reviewed my own code changes. **If PR contains AI-assisted content:** - [x] I have disclosed the use of AI tools in the PR description as per our [AI Usage Guidelines](https://github.com/munich-quantum-toolkit/core/blob/main/docs/ai_usage.md). - [x] AI-assisted commits include an `Assisted-by: [Model Name] via [Tool Name]` footer. - [x] I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it. (cherry picked from commit e78c99a) --------- Co-authored-by: Lukas Burgholzer <burgholzer@me.com>
1 parent 0e6a4e9 commit 73c364f

13 files changed

Lines changed: 450 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ releases may include breaking changes.
1212

1313
### Added
1414

15+
- ✨ Add support for QDMI child devices to the driver and FoMaC libraries
16+
([#1897]) ([**@burgholzer**])
1517
- ✨ Add typed custom property and result queries to the C++ and Python FoMaC
1618
libraries ([#1895]) ([**@burgholzer**])
1719
- ✨ Add support for custom job parameters to C++ and Python FoMaC library
1820
([#1887]) ([**@flowerthrower**], [**@burgholzer**])
1921

22+
### Changed
23+
24+
- ⬆️ Raise the minimum supported QDMI version to 1.3.2 ([#1897])
25+
([**@burgholzer**])
26+
2027
## [3.7.0] - 2026-07-09
2128

2229
_If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#370)._
@@ -572,6 +579,7 @@ changelogs._
572579

573580
<!-- PR links -->
574581

582+
[#1897]: https://github.com/munich-quantum-toolkit/core/pull/1897
575583
[#1895]: https://github.com/munich-quantum-toolkit/core/pull/1895
576584
[#1887]: https://github.com/munich-quantum-toolkit/core/pull/1887
577585
[#1873]: https://github.com/munich-quantum-toolkit/core/pull/1873

UPGRADING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ of changes including minor and patch releases, please refer to the
66

77
## [Unreleased]
88

9+
### QDMI child devices
10+
11+
The QDMI driver now translates device-library-specific `QDMI_Child_Device`
12+
handles into client-facing `QDMI_Device` handles backed by dedicated child
13+
sessions. Direct child devices can be queried through
14+
`fomac::Device::getChildDevices()` in C++ and `Device.child_devices()` in
15+
Python. Devices without child-device support continue to behave unchanged.
16+
917
## [3.7.0]
1018

1119
The shared library ABI version (`SOVERSION`) is increased from `3.6` to `3.7`.

bindings/fomac/fomac.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,9 @@ when the custom slot is unsupported.)pb");
359359
&fomac::Device::getSupportedProgramFormats,
360360
"Returns the list of program formats supported by the device.");
361361

362+
device.def("child_devices", &fomac::Device::getChildDevices,
363+
"Returns the direct child devices managed by this device.");
364+
362365
device.def(
363366
"query_custom_property",
364367
[](const fomac::Device& self, const fomac::CustomProperty customProperty,

cmake/ExternalDependencies.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ if(BUILD_MQT_CORE_TESTS)
7575
endif()
7676

7777
# cmake-format: off
78-
set(QDMI_MINIMUM_VERSION 1.3.0
78+
set(QDMI_MINIMUM_VERSION 1.3.2
7979
CACHE STRING "Minimum QDMI version")
8080
set(QDMI_VERSION 1.3.2
8181
CACHE STRING "QDMI version")

include/mqt-core/fomac/FoMaC.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,14 @@ class Device {
472472
[[nodiscard]] std::vector<QDMI_Program_Format>
473473
getSupportedProgramFormats() const;
474474

475+
/**
476+
* @brief Returns the direct child devices managed by this device.
477+
* @return The child devices, or an empty vector if child devices are not
478+
* supported.
479+
* @see QDMI_DEVICE_PROPERTY_CHILDDEVICES
480+
*/
481+
[[nodiscard]] std::vector<Device> getChildDevices() const;
482+
475483
/**
476484
* @brief Queries an implementation-defined custom device property.
477485
* @tparam T Expected value type. Use `std::vector<std::byte>` to retrieve the

include/mqt-core/qdmi/driver/Driver.hpp

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,33 +167,48 @@ struct QDMI_Device_impl_d {
167167
* @note This must be a pointer type as we need access to dynamic and static
168168
* libraries that are subclasses of qdmi::DeviceLibrary.
169169
*/
170-
std::unique_ptr<qdmi::DeviceLibrary> library_;
170+
std::shared_ptr<qdmi::DeviceLibrary> library_;
171171
/// @brief The device session handle.
172172
QDMI_Device_Session deviceSession_ = nullptr;
173+
/// Client-facing wrappers for direct child devices.
174+
std::vector<std::unique_ptr<QDMI_Device_impl_d>> childDevices_;
173175
/**
174176
* @brief Map of jobs to their corresponding unique pointers of
175177
* QDMI_Job_impl_d objects.
176178
*/
177179
std::unordered_map<QDMI_Job, std::unique_ptr<QDMI_Job_impl_d>> jobs_;
178180

179181
public:
182+
/**
183+
* @brief Constructs a top-level QDMI device from an exclusively owned
184+
* library.
185+
* @param lib is the device library to take ownership of.
186+
* @param config is the configuration for device session parameters.
187+
*/
188+
explicit QDMI_Device_impl_d(std::unique_ptr<qdmi::DeviceLibrary>&& lib,
189+
const qdmi::DeviceSessionConfig& config = {})
190+
: QDMI_Device_impl_d(std::shared_ptr(std::move(lib)), config) {}
191+
180192
/**
181193
* @brief Constructor for the QDMI device.
182194
* @details This constructor initializes the device session and allocates
183195
* the device session handle.
184-
* @param lib is a unique pointer to the device library that provides the
196+
* @param lib is a shared pointer to the device library that provides the
185197
* device interface functions.
186198
* @param config is the configuration for device session parameters.
199+
* @param childDevice optionally selects a child device for this wrapper.
187200
*/
188-
explicit QDMI_Device_impl_d(std::unique_ptr<qdmi::DeviceLibrary>&& lib,
189-
const qdmi::DeviceSessionConfig& config = {});
201+
explicit QDMI_Device_impl_d(std::shared_ptr<qdmi::DeviceLibrary> lib,
202+
const qdmi::DeviceSessionConfig& config = {},
203+
QDMI_Child_Device childDevice = nullptr);
190204

191205
/**
192206
* @brief Destructor for the QDMI device.
193207
* @details This destructor frees the device session and clears the jobs map.
194208
*/
195209
~QDMI_Device_impl_d() {
196210
jobs_.clear();
211+
childDevices_.clear();
197212
if (library_ && deviceSession_ != nullptr) {
198213
library_->device_session_free(deviceSession_);
199214
}

python/mqt/core/fomac.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@ class Device:
307307
def supported_program_formats(self) -> list[ProgramFormat]:
308308
"""Returns the list of program formats supported by the device."""
309309

310+
def child_devices(self) -> list[Device]:
311+
"""Returns the direct child devices managed by this device."""
312+
310313
@overload
311314
def query_custom_property(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ...
312315
@overload

src/fomac/FoMaC.cpp

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,34 @@ std::vector<QDMI_Program_Format> Device::getSupportedProgramFormats() const {
286286
QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS);
287287
}
288288

289+
std::vector<Device> Device::getChildDevices() const {
290+
size_t size = 0;
291+
auto result = QDMI_device_query_device_property(
292+
device_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, &size);
293+
if (result == QDMI_ERROR_NOTSUPPORTED) {
294+
return {};
295+
}
296+
qdmi::throwIfError(result, "Querying child devices size");
297+
if (size % sizeof(QDMI_Device) != 0) {
298+
throw std::runtime_error("Invalid child device list size");
299+
}
300+
301+
std::vector<QDMI_Device> handles(size / sizeof(QDMI_Device));
302+
if (size != 0) {
303+
result = QDMI_device_query_device_property(
304+
device_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, size,
305+
static_cast<void*>(handles.data()), nullptr);
306+
qdmi::throwIfError(result, "Querying child devices");
307+
}
308+
309+
std::vector<Device> devices;
310+
devices.reserve(handles.size());
311+
std::ranges::transform(
312+
handles, std::back_inserter(devices),
313+
[](QDMI_Device_impl_d* const handle) { return Device(handle); });
314+
return devices;
315+
}
316+
289317
Job Device::submitJob(const std::string& program,
290318
const QDMI_Program_Format format, const size_t numShots,
291319
const std::optional<CustomJobParameter>& custom1,
@@ -751,7 +779,7 @@ std::vector<Device> Session::getDevices() {
751779
devices.reserve(qdmiDevices.size());
752780
std::ranges::transform(
753781
qdmiDevices, std::back_inserter(devices),
754-
[](const QDMI_Device& dev) -> Device { return Device(dev); });
782+
[](QDMI_Device_impl_d* const& dev) -> Device { return Device(dev); });
755783
return devices;
756784
}
757785
} // namespace fomac

src/qdmi/driver/Driver.cpp

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <qdmi/device.h>
1717
#include <spdlog/spdlog.h>
1818

19+
#include <algorithm>
1920
#include <array>
2021
#include <cassert>
2122
#include <cstddef>
@@ -169,8 +170,9 @@ DynamicDeviceLibrary::~DynamicDeviceLibrary() {
169170
} // namespace qdmi
170171

171172
QDMI_Device_impl_d::QDMI_Device_impl_d(
172-
std::unique_ptr<qdmi::DeviceLibrary>&& lib,
173-
const qdmi::DeviceSessionConfig& config)
173+
std::shared_ptr<qdmi::DeviceLibrary> lib,
174+
const qdmi::DeviceSessionConfig& config,
175+
QDMI_Child_Device_impl_d* const childDevice)
174176
: library_(std::move(lib)) {
175177
if (library_->device_session_alloc(&deviceSession_) != QDMI_SUCCESS) {
176178
throw std::runtime_error("Failed to allocate device session");
@@ -213,10 +215,77 @@ QDMI_Device_impl_d::QDMI_Device_impl_d(
213215
setParameter(config.custom4, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM4);
214216
setParameter(config.custom5, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM5);
215217

218+
if (childDevice != nullptr) {
219+
const auto status =
220+
static_cast<QDMI_STATUS>(library_->device_session_set_parameter(
221+
deviceSession_, QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE,
222+
sizeof(QDMI_Child_Device), static_cast<const void*>(&childDevice)));
223+
if (status != QDMI_SUCCESS) {
224+
library_->device_session_free(deviceSession_);
225+
deviceSession_ = nullptr;
226+
std::ostringstream ss;
227+
ss << "Failed to select child device: " << qdmi::toString(status);
228+
throw std::runtime_error(ss.str());
229+
}
230+
}
231+
216232
if (library_->device_session_init(deviceSession_) != QDMI_SUCCESS) {
217233
library_->device_session_free(deviceSession_);
234+
deviceSession_ = nullptr;
218235
throw std::runtime_error("Failed to initialize device session");
219236
}
237+
238+
// Child sessions represent leaf devices in the QDMI multicore
239+
// workflow. Only top-level sessions discover and wrap child handles.
240+
if (childDevice != nullptr) {
241+
return;
242+
}
243+
244+
size_t childrenSize = 0;
245+
auto status =
246+
static_cast<QDMI_STATUS>(library_->device_session_query_device_property(
247+
deviceSession_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr,
248+
&childrenSize));
249+
if (status == QDMI_ERROR_NOTSUPPORTED) {
250+
return;
251+
}
252+
if (status != QDMI_SUCCESS || childrenSize % sizeof(QDMI_Child_Device) != 0) {
253+
library_->device_session_free(deviceSession_);
254+
deviceSession_ = nullptr;
255+
if (status != QDMI_SUCCESS) {
256+
throw std::runtime_error("Failed to query child devices: " +
257+
std::string(qdmi::toString(status)));
258+
}
259+
throw std::runtime_error("Device returned an invalid child device list");
260+
}
261+
262+
std::vector<QDMI_Child_Device> children(childrenSize /
263+
sizeof(QDMI_Child_Device));
264+
if (childrenSize != 0) {
265+
status =
266+
static_cast<QDMI_STATUS>(library_->device_session_query_device_property(
267+
deviceSession_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, childrenSize,
268+
static_cast<void*>(children.data()), nullptr));
269+
if (status != QDMI_SUCCESS) {
270+
library_->device_session_free(deviceSession_);
271+
deviceSession_ = nullptr;
272+
throw std::runtime_error("Failed to query child devices: " +
273+
std::string(qdmi::toString(status)));
274+
}
275+
}
276+
277+
try {
278+
childDevices_.reserve(children.size());
279+
for (auto* const child : children) {
280+
childDevices_.emplace_back(
281+
std::make_unique<QDMI_Device_impl_d>(library_, config, child));
282+
}
283+
} catch (...) {
284+
childDevices_.clear();
285+
library_->device_session_free(deviceSession_);
286+
deviceSession_ = nullptr;
287+
throw;
288+
}
220289
}
221290

222291
auto QDMI_Device_impl_d::createJob(QDMI_Job* job) -> int {
@@ -244,6 +313,25 @@ auto QDMI_Device_impl_d::freeJob(QDMI_Job job) -> void {
244313
auto QDMI_Device_impl_d::queryDeviceProperty(QDMI_Device_Property prop,
245314
const size_t size, void* value,
246315
size_t* sizeRet) const -> int {
316+
if (prop == QDMI_DEVICE_PROPERTY_CHILDDEVICES) {
317+
if (childDevices_.empty()) {
318+
return QDMI_ERROR_NOTSUPPORTED;
319+
}
320+
const auto requiredSize = childDevices_.size() * sizeof(QDMI_Device);
321+
if (value != nullptr) {
322+
if (size < requiredSize) {
323+
return QDMI_ERROR_INVALIDARGUMENT;
324+
}
325+
auto* devices = static_cast<QDMI_Device*>(value);
326+
std::ranges::transform(
327+
childDevices_, devices,
328+
[](const auto& child) -> QDMI_Device { return child.get(); });
329+
}
330+
if (sizeRet != nullptr) {
331+
*sizeRet = requiredSize;
332+
}
333+
return QDMI_SUCCESS;
334+
}
247335
return library_->device_session_query_device_property(deviceSession_, prop,
248336
size, value, sizeRet);
249337
}
@@ -424,7 +512,7 @@ auto Driver::addDynamicDeviceLibrary(const std::string& libName,
424512
const DeviceSessionConfig& config)
425513
-> QDMI_Device {
426514
devices_.emplace_back(std::make_unique<QDMI_Device_impl_d>(
427-
std::make_unique<DynamicDeviceLibrary>(libName, prefix), config));
515+
std::make_shared<DynamicDeviceLibrary>(libName, prefix), config));
428516
return devices_.back().get();
429517
}
430518

test/fomac/test_fomac.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,10 @@ TEST_P(DeviceTest, SupportedProgramFormats) {
440440
EXPECT_NO_THROW(std::ignore = device.getSupportedProgramFormats());
441441
}
442442

443+
TEST_P(DeviceTest, ChildDevices) {
444+
EXPECT_TRUE(device.getChildDevices().empty());
445+
}
446+
443447
TEST_P(DeviceTest, UnsupportedCustomPropertyReturnsNullopt) {
444448
EXPECT_EQ(device.queryCustomProperty<std::vector<std::byte>>(
445449
CustomProperty::Custom1),

0 commit comments

Comments
 (0)