NetQMPI is a Python library that brings the classical MPI (Message Passing
Interface) programming model to Distributed Quantum Computing (DQC). Following
a Single-Program, Multiple-Data (SPMD) paradigm, the developer writes a single
script that runs across N quantum nodes and coordinates them through
message-passing primitives — including quantum-aware ones such as qsend,
qrecv and quantum collectives — without manually orchestrating low-level
entanglement, teleportation or classical messaging.
Crucially, NetQMPI is now backend-agnostic: the same, unmodified application runs on several execution engines (a quantum-network simulator, an HPC emulator, a circuit simulator, …) simply by selecting a backend on the command line.
📖 Full documentation — installation, programming model, communication primitives, per-backend guides and the complete API reference.
- Contributors
- Architecture: a decoupled design
- Available backends
- Installation
- Quick start
- Backend hardware configuration (
--config) - Writing a new backend
- Examples
- Cite this work
Earlier versions of NetQMPI were implemented directly on top of the NetQASM SDK, which tied programs to a single execution stack. NetQMPI has since been restructured into a decoupled architecture that strictly separates what a distributed quantum program does from how and where it runs (see Vázquez-Pérez et al.):
- SDK (user-facing). Backend-agnostic abstractions:
Environment(the local node context and a factory for circuits),Circuit(a fluent gate +qsend/qrecvAPI that records operations into anOperationContainer), and theQMPICommunicator(rank/size and communication primitives). Application code depends only on these. - Runtime (execution-facing). Selects and drives a concrete backend through
the Adapter pattern + dependency injection: an
Executorbootstraps the processes and injects a backend-specific communicator into theEnvironment; aCircuitAdaptertranslates the recorded operations into native backend instructions; a concreteCommunicatormaps ranks and communication onto the platform's resources.
Because the boundary between the two layers is strict, the same app.py runs
on any backend by switching a flag — no changes to application logic.
| Backend | CLI flag | What it targets | Key dependencies |
|---|---|---|---|
| NetQASM / SquidASM | --netqasm |
Low-level quantum-network simulation (EPR sockets, NetQASM routines) | squidasm, netsquid, netqasm 2.x, Python ≥ 3.9 |
| NetQASM / SquidASM (legacy) | --netqasm1.0 |
The same backend against the older NetQASM release | squidasm, netsquid, netqasm 1.x |
| CUNQA | --cunqa |
HPC emulation of DQC through virtual QPUs (vQPUs) | cunqa (HPC / Slurm environment) |
| Qiskit Aer | --aer |
Shot-based circuit simulation (swap- or teleportation-based transfer) | qiskit, qiskit-aer |
| Qoala | --qoala |
Quantum-internet node execution environment with task scheduling & multitasking — simulation only | qoala, netsquid, netqasm 2.x, Python 3.10–3.12 |
NetSquid account. The NetQASM and Qoala backends depend on NetSquid, which requires a (free) account and is installed from its private index:
pip install netsquid --extra-index-url https://<user>:<pwd>@pypi.netsquid.org.NetQASM 1.x vs 2.x — and why they still need separate environments.
--netqasmtargets NetQASM 2.x and--netqasm1.0the older 1.x. Both drive the same adapter: the API this backend uses —Qubit,EPRSocket,NetQASMConnection,Socket, theApplication/ApplicationInstance/Programtrio and SquidASM'ssimulate_application— is unchanged between the two releases, and SquidASM is a pure-Python wheel that accepts either. The flag selects an environment, and the run stops immediately, naming both versions, if the one installed is not the one asked for.Two boundaries are real and worth knowing:
- NetQASM 2.x needs Python ≥ 3.9 (it uses PEP 585 generics), so it cannot be dropped into a 3.8 environment built for 1.x.
- SquidASM and Qoala still cannot share an environment, but not because of NetQASM: they require incompatible majors of
netsquid-magic(15.x and 14.x respectively). Installing one over the other leaves the displaced backend unable to build a link layer.The adapter also stays inside the instruction set both NetQASM releases share. 2.x adds more —
Qubit.swap, for one — but the SquidASM release available here does not execute those, and reaching for one hangs the simulation rather than failing, so a swap is still assembled from three CNOTs. CUNQA and Aer have no such constraints.Qoala is simulation-only. It models the software/hardware architecture of a quantum-internet node on NetSquid; it is not a path to real-hardware execution.
Install the core package with pip:
pip install netqmpiThen install the backend(s) you intend to use. Each backend lives behind a lazy import, so you only need the dependencies of the backend you actually run.
NetQASM / SquidASM backend
pip install squidasm --extra-index-url https://<user>:<pwd>@pypi.netsquid.org
# pulls in netsquid and netqasm 1.xSee the NetQASM installation docs.
Qoala backend (separate environment, simulation only)
conda create -n qoala python=3.11 -y
conda activate qoala
pip install netsquid --extra-index-url https://<user>:<pwd>@pypi.netsquid.org
pip install qoala --extra-index-url https://<user>:<pwd>@pypi.netsquid.org # netqasm 2.x
pip install netqmpiCUNQA backend (HPC, or a container on your own machine)
On a cluster: install and configure
CUNQA (it provisions vQPUs via
the job scheduler), then install netqmpi in the same environment.
On an ordinary computer, use the container — it packs a single-node SLURM, CUNQA and NetQMPI together, so your machine stands in for the HPC environment:
docker pull jvazquezperez/cunqa_netqmpi
docker run --rm -it -p 8888:8888 jvazquezperez/cunqa_netqmpiSLURM and a Jupyter server on port 8888 (token cunqa) come up, and you get a
shell. It is the real backend, so it also runs out of room like one: a single
machine tops out around five or six ranks. See the
CUNQA backend page for
mounting your own checkout and sizing the vQPUs.
Qiskit Aer backend
pip install qiskit qiskit-aerNetQMPI is launched with an MPI-like command that selects the number of nodes and the backend:
netqmpi -n <NUM_NODES> app.py --netqasm # quantum-network simulation
netqmpi -n <NUM_NODES> app.py --cunqa --shots 1024 # HPC vQPU emulation
netqmpi -n <NUM_NODES> app.py --aer --shots 1024 # circuit simulation
netqmpi -n <NUM_NODES> app.py --qoala --shots 100 # Qoala node exec. environmentThe example below (examples/1_send_recv.py) prepares a qubit in
superposition on one node and teleports it to a neighbour with qsend/qrecv.
It uses only SDK abstractions, so the very same file runs on every backend:
from netqmpi.sdk.environment import Environment
def main(env: Environment = None):
comm = env.comm
rank = comm.rank
next_rank = comm.get_next_rank(rank)
previous_rank = comm.get_prev_rank(rank)
with comm: # everything inside this block is executed on the backend
if rank == 0:
circuit = env.create_circuit(num_qubits=1, num_clbits=1)
circuit.h(0) # prepare |+>
comm.qsend(circuit, [0], next_rank) # teleport the qubit
else:
circuit = env.create_circuit(num_qubits=1, num_clbits=1)
comm.qrecv(circuit, [0], previous_rank)
circuit.measure(0, 0)
results = comm.results
if rank != 0:
print(f"measure: {results}")
else:
print("teleportation complete")netqmpi -n 2 examples/1_send_recv.py --netqasm
netqmpi -n 2 examples/1_send_recv.py --qoala --shots 100The programmer only invokes comm.qsend() / comm.qrecv(); entanglement
generation, teleportation and classical corrections are handled by the selected
backend adapter.
qscatter and qgather are MPI_Scatter and MPI_Gather with qubits instead
of bytes. Both are collective — every rank of the communicator must call
them — and rooted: the root passes the whole buffer and every other rank
passes its own chunk. The call returns the local qubits holding this rank's
share.
Since a quantum state cannot be copied, the chunks are moved: each one is
teleported with the same qsend/qrecv machinery as above, so the sending side
does not keep the data. That is also where qscatter parts company with
MPI_Scatter: the root's buffer is split into one chunk per rank other than
the root, which keeps nothing back — scatter two qubits over two other ranks
and the root ends up empty-handed, its slots back in |0>. A qgather is the
other way round: the root's buffer has one slot per rank, its own contribution
already in place, and the contributors are left with |0> once their qubits
have moved. The qubits a chunk lands on must be in |0> when the call is
reached, exactly as for a plain qrecv.
with comm:
if rank == ROOT:
# One qubit for each of the other ranks; the root gives them all away.
circuit = env.create_circuit(num_qubits=size - 1, num_clbits=size - 1)
for q in range(size - 1):
circuit.x(q)
comm.qscatter(circuit, list(range(size - 1)), root=ROOT)
circuit.measure_all() # reads 0 everywhere
else:
circuit = env.create_circuit(num_qubits=1, num_clbits=1)
mine = comm.qscatter(circuit, [0], root=ROOT) # lands on qubit 0
circuit.measure(mine[0], 0)Each transfer borrows one communication qubit and two protocol classical bits and
gives them straight back, so a whole scatter costs the same resources as a single
qsend. examples/3_scatter.py and examples/4_gather.py run the
two collectives end to end.
Moving a qubit is not always what a distributed algorithm needs. When several ranks only want to apply gates controlled by a remote qubit — the crossing rotations of a QFT, for instance — the qubit can stay where it is and be lent to them instead, through a shared GHZ state (telegate).
expose and unexpose are collective, like an MPI_Bcast: every rank of
the window must call them, and root names the rank lending the qubit. The call
returns the index each rank must use as control — its own data qubit on the root,
a freshly reserved communication qubit on every receiver — so the gate itself is
written exactly like a local one:
with comm:
circuit = env.create_circuit(num_qubits=1, num_clbits=1)
# Rank 1 lends its qubit 0 to rank 0, which drives a CS with it.
control = comm.expose(circuit, 0, [0], root=1)
if rank == 0:
circuit.h(0)
circuit.cs(control, 0)
comm.unexpose(circuit, [0], root=1) # the control goes back untouchedCommunication qubits and the classical bits carrying the protocol corrections are
reserved when a window opens and released when it closes, so windows that do not
overlap reuse the same resources and user classical bits are never clobbered.
examples/5_qft_expose.py builds a full 3-rank QFT this way.
Backend-specific parameters are passed through a single YAML file with --config
(instead of a proliferation of per-backend flags). The file has generic settings
at the top level plus an optional block named after the backend; only the block
for the selected backend is read. For example, configuring the Qoala qdevice and
its entanglement link:
# config.yaml
shots: 1000
seed: 7
qoala:
link_fidelity: 0.8 # EPR-pair fidelity in [0.25, 1.0]
hardware: # qdevice noise model (omit for a perfect device)
t1: 0
t2: 0
single_qubit_gate_depolar_prob: 0.1
two_qubit_gate_depolar_prob: 0.0netqmpi -n 2 app.py --qoala --config config.yamlA run needs one vQPU per rank, and by default it expects them to be already raised, so one allocation can serve many runs:
qraise -n 3 -t 00:10:00 --quantum_comm --co-located # once
netqmpi -n 3 examples/3_scatter.py --cunqa # as often as you likeThe family may hold more vQPUs than the run needs — three raised, -n 2
run — and the extra ones cost the program nothing. CUNQA runs a single executor
per family and it starts a round only once every vQPU of that family has
submitted something, so a vQPU left out would not sit idle, it would hang the
run; NetQMPI hands each spare one a trivial circuit instead and discards its
counts.
What a run cannot do is spread across families, since each family is executed
on its own. If vQPUs of several families are up, name the one to use with
family: in the cunqa block.
If there are no vQPUs up at all, the run stops before building anything and says what to raise.
To have NetQMPI raise them for the run and drop them again afterwards, ask for
it in the cunqa block. backend is the vQPU definition file the vQPUs are
raised with, which is what fixes the qubit budget of the run:
# cunqa.yaml
shots: 1024
cunqa:
qraise: true # raise for this run, drop after it
backend: examples/qft_expose.json # vQPU definition (qubit budget)
time: "00:10:00" # SLURM reservation
simulator: Munichnetqmpi -n 3 examples/3_scatter.py --cunqa --config cunqa.yamlSize that definition to what the program needs. The executor simulates the
whole family in one register, spanning every qubit each vQPU declares
whether the circuits use it or not, so the cost of a run is set by
num_qubits × the number of ranks — not by the circuits. With a statevector
simulator that register is 2^N amplitudes:
| vQPU definition | -n 2 |
-n 3 |
-n 4 |
-n 5 |
|---|---|---|---|---|
[4, 4] — 8 qubits each |
1 MiB | 256 MiB | 64 GiB | 16 TiB |
[3, 2] — 5 qubits each |
16 KiB | 512 KiB | 16 MiB | 512 MiB |
That is why simulator matters. NetQMPI defaults to Munich, whose decision
diagrams keep a mostly-idle register small, so oversized vQPUs go unnoticed.
CUNQA's own default, Aer, allocates the dense statevector and reinitialises
it once per shot, so the same program on generous vQPUs turns into a run that
never seems to finish — it is waiting on the simulator, not deadlocked.
examples/cunqa_backend.json is sized for the examples and runs on either.
It fits them all up to three ranks; 4_gather.py's root holds one qubit per
rank, so a four-rank run of it wants [4, 2].
family picks which raised vQPUs to attach to, or names the family to raise,
and co_located has to match how they were raised. backend, time and
simulator only mean anything when qraise: true, so setting them while
attaching to running vQPUs is reported rather than silently ignored.
Adding a backend never requires touching the SDK. Following the architecture
above, you provide three Runtime components under
netqmpi/runtime/adapters/<backend>/ (mirroring the existing netqasm/,
cunqa/, aer/, qoala/ packages):
Executor(subclass ofnetqmpi.runtime.executor.Executor) — bootstraps the execution environment, discovers resources, and injects a backend-specific communicator into each node'sEnvironment(build_apps+run).CircuitAdapter(subclass ofnetqmpi.sdk.circuit.Circuit) — implements the_translate_*hooks that map the abstract operations recorded in theOperationContainer(local gates,measure,qsend/qrecv, …) to the backend's native instructions. Operations deriving fromCollectiveOperation(expose/unexpose) are the exception: if the backend expands them into all the participating circuits at once, as CUNQA's cat-entangler does, the adapter translates the ranks jointly, stopping each of them at the matching collective (seetranslate_groupin the CUNQA adapter).QMPICommunicator(subclass of the abstract communicator) — maps rank / size and the communication primitives onto the backend's real resources, and triggers execution on context exit.
Finally, register a --<backend> flag in netqmpi/runtime/cli.py (with a lazy
import so users without that backend's dependencies are unaffected). The
integration workflow is identical for every backend; only the realization of
these three components differs.
Ready-to-run scripts live in examples/:
1_send_recv.py (distributed superposition / teleportation),
2_round_robin.py, 3_scatter.py, 4_gather.py, 5_qft_expose.py.
examples/frequent_errors/ collects programs that are
meant to fail, one failure mode each, with a run_all.py that reports what
NetQMPI says about every one of them without needing any vQPU.
Validation experiments for the Qoala backend (hardware-parameter propagation, EPR
fidelity sweep, and scheduling/multitasking) are documented in
scripts/experiments/.
If you use NetQMPI in your research, please cite the following works:
NetQMPI: An MPI-Inspired Library for Programming Distributed Quantum Applications Over Quantum Networks Using NetQASM SDK
F. Javier Cardama, Jorge Vázquez-Pérez, Tomás F. Pena, Andrés Gómez IEEE Access, Vol. 14, 2026, pp. 125459-125475 DOI: 10.1109/ACCESS.2026.3723566
Jorge Vázquez-Pérez, F. Javier Cardama, Tomás F. Pena, Andrés Gómez Proceedings of the IEEE International Conference on Distributed Computer Systems (ICDCS 2026). PDF: PDF Paper
F. Javier Cardama, Jorge Vázquez-Pérez, C. Piñeiro, T. F. Pena, J. C. Pichel, Andrés Gómez Future Generation Computer Systems, Vol. 174, 2026, Article 107989 DOI: 10.1016/j.future.2025.107989
