How I built MettleQ — a GPU-accelerated quantum simulator for Apple Silicon

Almost every serious GPU quantum simulator assumes you have an NVIDIA card and CUDA. Some researchers type on a Mac. This is the story of closing that gap — honestly.
The Premise
A datacenter has NVIDIA GPUs. Your laptop probably doesn’t. Yet the machine where quantum-computing research actually gets prototyped — the Jupyter notebook open right now — is very often a MacBook. There’s a real gap between where the fast simulators run and where the work happens.
Apple Silicon has one property that makes closing that gap worthwhile: unified memory. The CPU and GPU address the same physical pool, so there’s no PCIe round-trip to move data on and off the accelerator. A statevector simulator is, at its core, one enormous complex-valued array that gets hit repeatedly by structured passes. That’s a memory-bandwidth game, and unified memory changes its economics.
So I set myself a concrete, falsifiable goal: get measurable GPU acceleration for quantum simulation on a MacBook — and prove it, rather than claim it.
The result is MettleQ. This article is about what worked, what didn’t, and the discipline I tried to hold along the way.
MettleQ is an independent, MIT-licensed fork of the Qupertino project by Shlomo Kashani. I kept the MLX/Metal base and rebuilt the project around correctness, trust, and reproducible evidence.

Custom Metal kernels vs the pure-MLX GPU path: 29 workloads at 25 qubits on an M3 Pro. Median 10.2×, up to 32.6×.
Why not just call `matmul` on the GPU?
The textbook way to apply a gate to a statevector is to build a matrix and multiply. It works, and MLX will happily run it on the Apple GPU. But it leaves an order of magnitude on the table, because real circuits aren’t made of arbitrary dense matrices — they’re made of structured layers:
A Trotter step of a spin Hamiltonian is a layer of ZZ rotations. They all commute, and they’re all diagonal — no matrix multiply needed at all.
A layer of single-qubit rotations touches every qubit independently, so the whole layer factorizes.
A QFT is a fixed ladder of Hadamards and controlled phases with a closed-form structure.
Each of these has structure you can exploit if you write the kernel by hand. That’s the core of MettleQ: hand-written Metal compute kernels — dispatched through MLX’s custom-kernel API — for exactly these patterns.
Fused ZZ Trotter layers. Instead of looping over every bond and calling trig inside the kernel, each amplitude computes a parity with a `popcount` and looks up its phase in a tiny table. One GPU pass applies the entire layer.
Radix-16 single-qubit layers. Because single-qubit gates on different wires commute, a full layer becomes `floor(n/4)` fused four-qubit passes plus a small tail — far fewer kernel launches than one-gate-at-a-time.
Radix-4 fused QFT stages. Here’s the ZZ chain kernel — the actual Metal source, not pseudocode:
uint i = thread_position_in_grid.x; if (i >= n_state) return; uint mismatches = metal::popcount((i ^ (i >> 1)) & chain_mask); complex64_t p = phase_lut[mismatches]; complex64_t a = state[i]; out[i] = complex64_t(a.real * p.real - a.imag * p.imag, a.real * p.imag + a.imag * p.real);
No per-bond loop. No trigonometry. One XOR, one popcount, one table lookup, one complex multiply — per amplitude. That’s the whole layer.
The numbers
Benchmark claims in this space are frequently unfalsifiable. A “100× speedup” against an unnamed baseline on an undisclosed machine is marketing, not evidence. I held a stricter bar: a result is only a “speedup” after it beats a named baseline on the same machine under the same protocol, with the raw data frozen next to the commit that produced it.
The cleanest, most defensible result is the custom Metal path versus MettleQ’s own pure-MLX GPU path — an apples-to-apples ablation of “did hand-writing the kernels actually help?” On an M3 Pro, across 29 workloads at 25 qubits, with paired repeats that alternate the two paths inside each trial:

Some representative absolute wall times:


The same 29 workloads as absolute wall time (log scale). Lower is better; the gap between the two bars is the story
Here’s the caveat I want to be scrupulous about: that speedup is over my own fallback path, not over Qiskit Aer or PennyLane Lightning, that will come later. It answers “were the custom kernels worth writing?” — emphatically yes — but it is not a claim that MettleQ is the fastest simulator in existence. Different question, different baseline. Conflating the two is how benchmark folklore gets started, and I’d rather not start any.
It plugs into the tools you already use
A simulator nobody can call is a demo. MettleQ exposes real integration points:
A native Qiskit `BackendV2`, plus SamplerV2 / EstimatorV2 primitives — transpile and run unitary circuits, get native `Result` objects, device-sampled counts, and optional analytic convergence reports.
A registered PennyLane device — a normal QNode with analytic or finite-shot measurements and framework-managed parameter-shift gradients.
import pennylane as qml
dev = qml.device(“mettleq”, wires=20) # runs on the Apple GPU
@qml.qnode(dev) def circuit(theta): qml.RX(theta, wires=0) for i in range(19): qml.CNOT(wires=[i, i + 1]) return qml.expval(qml.PauliZ(0))
Both adapters route through one tested canonical operation layer, so Qiskit’s little-endian conventions and PennyLane’s wire ordering are covered by reference-parity tests rather than reimplemented per adapter.

