
Useful work on NISQ hardware rarely means running one circuit; it means thousands of near-identical variants of one circuit, and you’ve probably watched your stack spend more time assembling that batch, rebuilt and re-serialized on every iteration, than executing it. The frustrating part is that the ecosystem already invented everything needed to fix this; the fixes just live locked inside individual stacks, each optimized for its own execution path.
The quantum software ecosystem is full of genuinely good engineering. Xanadu’s PennyLane built composable circuit transformations: a TransformProgram you stack like middleware. IBM’s Qiskit rewrote its circuit core in Rust (qiskit._accelerate) and exposes a primitive model that broadcasts one parametric circuit over an entire parameter array. Google’s Cirq made the parameter sweep a first-class execution primitive: one symbolic circuit plus a composable sweep (Linspace, Points, Zip) runs as a single run_sweep job. Mitiq made error mitigation frontend-agnostic, folding and twirling circuits from any framework. And OpenQASM 3 is standardizing parametric programs through input declarations.
Each is a sharp answer to a real problem, yet none of them, on its own, gives you a fast pipeline for the batch a variational run actually submits. Run VQE, QAOA, or a Trotterized time evolution on hardware and you don’t submit a circuit; you submit a batch. A single optimizer step fans out into:
- observable groups: qubit-wise-commuting (QWC) partitions of the Hamiltonian you’re measuring;
- error-mitigation copies: e.g. zero-noise extrapolation (ZNE) folds at
Knoise-scale factors; - twirl samples:
Trandomized-compiling / Pauli-twirl variants; - parameter bindings:
Pparameter sets (a gradient batch, a sweep, a restart set).
The submit-ready list has N_groups ร K ร T ร P circuits: observable groups ร ZNE folds ร twirl samples ร parameter bindings. Every framework can build each ingredient. The open question is how you compose them into that flat list, and how the composition scales as the batch grows.
So we wired those ideas (composable transforms, broadcast parameter handling, late binding, frontend-agnostic mitigation, parametric serialization) into one pipeline built for the batch: divi’s circuit-batch generation. This post benchmarks what that buys against PennyLane, Qiskit, and Cirq, and pins down why the numbers fall the way they do.
Summary
Useful quantum work on near-term hardware rarely means running a single circuit; it means running thousands of near-identical variants. Traditionally, quantum stacks force you to rebuild and re-serialize every single circuit in that batch, leaving your team paying for idle cloud GPU time while waiting for queues to process.
In this post, we benchmark Qoroโs divi SDK against PennyLane, Qiskit, and Cirq. By treating batch generation as a composable pipeline that binds parameters last and serializes structure once, divi achieves up to 148x faster pipeline execution.
Crucially, divi maintains strict vendor agnosticism. Where other frameworks lock your IP into proprietary serialization schemes (like QPY for Qiskit or custom protobufs for Google), Qoro outputs standard OpenQASM. The result: significantly faster batch generation, smaller cloud compute bills, a massive acceleration in your time-to-result, and the freedom to route your workloads to whichever QPU or cloud provider offers the best price and availability today.
divi is open source (Apache-2.0), and pip install qoro-divi gets you the pipeline from this post. Docs and examples live at divi.readthedocs.io. If variational workloads are your bottleneck, sign up to Qoro Solo and try it for yourself and run real-scale quantum algorithms in the cloud. We would love to hear what you’re building.
How divi thinks about a batch
divi treats the whole batch as one composable pipeline over a templated circuit. You declare the algorithm and the mitigation stack, and call run():

Under the hood that composes an ordered CircuitPipeline of stages:

The pipeline is a public object, not internal machinery. You can assemble the same stack by hand, starting from your own circuit in whichever format you already have (Qiskit and PennyLane spec stages ship today):

