QIR Support in the MQT¶
The Quantum Intermediate Representation (QIR) is a standardized intermediate representation for quantum programs based on the LLVM intermediate representation (LLVM IR).
Compiling and Executing QIR¶
The MQT Compiler Collection generates QIR in LLVM assembly or bitcode form. Execute this output with the DDSIM QDMI device or a compatible external QIR runtime.
See [38] for more details about QIR support in MQT.
Executing Generated QIR from Python¶
The QIR-Runner project provides
the qir-runner command-line executable and the qirrunner Python package. The
Python package can execute statically allocated Base Profile bitcode without an
intermediate file. Install it with uv pip install qirrunner, then pass the
result of to_bitcode() to run_bytes:
1from qirrunner import OutputHandler, run_bytes
2
3from mqt.core.mlir import OutputFormat, compile_program
4
5bell_qasm = """OPENQASM 3.0;
6include "stdgates.inc";
7qubit[2] q;
8h q[0];
9ctrl @ x q[0], q[1];
10bit[2] c = measure q;
11"""
12
13qir = compile_program(bell_qasm, output=OutputFormat.QIR_BASE)
14output = OutputHandler()
15run_bytes(qir.to_bitcode(), shots=4, rng_seed=7, output_fn=output.handle)
16
17# Display the records produced for the first shot.
18print(output.get_output().split("END", maxsplit=1)[0] + "END")
START
METADATA entry_point
METADATA output_labeling_schema labeled
METADATA qir_profiles base_profile
METADATA required_num_qubits 2
METADATA required_num_results 2
OUTPUT ARRAY 2 c
OUTPUT RESULT 1 c_0
OUTPUT RESULT 1 c_1
END
This path is tested for Base Profile programs with static qubit and result allocation, including dedicated one- and two-control QIS functions and the generic QIR controlled specialization used for three or more controls. QIR-Runner does not currently implement every QIR 2.1 dynamic resource management function supported by the DDSIM QDMI device. Submit dynamically allocated programs to that device instead.
QIR entry points take no arguments and return an i64 exit code. Runtime and
QIS declarations are checked before JIT compilation; a mismatched or unsupported
declaration is reported with its actual and accepted LLVM function types.
MQT Core implements the QIR 2.1 Base and Adaptive Profile runtime APIs. The JIT accepts one exact LLVM type for each runtime declaration, so unsupported or outdated overloads fail before execution.
MQT Core provides dedicated QIS functions for variants with one or two control
qubits, using the c<gate> and cc<gate> names. Operations with three or more
controls use generic __ctl and __ctladj specializations. The control qubits
are passed in an Array; parameterized and multi-target gates pass their original
arguments in a Tuple, following the QIR-Runner calling convention. MQT accepts
these functions as implementation-specific extensions to the QIR 2.1 Base and
Adaptive profiles, so the entry point keeps its base_profile or
adaptive_profile attribute.
MQT’s two-angle phased-X rotation gate uses the prx QIS stem. The incompatible
QIR-Runner Pauli-axis operation named r is not part of MQT’s QIS.
QIR Support in the DDSIM QDMI Device¶
The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR Base/Adaptive Profile Module (LLVM bitcode), and QIR Base/Adaptive Profile String (LLVM assembly).
QDMI C++ applications submit textual programs through the
Device::submitJob(const std::string&, ...) overload, which includes the
terminating null byte required by QDMI. Binary module payloads use the
Device::submitJob(std::span<const std::byte>, ...) overload instead. It
preserves embedded null bytes and submits exactly the span’s size without
appending a terminator. Job::getProgramBytes() retrieves such a payload
without interpreting its format or removing terminal null bytes; the existing
Job::getProgram() remains the textual, null-terminated accessor. It rejects
known binary and non-text formats based on their QDMI format identifier, even if
their payload happens to end in a null byte.
The Python API follows the same distinction: pass str to Device.submit_job
for a textual program and bytes for an exact binary payload.
Job.program_bytes always returns the unmodified payload, while Job.program
expects a null-terminated UTF-8 text payload and rejects known binary or
non-text formats. The num_shots argument is optional for device-defined
formats that encode their repetition count in the program payload.
Every DDSIM QIR job owns its JIT session, runtime, simulator state, random-number generator, and output settings. QIR jobs can therefore execute concurrently without sharing measurements. DDSIM records result bits directly; it does not format or retain the textual QIR output stream. Direct runtime callers can still request that stream, including its per-shot framing.
Sampling supports Base and Adaptive formats. For either profile with an acyclic, unconditional entry path, constant gate arguments, terminal Z measurements and scalar result records, DDSIM prepares the DD once and samples it for all shots. Repeated and reordered result records retain their program order, including after SWAPs. Programs with classical memory accesses, helper calls, conditional branches, resets, dynamic resources or generic controlled argument arrays use ordinary per-shot execution. These inputs remain supported by the runner; they are not eligible for this sampling optimization. A fixed seed reproduces a shot sequence for the same execution path; sequences need not match across different sampling algorithms or software versions.
When provided for static resources, required_num_qubits and
required_num_results specify capacities, and out-of-range IDs are rejected.
Extracted states include unused qubits within the declared capacity, initialized
to zero. Programs without those attributes retain the runtime’s inference of
static IDs or dynamic allocations. During sampling, dynamic qubit release resets
and recycles the simulator wire; the qubit limit applies to peak simultaneous
allocations rather than their cumulative number in a shot. Released handles
remain invalid.
Statevector extraction supports Base and Adaptive formats. Base extraction stops
before the first irreversible call and requires a terminal irreversible
region; defined or indirect helpers remain unsupported for that path.
Adaptive extraction executes classical loops, branches, dynamically computed gate arguments, dynamic allocations and direct helper calls while deferring Z measurements. A measured wire cannot participate in later gates, controls or SWAPs; independent wires may still evolve. Resets and measurement-dependent computation are unsupported. Result reads must be unused or feed only direct boolean output records. Indirect calls and unknown external functions are rejected before execution. Calls cannot re-enter the entry point. Initialization, when present, must be the first instruction of the entry point. These restrictions also apply inside helpers.
During Adaptive extraction, each executed allocation adds a zero-initialized wire. Release calls mark lifetimes without resetting or recycling simulator wires, so the exported state retains all allocated wires in allocation order, including unused and released wires. The qubit limit therefore applies to all allocations in one extraction run. Each run resets the quantum runtime. Output records are suppressed, and unsupported operations produce a failed QDMI job. Both profiles preserve global phase and logical wire order, including SWAPs. LLVM target triples must match the host architecture and operating system because the JIT executes in process.
The generic submission APIs reject QDMI calibration and batch-job formats. Use
submit_calibration_job() or
qdmi::Device::submitCalibrationJob for calibration. These APIs
accept an optional provider-defined configuration payload and no shot count; the
payload is not an executable circuit. Batch jobs contain job handles rather than
serialized program bytes and require a separate typed API.