✨ Add program serializers to the Qiskit backend and hand IQM JSON to QDMI-on-IQM - #2114
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
burgholzer
left a comment
There was a problem hiding this comment.
Hey @marcelwa 👋🏼
Thanks a lot! I generally did not think too much about the replacement of the custom converters in MQT Core when the initial issue was created and thought it could simply be removed. But I now fully see and agree that it needs some kind of conversion registration that external packages can provide.
I purposely only reviewed the ExecPlan here and added requests for changes to it because I think this most effectively steers the implementation in the right direction.
I hope the comments themselves make sense.
Address the review on #2114: rename the seam from "program codec" to program serializer, split the signature into text and binary payloads, encode the format preference as one explicit order, register MQT Core's own OpenQASM serializers through the same registry, record the Qiskit provider cross-check and the mid-term mqt-cc outlook, and restructure the plan to follow .agent/PLANS.md.
burgholzer
left a comment
There was a problem hiding this comment.
Thanks @marcelwa 🙏🏼 This is looking great already!
I have a couple of requests for changes inline; some of them are related to pre-existing code that may not be fully ideal.
More generally, I believe this PR's title and description should be updated to better match what the PR adds.
burgholzer
left a comment
There was a problem hiding this comment.
This feels like it is ready to go in 🚀
I'll wait for the stacked PR to also turn green and merge this as one stack afterwards! Then, I'll likely kick off a backport after the PennyLane PR is merged so that this makes it onto the v3.x branch and into the 3.9 release.
MQT Core's Qiskit adapter converted circuits to IQM JSON and shipped an IQM MoveGate, putting one vendor's format inside a generic adapter. No device MQT Core ships advertises IQM JSON. Add a program codec registry: a package that owns a device registers the conversion into its format, either through the mqt.core.qiskit.program_codecs entry point group or through register_program_codec. The backend prefers a registered codec over its built-in OpenQASM 3 and OpenQASM 2 exporters, so installing iqm-qdmi keeps IQM JSON submission working, including for a plain QDMIBackend opened through QDMIProvider. A subclass supplies a device-native gate outside Qiskit's standard library through _EXTRA_GATES. Remove qiskit_to_iqm_json and MoveGate. Assisted-by: claude-opus-5 via Claude Code
Assisted-by: claude-opus-5 via Claude Code
Address the review on #2114: rename the seam from "program codec" to program serializer, split the signature into text and binary payloads, encode the format preference as one explicit order, register MQT Core's own OpenQASM serializers through the same registry, record the Qiskit provider cross-check and the mid-term mqt-cc outlook, and restructure the plan to follow .agent/PLANS.md.
`src/qdmi/Client.cpp` already decided which program formats carry a binary payload and which carry no program payload at all, but it kept both predicates in an anonymous namespace. The Python side needs the same answers to choose a program serializer, and a second copy would drift the first time QDMI adds a format. Move `isBinaryProgramFormat` and `hasProgramPayload` into the public `include/mqt-core/qdmi/Client.hpp` and bind them as `mqt.core.qdmi.is_binary_program_format` and `mqt.core.qdmi.has_program_payload`. The private helper stated the negative, `hasNoGenericProgramPayload`; the public one states the positive. Both `Device::submitJob` overloads behave as before. The new C++ test states its expectations through a switch with no default case, so a program format added to QDMI later produces an unhandled-enumerator warning instead of an unnoticed classification.
…egistry Qiskit calls this operation serialization: `qiskit.qasm3.dumps` serializes a circuit into a string and `qiskit.qpy.dump` writes the binary form. IQM uses the same word. "Codec" is not established in this community, and "translator" would collide with basis translation, which the same adapter relies on. Rename the registry module to `serializers.py` and the entry point group to `mqt.core.qiskit.program_serializers`. Split the callable into `TextProgramSerializer`, which returns `str`, and `BinaryProgramSerializer`, which returns `bytes`. A program format fixes the kind of payload it carries, so one registry keyed by format is enough and the backend checks the returned type before submission. A serializer takes the backend rather than the device, because MQT Core's own OpenQASM 3 serializer needs the Target to decide which gate definitions to suppress. `QDMIBackend` gains a public `device` property so a serializer can still reach the device. Add `PROGRAM_FORMAT_PREFERENCE`, an ordered tuple, and `preferred_program_formats`, which applies it to the formats a device reports and drops any that carry no program payload. A device-native format comes first, because a package that registers a serializer for its own device's format wants that format used. One tuple in one module means the whole policy can be read, and changed, in one place. Register MQT Core's own OpenQASM 2 and OpenQASM 3 serializers through the same registry and reduce `_convert_circuit` to `_serialize_circuit`: one walk over the preferred formats that calls the first registered serializer it finds. The backend now holds no format-specific branch, and a provider whose device needs a different OpenQASM 3 export can replace ours the way it registers anything else. A binary payload travels to `submit_job` as `bytes`. `register_program_serializer` does not read the entry points. A registration must be able to precede them, because that is what gives it precedence, and because `backend.py` registers its own formats while the adapter is still importing. Milestone 4 of the plan needed no code change: the `_EXTRA_GATES` seam, the classmethod gate maps, and the removal of `converters.py` and `gates.py` already match the revised design. Only the package exports moved to the serializer names.
Rewrite the "Program Codecs" section of the QDMI backend guide as "Program Serializers": what a serializer is, the two payload signatures and how the program format decides between them, the entry point declaration, the runtime call, and the preference order with the reasoning behind it. Update the numbered list that describes what happens when a circuit runs. Bring the changelog and upgrade guide to the serializer names, add an entry for the two new `mqt.core.qdmi` functions, and give the upgrade guide an example serializer that takes the backend and returns bytes. Record the outcome, the two ordering discoveries, and the parameter-name change in the ExecPlan.
An earlier draft grouped the QIR formats together and ranked both profiles above QPY and OpenQASM 3, which treats QIR as one capability tier. The QIR base profile forbids classical feedback and mid-circuit control, while QPY carries a Qiskit circuit without loss and OpenQASM 3 expresses control flow, so both belong between the two QIR profiles. Shorten the changelog entries to one sentence each, as the surrounding entries are, and split the upgrade guide into one section for the serializer concept and one for the IQM JSON move.
An earlier revision also bound `has_program_payload`, renamed from the private `hasNoGenericProgramPayload`. Dropping "generic" made the name claim more than QDMI supports: QDMI declares `CALIBRATION` as `void*` "A calibration program" and says only that triggering a calibration run does not require a program to be set, so its payload is optional rather than absent, while `BATCH_JOB` takes a list of job handles rather than a byte blob. One predicate cannot state both. Withdraw that binding and restore the private helper under its original name, so `Device::submitJob` behaves and reads as before. The serializer registry now states what it actually needs on its own terms: which formats can hold a serialized circuit.
Two module globals guarded discovery with a boolean that meant both "loading" and "loaded", and the tests reached in to monkey-patch them. `functools.cache` cannot replace the flag: it records its result only after the call returns, so it does not guard the re-entry that happens while an entry point imports third-party code. Move both to a private registry object. It publishes the discovered serializers in one step at the end, so a re-entrant lookup sees the same thing whatever order the entry points arrive in; it restores the cold state when discovery aborts, so unreadable distribution metadata no longer marks the registry loaded forever; and it takes the discovery function as an argument, so a test builds an isolated registry rather than patching module state. Discovery is still not thread-safe, as before, and says so. Cover re-entrant discovery, registration during discovery, retry after an aborted discovery, and reading the entry points once.
Replace the process-wide registry in the facade test so the test does not load installed entry points or retain serializers for later tests. State the registry concurrency limit without describing one possible race. Assisted-by: GPT-5 via Codex
7d6fcc4 to
b8d2902
Compare
🤖 AI text below 🤖
Description
This adds a registration seam that lets any package supply the conversion from a Qiskit circuit into a program format, and moves MQT Core's own vendor-specific conversion out through it.
The Qiskit backend used to decide in its own code how to turn a circuit into a program, with one branch per format. It now takes every format from a registered program serializer, and MQT Core registers its OpenQASM 2 and OpenQASM 3 serializers the same way as everyone else.
_serialize_circuitis one ordered walk with no format-specific branch left.That makes the vendor carve-out possible. MQT Core carried
qiskit_to_iqm_jsonand an IQMMoveGateinside its vendor-neutral adapter; nothing in Core used either, and the sole consumer is the externaliqm-qdmipackage. Removing them alone would have broken IQM execution, because the IQM QDMI device advertises onlyQIR_BASE_STRINGandIQM_JSON, with no OpenQASM to fall back to. Registered through the seam, IQM JSON keeps working from outside Core.The seam
A new
mqt.core.plugins.qiskit.serializersmodule holds a registry keyed byProgramFormat. A program serializer turns one circuit into one program in one format.A format fixes the kind of payload it carries, so there are two signatures:
The serializer takes the backend, so it can reach the device through the new
QDMIBackend.deviceproperty and the supported operations throughQDMIBackend.target. The API isregister_program_serializer(fmt, fn, *, replace=False),unregister_program_serializer(fmt),program_serializer(fmt), andpreferred_program_formats(formats).A package advertises a serializer through an entry point group:
Entry points rather than only a runtime call, because
QDMIProviderbuilds a plainQDMIBackendfor every registered device: a user can reach a registered IQM device without ever importingiqm.qdmi, and an import-time registration would miss that path. An entry point naming an unknown format, naming a format with no program payload, or failing to import warns and is skipped, so one broken package cannot hide the others.Core registers its own OpenQASM 2 and OpenQASM 3 serializers through the same registry, so
_serialize_circuitis one ordered walk with no format-specific branch, and a provider can replace the built-ins withreplace=True.Which format wins
PROGRAM_FORMAT_PREFERENCEstates the order explicitly, most preferred first:A device-native format comes first, because a package that registers a serializer for its own device's format wants that format used, and that is the precedence IQM JSON had. The standardized formats follow in order of what a circuit may contain: the QIR adaptive profile allows classical control, QPY carries a Qiskit circuit without loss, and OpenQASM 3 expresses control flow, while the QIR base profile forbids classical feedback and OpenQASM 2 has no control flow at all. Encoding only breaks a tie within one profile.
CALIBRATIONandBATCH_JOBare absent because a serialized circuit is not what they carry. The list decides, not the order the device reports —test_backend_respects_format_preferencecovers that.Text and binary payloads
Core's C++ client already knew which formats are binary and which carry no program at all:
src/qdmi/Client.cpphadisBinaryProgramFormatandhasNoGenericProgramPayloadin an anonymous namespace, and the textsubmitJoboverload rejects a binary format. Rather than restate that in Python, both move toinclude/mqt-core/qdmi/Client.hppand are bound asmqt.core.qdmi.is_binary_program_formatandmqt.core.qdmi.has_program_payload. The backend checks a serializer's return type against its format and raisesTranslationErroron a mismatch.Device.submit_jobalready overloads onstr | bytes, so a binary payload reaches the device unchanged.The gate
MoveGatemoves toiqm.qdmi.gates.QDMIBackendgains an empty_EXTRA_GATESclass variable and an__init_subclass__that rebuilds the gate maps per subclass, so a backend for a device with a native gate outside Qiskit's standard library supplies it without global state._map_operation_to_gateand_map_qiskit_gate_to_operation_namesbecome classmethods so a subclass reads its own maps. Every Qiskit provider with a non-standard native gate does the same:GPIGateand friends in qiskit-ionq,HamiltonianGatein qiskit-pasqal-provider,MoveGatein qiskit-on-iqm.Deliberately left alone
"r": {"prx"}alias stays in_GATE_ALIASES.prxis IQM terminology, but Core's own SC device model uses it and 🧹 MQT Core v4 cleanup and ownership tracking #2085 keeps that device supported.json/sc/iqm-garnet.jsonandjson/sc/iqm-emerald.jsonstay: device models, not conversion logic.TranslationErrorkeeps its name. It belongs to a hierarchy shared with the PennyLane plugin (PennyLaneTranslationErrorand friends), so renaming it is a separate cross-plugin change.Related
The downstream half is iqm-finland/QDMI-on-IQM#189, which adds the serializer, the
MoveGate, and the entry point. It pins this branch and stays open until MQT Core v4 ships. It must follow this PR's renames:iqm.qdmi.serializers, themqt.core.qiskit.program_serializersgroup, and the(circuit, backend)signature.This also reframes #1348: QIR and QPY support becomes a serializer registered by whoever owns the format rather than another branch in the backend.
Design notes, including the Qiskit ecosystem cross-check and the decision log, are in
.agent/plans/2094-iqm-conversion-carveout.md.Fixes #2094
Validation
uv run --no-sync pytest test/python— 619 passed, 4 skippeduv run --no-sync pytest test/python/plugins/qiskitandtest/python/qdmi— 356 passed./build/release/test/qdmi/mqt-core-qdmi-test— 274 passed, no compiler warningsuvx nox -s stubs— the stub diff contains only the two new functionsuvx nox --non-interactive -s docs— passes with-W -nuvx nox -s lint— passes, fullprekhook setBuilt against LLVM/MLIR 22.1.0. Not verified locally: the cross-repository check with
iqm-qdmiinstalled alongside, which needs QDMI-on-IQM#189.Checklist
If PR contains AI-assisted content:
🤖 *AI text below* 🤖(titles are exempt).AI assistance: written by Claude Opus 5 via Claude Code, under my direction. The last checkbox is left for me to tick after my own review.