Parameters are bound last. That is the obvious move, and every framework gestures at it: it’s the same idea as Qiskit’s parametric PUBs, Cirq’s sweeps, and OpenQASM 3’s input declarations, just as divi’s composable stages are PennyLane’s TransformProgram pattern. But in practice the deferral breaks early: PennyLane’s transform stack re-runs on every execution, PUB and sweep deferrals only pay off where a server does the binding (IBM Runtime, Google’s Quantum Engine), and OpenQASM 2 export refuses unbound circuits.
So on most paths the sweep gets materialized into P copies of everything long before submission. In divi it never breaks: the structural stages (grouping, folding, twirling, basis rotation) operate on a parameter-free template; the circuit body is serialized once; and each parameter set is then a cheap substitution (or, against a backend that accepts it, is deferred to the backend entirely). The expensive, parameter-independent work happens once and is shared across the whole fan-out. As we’ll see, the difference between “amortize the structure” and “rebuild per circuit” is most of the story.
First finish line: generating the batch in memory
There are two finish lines worth measuring, and we take them in order. The first, covered here, is in-memory generation: how long until each library holds the complete batch in its own execution-layer representation (divi’s MetaCircuit batch; PennyLane’s tape list from construct_batch; Qiskit’s bound-circuit list; Cirq’s cirq.Circuit list). We call this wall_ir. One asterisk: divi’s wall_ir defers parameter binding into its serialization phase, so the strict common finish line is the fully serialized batch, which gets its own section below.
divi is faster in every cell where a comparator completed, on both finish lines and against the comparators’ standard (idiomatic + hoisted) paths. On in-memory generation:
wall_ir (in-memory generation) | |
|---|---|
| vs PennyLane | 3โ88ร |
| vs Qiskit | 3โ21ร |
| vs Cirq | 8โ83ร |
Each range spans all three workloads: the low ends are QAOA cells, the high ends Trotter.

The shape of this result matters. For PennyLane the generation gap is large on its own: its transform program re-runs per execution, and tape construction is genuinely the cost. For Qiskit and Cirq the generation gap is real but modest: single-to-low-double digits. If you’ve seen much larger numbers (hundreds-ร) attached to this benchmark, those belong to the second finish line, text serialization, and we’ll give them their own context rather than blending them into one headline.
What we benchmarked
One algorithm instance per cell (a single VQE / QAOA / Trotter), generated with no backend execution (a no-op backend). The stack composed on top of each: QWC grouping โ ZNE fold (K) โ Pauli twirl (T) โ parameter binding (P), with an identical circuit count (N_groups ร K ร T ร P) across all four libraries, asserted at smoke and audited per cell. We sweep five axes (qubit count M, Hamiltonian term count T_h, K, T, P) and report the median of 3 timed runs after a discarded warmup, each shard pinned to its own CPU with single-threaded BLAS (for isolation; it does cap any internal library parallelism). Pinned versions: Qiskit 2.3.1, Cirq 1.6.1, PennyLane 0.44.1, Mitiq 1.0.0.
We measure client-side construction of a materialized batch, the path any backend that ingests a list of circuits requires. The two server-side exceptions (IBM Runtime, Google’s Quantum Engine) get their own section below.
The ideas we borrowed, and where each lives today
Each framework nails one piece of the batch problem, and stops short of the rest; divi’s job is composing them.
- PennyLane has the closest thing to a composable transform pipeline:
TransformProgram+construct_batch, with nativemitigate_with_zneand QWC grouping viasplit_non_commuting(no native Pauli twirl as of 0.44.1, so we wrote one as a small@qml.transform). Its gap is in generation itself: building the transformed tape batch, before any OpenQASM, takes ~0.4 / 1.6 / 3.7 s at 12 / 24 / 48 qubits (30โ80ร divi), because the transform program re-runs on every QNode execution. PennyLane’s own fix for that re-execution cost is Catalyst, which compiles the whole workload down to QIR, but only for its own runtimes (the Lightning simulators and Braket): it executes the result itself rather than producing a submit-ready batch you could hand to anything else. - Qiskit raises the parameter axis to first class: an Estimator V2 PUB takes a parametric circuit plus a parameter array and broadcasts, so nobody hand-loops over
P. What the primitive does not fold in is error mitigation. On IBM Runtime, ZNE folds and twirls are server-side resilience options applied on IBM hardware. Everywhere else (IonQ, Braket, Azure Quantum, IQM, local simulators) there is no such server: mitigation is Mitiq, client-side, which materializes the folded/twirled batch. Grouping is native (`SparsePauliOp.group_commuting`), and `PassManager` is a transpilation layer rather than a QEM transform stack, so outside IBM-on-IBM the pipeline is assembled by the user across two libraries.