End-to-end timing through both SDKs, across statevector/MPS and CPU/GPU. CPU and GPU are independent alternatives, never summed and marketed as cooperative acceleration
The part most write-ups leave out: where it’s slow
If I only showed you the winning numbers, you’d be right to distrust the rest. So here’s what *doesn’t* work well yet — straight from the project’s own “Known limits.”
The MPS backend is currently slower than Qiskit Aer. On matched matrix-product-state workloads through Qiskit’s EstimatorV2, MettleQ runs at roughly 0.1–0.2× of Aer’s speed on most topologies (i.e. several times slower), winning on only one grid case. That’s in the repo, with the CSVs.

GPU-side MPS is disabled — deliberately. I tried running the MPS tensor contractions on the Apple GPU. The CPU beat it at every bond dimension from 8 to 128, because the SVD/truncation step stays on the CPU and forces round-trips (at D=64, SVD alone was ~98% of contraction-plus-SVD time). Rather than ship a “GPU MPS” checkbox that’s actually slower, I disabled it and wrote down exactly why. It should be revisited only once SVD can stay resident on the GPU.
This is the honest shape of the project: the exact-statevector path with custom Metal kernels is genuinely fast; the approximate MPS path is correct but not yet competitive.
Trust as a feature, not an afterthought
That framing isn’t decoration. MettleQ began as a fork motivated by a technical audit.
Incorrect simulation that runs quickly is worse than a correct baseline. So MettleQ is built around a few non-negotiables:
Statevector preflight. Before allocating a 2^n array, it checks the requested size against available unified memory and refuses (or demands an explicit override) instead of thrashing your machine into swap.
Capability-gated dispatch. The Metal kernels run only on GPUs that pass a capability probe; everything falls back to a pure-MLX path you can force on with an environment variable for clean ablation.
Numerical parity tests. 354 tests, including reference-parity checks against Qiskit and PennyLane on identical circuits — covering their opposite endianness and wire-order conventions.
A memory policy that pays for itself. An adaptive checkpointing scheme cuts peak statevector memory by 65–95% versus the naive path while running faster— because fewer large temporaries means less allocator pressure in unified memory.

Taking on the Dedicated GPUs: M3 Pro vs. RTX 3070 (WSL2)
Up to this point, the baseline was internal — Metal kernels against pure MLX. But to answer the real-world question every developer asks (“Can my MacBook actually replace a dedicated NVIDIA rig for prototyping?”), I ran the exact same scaling benchmarks on a Windows machine running WSL2 with an NVIDIA RTX 3070 (8 GB).
I benchmarked MettleQ on Apple Silicon against three major CUDA-backed frameworks: CUDA-Q, PennyLane Lightning GPU, and Qiskit Aer GPU across 15 to 28 qubits.

Exact-width GPU Acceleration Scaling across 6 representative workloads. MettleQ (M3 Pro) vs CUDA-Q, PennyLane Lightning GPU, and Qiskit Aer GPU (RTX 3070).
The scaling plots reveal two fascinating regime shifts:
Unified Memory dominates at low-to-medium qubit counts (15≤n≤24)
At lower qubit counts (15–20 qubits), dedicated GPUs suffer heavily from kernel launch overhead and memory transfer costs. CUDA-Q displays a near-flat latency floor (~60 ms) at 15 qubits across almost all algorithms.
Because Apple Silicon uses unified memory — and MettleQ avoids heavy memory movement — the M3 Pro stays well below 10 ms for 15–20 qubit circuits, often running 5× to 20× faster than the RTX 3070 baselines in this regime.
2. Standard CUDA backends bottleneck early; CUDA-Q takes over at high width (n≥26)
Across every workload tested — QFT, QAOA, GHZ, Grover, Phase Estimation, and TFIM Trotter — both Qiskit Aer GPUand PennyLane Lightning GPU scale significantly worse than MettleQ on the M3 Pro, even at 28 qubits.
However, NVIDIA’s CUDA-Q is a different beast. Once circuit size hits 26–28 qubits and compute density dominates memory overhead, CUDA-Q’s specialized CUDA kernels overtake MettleQ:

Direct head-to-head comparison between MettleQ (M3 Pro) and CUDA-Q (RTX 3070). MettleQ leads through 24 qubits, while CUDA-Q takes the lead at 28 qubits.
At 28 qubits:
CUDA-Q (RTX 3070) achieves the absolute fastest runtimes, running 2.1× to 6.7× faster than MettleQ depending on the workload (e.g., 6.7× faster on Phase Estimation, 6.0× on Grover).
MettleQ (M3 Pro) consistently beats Qiskit Aer GPU and PennyLane Lightning GPU across the entire 15–28 qubit spectrum.
If you are running massive 28+ qubit heavy-compute workloads, a dedicated NVIDIA GPU paired with CUDA-Q remains unbeatable. But for interactive development, prototyping, and simulations up to 25 qubits, a MacBook Pro running hand-written Metal kernels isn’t just viable — it actually beats standard desktop CUDA setups.
Try It
MettleQ is MIT-licensed and open source. If you work on quantum simulation, MLX/Metal, or Apple Silicon performance, I’d genuinely value your eyes on it — especially the MPS SVD path.
- Repo: [github.com/MonitSharma/MettleQ] - Technical report + all frozen benchmark evidence: in the repo
If you take one thing from this: on Apple Silicon, hand-written Metal kernels for structured quantum layers are worth roughly an order of magnitude over the generic GPU path — and the most useful thing you can publish next to a speedup is the honest list of things that are still slow.
Built on top of Qupertino by Shlomo Kashani (MIT). MettleQ is an independent fork.
Originally published on Medium.