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).
The QIR Runtime in MQT Core¶
MQT Core provides a runtime for QIR that is based on its decision diagram-based quantum simulator. This allows for the execution of QIR programs using MQT Core’s high-performance simulation capabilities.
The runtime can be utilized in two ways:
As a standalone library that can be linked to any QIR program, resulting in a binary executable.
By using the
mqt-core-qir-runnercommand-line tool, which interprets QIR programs directly.
See [38] for more details.
Building the Runner¶
The runner is part of every MQT Core build. From the root of the repository, you can build it as follows:
cmake -S . -B build
cmake --build build --target mqt-core-qir-runner
After building, the tool can be found in the build directory under
bin/mqt-core-qir-runner.
Executing a QIR Program¶
The mqt-core-qir-runner can be used to execute a QIR file (typically with a
.ll extension).
./build/bin/mqt-core-qir-runner bell.ll
The entry-point function may have any valid LLVM name. If a module contains more
than one function with the entry_point attribute, select one explicitly. The
runner also supports repeated, reproducible execution:
./build/bin/mqt-core-qir-runner \
--entry-point=bell_entry --shots=1024 --seed=7 bell.ll
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 MQT runner and DDSIM QDMI device; use those MQT runtimes for dynamically allocated programs.
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.
The runner prints the program’s outputs to the console in one of the two
QIR Output Schemas (Labeled or Ordered): the two HEADER
records announce the schema, and each shot is wrapped in START and END
records with a METADATA\toutput_labeling_schema\t<schema> line inside.
The active schema is selected by the output_labeling_schema function attribute
on the entry-point function of the QIR program. The value ordered selects
Ordered; anything else, or a missing attribute, selects Labeled.
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.
Every DDSIM QIR job owns its JIT session, runtime, simulator state,
random-number generator, and output sink. QIR jobs can therefore execute
concurrently without sharing measurements or interleaving runtime output.
Sampling supports Base and Adaptive formats. Statevector extraction is limited
to Base formats: the JIT stops the selected entry point immediately before the
first call to a function marked irreversible, following the semantic boundary
defined by the Base Profile. It rejects other profiles and Base Profile programs
whose irreversible region is not terminal.
The generic submission APIs intentionally reject QDMI calibration and batch-job formats. Calibration jobs do not carry a program, while batch jobs contain job handles rather than serialized program bytes. Their format identifiers remain available for capability discovery; they require dedicated typed APIs.