- Cirq makes parameter sweeps a first-class execution primitive:
run_sweeptakes one symbolic circuit plus a sweep descriptor (Linspace/Points/Zip) instead ofPmaterialized circuits. It has no Estimator-style batch primitive, though, so the QEM/grouping/binding pipeline is hand-assembled (Mitiq is its native frontend for ZNE and twirling). Its in-memory generation is competitive; the slow part is OpenQASM-2 export (cirq.qasm; QASM-3 output exists for bound circuits), which, notably, isn’t Cirq’s submission format at all: its hardware backends consumecirq.Circuit, serialized internally to protobuf for Google’s Quantum Engine or vendor JSON for IonQ/AQT.
The common thread: each framework supplies a brilliant *piece* (composable transforms, primitive broadcasting, sweep execution, frontend-agnostic mitigation), but assembling them into the batch is left to the user, across libraries, and (for error mitigation) materialized client-side on every path except IBM-on-IBM. divi’s bet is that those pieces belong together.
There’s a deeper version of this point. Each framework’s internal representation (PennyLane’s tapes, Qiskit’s QuantumCircuit, pytket’s Circuit) lives only until the device boundary, where a plugin rebuilds every circuit, object by object and uncached, in the target SDK’s structures: pennylane-qiskit’s circuit_to_qiskit, the Braket plugin’s per-execution apply(), qiskit-braket-provider’s to_braket. The target SDK then serializes again for the wire, so a PennyLane circuit headed to IBM hardware crosses three representations (tape โ QuantumCircuit โ QPY); headed to AWS, it ends as OpenQASM 3 text.
The ecosystem even ships graph-search transpilers to manage this (qBraid’s conversion graph spans ten program types and twenty-five pairwise converters), and recent benchmarking treats converter correctness, failure rate, and transpile time as first-class metrics. Below we measure one edge of that graph, Mitiq’s QiskitโCirqโQiskit fold round-trip, at 1.6โ3.1ร. Keeping the whole batch in one representation from construction through mitigation to the submit format is a large part of what the synthesis buys.
Where the batch gets built: client or server
We benchmark client-side batch construction. Two deployments are the exception: on IBM Runtime on IBM hardware, a Qiskit user submits a parametric PUB and IBM’s servers do the grouping, folding, twirling, and binding; and on Google’s Quantum Engine, run_sweep ships one symbolic circuit plus a sweep descriptor (as protobuf) and the server fans out the parameter axis. On those paths the client builds nothing, so our parameter-axis numbers don’t apply to those users.
For every other target (local simulators, QPUs via provider plugins, heterogeneous platforms) the client builds the materialized batch, which is what we measure. This is also why we didn’t benchmark a Sweep-based Cirq path: constructing the descriptor is free precisely because it defers the work being measured. Anywhere the server doesn’t accept it, the sampler resolves the sweep into concrete circuits client-side, which is exactly the per-binding resolve_parameters path we benchmark for Cirq below. And a sweep spans only the parameter axis: the fold, twirl, and measurement-group copies are structural and materialize regardless.
The symmetry cuts both ways: divi’s own parametric path (submit_circuit_templates, where our server binds) is the same idea as a parametric PUB or a shipped sweep. We measured the materialized path for divi and the comparators alike, because backends that ingest a circuit list require it, so neither side skips the work by deferring it to a server.
Making the comparison fair
We benchmark each comparator at its best achievable codepath, not a naive loop. The difference between the two is where the binding step sits: Part 1 shows the loop the standard workflows produce, with binding at the top so everything downstream repeats per parameter set; Part 2 hoists every structural pass the public APIs allow ahead of the bind, leaving only the bind itself in the loop.
Part 1: the idiomatic path (where we started)
Nobody chooses to bind first; the standard workflows land you there. Mitiq’s `execute_with_zne` wraps a concrete, executable circuit, and OpenQASM 2 export demands bound ones, so the loop a real comparator user ends up writing looks like this:

