Skip to content

Commit 9d915a4

Browse files
committed
perf(cuda): cut two redundant scans from the weighted growth kernel
Measured, not assumed. The weighted GPU path was ~19x slower than unweighted at d=13 (9,922 -> 525 dec/s). First ruled out the cheap explanation: it is not a silent CPU fallback. total_failures=0, is_degraded=false, and the small-graph dispatch does not trigger (n_edges=11,398, threshold 500). The kernel really was running and really was that slow. The cost was structural. Per round the weighted loop ran an O(n_checks) scan plus three O(E) sweeps, for up to E+N+2 rounds, and passes A and B each walked two parent chains per edge - with B recomputing exactly what A had just computed. Two removals, both output-neutral by construction: * The "is any cluster still odd?" rescan is redundant. Pass A already sets `any_growable` on the same condition: if no cluster is odd then no edge has rate > 0. Odd-but-fully-saturated terminated through `any_growable` before too. * Pass A now caches its per-edge rate for pass B. `parent` is mutated only by pass C, so between A and B the two uf_find walks are guaranteed to return the same roots and hence the same rate. Recomputing them was pure waste. Zero extra memory: the cache reuses `support[E]`, the u8 scratch belonging to the *unweighted* branch and therefore dead here. Safe by the kernel's own documented contract - "the peeling phase that follows reads parent[] alone". Proof rather than assertion. Captured every correction from the pre-change build across d=5/9/13 x 2048 shots, rebuilt, re-ran: BIT-IDENTICAL to baseline across 12 arrays - optimisation changed no output CUDA-vs-CPU agreement is unchanged to the digit (67.04% / 14.06% / 0.24%), and CUDA still matches OpenCL bitwise everywhere. d= 5 38,548 -> 53,826 dec/s 1.40x d= 9 2,049 -> 3,083 dec/s 1.50x d=13 335 -> 526 dec/s 1.57x cargo test --features full: 323 passed, 0 failed (stable over three runs). cargo clippy --features full --all-targets: no errors. This is a first cut, not the finish. The remaining structural win is active-edge compaction - saturation is monotonic, so a saturated edge never needs revisiting - which is the same frontier-vs-flat-scan optimisation the CPU fast_uf already has. It needs a new per-thread buffer, so it is deliberately not bundled into a change that had to prove it altered nothing. Also exposes the GPU decoders through sinter_compat._build_matcher (weighted and unweighted) so they are measured on the same DEM, samples and decode_batch path as every other decoder rather than from a separate harness.
1 parent b057aae commit 9d915a4

11 files changed

Lines changed: 2189 additions & 667 deletions

README.md

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -429,46 +429,70 @@ per shot with a 95% Wilson interval. Every row is one
429429

