Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Engine performance

Where the engine’s measured limits are: a roofline analysis of the current engine against the reference host’s measured memory bandwidth, then the partitioned and distributed measurements. Every number is copied from a committed source, named inline and in the footer.

Manual: Stats, memory and logging, NUMA partitions, MPI ranks

The design these numbers measure is Inside the engine; the thread-count advice they support is in Stats, memory and logging.

Layer time breakdown

Measured on the reference host (2× Xeon Gold 6244, 16c/32t) with the repo’s phase-timing probe, per layer class, as a share of summed worker busy time (gather / sort / merge):

layer class1 thread16 threads32 threads
ZZ rotation (m = 4.50e6)54 / 9 / 3753 / 10 / 3668 / 9 / 22
general 2q unitary, sparse PTM (m = 3.0e6)38 / 33 / 2935 / 31 / 3339 / 29 / 31
dense 2q PTM, su4 (m = 4.24e7)41 / 51 / 835 / 55 / 1026 / 65 / 9

Gather + merge dominate the sparse classes; the sort dominates only dense two-qubit PTMs. Parallel efficiency (busy time over coset-loop wall × threads) is 0.99 for su4 at 16 threads: load balance is a solved problem, and the scaling limits below are memory-system effects, not imbalance.

Per-term cost is flat in the sum size over the measured range: 30.5–30.6 ns/term for the ZZ rotation across m = 1.50e6 → 4.50e6, 138.5–141.3 for the sparse 2q unitary, 322–327 for su4 (single thread).

Memory wall

The ceiling comes from a STREAM-style probe (crates/membench via scripts/bandwidth.sh), nominal bytes with plain write-allocating stores — the same store pattern the engine uses. Best GB/s on the reference host:

placementreadwritecopytriad
1 core, node-local11.310.19.511.3
one socket, 8 physical39.018.635.628.1
both sockets, 16 physical45.025.340.038.1
both sockets, 32 threads48.823.133.836.5

Two structural facts from the ceiling measurement: hyperthreads add no bandwidth (39.0 → 39.2 GB/s), and the second socket adds only 15–25% under first-touch page placement with work-stealing, not 2×.

The modeled traffic side prices each layer from its row counts at T = 48 B/term (benchmarks/PROFILING.md §Roofline model):

bytes/layer = terms_in×T + 2×(rows_gathered−rows_id)×T + 2×rows_id×16
              + 2×rows_sorted×T + terms_out×T

Reading the ratio of measured DRAM traffic to ceiling as a classification: at or above ~70% of the measured ceiling a phase is bandwidth-bound; modeled traffic far above measured means the working set is cache-served; far below ceiling with a high LLC miss rate points at latency, not bandwidth.

Single-thread roofline

Measured on the current engine (fact sheet: research/HARDWARE.md §Engine roofline, single thread; ceilings per the 1-core row above):

cellns/termmodel GB/smeasured GB/s% of copy ceilingIPCLLC load-missverdict
ZZ rotation, m = 4.50e630.58.33.2935%2.2438.3%latency-bound
sparse 2q, m = 3.0e6141.39.02.0822%2.6234.9%latency-bound
su4, m = 1.41e7327.38.60.677%2.982.1%compute-bound

The byte model over-counts DRAM traffic by 2.5–12.8× at one thread because most modeled traffic is served from cache. The sparse classes are limited by load latency (IPC ≈ 2.2–2.6 with 34–41% LLC load-miss); su4 is compute-bound in its sort (IPC 2.98, 2.1% LLC load-miss).

Multi-thread roofline

su4 at m = 1.41e7 (both-socket ceilings: read 45.0 / write 25.3 at 8–16t, 48.8 / 23.1 at 32t):

threadsns/termspeedupread GB/swrite GB/s% read ceil% write ceilverdict
1327.3—0.610.275%1%compute-bound
856.25.8×25.918.258%72%approaching write ceiling
1649.36.6×39.328.187%111%write-bandwidth-bound
3255.35.9×39.527.681%120%write-bandwidth-bound

By 16 threads the dense-PTM class sits at the machine’s write ceiling — the figures above 100% are against the STREAM nominal-store ceiling, which the engine’s write-allocating stores share, so “at the ceiling” is the correct reading. 32 threads buy zero additional bandwidth (67.0 vs 67.0 GB/s attributable) and cost 12% of wall time.