The bind sits at the top of the loop, so every fold, twirl, grouping, and serialization underneath it repeats P times. The runners we actually timed apply the Part-2 hoists below; the headline tables reflect best-effort comparator code, not this loop.
Part 2: benchmarking each library at its best
We optimized each comparator as far as its public API allows:
- Qiskit: hoist
transpileonce onto the parametric ansatz and onlyassign_parametersper binding. We also added a control: Mitiq’sfold_globalaccepts a Qiskit circuit but folds it through an internal QiskitโCirqโQiskit round-trip. A native Qiskit global fold (U(Uโ U)โฟ) is unitarily identical (same CX count) but 1.6โ3.1ร faster at building the IR, and avoids a ~1.5ร single-qubit-gate inflation from basis translation (cosmetic; onetranspilepass collapses it). Mitiq stays the headline path because it’s the idiomatic Qiskit+Mitiq ZNE workflow; the control quantifies how much of the gap is tool integration rather than Qiskit itself. - PennyLane: for parameter sweeps, hoist
construct_batchonce and produce subsequent bindings viaQuantumScript.bind_new_parameters, recovering the userโtape parameter map by finite-difference fingerprinting (public APIs throughout). - Cirq: build the circuit topology once with
sympy.Symbolangles andcirq.resolve_parametersper binding, the analogue of Qiskit’sassign_parameters.
These hoists are reflected in the in-memory numbers you’ve already seen. The remaining (and largest) divergence between divi and the comparators lives at the second finish line.
Second finish line: the serialized OpenQASM batch
Why benchmark a serialized finish line at all? Because for a large slice of the ecosystem, a batch of OpenQASM text is literally the submission artifact: the Braket SDK’s default circuit IR is OpenQASM 3, Quantinuum’s job payloads are extended OpenQASM 2, and heterogeneous platforms like ours ingest OpenQASM 2 directly. Other stacks ship binary formats instead (IBM’s Python client sends QPY, Google’s Engine protobuf, IonQ and IQM JSON), which is why we report serialization separately from generation rather than blending it into the headline.
So the second metric is wall_total = wall_ir + wall_qasm: the time to a flat batch of OpenQASM 2.0 text, the format divi natively emits, our platform ingests, and the comparators produce via their export utilities (qasm2.dumps, cirq.qasm, qml.to_openqasm). Here the gaps widen dramatically:
wall_ir (generation) | wall_total (incl. OpenQASM text) | |
|---|---|---|
| vs PennyLane | 3โ88ร | 5โ148ร |
| vs Qiskit | 3โ21ร | 4โ94ร |
| vs Cirq | 8โ83ร | 28โ622ร |
Same workload-spanning ranges as before: low ends QAOA, high ends Trotter.

Compare this wall_total sweep with the wall_ir figure earlier: the Qiskit and Cirq curves jump by an order of magnitude or more. That difference is serialization. Here’s where the time goes for VQE at 24 qubits:

The fraction of wall_total spent serializing to OpenQASM text: divi ~3%, PennyLane ~53%, Qiskit ~92%, Cirq ~98%. When you see “600ร faster than Cirq,” that number is almost entirely cirq.qasm text export, which is why the generation story came first in this post. divi’s serialization is cheap for the same reason its generation is: the body is serialized once, parameters are substituted.
A per-circuit summary makes the architecture visible. These are each library’s own largest completed batch, not a common size (divi and Cirq at ~54k circuits, PennyLane and Qiskit at ~6.4k): divi ~0.004 ms/circuit (template and substitute) versus ~3.4 ms (Qiskit), ~5.8 ms (PennyLane), and ~23 ms (Cirq) for a full build-and-serialize per circuit. The scatter below compares all libraries at equal batch sizes, cell by cell.

The one-liner: divi reaches the submit-ready batch faster because its pipeline amortizes structure and serialization across the fan-out, while the idiomatic comparator path rebuilds and re-serializes each circuit. That’s a property of pipeline design, not engine speed.
Giving the comparators divi’s trick
The deepest question is the one a maintainer would ask: isn’t divi just faster by construction, because it binds parameters last and serializes once? Largely yes. So we gave the comparators the same trick and measured what’s left: a hand-rolled bind-last / serialize-once path for Qiskit and Cirq that applies the structural stages to the parametric circuit once, builds a QASM template, and substitutes parameter values per binding.

One wrinkle keeps this from being free: OpenQASM 2 can’t represent unbound parameters, so qasm2.dumps / cirq.qasm reject a parametric circuit. build_qasm_template therefore has to recover the parameterโangle map by fingerprinting (serialize a base set, perturb each parameter, diff the numeric tokens), an O(n_params) setup cost per structural circuit. divi pays none of this: its IR already carries the map.
This controlled comparison runs at a small cell (M=6) because that is where every strategy completes: at the main benchmark’s 24-qubit cell the idiomatic path doesn’t even finish the sweep (Qiskit and Cirq DNF at P=100). There, the templated path produces Operator-identical circuits and flattens the curve:
| QAOA, M=6, P=100 | idiomatic รท divi | templated รท divi |
|---|---|---|
| Qiskit | ~400ร | ~11ร |
| Cirq | ~2000ร | ~70ร |

