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
36 changes: 36 additions & 0 deletions .agent/plans/qdmi-multi-program-adoption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Independent native multi-program adoption

Status: extracted from Core PR #2226; local validation complete.

## Scope and decisions

Core issue #2362 owns Client APIs, bindings, bundled-device adaptation, and
indexed results for QDMI PR #509. The program-format enum remains unchanged.
Metadata removal, replaceable drivers, and payload capabilities are independent.
This targets Core 4.1 with released QDMI 1.4 before publication.

Retain existing single-program and calibration APIs, optional shot counts,
byte-exact binary payloads, session ownership and current concurrent execution.
DDSIM supports one program per native job for now; the model-only SC device does
not execute jobs. Neither device claims unsupported aggregate semantics. An
isolated test provider exercises multiple programs and indexed retrieval.

## Validation

Run the release build and CTest suite, generated stubs, Python QDMI/SDK tests,
repository lint, C++ lint, and the documented DDSIM example. Cover atomic
setters, indexed ordering and invalid indices, deep copies, binary bytes,
optional shots, retrieval, failure/cancellation and the single-program path.
Preserve current target inference and simulator concurrency regressions.

Local results: 3,879 native tests passed with one existing skip; 413 Python
QDMI/SDK tests passed; minimum-dependency testing passed 412 tests with one
expected skip. Generated stubs, repository lint and C++ lint passed. Hosted CI
remains a separate publication gate.

## Follow-ups

Core issue #2359 coordinates independent SDK and provider consumers. Do not
implement their fallback policy here or equate concurrent single submissions
with a native aggregate job. No payload descriptors or execution-capability
properties are introduced by this extraction.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ releases may include breaking changes.