The sparse classes at 32 threads stay latency-bound and keep scaling:

| cell | ns/term | speedup vs 1t | read GB/s | write GB/s | % write ceil | verdict | |—|—:|—:|—:|—:|—:|—:|—| | ZZ rotation, m = 4.50e6 | 2.7 | 11.3× | 8.9 | 11.5 | 50% | latency-bound | | sparse 2q, m = 3.0e6 | 10.8 | 13.1× | 11.7 | 11.3 | 49% | latency-bound |

They stop at half the write ceiling with 70–90% of their modeled traffic cache-served: bandwidth is not what limits them, and they take the full thread count profitably.

Partitioned: P = 2 vs P = 1

Two facts above — the second socket adding 15–25% rather than 2×, and the dense-PTM class pinned to the write ceiling — are the same effect: pages are placed by first touch and the thread pool then steals work across sockets, so roughly half of the second socket’s reads are remote. The engine can instead be run partitioned, one pinned pool and one share of the sum per NUMA domain, with only the rows a layer moves across a domain boundary exchanged (NUMA partitions).

Locality decides everything. A layer whose gates leave the partition rows fixed runs entirely inside its own domain and gains; a layer that moves any row across a boundary pays for the export pass, the copy and the receiver’s larger merge stream. Measured at P = 2 against P = 1 on the same binary, on two-socket hosts from 8 to 48 cores per socket:

layer classP = 2 vs P = 1
dense two-qubit unitaries, no row crossing4–18% faster, direction-consistent on every host
Pauli rotations, no row crossingcoset loop 12–22% faster on two of three hosts
any layer that exports rows (random rows)2–8.4× slower, and worse at higher core counts

The gain grows with cores per socket, as a memory-system effect should: −4% on an 8-core-per-socket workstation, −18% on a 48-core Genoa at 96 threads. Load imbalance across partitions stays within 1.000–1.004 with random rows. Random partition rows put roughly half of a dense two-qubit gate’s deltas across a boundary, so the default draw lands a mixed circuit in the bottom row of that table.

The per-host cells behind the summary, copied from research/HARDWARE.md §Partitioned engine, P=1 vs P=2 in-process: scripts/ab-compare.sh runtime-knob A/B, 5 pairs abba, --n 1000000 --qubits 128 (W = 2), random partition rows, each partition pinned to its socket with MPOL_BIND; steady-state m ≈ 1.5e6 for rotations, 1.0e6 for gu2q, 1.4e7 for su4/su4_local. Wall time per layer, P=2 vs P=1 (median Δ%, pairs agreeing):

cellccqlin038 16tccqlin038 32tIcelake 32tIcelake 64tGenoa 48tGenoa 96t
su4 (random rows)+128% 6/6+118% 5/5+241% 5/5+274% 5/5+403% 5/5+373% 5/5
gu2q—+282% 5/5——+597% 5/5+574% 5/5
rotation_remote+204% 5/5+217% 5/5+310% 5/5+461% 5/5+730% 5/5+737% 5/5
rotation_local (no exchange)−13.6% 4/5−4.9% 4/5+16% 4/5+21% 5/5−12.0% 5/5−29.9% 4/5
su4_local (no exchange)−4.1% 5/5−3.5% 5/5−6.6% 5/5−7.3% 5/5−8.7% 5/5−18.2% 5/5

Exchange-free rotation layer, per layer (from the probe sidecars):

host, threadsP=1 wallP=2 wallP=2 collective (barrier_ns)coset loop P=1 → P=2
ccqlin038, 164.24 ms3.33 ms0.11 ms4.15 → 3.25 ms (−22%)
Genoa, 481.10 ms0.97 ms0.04 ms1.02 → 0.88 ms (−14%)
Icelake, 321.62 ms2.14 ms0.30 ms1.54 → 1.77 ms (+15%)

Counters, ccqlin038, dense layers at 16 threads (8 per socket at P=2); scripts/perf-stat.sh, shared box:

