Status: release report. Every number below is traceable to a JSON receipt in
bench/receipts/ (provenance in that directory's
README). Receipts marked carried come byte-verbatim from the source
research runs; the rest were generated by scripts in bench/ against the
packaged library. Remaining TODOs (latency-table distillation, a
parameter-matched CUDA-Q re-run, a one-shot bench driver) are post-0.1.0.
The Triton min-sum BP and Relay-BP decoders in this package are
vendor-portable (the same kernel source runs on NVIDIA CUDA and AMD ROCm)
and LER-faithful (logical error rates statistically indistinguishable from
the standard CPU references: ldpc's min-sum BP family and IBM's relay-bp
Rust decoder) on circuit-level quantum LDPC decoding problems, while
delivering GPU-batch throughput.
No claim of being fastest. No claim of being first. The claim is: open, portable across the two major GPU vendors, and validated against the references the field already trusts.
As of June 2026, NVIDIA's nv-qldpc-decoder is closed-source; this library provides an open, vendor-portable alternative validated against the standard CPU references.
Workload. Circuit-level memory experiments on the [[72,12,6]]
bivariate-bicycle (BB) code, 6 syndrome rounds, SI1000 superconducting noise,
p ∈ {0.001, 0.002, 0.003, 0.005} × basis ∈ {X, Z}. The canonical circuits ship
as .stim fixtures in tests/fixtures/bb72/. DEM structure at the reference
cell (p=0.003, Z): 252 detectors, 1584 error mechanisms, 4536 Tanner edges,
check degree 13/20, bit degree 2/3. A rotated surface-code fixture
(d=3, r=3) covers code-agnosticism.
Matched protocol. Every comparison decodes ONE shared DEM
(decompose_errors=False) and ONE sampled shot set (fixed seed) — the
run_matched harness in tridec.validation enforces this with
fail-fast gates: G1 (every decoder's DEM hashes to the same sha256) and G2
(every decoder pre-declares a deterministic tie-break). Per-cell DEM sha256s
are pinned in zoo_grid.json and re-verified by this package's test suite.
Statistics. Wilson 95% CIs on failure counts; gap-to-MLE as a per-shot PAIRED bootstrap of decoder-vs-anchor failure indicators on the same shots (10,000 resamples), never an aggregate ratio of marginal counts; Holm/BH for grid-wide multiplicity.
Configurations. BP family: normalized min-sum, ms_scaling_factor=0.625,
max_iter=30, parallel schedule. Relay-BP: gamma0=0.1, pre_iter=80,
num_sets=60, set_max_iter=60, gamma ∈ (-0.24, 0.66), stop_nconv=5
(lowest-weight valid solution) — the relay-bp 0.2.2 oracle configuration.
The binding portability result: the SAME kernel source, validated on both
vendors against the relay-bp Rust oracle (F64) on 2000 canonical shots
(p=0.003, Z, seed 0). Receipts: bench_relay_triton.json (H200),
bench_relay_mi300x.json (MI300X).
Reading note: the table below shows the carried receipts — both vendors decoding the source-platform shot set, hence identical counts. The packaged-API re-validation further down re-samples shots locally on each platform (stim's sampler is platform-dependent, §5.1), so its counts (e.g. 34 vs oracle 31) differ from this table without being inconsistent with it.
| NVIDIA H200 (CUDA 12.4, triton 3.0) | AMD MI300X (ROCm 7.0, triton 3.4) | |
|---|---|---|
| Pre-leg posterior max-diff vs oracle | 1.78e-15 | 1.78e-15 |
| Memory-term per-bit agreement | 99.96% | 99.96% |
| Oracle logical errors (n=2000) | 31 | 31 |
| Triton logical errors (n=2000) | 38 | 38 |
| Wilson CIs overlap | yes | yes |
The oracle-vs-Triton gap (31 vs 38) is fp/ensemble noise (the gamma-draw RNG differs from the Rust oracle's by construction); the two are statistically indistinguishable, and identical across vendors.
Throughput (Relay-BP decode_batch, real canonical syndromes):
| Device | dtype | batch | per-syndrome | shots/s |
|---|---|---|---|---|
| H200 | fp32 | 2000 | 483 µs | 2,070 |
| H200 | fp64 | 2000 | 679 µs | 1,472 |
| MI300X | fp32 | 2000 | 526 µs | 1,903 |
| MI300X | fp32 | 8192 | 278 µs | 3,603 |
| MI300X | fp64 | 8192 | 403 µs | 2,479 |
Packaged re-validation (v0.1.0, MI300X). The carried MI300X receipt above
was produced in the source research repo. For the release, the same machine
class was re-validated through the installed package on a fresh MI300X
(gfx942, ROCm 7.0, torch 2.9, triton 3.4; receipt mi300x_packaged.json):
the full test suite passes (88 passed, 10 skipped — GPU tiers bound on
gfx942, the strict darwin-only receipt tiers skip as designed), and the
public from_dem/decode_batch API reproduces the canonical-cell numbers:
Triton BP 166 fails = numpy fp64 reference 166 on the same 2000
platform-local shots (99.6% per-shot agreement); Triton Relay-BP fp32 34
fails vs the relay-bp Rust oracle's 31 (98.8% per-shot agreement,
overlapping Wilson CIs); Relay-BP throughput within ±2.2% of the carried
receipt (514 vs 526 µs/syndrome at batch 2000, 281 vs 278 µs at 8192 —
fp32). One honest note: with an unpinned stim 1.16.0 the two plain-BP
statistical-tier count checks landed 5.7% from the pins (bar: 5%, Wilson
CIs still overlapped); under the documented receipt pin stim==1.15.0 the
suite is fully green. The receipt records the pinned-environment run.
Receipt: bench_triton_results.json. 2000 canonical shots, max_iter=30.
- One-iteration hard-decision agreement vs the fp64 numpy reference: 100%.
- Full-decode logical failures: numpy 156, torch 156, Triton 156 (per-shot exact prediction match 99.35%; fp32 near-tie flips cancel in LER).
decode_batchthroughput: 229k shots/s at batch 256 rising to 1.26M shots/s at batch 16384 (12×–55× over the batched torch fp64 baseline on the same device).
From the matched grid (zoo_grid.json), reference cell p=0.003, both bases,
16,667 shots each, Tesseract (A*, det_beam=64) as the MLE-proxy anchor.
Paired-bootstrap ratio to anchor [95% CI]:
| Decoder | p=0.003 X | p=0.003 Z |
|---|---|---|
| Tesseract (anchor) | 1.000 | 1.000 |
| Relay-BP | 1.050 [0.963, 1.146] | 1.004 [0.920, 1.090] |
| BP-OSD-10 | 1.058 [0.981, 1.142] | 1.035 [0.957, 1.118] |
| BP-LSD | 1.100 [1.004, 1.203] | 1.053 [0.963, 1.148] |
| BP-OSD-0 | 1.131 [1.032, 1.239] | 1.099 [1.004, 1.199] |
| plain BP (min-sum) | 5.323 [4.777, 5.991] | 4.756 [4.278, 5.313] |
Plain min-sum BP alone is ~5× off MLE on this code — that is exactly why Relay-BP is the headline algorithm and BP is positioned as the portable baseline/building block, not a recommendation.
Receipt: surface_cpu.json (generated by bench/surface_cpu.py; 50,000 shots
per cell, seed 20260610, darwin/arm64, stim 1.15.0, pymatching 2.3.1). Rotated
surface-code memory-Z, stim-generated circuits (all four noise knobs = p).
tridec BP decodes the raw DEM (torch CPU backend, fp64, validated defaults
max_iter=30, ms=0.625; numpy reference cross-checked 100% prediction-identical
on 2000-shot subsets per cell); PyMatching decodes the decomposed DEM on the
same shots.
| Cell | tridec BP fails / LER | PyMatching fails / LER | BP/MWPM ratio |
|---|---|---|---|
| d=3, r=3, p=0.003 | 1966 / 0.0393 | 321 / 0.0064 | 6.1× |
| d=3, r=3, p=0.005 | 3725 / 0.0745 | 841 / 0.0168 | 4.4× |
| d=5, r=5, p=0.003 | 4340 / 0.0868 | 173 / 0.0035 | 25.1× |
| d=5, r=5, p=0.005 | 7964 / 0.1593 | 699 / 0.0140 | 11.4× |
The honest reading, plainly: plain min-sum BP without post-processing
loses badly to matching on surface codes — this is the known landscape, not a
surprise. Degenerate weight-4 loops split BP's beliefs; the BP LER actually
increases from d=3 to d=5 at fixed p (no threshold behavior), while
matching improves exactly as it should. These receipts exist to demonstrate
code-agnostic operation with measured numbers (the same from_dem path
decodes BB codes and surface codes unmodified), not to compete with matching
on matching's home turf. On surface codes, use a matching decoder or a
post-processed BP variant.
Throughput context (same cells): torch-CPU batched BP decoded 50k shots in 3.7–34.7 s (1.4k–13.5k shots/s); PyMatching took ≤0.1 s.
OFFICIAL Relay-BP surface receipts (MI300X, triton fp32). Receipt:
surface_relay_mi300x.json (generated by bench/surface_relay_gpu.py;
50,000 shots/cell, seed 20260610, ROCm 7.0/gfx942, packaged API). Same four
cells; Relay-BP and Triton-BP decode the raw DEM, PyMatching the decomposed
DEM — all on the SAME shots, regenerated on the GPU host (stim's seeded
sampler is platform-dependent, §5.1, so the darwin counts above are not
shot-comparable; this MWPM column is):
| Cell (N=50,000) | Relay-BP (triton fp32) fails / LER | PyMatching fails / LER | Relay/MWPM | Wilson CIs |
|---|---|---|---|---|
| d=3, r=3, p=0.003 | 359 / 0.0072 | 329 / 0.0066 | 1.09 | overlap |
| d=3, r=3, p=0.005 | 928 / 0.0186 | 935 / 0.0187 | 0.99 | overlap |
| d=5, r=5, p=0.003 | 729 / 0.0146 | 168 / 0.0034 | 4.34 | disjoint |
| d=5, r=5, p=0.005 | 2571 / 0.0514 | 685 / 0.0137 | 3.75 | disjoint |
(Plain Triton-BP on the same shots: 2060 / 3840 / 4382 / 7987 fails — the §3.4 CPU picture, unchanged on GPU.)
The honest reading, plainly: Relay-BP's disordered-memory ensemble
recovers essentially the entire plain-BP-vs-matching gap at d=3 — it is
statistically indistinguishable from MWPM there (overlapping CIs at both p).
At d=5 it does NOT: Relay-BP trails matching by 3.8–4.3× with disjoint CIs.
The preliminary Metal sample's directional finding is confirmed at proper
shot counts: on surface codes Relay-BP at the validated BB-code
configuration closes the gap at small distance but falls behind matching as
distance grows. For surface codes at scale, use a matching decoder; Relay-BP's
home turf in these receipts remains the qLDPC (BB) family, where it tracks
the MLE anchor within ~5% (§3.3). Timing note: per-cell decode_s in the
receipt includes one-time Triton JIT compilation for the first cell at each
DEM shape; the second cell at the same shape reflects steady-state.
PRELIMINARY (experimental Metal, fp32 — kept as the Metal-path receipt):
before the GPU session, the experimental triton-msl path produced a small
preliminary sample (surface_relay_metal_preliminary.json; 2000 shots/cell,
same protocol/seed, fp32, one M4 Max):
| Cell (N=2000) | Relay-BP (metal fp32) | plain BP | PyMatching |
|---|---|---|---|
| d=3, p=0.003 | 12 | 90 | 7 |
| d=3, p=0.005 | 37 | 164 | 33 |
| d=5, p=0.003 | 33 | 154 | 4 |
| d=5, p=0.005 | 98 | 332 | 25 |
Preliminary reading (now confirmed by the official MI300X receipts above): Relay-BP recovers most of the plain-BP-vs-matching gap at d=3, but a real gap remains at d=5. The Metal numbers stay here as the experimental Metal-path record; cite the MI300X table for official numbers.
Receipt: bench_cudaq_compare.json. Same 2000-shot canonical workload, same
H200. Config asymmetry, stated up front: the CUDA-Q decoder was run with
its own configuration surface — its relay selection is FirstConv-style, while
ours matches the relay-bp oracle's lowest-weight-over-nconv=5 selection.
The two columns are therefore different operating points, not a
parameter-matched kernel duel.
| CUDA-Q QEC 0.6 | tridec Triton (fp32) | relay-bp Rust (CPU) | |
|---|---|---|---|
| LER (n=2000) | 0.031 (62 fails; 0.0295 w/ OSD) | 0.016 (32 fails) | 0.0155 (31 fails) |
| Throughput @ batch 2000 | 113,585 shots/s | 2,112 shots/s | 606 shots/s |
| Best measured throughput | 503,381 shots/s @ 8192 | 8,659 shots/s @ 8192 | — |
Honest reading: CUDA-Q is ~54× faster at matched batch size and ~2× worse in LER at the configurations measured; the LER gap is a selection-rule asymmetry, not a kernel-speed artifact, and the throughput gap is real. CUDA-Q is CUDA-only and partially closed; this package is open and runs the same source on AMD. TODO: parameter-matched re-run if/when CUDA-Q exposes the equivalent stopping/selection configuration.
Receipt: latency_results.json — bootstrap-CI'd per-syndrome latency for the
CPU zoo (ldpc BP/BP-OSD/BP-LSD, relay-bp, Tesseract) and the GPU paths at
batch 1–16384 on the same H200 host. Headline: GPU batching trades
single-shot latency for throughput; at batch 1 the Triton path is NOT faster
than the Rust CPU decoder. TODO: distill the full table here.
- The matched cross-decoder grid covers one code family. The zoo/grid receipts are the [[72,12,6]] BB code at 6 rounds under SI1000. Surface-code memory carries its own receipts (§3.4): plain BP loses to matching there, as expected, and the official MI300X Relay-BP receipts show Relay-BP matching MWPM at d=3 but trailing it 3.8–4.3× at d=5 (disjoint CIs). On surface codes at growing distance, matching remains the right tool.
- Relay-BP has no in-package CPU implementation (triton backend only);
the CPU reference is the external
relay-bpRust package. - fp32 GPU messages are not bit-identical to fp64 references; the gates are ≥99.5% hard-decision agreement and LER equivalence, and near-tie flips are documented in the receipts.
- Throughput, not real-time latency. No claim of meeting a per-round decode deadline at batch 1; see §4.2.
- CUDA-Q comparison is config-asymmetric (§4.1).
- Exact-count reproduction is environment-pinned: the no-regression test reproduces grid counts exactly under stim 1.15.0 + ldpc 2.4.1 on the receipt platform (darwin/arm64) and falls back to a statistical-equivalence tier elsewhere — see §5.1.
Two cross-platform facts we measured while validating this package on
darwin/arm64 (Apple M-series) and linux/x86_64 (H200 host), same stim 1.15.0,
same .stim circuit file:
- stim's circuit→DEM computation is platform-dependent at the ulp level. 1512 of 1584 mechanism priors differed between the two platforms, max relative difference 5.4e-16 (x86 long-double intermediates vs arm64 doubles). The float text rendering differs too (x86 prints more digits). Consequence: a sha256 over the DEM's text — a common "byte-identical DEM" gate, including the one this benchmark inherited from its source experiments — is platform-local. It guarantees every decoder in one run consumes the identical DEM; it does not transfer across platforms.
- stim's seeded detector sampler is platform-dependent. The same seed on the same circuit produced entirely different detector samples on the two platforms, so exact failure-count reproduction across platforms is not merely fragile — it is impossible by construction.
Design consequence — pin artifacts, not generators: the canonical
fixtures of this package are the .dem files themselves (file bytes are
platform-independent and sha256-pinned in tests/fixtures/bb72/ dem_manifest.json); the .stim circuits are provenance, checked by a
structure-identical + priors-within-1e-12 regeneration test on every
platform, and by strict text-hash identity against the source-grid pins on
the receipt platform only. Matched-decoder comparisons that must be
reproducible across machines should distribute the DEM artifact, not the
generating circuit.
git clone <repo> && cd tridec
python -m venv .venv && . .venv/bin/activate
pip install -e ".[decoders,dev]" "stim==1.15.0" "ldpc==2.4.1"
pytest tests/ # CPU: all gates; GPU tests skip cleanly- DEM-hash gate:
pytest tests/test_dem_hash_gate.py— 8/8 fixture DEMs must hash to the receipts' pinned sha256s. - No-regression cell:
pytest tests/test_no_regression_cell.py— re-decodes the full 16,667-shot p=0.003/Z cell and requires the ldpc adapters' failure counts to equalzoo_grid.json's exactly (293 / 1346). - Full-grid no-regression (receipt env only — darwin/arm64 + stim 1.15.0 +
ldpc 2.4.1):
python bench/full_grid_noregression.py— all 8 cells × the ldpc-family adapters at the grid's exact shots/seed; receiptfull_grid_noregression.json. v0.1.0 result: 31/32 (cell, decoder) counts EXACT — all 24 BP / BP-OSD-0 / BP-OSD-10 counts reproduce exactly; BPLSD reproduces 7/8 with a single-shot deviation on p=0.002/X (879 vs 880) that a same-environment repeat experiment attributes to run-to-run nondeterminism inside ldpc'sBpLsdDecoderitself (three repeats on the byte-identical shots: 879 / 880 / 879, exactly one shot flipping — see thebplsd_determinism_probeblock in the receipt). This is why the strict per-cell test pins BP and BP-OSD-10, not BPLSD. - GPU gates (on a CUDA or ROCm box with the
[gpu]extra):pytest tests/test_bp_triton.py tests/test_relay_triton.py. - TODO: a
bench/driver script that regenerates the throughput tables from scratch on the current machine.