- ✨ Expose ordered shots from DDSIM QDMI OpenQASM and QIR jobs, with matching
histograms ([#2368]) ([**@burgholzer**])
- ✨ Add native multi-program QDMI submissions and indexed results to the C++
and Python clients, independently of concurrent single-program execution
([#2373]) ([**@burgholzer**])
- 🐳 Add dev container configuration for a consistent local development
environment ([#1786]) ([**@denialhaag**])

Expand Down Expand Up @@ -883,6 +886,7 @@ for previous changelogs._
<!-- PR links -->

[#2380]: https://github.com/munich-quantum-toolkit/core/pull/2380
[#2373]: https://github.com/munich-quantum-toolkit/core/pull/2373
[#2368]: https://github.com/munich-quantum-toolkit/core/pull/2368
[#2358]: https://github.com/munich-quantum-toolkit/core/pull/2358
[#2349]: https://github.com/munich-quantum-toolkit/core/pull/2349
Expand Down
14 changes: 14 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ of changes including minor and patch releases, please refer to the

## [Unreleased]

### QDMI multi-program job interface

Native QDMI consumers must rebuild against QDMI 1.4. Replace the
`QDMI_JOB_PARAMETER_PROGRAM` setter with `QDMI_job_set_programs`, including for
one program. Device implementations provide `QDMI_device_job_set_programs` and
accept a program index in result retrieval. The program-format enum remains
unchanged. Calibration without a payload can still set only the format.

Existing C++ and Python single-program submission and result calls remain valid.
Use `submitPrograms` or `submit_programs` for native program lists; omit the
shot count to preserve device defaults. A provider that cannot implement the
aggregate lifecycle must reject larger lists. Concurrent single-program
submission remains a separate fallback.

### Removal of the classic circuit representation

MQT Core 4 removes the complete classic circuit surface. This includes the C++
Expand Down
91 changes: 84 additions & 7 deletions bindings/qdmi/qdmi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,27 +106,42 @@ NB_MODULE(MQT_CORE_MODULE_NAME, qdmiModule) {

job.def("cancel", &qdmi::Job::cancel, "Cancels the job.");

job.def("get_shots", &qdmi::Job::getShots,
job.def(
"get_results",
[](const qdmi::Job& self, const size_t programIndex,
const QDMI_Job_Result result) {
const auto value = self.getResults(programIndex, result);
return nb::bytes(reinterpret_cast<const char*>(value.data()),
value.size());
},
"program_index"_a, "result"_a, nb::call_guard<nb::gil_scoped_release>(),
"Returns one indexed result as exact bytes.");

job.def("get_shots", &qdmi::Job::getShots, "program_index"_a = 0U,
nb::call_guard<nb::gil_scoped_release>(),
"Returns the raw shot results from the job.");

job.def("get_counts", &qdmi::Job::getCounts,
job.def("get_counts", &qdmi::Job::getCounts, "program_index"_a = 0U,
nb::call_guard<nb::gil_scoped_release>(),
"Returns the measurement counts from the job.");

job.def("get_dense_statevector", &qdmi::Job::getDenseStateVector,
"program_index"_a = 0U,
"Returns the dense statevector from the job (typically only "
"available from simulator devices).");

job.def("get_dense_probabilities", &qdmi::Job::getDenseProbabilities,
"program_index"_a = 0U,
"Returns the dense probabilities from the job (typically only "
"available from simulator devices).");

job.def("get_sparse_statevector", &qdmi::Job::getSparseStateVector,
"program_index"_a = 0U,
"Returns the sparse statevector from the job (typically only "
"available from simulator devices).");

job.def("get_sparse_probabilities", &qdmi::Job::getSparseProbabilities,
"program_index"_a = 0U,
"Returns the sparse probabilities from the job (typically only "
"available from simulator devices).");

Expand Down Expand Up @@ -154,17 +169,19 @@ when the custom slot is unsupported.)pb");
job.def(
"get_custom_result",
[](const qdmi::Job& self, const qdmi::CustomProperty customProperty,
const nb::handle valueType) {
const nb::handle valueType, const size_t programIndex) {
return queryCustomValue(
[&self, customProperty]<qdmi::custom_property_value T> {
return self.getCustomResult<T>(customProperty);
[&self, customProperty,
programIndex]<qdmi::custom_property_value T> {
return self.getCustomResult<T>(customProperty, programIndex);
},
valueType);
},
"custom_property"_a, "value_type"_a,
"custom_property"_a, "value_type"_a, "program_index"_a = 0U,
nb::sig("def get_custom_result(self, custom_property: CustomProperty, "
"value_type: type[str] | type[bool] | type[int] | type[float] | "
"type[bytes]) -> str | bool | int | float | bytes | None"),
"type[bytes], program_index: int = 0) -> str | bool | int | "
"float | bytes | None"),
R"pb(Return an implementation-defined custom job result.

The caller must provide the type documented by the device implementation.
Expand All @@ -186,6 +203,9 @@ when the custom slot is unsupported.)pb");
},
"The exact bytes of the submitted program.");

job.def_prop_ro("programs_num", &qdmi::Job::getProgramsNum,
"The number of programs in the job.");

job.def_prop_ro("num_shots", &qdmi::Job::getNumShots, "The number of shots.");

job.def_prop_ro(
Expand All @@ -209,6 +229,20 @@ when the custom slot is unsupported.)pb");
.value("FAILED", QDMI_JOB_STATUS_FAILED);

// ProgramFormat enum
nb::enum_<QDMI_Job_Result>(job, "Result", "One raw job result format.")
.value("SHOTS", QDMI_JOB_RESULT_SHOTS)
.value("HIST_KEYS", QDMI_JOB_RESULT_HIST_KEYS)
.value("HIST_VALUES", QDMI_JOB_RESULT_HIST_VALUES)
.value("STATEVECTOR_DENSE", QDMI_JOB_RESULT_STATEVECTOR_DENSE)
.value("PROBABILITIES_DENSE", QDMI_JOB_RESULT_PROBABILITIES_DENSE)
.value("STATEVECTOR_SPARSE_KEYS", QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS)
.value("STATEVECTOR_SPARSE_VALUES",
QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES)
.value("PROBABILITIES_SPARSE_KEYS",
QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS)
.value("PROBABILITIES_SPARSE_VALUES",
QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES);

nb::enum_<QDMI_Program_Format>(qdmiModule, "ProgramFormat",
"Enumeration of program formats.")
.value("QASM2", QDMI_PROGRAM_FORMAT_QASM2)
Expand Down Expand Up @@ -436,6 +470,49 @@ optional and may be a string or bytes. When it is given, the device defines
what it means, which is usually a configuration for the run. A calibration run
executes no circuit, so it takes no shot count.)pb");

device.def(
"submit_programs",
[](const qdmi::Device& self, const std::vector<std::string>& programs,
const QDMI_Program_Format format, const std::optional<size_t> numShots,
const std::optional<qdmi::CustomJobParameter>& custom1,
const std::optional<qdmi::CustomJobParameter>& custom2,
const std::optional<qdmi::CustomJobParameter>& custom3,
const std::optional<qdmi::CustomJobParameter>& custom4,
const std::optional<qdmi::CustomJobParameter>& custom5) {
return self.submitPrograms(programs, format, numShots, custom1, custom2,
custom3, custom4, custom5);
},
"programs"_a, "program_format"_a, "num_shots"_a = nb::none(),
nb::kw_only(), "custom1"_a = nb::none(), "custom2"_a = nb::none(),
"custom3"_a = nb::none(), "custom4"_a = nb::none(),
"custom5"_a = nb::none(), nb::rv_policy::reference_internal,
"Submits an ordered list of text programs atomically.");

device.def(
"submit_programs",
[](const qdmi::Device& self, const std::vector<nb::bytes>& programs,
const QDMI_Program_Format format, const std::optional<size_t> numShots,
const std::optional<qdmi::CustomJobParameter>& custom1,
const std::optional<qdmi::CustomJobParameter>& custom2,
const std::optional<qdmi::CustomJobParameter>& custom3,
const std::optional<qdmi::CustomJobParameter>& custom4,
const std::optional<qdmi::CustomJobParameter>& custom5) {
std::vector<std::vector<std::byte>> bytes;
bytes.reserve(programs.size());
for (const auto& program : programs) {
const std::span value{static_cast<const std::byte*>(program.data()),
program.size()};
bytes.emplace_back(value.begin(), value.end());
}
return self.submitPrograms(bytes, format, numShots, custom1, custom2,
custom3, custom4, custom5);
},
"programs"_a, "program_format"_a, "num_shots"_a = nb::none(),
nb::kw_only(), "custom1"_a = nb::none(), "custom2"_a = nb::none(),
"custom3"_a = nb::none(), "custom4"_a = nb::none(),
"custom5"_a = nb::none(), nb::rv_policy::reference_internal,
"Submits an ordered list of exact byte programs atomically.");

device.def(
"retrieve_job_by_id",
[](const qdmi::Device& self, const std::string& jobId) {
Expand Down
6 changes: 3 additions & 3 deletions cmake/ExternalDependencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ if(BUILD_MQT_CORE_TESTS)
endif()

# cmake-format: off
set(QDMI_MINIMUM_VERSION 1.3.3
set(QDMI_MINIMUM_VERSION 1.4
CACHE STRING "Minimum QDMI version")
set(QDMI_VERSION 1.3.3
set(QDMI_VERSION 1.4.0
CACHE STRING "QDMI version")
set(QDMI_REV "18cfb67fd9042761d3005c2f8655751c1758f9c5" # v1.3.3
set(QDMI_REV "4949ecba4b61e9492160b1be96507a570bea3765" # enum-based multi-program jobs
CACHE STRING "QDMI identifier (tag, branch or commit hash)")
set(QDMI_REPO_OWNER "Munich-Quantum-Software-Stack"
CACHE STRING "QDMI repository owner (change when using a fork)")
Expand Down
5 changes: 5 additions & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ legalization
The act of replacing or rejecting IR until every remaining operation and type
satisfies a declared conversion target or target capability.

native multi-program job
One submitted job containing an ordered list of programs with a shared
lifecycle. Results use the input program indices. This differs from concurrent
submission of independent single-program jobs.

compiler target
An immutable MQT description of the operations, topology, and properties that
a compiler pipeline may use for one destination. It is a snapshot used for
Expand Down
37 changes: 37 additions & 0 deletions docs/qdmi/driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,40 @@ for device_id in registered_device_ids():
device = open_device(device_id)
print(device.name())
```

## Native multi-program jobs

`Device.submit_programs` submits an ordered list of programs with one format and
an optional shot count. A supporting provider returns one job ID and one
lifecycle for the complete list. Result index `i` refers to input program `i`,
regardless of execution order. Results are available only after every program
succeeds. Cancellation and failure apply to the aggregate job.

The list may contain one program. DDSIM supports this case; it does not yet
support larger lists. The superconducting model device does not execute jobs.

```{code-cell} ipython3
from mqt.core.qdmi import ProgramFormat

device = open_device("mqt.ddsim.default")
program = 'OPENQASM 3.0; include "stdgates.inc"; qubit q; bit c; x q; c = measure q;'
job = device.submit_programs([program], ProgramFormat.QASM3, 32)
assert job.wait()
assert job.programs_num == 1
assert job.get_counts(program_index=0) == {"1": 32}
```

Pass strings for text formats and bytes for binary formats. Binary payloads
retain every byte, including embedded NULs. Omit `num_shots` to leave the device
default unchanged. Existing single-program calls and result access without an
index continue to work; the default index is zero.

Native multi-program submission is not concurrent submission of independent
jobs. A provider may reject lists with more than one program. Applications and
SDK integrations must then retain their separate single-program workflow; they
must not represent unrelated remote jobs as one native aggregate job.

At the C interface, `QDMI_job_set_programs` replaces the program setter and
copies the whole list atomically. A rejected update leaves the previous list
unchanged. Result retrieval takes a program index. The C++ counterparts are
`Device::submitPrograms`, `Job::getProgramsNum`, and the indexed result methods.
Loading
Loading