QuPort is a research software framework developed in Python using the Qiskit toolkit for modeling, mapping, routing, splitting, scheduling, and benchmarking quantum circuits on modular multi-QPU machines. It treats the machine as a collection of QPUs with local compute qubits, communication-port qubits, an inter-QPU network, finite link capacity, finite port count, and a configurable latency model.
The central problem solved by QuPort is:
Given a logical quantum circuit
that assigns every logical qubit to one of
that places logical qubits on physical compute or communication qubits, and finally estimate or generate executable local programs plus remote-operation metadata while respecting capacity, topology, and routing constraints.
QuPort supports two complementary compilation modes:
- Global mapping and routing: build one global directed Qiskit
CouplingMapfor all QPUs, provide a partition-aware initial layout, and let Qiskit/SABRE route the full circuit on the global graph. - Distributed compilation: partition the circuit, assign physical qubits, keep cross-QPU two-qubit operations as explicit remote events, split local operations into per-QPU circuits, and route only inside each QPU so remote execution is not hidden behind artificial cross-device SWAPs.
QuPort implements an end-to-end stack for multi-QPU circuit experiments:
- Modular device construction with
$N$ QPUs,$C$ compute qubits per QPU, and$P$ communication qubits per QPU. - Local QPU topologies:
clique,line,ring, andgrid2d. - Inter-QPU network topologies:
switch,mesh,ring,degree_d,clos, andfat_tree. - Directed Qiskit coupling maps where every undirected physical link is represented by two directed Qiskit edges.
- Logical interaction-graph extraction from arbitrary two-qubit circuit instructions.
- Optional temporal interaction weights that emphasize earlier two-qubit gates.
- Capacity-constrained partitioning baselines and topology-aware partitioning.
- Communication-port placement hints for boundary-heavy and neighbor-diverse logical qubits.
- Global transpilation with configurable basis gates, layout method, routing method, optimization level, and seed.
- Distributed compilation into per-QPU OpenQASM 3 programs, remote-operation JSON, and schedule JSON.
- Schedule estimation under QPU-port, link-capacity, network-hop, switch-pair, and switch-reconfiguration constraints.
- Metrics for SWAP count, depth, circuit size, one-qubit gates, two-qubit gates, remote two-qubit operations, cut weight, congestion, remote rounds, peak link utilization, and makespan.
- CLI commands for configuration generation, topology inspection, mapping, benchmarking, topology sweeps, schedule estimation, splitting, and distributed compilation.
- Programmatic APIs for custom pipelines and automated experiments.
A QuPort device is configured with MultiQPUConfig.
Let:
-
$N$ ben_qpus. -
$C$ becompute_qubits_per_qpu. -
$P$ becomm_qubits_per_qpu. -
$B=C+P$ be the physical block size of one QPU. -
$Q_{\mathrm{phys}}=N(C+P)$ be the total physical qubit count.
For QPU
The physical-to-QPU map is:
For each QPU, QuPort builds local edges over compute + comm qubits:
intra_topology |
Meaning | Typical use |
|---|---|---|
clique |
Every local qubit connects to every other local qubit. | Idealized all-to-all QPU. |
line |
Local qubits form a path. | Strict nearest-neighbor baseline. |
ring |
Local qubits form a cycle when possible. | Slightly richer nearest-neighbor model. |
grid2d |
Local qubits are placed row-major on a 2D grid. | Planar/local-lattice style devices. |
For an undirected local edge
Inter-QPU edges are created only between communication qubits.
inter_topology |
Meaning |
|---|---|
switch |
All QPU pairs can communicate through a switch-like all-to-all model. |
mesh |
All QPU pairs are adjacent in the QPU graph. |
ring |
QPU |
degree_d |
Each QPU connects to a bounded number of nearby QPUs controlled by inter_degree. |
clos |
Two-level approximation with pod-local and spine-style links when at least two ports exist. |
fat_tree |
Tree-like QPU graph; physical inter-QPU adjacency uses representative communication ports. |
The QPU graph is an undirected graph
For scheduling and congestion, shortest paths are computed on
If no path exists, QuPort treats the pair as unreachable and assigns a large unschedulable penalty in topology-aware estimators.
For a circuit
The weighted logical interaction graph is:
The weighted degree of logical qubit
For strategies that use temporal weighting, QuPort orders two-qubit interactions by their two-qubit-operation index
where temporal_decay is
For an edge
where
Two consequences are worth keeping in mind when choosing
- The total weight of a circuit converges to
$\sum_{t\ge 0}\gamma^{t}=1/(1-\gamma)$ regardless of how long the circuit is. For the default$\gamma=0.98$ that is$50$ , and$99%$ of it falls in the first$\approx 228$ two-qubit operations. On a circuit with thousands of two-qubit gates the partitioner is therefore driven almost entirely by a prefix, which is the intended emphasis but is easy to overlook. - Once
$\gamma^{t}$ underflows to zero in double precision (at$t=36{,}883$ for$\gamma=0.98$ ), an edge that first appears after that point contributes exactly zero and is dropped from the interaction graph.
Which pipelines apply temporal weighting is not uniform, and it matters when comparing strategies:
| entry point | tpccap |
tpccap_sa |
|---|---|---|
compile_distributed |
temporal_decay, default |
temporal_decay, default |
map_and_transpile |
temporal_decay, default uniform counts |
temporal_decay, default |
Both entry points accept temporal_decay, and both ignore it for balanced and
cluster, which always partition on uniform interaction counts. What differs is
the default: compile_distributed applies map_and_transpile leaves tpccap on uniform counts and puts
tpccap_sa on
That default asymmetry matters when reading benchmark output. benchmark_random_circuits
and sweep_topologies both call map_and_transpile without a decay, so their method=2
and method=3 rows differ in the interaction weights as well as the search procedure and
are not an ablation of the annealing alone. On random circuits much of the gap between
those rows comes from the weighting rather than the annealing, and tpccap_sa can score
worse on uniform cut precisely because it is optimizing an early-weighted objective. To
measure the annealing itself, pass the same temporal_decay to both strategies, or use
compile_distributed, whose defaults already match.
Each QPU can host at most
logical qubits in the global mapping model. A partition
A two-qubit interaction is remote when its endpoints are assigned to different QPUs. The partition cut is:
A lower cut usually means fewer remote two-qubit operations, although final routed metrics also depend on layout, topology, and Qiskit routing.
For a partition
for
This matrix quantifies the amount of logical interaction weight that must cross between QPUs.
For each traffic pair
In single-path mode, traffic follows one shortest path. If path edges are
then each undirected link
In ECMP mode, traffic is split evenly across all shortest paths. If there are
QuPort reports congestion metrics:
and
QuPort supports four main partitioning strategies.
This baseline uses a disjoint-set union structure.
- Sort interaction edges by descending weight.
- Merge clusters connected by heavy edges when the merged cluster size stays within capacity
$K$ . - Place clusters into QPUs with first-fit decreasing bin packing.
- If a cluster cannot be placed whole, place its vertices individually.
The guiding idea is that large
The balanced greedy strategy orders logical qubits by descending weighted degree. When placing a qubit
where alpha_balance and
The first term rewards placing
After greedy placement, QuPort runs local move refinement. Moving vertex
tpccap extends cut minimization with architecture-aware terms. It considers:
- cut weight;
- QPU-network hop distance;
- communication-port pressure;
- routed link congestion;
- disconnected-pair penalties;
- load balance.
A simplified objective has the structure:
The terms mean:
-
$\mathrm{cut}(\pi)$ counts remote interaction weight. -
$\sum T_{ab}d(a,b)$ prefers remote traffic between nearby QPUs. -
$L_2$ penalizes concentrating routed traffic on the same network links. -
$\Phi_{\mathrm{port}}$ penalizes boundary pressure that exceeds available communication ports. -
$\Phi_{\mathrm{bal}}$ discourages imbalanced QPU loads. -
$\Phi_{\mathrm{disc}}$ penalizes traffic between disconnected QPU pairs.
The implementation validates all numeric controls and normalizes inputs before search so invalid capacities, probabilities, infinities, booleans, negative weights, malformed matrices, and disconnected routing cases fail deterministically or are penalized consistently.
tpccap_sa starts from the topology-aware partition and then performs simulated annealing moves. If a candidate move changes the objective by
then QuPort accepts the move when
where
The objective w_cong = 0.05, matching tpccap, while the annealing optimizes
anneal_w_cong = 0.2 — four times the congestion penalty. The annealing returns the
best state it saw under its objective, so it never loses ground there, but the
partition it hands back can score worse than its own seed when measured with the
seed's weighting; on random instances that happens for roughly tpccap_sa_partition, and anneal_w_cong=None anneals on exactly
the objective the seed was built for. The defaults are the values every published
QuPort result was produced with.
Together with the interaction-weight difference described above, this is the second
reason a tpccap versus tpccap_sa comparison has to state its configuration: the
two strategies can otherwise be ranked on different scales.
After partitioning, QuPort must map logical qubits onto physical qubits.
For each QPU
- compute pool: ordinary local execution qubits;
- communication pool: qubits that can connect to other QPUs.
QuPort identifies boundary logical qubits:
Boundary-heavy qubits are good candidates for communication ports because remote interactions require inter-QPU resources.
Two communication-selection modes are implemented:
-
topk: choose the$P$ logical qubits in each QPU with the largest remote-boundary score; -
diverse: prefer qubits that interact with many distinct remote QPUs, which spreads port access across different network destinations.
A simple boundary score is:
A diversity-aware score also considers
The diversity term is applied as a penalty against destinations already covered by
ports chosen earlier in the same QPU, so it has nothing to act on until the second
port. tpccap and tpccap_sa request diverse under both entry points, but at the
default comm_qubits_per_qpu of topk would; give each
QPU at least two ports before attributing any result to port diversity.
The final layout maps selected boundary qubits to communication physical qubits first, then maps remaining qubits to compute qubits and any unused communication qubits.
The map_and_transpile pipeline performs:
-
Capacity check: reject circuits where
$n>Q_{\mathrm{phys}}$ . -
Basis translation: translate the circuit to configured basis gates, defaulting to
("rz", "sx", "x", "cx"). -
Interaction extraction: compute
$w_{ij}$ or temporal weights$W_{ij}$ . -
Partitioning: apply
balanced,cluster,tpccap, ortpccap_sa. - Layout hinting: choose communication-port logical qubits and create an initial Qiskit layout.
- Global coupling map construction: create a directed coupling map for all local and inter-QPU physical links.
- Qiskit transpilation: run Qiskit with the configured optimization, layout, and routing settings.
- Metric computation: count SWAPs, depth, size, one-qubit gates, two-qubit gates, and remote two-qubit operations.
- Cost estimation: evaluate the configured latency/cost model.
This mode is useful when you want one routed Qiskit circuit for the entire modular device graph.
The compile_distributed pipeline is designed for explicit multi-QPU execution artifacts:
- Translate the input circuit into the configured basis.
- Extract logical interaction weights.
- Partition logical qubits across QPUs.
- Build a physical circuit with the partition-aware initial layout but without global inter-QPU routing.
- Split the physical circuit into local per-QPU circuits plus remote operations.
- Route each local circuit using that QPU's intra-QPU coupling map only.
- Estimate topology-aware remote-operation scheduling.
- Return all local circuits, remote-operation trace, metrics, and timing summaries.
A remote operation records:
- operation name;
- global instruction index;
- the two physical qubit indices it acts on;
- the QPU id owning each of those qubits;
- gate parameters;
- classical bit indices the operation reads or writes.
This split makes the boundary explicit: local gates remain in QPU-local programs, while cross-QPU two-qubit gates become remote events handled by orchestration, entanglement generation, teleportation-style protocols, or another execution backend.
QuPort includes progressively richer schedule estimators.
The simple estimator treats QPUs as parallel local processors and adds synchronization costs at remote operations.
A local one-qubit operation costs oneq, a local two-qubit operation costs twoq, a SWAP costs swap, and a remote two-qubit operation costs:
The layered estimator uses Qiskit DAG layers. Local operations in a layer can run in parallel across QPUs. The layer duration is approximately:
The topology-aware estimator considers:
- available communication ports per QPU;
- per-link capacity
link_capacity; - QPU-network reachability;
- hop-dependent remote costs;
- switch pair limits through
switch_parallel_links; - switch reconfiguration delay through
switch_reconfig_delay; - optional classical-latency hiding through
async_classicalandasync_overlap.
If classical latency hiding is enabled, the effective classical round-trip term is:
where
For QPU pair
Remote operations in the same DAG layer are greedily packed into rounds. A remote operation can be placed in a round only if:
and every link
The estimator returns:
makespan;- number of DAG
layers; - total
remote_ops; remote_rounds;- absolute per-layer
start_time/end_timeoffsets; - absolute per-round
start_time/end_timeoffsets for timeline visualization and simulator ingestion; peak_link_util;peak_qpu_ports_used.
Use schedule.to_dict() or schedule_plan.to_dict() when exporting these values.
Those serializers normalize tuple-valued QPU pairs and link-utilization entries to
JSON-native arrays/objects and validate finite non-negative timings, non-negative
counts, and non-self QPU/link pairs before emitting a payload.
For a transpiled or physical circuit, QuPort computes:
| Metric | Meaning |
|---|---|
swaps |
Number of swap instructions. |
depth |
Qiskit circuit depth. |
size |
Qiskit circuit size. |
n_1q |
Number of one-qubit instructions. |
n_2q |
Number of two-qubit instructions. |
remote_2q |
Number of two-qubit instructions whose physical endpoints belong to different QPUs. |
A two-qubit physical operation on physical qubits
qpu_of_physqpu_of_phys
swaps counts instructions literally named swap, so it is basis-dependent. The
default basis_gates of ("rz", "sx", "x", "cx") contains no swap, so Qiskit
rewrites every routing SWAP into CX gates and swaps reads n_2q instead. Add "swap" to
basis_gates to keep SWAP instructions intact and make the metric non-zero.
This applies wherever the metric surfaces, including the SWAPs: line printed
by quport map, the swaps column of the benchmark CSV, and swaps_mean in the
topology sweep.
A swap instruction is also a two-qubit instruction, so it is counted in both
swaps and n_2q.
The default LatencyModel contains:
| Field | Default | Meaning |
|---|---|---|
oneq |
Cost of one local one-qubit gate. | |
twoq |
Cost of one local two-qubit gate. | |
swap |
Cost of one SWAP. | |
epr_gen |
Entanglement-generation component of a remote operation. | |
classical_rtt |
Classical round-trip component. | |
remote_gate_overhead |
Additional remote-gate overhead. |
The local component is:
Because a swap instruction is counted in both
The remote component is:
The depth penalty is:
The total reported cost is:
QuPort requires Python
python -m pip install -e .python -m pip install -e ".[viz,yaml,graph]"Optional extras:
| Extra | Installs | Why use it |
|---|---|---|
viz |
pandas, matplotlib, tqdm |
CSV analysis, plotting, and progress helpers. |
yaml |
PyYAML |
YAML config input/output. |
graph |
networkx |
Graph-heavy downstream experiments. |
Check the CLI:
quport --helpor:
python -m quport --helpquport gen-configThis writes a default MultiQPUConfig to quport_config.json. Pass --out with a
.yaml/.yml suffix to emit YAML instead, which requires the yaml extra
(pip install quport[yaml]); the format follows the file extension.
quport map --n-logical 80 --depth 20 --seed 7 --strategy tpccap_saWrite the mapped circuit as OpenQASM 3:
quport map \
--n-logical 80 \
--depth 20 \
--seed 7 \
--strategy tpccap_sa \
--out mapped.qasmUse a custom config:
quport map \
--n-logical 80 \
--depth 20 \
--seed 7 \
--strategy tpccap_sa \
--config quport_config.jsonquport bench \
--n-logical 80 \
--depth 20 \
--trials 20 \
--seed 7 \
--strategies baseline,balanced,tpccap \
--out results.csvquport sweep \
--n-logical 80 \
--depth 20 \
--trials 5 \
--seed 7 \
--out sweep.csvCreate a plot when viz dependencies are installed:
quport sweep \
--n-logical 80 \
--depth 20 \
--trials 5 \
--seed 7 \
--out sweep.csv \
--plot sweep.pngquport topology-info --config quport_config.jsonPrints structural metrics for the configured interconnect (degree, diameter, average shortest path, connectivity) without compiling anything, which is a cheap way to compare candidate topologies before a sweep.
quport schedule --n-logical 80 --depth 20 --seed 7 --strategy tpccapquport split \
--n-logical 80 \
--depth 20 \
--seed 7 \
--strategy tpccap \
--out-dir distributed_outquport compile-dist \
--n-logical 80 \
--depth 20 \
--seed 7 \
--strategy tpccap_sa \
--temporal-decay 0.98 \
--out-dir compile_outThis produces per-QPU routed programs, an ordered remote-operation trace, and a topology-aware schedule summary.
from quport import LatencyModel, MultiQPUConfig, map_and_transpile
from quport.pipeline import random_benchmark_circuit
cfg = MultiQPUConfig(
n_qpus=10,
compute_qubits_per_qpu=8,
comm_qubits_per_qpu=1,
intra_topology="clique",
inter_topology="switch",
)
qc = random_benchmark_circuit(n_logical=80, depth=20, seed=7)
result = map_and_transpile(qc, cfg, latency=LatencyModel(), seed=7, strategy="tpccap_sa")
print(result.metrics)
print(result.cost)
print(result.partition)from quport.compiler import compile_distributed
from quport.config import LatencyModel, MultiQPUConfig
from quport.pipeline import random_benchmark_circuit
cfg = MultiQPUConfig(n_qpus=10, compute_qubits_per_qpu=8, comm_qubits_per_qpu=2)
qc = random_benchmark_circuit(n_logical=80, depth=20, seed=7)
result = compile_distributed(
qc,
cfg,
latency=LatencyModel(),
seed=7,
strategy="tpccap_sa",
temporal_decay=0.98,
)
print(result.schedule.makespan)
print(result.schedule.to_dict())
print(result.schedule_plan.to_dict()["summary"])
print(result.schedule_plan.layers[0].remote_rounds)
print(len(result.program.remote_ops))
print(result.local_metrics)from quport.architecture import MultiQPUArchitecture
from quport.config import MultiQPUConfig
cfg = MultiQPUConfig(inter_topology="ring", intra_topology="grid2d", grid_rows=3)
arch = MultiQPUArchitecture(cfg)
print(arch.block_of_qpu(0))
print(arch.build_coupling_map())
print(arch.qpu_shortest_paths().dist)MultiQPUConfig fields:
| Field | Default | Description |
|---|---|---|
n_qpus |
10 |
Number of QPUs. |
compute_qubits_per_qpu |
8 |
Compute qubits in each QPU. |
comm_qubits_per_qpu |
1 |
Communication-port qubits in each QPU. |
intra_topology |
clique |
Local QPU topology. |
inter_topology |
switch |
Inter-QPU topology. |
inter_degree |
2 |
Degree control for degree_d. |
link_capacity |
1 |
Max simultaneous remote ops per inter-QPU link per round. |
switch_parallel_links |
1000000 |
Max distinct QPU pairs per round for switch-like models. |
switch_reconfig_delay |
0.0 |
Additional delay per switch communication round. |
async_classical |
True |
Enable classical-latency overlap in topology-aware scheduling. |
async_overlap |
0.5 |
Fraction of classical_rtt hidden when async classical mode is enabled. |
grid_rows |
None |
Optional row count for grid2d. |
grid_cols |
None |
Optional column count for grid2d. |
basis_gates |
("rz", "sx", "x", "cx") |
Basis gates for Qiskit translation/transpilation. |
optimization_level |
3 |
Qiskit optimization level. |
layout_method |
sabre |
Qiskit layout method for global transpilation. |
routing_method |
sabre |
Qiskit routing method. |
JSON example:
{
"n_qpus": 6,
"compute_qubits_per_qpu": 8,
"comm_qubits_per_qpu": 2,
"intra_topology": "grid2d",
"inter_topology": "ring",
"link_capacity": 1,
"async_classical": true,
"async_overlap": 0.5
}YAML example:
n_qpus: 6
compute_qubits_per_qpu: 8
comm_qubits_per_qpu: 2
intra_topology: grid2d
inter_topology: ring
link_capacity: 1
async_classical: true
async_overlap: 0.5Unknown config fields are rejected so typos do not silently alter experiments.
Writes a single OpenQASM 3 circuit after global mapping and routing.
Produces:
| File | Description |
|---|---|
qpu_<id>.qasm |
Local OpenQASM 3 circuit for QPU <id>. |
remote_ops.json |
Ordered list of cross-QPU operations. |
Produces:
| File | Description |
|---|---|
qpu_<id>_routed.qasm |
Locally routed OpenQASM 3 circuit for QPU <id>. |
remote_ops.json |
Ordered remote-operation trace. |
schedule.json |
Strict JSON topology-aware schedule summary produced from TopologyScheduleSummary.to_dict(). |
schedule_trace.json |
Strict JSON per-layer/per-round communication plan produced from TopologySchedulePlan.to_dict(), with absolute timing, QPU-pair packing, port use, link utilization, and unschedulable penalty rounds. |
Remote operation entries have the shape:
{
"index": 12,
"name": "cx",
"q0_phys": 7,
"q1_phys": 84,
"qpu0": 0,
"qpu1": 9,
"params": [],
"clbits": []
}Schedule artifacts are written with allow_nan=False, so non-finite values are
rejected instead of being emitted as Python-specific NaN/Infinity tokens.
quport bench writes rows with:
| Column | Meaning |
|---|---|
trial |
Trial index. |
seed |
Random seed used for the trial. |
method |
Numeric method id: baseline 0, balanced 1, tpccap 2, tpccap_sa 3, cluster 4. |
strategy |
Strategy name. |
swaps |
SWAP count. |
remote_2q |
Remote two-qubit operation count. |
depth |
Circuit depth. |
size |
Circuit size. |
cost_total |
Total estimated cost. |
cost_local |
Local estimated cost. |
cost_remote |
Remote estimated cost. |
mapping_time_s |
Partition/layout time. |
transpile_time_s |
Qiskit transpilation time. |
quport sweep writes summary rows with:
| Column | Meaning |
|---|---|
intra |
Local topology. |
inter |
Inter-QPU topology. |
ports |
Communication ports per QPU. |
method |
Numeric method id. |
swaps_mean |
Mean SWAP count. |
remote_2q_mean |
Mean remote two-qubit count. |
depth_mean |
Mean depth. |
cost_mean |
Mean total estimated cost. |
cost_median |
Median total estimated cost. |
transpile_time_mean |
Mean transpilation time. |
Cost is reported both ways because it is heavily skewed across random circuits.
Comparing two strategies instance by instance, the per-instance ratio spans roughly
cost_mean as a ranking on its own.
Install the project and run:
pytestThe repository sets addopts = "-q" under [tool.pytest.ini_options] in
pyproject.toml, so both invocations are quiet and pick up the same settings. The
module form additionally prepends the current directory to sys.path:
python -m pytestUseful optional checks:
python -m compileall src tests examplesquport --help- Qiskit
CouplingMapedges are directed, so QuPort explicitly inserts both directions for physically symmetric links. - Inter-QPU physical connectivity is modeled through communication qubits only.
- The default latency model is intentionally simple and configurable; values are comparative cost units unless you calibrate them to a hardware backend.
- Global mapping can insert cross-QPU routing operations because it exposes the whole modular graph to Qiskit. Use distributed compilation when you need remote operations to remain explicit.
- Topology-aware scheduling is a deterministic estimator, not a full hardware-control stack.
- Disconnected QPU pairs and zero-capacity communication resources are penalized rather than silently ignored.
- Random benchmark circuits are generated for repeatable experiments; application-specific circuits can be passed directly through the Python API.
If you use quport in your work and wish to refer to it, please use the following BibTeX entry.
@misc{sarkar2026quporttopologyportcongestionaware,
title={QuPort: Topology-, Port-, and Congestion-Aware Compilation for Modular Multi-QPU Quantum Systems},
author={Soumyadip Sarkar and Subhasree Bhattacharjee},
year={2026},
eprint={2605.12583},
archivePrefix={arXiv},
primaryClass={quant-ph},
url={https://arxiv.org/abs/2605.12583},
}QuPort is licensed under the Apache License 2.0. See LICENSE.