The figure has two panels: solid lines are the idiomatic per-binding path, dashed are templated, on the same cells. In QAOA (n_params=4) the dashed lines stay flat as P grows while the solid lines climb: the parameter-axis gap is composition strategy, not engine speed. The residual is cirq.qasm‘s intrinsic serialization cost (Cirq, ~70ร) and template-recovery overhead (Qiskit, ~11ร). In VQE (n_params=24) the templated lines start above idiomatic and only cross below once P > n_params: the fingerprint recovery (O(n_params) serializations) costs more than it saves at low P. That break-even is a limitation of recovering a template the format won’t hand you natively, which is the perfect segue.
The OpenQASM-2 vs OpenQASM-3 angle
A large slice of the Qiskit/Cirq serialization gap exists because OpenQASM 2 has no notion of a parameter. Every circuit must be fully bound before it can be written, so a sweep becomes P separate texts.
OpenQASM 3 fixes exactly this with input parameters. qiskit.qasm3.dumps will happily serialize an unbound parametric circuit:

That’s a native template: one program for the whole sweep, no fingerprint recovery, no per-binding re-serialization. It would hand Qiskit most of what we hand-rolled, for free. The catch: neither Cirq nor PennyLane can export a parametric program this way (as of Cirq 1.6.1 / PennyLane 0.44.1). Cirq’s QASM-3 output mode (cirq.qasm(..., version="3.0")) serializes only bound circuits, and PennyLane’s exporter is QASM-2-only (its QASM-3 support is import-side). divi’s template form, and our platform’s “one parametric body + parameter table” submission, is morally the QASM3-input idea expressed over QASM2.
This is the cleanest way to see what the synthesis is really about: not a flaw in any framework, but a composition that lives between them. The capability (fold the mitigation/grouping/binding fan-out into a parametric, serialize-once batch) is valuable; divi packages it as a first-class pipeline today, and the broader ecosystem is converging on the very primitives (QASM3 input, sweep protos, parametric submission) that will make the same synthesis cheap everywhere.
Beyond speed: what the pipeline abstraction unlocks
The same pipeline that makes batches cheap also makes non-trivial constructions declarative. Two divi capabilities with no drop-in comparator equivalent:
- Constrained-encoding QAOA (CE-QAOA) for the TSP: W-state initialization + XY mixer + reduced phase operator, where the ansatz structurally enforces the one-hot constraints.

- QuEPP: a quantum-error-mitigation fan-out whose batch structure has no counterpart in the Mitiq + Qiskit/PennyLane/Cirq stack.

These are examples of what becomes a one-liner when the batch is a pipeline rather than a loop.
Scope
These numbers measure client-side batch generation. Real wall-time is generate + submit + execute + collect, and backend execution usually dominates; this post is about the generation step. The advantage is pipeline design (bind-last, serialize-once, amortized fan-out), not engine speed: a comparator that adopts the same composition closes most of the gap. And the Hamiltonians are random Pauli sums on a single machine; the load-bearing claim is the scaling with batch fan-out, not any single ratio.
Takeaway
The quantum ecosystem already invented the hard parts: composable transforms, symbolic and late parameter binding, frontend-agnostic mitigation, parametric serialization. What hasn’t been packaged is a single pipeline that composes them for the batch a variational run submits, so today that batch is assembled circuit-by-circuit in a user-written loop and serialized one circuit at a time (because OpenQASM 2 leaves parameters at the door). divi’s contribution is to put those ideas in one place: a composable pipeline that binds parameters last and serializes the structure once, amortized across the whole fan-out. That’s why it’s faster, and why a comparator handed the same pattern catches up.
None of this is a knock on any framework; it’s an argument that their best ideas belong together. The standards are already heading that way (OpenQASM 3 input, parametric submission), which will make the synthesis cheap everywhere. We think it’s worth building toward, and worth measuring.
Try it
Fast batch generation is only half the story: a batch built in milliseconds is worth little if it then crawls through a slow simulator. The other half is execution. divi hands its batches to Maestro, our simulation engine, for fast local runs, or to real QPUs through our Qoro platform, with the same code on both paths.
divi is open source (Apache-2.0), and pip install qoro-divi gets you the pipeline from this post. Docs and examples live at divi.readthedocs.io. If variational workloads are your bottleneck, sign up and try it for yourself. We would love to hear what you’re building.