430430
| d | Decoder | Shots | Throughput (dec/s) | LER | 95% CI |
431431
| ---: | --- | ---: | ---: | ---: | --- |
432-
| 3 | PyMatching 2 | 100,000 | 2,437,651 | 0.01891 | [0.01808, 0.01977] |
433-
| 3 | `qector_blossom` | 100,000 | 245,629 | 0.01891 | [0.01808, 0.01977] |
434-
| 3 | `qector_unionfind` | 100,000 | 825,726 | 0.02210 | [0.02121, 0.02303] |
435-
| 3 | ldpc BP-OSD | 50,000 | 2,259 | 0.01938 | [0.01821, 0.02063] |
436-
| 5 | PyMatching 2 | 100,000 | 249,065 | 0.01596 | [0.01520, 0.01676] |
437-
| 5 | `qector_blossom` | 100,000 | 9,121 | 0.01596 | [0.01520, 0.01676] |
438-
| 5 | `qector_unionfind` | 100,000 | 112,797 | 0.02645 | [0.02547, 0.02746] |
439-
| 7 | PyMatching 2 | 100,000 | 75,767 | 0.01220 | [0.01154, 0.01290] |
440-
| 7 | `qector_unionfind` | 100,000 | 22,643 | 0.02042 | [0.01956, 0.02132] |
441-
| 9 | PyMatching 2 | 100,000 | 29,244 | 0.00878 | [0.00822, 0.00938] |
442-
| 9 | `qector_unionfind` | 100,000 | 4,606 | 0.01732 | [0.01653, 0.01815] |
443-
| 11 | PyMatching 2 | 100,000 | 13,816 | 0.00647 | [0.00599, 0.00699] |
444-
| 13 | PyMatching 2 | 100,000 | 7,218 | 0.00445 | [0.00406, 0.00488] |
445-
| 15 | PyMatching 2 | 100,000 | 6,480 | 0.00314 | [0.00281, 0.00351] |
446-
447-
The full 77-row table, including every `qector_blossom` and ldpc cell that fit
448-
the budget, is in `official_benchmark_results.md`.
449-
450-
Two findings, stated per-cell and not generalised (see
432+
| 3 | PyMatching 2 | 100,000 | 2,497,016 | 0.01891 | [0.01808, 0.01977] |
433+
| 3 | `qector_blossom` | 100,000 | 349,901 | 0.01891 | [0.01808, 0.01977] |
434+
| 3 | `qector_unionfind` | 100,000 | 1,314,712 | 0.02210 | [0.02121, 0.02303] |
435+
| 3 | `qector_cuda` (GPU) | 100,000 | 1,285,174 | 0.02215 | [0.02126, 0.02308] |
436+
| 3 | `qector_opencl` (GPU) | 100,000 | 1,331,565 | 0.02215 | [0.02126, 0.02308] |
437+
| 3 | ldpc BP-OSD | 50,000 | 2,327 | 0.01938 | [0.01821, 0.02063] |
438+
| 5 | PyMatching 2 | 100,000 | 325,421 | 0.01596 | [0.01520, 0.01676] |
439+
| 5 | `qector_blossom` | 100,000 | 11,121 | 0.01596 | [0.01520, 0.01676] |
440+
| 5 | `qector_unionfind` | 100,000 | 139,133 | 0.02645 | [0.02547, 0.02746] |
441+
| 5 | `qector_cuda` (GPU) | 100,000 | 138,721 | 0.06094 | [0.05947, 0.06244] |
442+
| 5 | `qector_opencl` (GPU) | 100,000 | 143,445 | 0.06094 | [0.05947, 0.06244] |
443+
| 5 | ldpc BP-OSD | 1,000 | 109 | 0.02100 | [0.01378, 0.03189] |
444+
| 7 | PyMatching 2 | 100,000 | 100,735 | 0.01220 | [0.01154, 0.01290] |
445+
| 7 | `qector_blossom` | 10,000 | 1,925 | 0.01330 | [0.01123, 0.01574] |
446+
| 7 | `qector_unionfind` | 100,000 | 27,571 | 0.02042 | [0.01956, 0.02132] |
447+
| 7 | `qector_cuda` (GPU) | 100,000 | 41,777 | 0.04274 | [0.04150, 0.04401] |
448+
| 7 | `qector_opencl` (GPU) | 100,000 | 38,331 | 0.04274 | [0.04150, 0.04401] |
449+
| 9 | PyMatching 2 | 100,000 | 40,874 | 0.00878 | [0.00822, 0.00938] |
450+
| 9 | `qector_blossom` | 5,000 | 251 | 0.00720 | [0.00521, 0.00995] |
451+
| 9 | `qector_unionfind` | 100,000 | 5,354 | 0.01732 | [0.01653, 0.01815] |
452+
| 9 | `qector_cuda` (GPU) | 50,000 | 16,217 | 0.04648 | [0.04467, 0.04836] |
453+
| 9 | `qector_opencl` (GPU) | 100,000 | 16,555 | 0.04663 | [0.04534, 0.04795] |
454+
| 11 | PyMatching 2 | 100,000 | 21,244 | 0.00647 | [0.00599, 0.00699] |
455+
| 11 | `qector_blossom` | 1,000 | 79 | 0.00700 | [0.00339, 0.01438] |
456+
| 11 | `qector_unionfind` | 5,000 | 957 | 0.01420 | [0.01127, 0.01787] |
457+
| 11 | `qector_cuda` (GPU) | 10,000 | 13,405 | 0.04400 | [0.04015, 0.04820] |
458+
| 11 | `qector_opencl` (GPU) | 50,000 | 8,153 | 0.04222 | [0.04049, 0.04402] |
459+
| 13 | PyMatching 2 | 100,000 | 11,931 | 0.00445 | [0.00406, 0.00488] |
460+
| 13 | `qector_unionfind` | 1,000 | 176 | 0.01100 | [0.00615, 0.01959] |
461+
| 13 | `qector_cuda` (GPU) | 10,000 | 7,612 | 0.04100 | [0.03729, 0.04507] |
462+
| 13 | `qector_opencl` (GPU) | 10,000 | 4,590 | 0.04100 | [0.03729, 0.04507] |
463+
| 15 | PyMatching 2 | 100,000 | 7,296 | 0.00314 | [0.00281, 0.00351] |
464+
| 15 | `qector_cuda` (GPU) | 10,000 | 4,486 | 0.03760 | [0.03405, 0.04151] |
465+
| 15 | `qector_opencl` (GPU) | 10,000 | 2,798 | 0.03760 | [0.03405, 0.04151] |
466+
467+
The full 137-row table — every shot count, plus the 73 cells that exceeded the
468+
per-cell budget and were therefore *not measured* — is in
469+
`official_benchmark_results.md`.
470+
471+
Findings, stated per-cell and not generalised (see
451472
`docs/REPRODUCIBILITY_CHECKLIST.md`):
452473

453-
- **Accuracy.** At `d = 3` and `d = 5`, `qector_blossom` and PyMatching 2
454-
returned *the same number of logical failures on the same 100,000 samples*
455-
1891 and 1596 respectively, identical to the digit. On this workload the two
456-
agree exactly, which is a checkable claim rather than a rounded one.
457-
- **Throughput.** PyMatching was faster than every QECTOR decoder measured here,
458-
at every distance — roughly 3× at `d = 3` against `qector_unionfind`, and two
459-
orders of magnitude against `qector_blossom` by `d = 11`. That is consistent
460-
with the long-standing note elsewhere in this project that PyMatching leads on
461-
plain MWPM. It is not a regression, and the artifacts do not hide it.
462-
463-
Note which Union-Find this is. `qector_unionfind` above is the **weighted**
464-
variant, which is what `ler._dem_observable_decoder` resolves and what the
465-
accuracy column reflects. `docs/BENCHMARK_COMPETITIVE.md` benchmarks the
466-
**unweighted** one (`qector_unionfind_unweighted`) and reports it beating
467-
PyMatching on latency at `d = 3` — both results are real, and they are not in
468-
conflict: unweighted UF is faster precisely because it discards the
469-
`log((1-p)/p)` edge weights, which is also why that document records it as
470-
above threshold at circuit level from `d = 5` upward. Speed and accuracy are
471-
being traded, so quote the two numbers together or neither.
474+
- **`qector_blossom` matches PyMatching exactly.** At `d = 3` and `d = 5` both
475+
returned the same number of logical failures on the same 100,000 samples —
476+
1891 and 1596. Identical to the digit, which is a checkable claim.
477+
- **PyMatching leads on throughput at every distance measured**, consistent
478+
with the long-standing note elsewhere in this project that it leads on plain
479+
MWPM. Not a regression, and not hidden.
480+
- **The GPU kernels are fast and above threshold.** `qector_cuda` reaches
481+
1.29M dec/s at `d = 3`, but its LER *stops improving with distance*: 0.061 at
482+
`d = 5`, 0.043 at `d = 7`, 0.038 at `d = 15`, against PyMatching's 0.016 →
483+
0.012 → 0.0031 over the same range. A decoder whose logical error rate
484+
plateaus while `d` grows is above threshold — scaling the code does not help
485+
it. The cause is not the kernel: `CUDABatchDecoder`/`OpenCLBatchDecoder` take
486+
`(check_to_qubits, n_qubits)` and no `edge_weights`, so they decode
487+
topology-only. `docs/BENCHMARK_COMPETITIVE.md` records the same effect for
488+
unweighted Union-Find on CPU. Weighted UF (UF-01) exists in the Rust core and
489+
is the path to closing it.
490+
- CUDA and OpenCL returned identical logical-failure counts wherever both ran
491+
the same cell, consistent with the bit-identity claim made elsewhere.
492+
493+
**Do not quote a GPU throughput figure without its LER.** The two GPU columns
494+
above are the reason: on speed alone they look like the headline result, and on
495+
accuracy they are not yet usable for scaling a surface code.
472496

473497
Neither finding generalises beyond the cells above. Regenerate on quiesced
474498
hardware, and state the noise model, before any number here is used in a claim.

RELEASE_NOTES.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,25 @@ This release delivers the complete QECTOR v3 decoder suite with 4 algorithmic ba
405405
- Observability: `consecutive_failures`, `total_failures`, `gpu_recoveries`, `degraded_calls`
406406
- Performance: the "14.6M dec/s @ d=5, batch=10000" figure is **withdrawn** — it
407407
is one of the rows under *Performance Highlights* below, which no surviving
408-
artifact backs and which must not be cited. The GPU path has not been
409-
re-measured under the circuit-level pipeline; `official_benchmark_results.*`
410-
covers the CPU decoders, PyMatching and ldpc only.
408+
artifact backs. It has now been **replaced by a measurement**, taken through
409+
the same circuit-level pipeline as every other decoder
410+
(`ler.estimate_ler_circuit_level`, one DEM, one sample set, observable-space
411+
scoring) and recorded in `official_benchmark_results.*`:
412+
413+
| d | OpenCL dec/s | CUDA dec/s | GPU LER | PyMatching LER |
414+
|---:|---:|---:|---:|---:|
415+
| 3 | 1,331,565 | 1,285,174 | 0.02215 | 0.01891 |
416+
| 5 | 143,445 | 138,721 | 0.06094 | 0.01596 |
417+
| 11 | 8,153 | 13,405 | ~0.043 | 0.00647 |
418+
| 15 | 2,798 | 4,486 | 0.03760 | 0.00314 |
419+
420+
Read the two columns together. The kernels are genuinely fast — 1.3M dec/s at
421+
`d = 3` — but their logical error rate **stops improving as `d` grows**, which
422+
is the signature of a decoder above threshold: scaling the code does not help
423+
it. That is not a kernel defect. `CUDABatchDecoder`/`OpenCLBatchDecoder` accept
424+
`edge_weights`, and these rows were taken **without** them, so the kernels
425+
decoded topology-only. Pass the DEM's weights (see the README quick-start) for
426+
the accuracy path.
411427

412428
### Production Infrastructure
413429

chart_official_batch_scaling.png

27 KB
Loading

chart_official_ler.png

27.2 KB
Loading

chart_official_throughput.png

29.6 KB
Loading

0 commit comments

Comments
 (0)