cellIPCLLC load-misscycles/stringS0 read / write GB/s (% of 39.0 / 18.6)S1 read / writetotal GB/s
su4 P=11.1376%717815.8 / 14.3 (40% / 77%)15.8 / 12.8 (41% / 69%)60.2
su4 P=21.0389%822912.4 / 9.7 (32% / 52%)11.8 / 9.5 (30% / 51%)44.0
su4_local P=11.0975%—16.4 / 13.8 (42% / 74%)16.4 / 13.4 (42% / 72%)60.7
su4_local P=21.2775%—18.6 / 12.9 (48% / 70%)18.8 / 13.1 (48% / 70%)64.0

partition_imbalance is 1.000–1.004 everywhere; exchange volume is bytes ≈ rows × 48.

Benchmarking

P = 1 versus P = N is a runtime-knob A/B of one binary, not a code A/B:

scripts/ab-compare.sh partitions-1v2 --a . --b . \
  --probe   '--n 1000000 --threads 32 --layers su4_local --partitions 1' \
  --probe-b '--n 1000000 --threads 32 --layers su4_local --partitions 2 \
             --partition-cpus "0-15,32-47;16-31,48-63"'

--threads is the total at every P. A partitioned cell runs under no placement prefix: numactl and taskset both defeat the split the engine is making. On an exclusive cluster node, scripts/slurm/ab-campaign.sbatch builds the CPU lists from the node’s own topology. The protocol and the sidecar fields are in benchmarks/PROFILING.md.

Distributed: weak scaling

The same split across processes is one partition per MPI rank (MPI ranks), and there the goal is capacity rather than bandwidth. Measured on Icelake ccq nodes (2 × 32 cores), one rank per NUMA domain, 32 threads per rank, UCX shared memory within a node and InfiniBand between nodes, at 6·10⁶ terms per rank:

2 ranks (1 node)4 ranks (2 nodes)8 ranks (4 nodes)
layer with no row crossing10.6 ms11.1 ms10.3 ms
Pauli rotation whose generator crosses37.5 ms (3.5×)48.3 ms (4.4×)48.6 ms (4.7×)

Weak scaling is flat once the exchange leaves the node: the remote layer costs the same at 4 and 8 ranks, and local layers cost ~10.5 ms whatever the rank count. The step from 2 to 4 is shared memory giving way to InfiniBand. A remote layer is transfer-bound — the pipeline runs the coset loop under the transfer, so what is left is the export pass plus the bytes on the wire — which means the lever is fewer bytes (locality rows, a lower truncation), not more threads.

The full per-layer breakdown, copied from research/HARDWARE.md §Partitioned engine, MPI weak scaling: phase_breakdown --mpi, replicated input --n 4e6 × ranks → 6.0e6 terms per rank (4.0e6 for cnot), --reps 8; medians over ranks, ms per layer.

ranks (nodes)layerwallexporthidden transfer (chunk wait, busy/32)coset loopremote/localpeak RSS/rank
2 (1)rotation_local10.6008.612.3 GB
2 (1)rotation_remote37.58.0~2026.13.5×2.3 GB
2 (1)cnot29.510.0~914.6—2.4 GB
4 (2)rotation_local11.1006.913.3 GB
4 (2)rotation_remote48.37.2~2732.74.4×3.3 GB
4 (2)cnot28.78.8~1116.3—3.3 GB
8 (4)rotation_local10.3006.816.3 GB
8 (4)rotation_remote48.67.3~2833.34.7×6.3 GB
8 (4)cnot42.8————6.3 GB

The remote layer is transfer-bound: ~6–8 ms compute, 7–8 ms export, ~30 ms transfer for 192 MB per rank (two ranks share a NIC → ~13 GB/s per node). vmhwm growth with rank count is the probe’s replicated input, not the engine; engine-side peak per rank is flat.

Noise floor

Single-shot campaign noise on the reference host is ±5–8% single-threaded and ±10–26% at 8–32 threads; untouched code moves that much between campaigns. Effects below that need the interleaved A/B protocol (scripts/ab-compare.sh), whose acceptance criterion is direction consistency across every pair.

Sources: research/HARDWARE.md (roofline tables, phase shares, bandwidth ceilings, partitioned and distributed tables, all achieved numbers and verdicts); benchmarks/PROFILING.md (byte model, interpretation rules, noise floor); the Rust API (layout and dispatch).