Repository navigation
feat(statevector): add the host code that GPU backends share - #284
Conversation
☂️ Code Coverage
Overall Coverage
New Files
Modified Files
|
de67b88 to
3d33768
Compare
The CUDA and Metal backends of the statevector executor share their host side: `statevector/gpu/` holds the noise kernels' tables (`noise`), a tape generator that writes a component's tape in either language through a small `Dialect` (`tape`), and the packing of returned rows (`rows`). Two changes to the statevector plans come with it. `shared.common_prefix` returns the leading actions of a tape that every shot runs alike, which a GPU backend runs once. `plan_component` also plans each component with its measurements deferred (`schedule(defer=True)`) and keeps that plan when it is no wider and leaves less shot-dependent work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rk model - noise_tables raises when an outcome flips more than 255 words (the record's 8-bit count would wrap) and when the events alone do not fit shared_bytes without shared words (MIN_EVENTS is now a named constant). - Dialect rejects an amplitude_chunk that is not a positive power of two, which would declare fewer chunk arrays than the tape indexes. - _shot_work ends the shared prefix where shared.common_prefix does: an unread dormant measurement is a fair coin and no longer counts as shot work. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
3d33768 to
1c3a3d8
Compare
Below p ~ 2^-24, a kernel's 24-bit geometric skip reaches only some trials of a segment, so a group of many such channels fired them at biased per-channel rates (0.93 to 1.17 of their rates at p = 1e-9 on 3200 channels; the total rate was right). Such groups are now cut into segments of fewer than 2^-13 expected faults, which the kernels draw from 48 bits (their fine path). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild #219's CUDA backend on #284's tsim.statevector.gpu: the tape comes from gpu.tape with a CUDA dialect (byte-identical to #219's codegen), the noise tables from gpu.noise and the row packing from gpu.rows. cuda/ keeps the CUDA prelude, the noise kernels' source, the runtime and the sampler. The dispatch follows the Metal backend's shape (_gpu_shots): TSIM_DEVICE=cpu keeps the samplers on the CPU, sampler.stats.device and the repr say where a sampler samples, programs without shot words stay on the CPU, and noise beyond the GPU tables' limits falls back to the CPU with a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both dialects are internal constants, so a ValueError and its tests guard nothing a user can reach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both backends return every output, in order, so each packed word is a whole shot word and the piece tables only encoded the identity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both GPU backends built the read mask of a postselected batch, chose the deferred channels and estimated the screen's pass rate with the same code; gpu.noise now has split, screen_pass_rate and words_fit (the shared-memory fit test that Metal repeated with literals). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both GPU backends wrote the same component calls, seeds, word stores and postselection hits around the tape functions, and repeated MAX_WIDTH, the threads per shot, the deviation tolerance and the layout check. gpu.tape now has tape_kernel, threads_per_shot, deviation and supports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild #219's CUDA backend on #284's tsim.statevector.gpu: the tape comes from gpu.tape with a CUDA dialect (byte-identical to #219's codegen), the noise tables from gpu.noise and the row packing from gpu.rows. cuda/ keeps the CUDA prelude, the noise kernels' source, the runtime and the sampler. The dispatch follows the Metal backend's shape (_gpu_shots): TSIM_DEVICE=cpu keeps the samplers on the CPU, sampler.stats.device and the repr say where a sampler samples, programs without shot words stay on the CPU, and noise beyond the GPU tables' limits falls back to the CPU with a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a7f4cfaca
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| keep = ( | ||
| 1 << 24 | ||
| if hi - lo == 1 | ||
| else min(round(float(fire.alias_prob[k]) * (1 << 24)), 1 << 24) |
There was a problem hiding this comment.
Preserve rare outcomes in alias records
When a multi-outcome channel has a conditional outcome probability below roughly half of 2^-24, rounding its alias keep probability produces zero and makes that outcome impossible on the GPU. For example, HERALDED_PAULI_CHANNEL_1(0.1, 1e-10, 0, 0) produces an alias probability near 3e-9, which this line stores as zero; the fine geometric-skip path does not help because the channel's total fire probability is 0.1. This silently removes rare correlated outcomes, so the table builder should retain sufficient alias precision or reject/fall back for channels it cannot represent.
Useful? React with 👍 / 👎.
| # ~1e-12 of the total; it counts as impossible (CERTAIN), and the others | ||
| # are drawn against the full 64-bit draw, as the CPU executor draws them. | ||
| emit( | ||
| f" if (w1 <= {CERTAIN}f * total) outcome = 0; else if (w0 <= {CERTAIN}f * total) outcome = 1;" |
There was a problem hiding this comment.
Draw genuine measurement branches below the certainty cutoff
For circuits with a genuinely rare measurement branch, this cutoff turns the branch into an impossible outcome rather than merely compensating for float32 residue. A rotation chosen so that the measured branch has probability below 1e-11—for example, a small-angle rotation followed by measurement or postselection—will always produce the other outcome on the GPU, even though the CPU sampler assigns it nonzero probability. Structural certainty needs to be distinguished from a small but valid weight instead of applying this blanket threshold.
Useful? React with 👍 / 👎.
… executor The looping tape path (UNROLL_BUDGET exceeded) was never generated in CI. Compile each tape in a C++ dialect for one thread per shot with g++ and check that looping and unrolled tapes (in each dialect variant) agree with each other and with shared.run_lanes on random shot words. Also cover rows.select's partial last byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A slot's keep threshold had 24 bits, so an outcome below about 1e-8 of its channel rounded to zero and the GPU never drew it, e.g. the X branch of HERALDED_PAULI_CHANNEL_1(0.1, 1e-10, 0, 0). Thresholds now have 40 bits (the low 16 in the record's spare field), and a nonzero one never rounds to zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GPU takes an outcome below 1e-11 of the total weight as float32 residue and never draws it, which also dropped genuine branches of small-angle rotations. A layout with a rotation branch below 1e-8 (R_Z by less than about 1e-4 pi) now stays on the CPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stacked on #284. On an Apple-silicon Mac, `strategy="active_statevector"` now samples on the GPU through Metal whenever every component has at most 11 active qubits. A plain install includes it: Tsim depends on `pyobjc-framework-Metal` (about 7 MB) on Apple silicon only, so other platforms install nothing new. ## What users get - **Automatic selection:** no new argument. - The samplers fall back to the CPU, with a warning, when a component is wider than 11 qubits, the noise is beyond the GPU tables' limits, the GPU is out of memory, a kernel does not compile, or the process was forked after its parent used the GPU. A GPU whose SIMD lanes do not follow thread order (checked by a probe kernel) is not used at all. - The repr says where a sampler samples (`samples on metal`), and so does `sampler.stats.device`. - **Opt-out:** `TSIM_DEVICE=cpu` keeps the samplers on the CPU, in double precision. Tsim reads it each time it compiles a sampler. - **Precision and seeds:** the GPU computes outcome probabilities in float32 and never draws an outcome below about 1e-11. A seed's GPU samples differ from the CPU's and depend on `batch_size`. The install page, the docstrings, the user skill, AGENTS.md and the CHANGELOG say so. ## How it works - **Kernels:** the noise sampler and every component's tape run as Metal kernels that PyObjC compiles at runtime. Metal caches them across processes. - The tapes come from #284's shared generator, with a Metal dialect: no `goto`, SIMD-group shuffles, and amplitudes in arrays of 32. Metal moves an array of 64 to the stack, which costs 1.8×. - The rarer outcome is drawn against its own share, in float-float, since Metal has no double. - Noise below p ≈ 1e-12 per segment takes a 48-bit fine path. - **Rows:** postselection, row packing and the boolean unpack all run on the GPU. Buffers live in unified memory, so nothing is copied, and freed output buffers are reused. - **Wide rows:** rows over 47 words keep their noise words in device memory. ## Benchmark Every tsim-bench circuit that Metal runs (179 circuits with k ≤ 11, from 28 papers), Metal against one CPU core of the same Mac: `tsim-dev bench --suite corpus --max-width 11 --side .:device=gpu --side .:device=cpu,threads=1` at `58471a37`, which includes the harness fix from #292 (each device calibrates its own call size). Apple M4 Pro, 20-core GPU, macOS 27.2. Detector sampler, bit-packed, each circuit postselected as its paper does; the rate counts requested shots, kept or not. - **All 179 run on the GPU**, none failed. Speedup over one core: median **43×**, geometric mean **39×**, range 0.81–835×. 12 circuits are above 200×, 61 at 50–200×, 80 at 10–50×, 22 at 1–10×, and only `qv_q10` is below 1×. | circuit | k | kept | Metal | 1 CPU core | Metal ÷ 1 core | |---|--:|--:|--:|--:|--:| | **end2end_d5_dsurface15_p0.001** (cultivation d=5, end to end) | 6 | 0.052 | 313M | 1.37M | **229×** | | end2end_d3_dsurface15_p0.001 | 4 | 0.54 | 163M | 1.65M | 99× | | inject_cultivate_d3_p0.001 | 4 | 0.69 | 1.98G | 36.9M | 54× | | inject_cultivate_d5_p0.001 | 6 | 0.14 | 748M | 20.0M | 37× | | cult_growing_T_d5_to_d13_p0.001 | 6 | 0.12 | 351M | 1.99M | 176× | | gauging_cultivation_d7_T_p0.001 | 8 | 0.00023 | 435M | 4.22M | 103× | | distillation_color_d5_Z | 3 | 0.080 | 2.55G | 25.0M | 102× | | bb144_cfg369_inj1_T_p0.001 (qLDPC injection) | 1 | 0.070 | 1.32G | 8.54M | 155× | | hook_inject_T_d15_pd5_p0.001 | 1 | 0.67 | 62.4M | 1.76M | 35× | | surface_d7_r7 | 0 | – | 757M | 21.9M | 35× | | coherent_d3_r3 | 4 | – | 1.16G | 26.6M | 44× | | coherent_d5_r5 | 11 | – | 467k | 3.0k | 156× | | MSC_circuit_d7_p0.0005 | 8 | – | 31.4M | 56.1k | 560× | | stability_d3_X_t20_alpha1.0_p0.001 | 3 | – | 133M | 203k | 656× | | bh40-T (Magna) | 3 | 0.88 | 757M | 920k | 823× | | distillation_15to1_T_d25_p0.001 | 4 | – | 606k | 66.8k | 9.1× | | rotated-init-x-injection-double-HXY_d1=3_p=0.001 | 8 | 0.62 | 64.1M | 12.4M | 5.2× | | coherent_d3_r1 | 0 | – | 8.19G | 2.17G | 3.8× | | tricycle_6_3_2 | 9 | 0.92 | 17.5M | 5.16M | 3.4× | | star_d=11 | 2 | 0.00095 | 3.58M | 1.36M | 2.6× | | tricycle_9_3_2 | 11 | 0.87 | 757k | 378k | 2.0× | | qv_q10 | 10 | – | 11.8M | 14.6M | 0.81× | - **Largest gains** come from circuits whose cost is mostly noise sampling and postselection (the Magna `bh*-T` distillation, stability experiments, end-to-end cultivation): those parallelize across the GPU. - **Smallest gains** come from wide components without much noise (`qv_q10`, `tricycle_*`, k = 9–11), where every shot is limited by the GPU's arithmetic, and where the CPU shares trajectories between shots that draw the same outcomes. A component of k = 9 runs at about 58 ns per shot, close to the M4 Pro's FMA and shuffle throughput. - **Compile:** Metal adds about 0.1–0.5 s per process: about 0.1 s for PyObjC and the device, plus building the GPU sampler. - The machine had light other load (3–4 busy cores) during the run. Each ratio is the mean of 3–5 interleaved rounds; the CPU column is Metal's rate divided by it. <details><summary>All 179 circuits</summary> | Paper | Circuit | k | Kept | Metal (shots/s) | 1 CPU core (shots/s) | Metal ÷ 1 core | |---|---|--:|--:|--:|--:|--:| | 1811.12926_quantum_volume | `qv_q10` | 10 | - | 11.8 M | 14.6 M | 0.81× | | 2302.12292_hook_injection | `hook_inject_T_d15_pd5_p0.001` | 1 | 0.667 | 62.4 M | 1.76 M | 35.4× | | 2302.12292_hook_injection | `hook_inject_T_d15_pd7_p0.001` | 1 | 0.45 | 66.3 M | 1.7 M | 39× | | 2302.12292_hook_injection | `li_inject_T_d15_pd5_p0.001` | 1 | 0.457 | 29.6 M | 891 k | 33.2× | | 2302.12292_hook_injection | `li_inject_T_d15_pd7_p0.001` | 1 | 0.196 | 30.3 M | 853 k | 35.5× | | 2302.12292_hook_injection | `pregrown_hook_inject_T_d15_pd5_p0.001` | 1 | 0.587 | 53.5 M | 1.56 M | 34.4× | | 2302.12292_hook_injection | `pregrown_hook_inject_T_d15_pd7_p0.001` | 1 | 0.377 | 57.6 M | 1.56 M | 36.9× | | 2302.12292_hook_injection | `zz_inject_T_d15_pd5_p0.001` | 1 | 0.531 | 26.5 M | 830 k | 31.9× | | 2302.12292_hook_injection | `zz_inject_T_d15_pd7_p0.001` | 1 | 0.269 | 27.3 M | 832 k | 32.8× | | 2406.17653_transversal_algorithmic_ft | `s_injection_T_d3_p0.001_pinj0.1` | 1 | 0.912 | 3.97 G | 55.8 M | 71.1× | | 2406.17653_transversal_algorithmic_ft | `s_injection_T_d5_p0.001_pinj0.1` | 1 | 0.912 | 2.45 G | 45.7 M | 53.6× | | 2406.17653_transversal_algorithmic_ft | `s_injection_T_d7_p0.001_pinj0.1` | 1 | 0.912 | 1.08 G | 35.7 M | 30.2× | | 2406.17653_transversal_algorithmic_ft | `s_injection_T_d9_p0.001_pinj0.1` | 1 | 0.912 | 711 M | 26.4 M | 27× | | 2409.07707_color_code_msd | `cult_growing_T_d3_to_d11_p0.001` | 4 | 0.644 | 296 M | 3.63 M | 81.5× | | 2409.07707_color_code_msd | `cult_growing_T_d3_to_d7_p0.001` | 4 | 0.644 | 845 M | 11.5 M | 73.3× | | 2409.07707_color_code_msd | `cult_growing_T_d5_to_d13_p0.001` | 6 | 0.12 | 351 M | 1.99 M | 176× | | 2409.07707_color_code_msd | `cult_growing_T_d5_to_d9_p0.001` | 6 | 0.12 | 584 M | 5.42 M | 108× | | 2409.17595_magic_state_cultivation | `end2end_d3_dsurface15_p0.001` | 4 | 0.539 | 163 M | 1.65 M | 98.6× | | 2409.17595_magic_state_cultivation | `end2end_d5_dsurface15_p0.001` | 6 | 0.0519 | 313 M | 1.37 M | 229× | | 2409.17595_magic_state_cultivation | `inject_cultivate_d3_p0.001` | 4 | 0.687 | 1.98 G | 36.9 M | 53.7× | | 2409.17595_magic_state_cultivation | `inject_cultivate_d5_p0.001` | 6 | 0.144 | 748 M | 20 M | 37.3× | | 2502.01743_surface_code_cultivation | `expanded-rotated-init-x-injection-double-HXY_d1=3_d2=11_p=0.001` | 8 | 0.648 | 46.9 M | 2.9 M | 16.1× | | 2502.01743_surface_code_cultivation | `expanded-rotated-injection-double-ghz-HXY_d1=3_d2=11_p=0.001` | 8 | 0.626 | 52.1 M | 2.97 M | 17.6× | | 2502.01743_surface_code_cultivation | `expanded-rotated-injection-syndrome-double-ghz-HXY_d1=3_d2=11_p=0.001` | 8 | 0.573 | 57.9 M | 3.03 M | 19.1× | | 2502.01743_surface_code_cultivation | `expanded-unrotated_H_injection-ghz_d1=3_d2=11_p=0.001` | 9 | 0.689 | 11.6 M | 223 k | 52.1× | | 2502.01743_surface_code_cultivation | `expanded-unrotated_H_injection-syndrome-ghz_d1=3_d2=11_p=0.001` | 9 | 0.538 | 14.6 M | 380 k | 38.4× | | 2502.01743_surface_code_cultivation | `rotated-init-x-injection-double-HXY_d1=3_p=0.001` | 8 | 0.624 | 64.1 M | 12.4 M | 5.15× | | 2502.01743_surface_code_cultivation | `rotated-init-x-injection-syndrome-double-ghz-HXY_d1=3_p=0.001` | 8 | 0.571 | 70.2 M | 13.2 M | 5.3× | | 2502.01743_surface_code_cultivation | `rotated-injection-double-ghz-HXY_d1=3_p=0.001` | 8 | 0.603 | 65.5 M | 11.8 M | 5.55× | | 2502.01743_surface_code_cultivation | `rotated-injection-ghz-HXY_d1=3_p=0.001` | 6 | 0.558 | 210 M | 19.4 M | 10.8× | | 2502.01743_surface_code_cultivation | `rotated-injection-syndrome-double-ghz-HXY_d1=3_p=0.001` | 8 | 0.552 | 73.2 M | 13.2 M | 5.54× | | 2502.01743_surface_code_cultivation | `unrotated_HXY_injection-ghz_d1=3_p=0.001` | 7 | 0.561 | 136 M | 14.9 M | 9.15× | | 2502.01743_surface_code_cultivation | `unrotated_HXY_injection-syndrome-ghz_d1=3_p=0.001` | 7 | 0.474 | 179 M | 17.4 M | 10.3× | | 2502.01743_surface_code_cultivation | `unrotated_H_injection-ghz_d1=3_p=0.001` | 9 | 0.548 | 17.8 M | 646 k | 27.6× | | 2502.01743_surface_code_cultivation | `unrotated_H_injection-syndrome-ghz_d1=3_p=0.001` | 9 | 0.463 | 24.2 M | 1.4 M | 17.3× | | 2503.18657_rp2_cultivation | `rp_3_T_cult` | 4 | 0.514 | 1.7 G | 27 M | 63.1× | | 2503.18657_rp2_cultivation | `rp_3_rp_5_T_cult` | 10 | 0.0945 | 44 M | 7.97 M | 5.52× | | 2503.18657_rp2_cultivation | `rp_3_sc_7_end2end_4_full_rds` | 4 | 0.554 | 962 M | 13.9 M | 69.2× | | 2503.18657_rp2_cultivation | `rp_3_rp_5_sc_11_end2end_6_full_rds` | 9 | 0.116 | 73.9 M | 909 k | 81.3× | | 2504.02935_erasure_injection | `hook_inject_T_d3_p0.001` | 1 | 0.902 | 4.35 G | 83.7 M | 52× | | 2504.02935_erasure_injection | `inject_cultivate_T_d3_p0.001` | 4 | 0.687 | 2.57 G | 42.2 M | 61× | | 2505.13587_correlated_decoding_distillation | `distillation_15to1_T_d5_p0.001` | 4 | - | 49.1 M | 2.03 M | 24.2× | | 2505.13587_correlated_decoding_distillation | `distillation_15to1_T_d9_p0.001` | 4 | - | 8.94 M | 634 k | 14.1× | | 2505.13587_correlated_decoding_distillation | `distillation_15to1_T_d13_p0.001` | 4 | - | 3.4 M | 289 k | 11.8× | | 2505.13587_correlated_decoding_distillation | `distillation_15to1_T_d17_p0.001` | 4 | - | 1.68 M | 155 k | 10.9× | | 2505.13587_correlated_decoding_distillation | `distillation_15to1_T_d21_p0.001` | 4 | - | 962 k | 102 k | 9.41× | | 2505.13587_correlated_decoding_distillation | `distillation_15to1_T_d25_p0.001` | 4 | - | 606 k | 66.8 k | 9.07× | | 2507.12511_unfolded_distillation | `unfolded_repetition_no_preselection_pz1e-3_rounds2` | 4 | - | 1.89 G | 37.3 M | 50.7× | | 2507.12511_unfolded_distillation | `unfolded_repetition_no_preselection_pz1e-3_rounds4` | 4 | - | 1.33 G | 24.1 M | 55.2× | | 2507.12511_unfolded_distillation | `unfolded_repetition_no_preselection_pz1e-3_rounds6` | 4 | - | 1.13 G | 18.4 M | 61.6× | | 2507.12511_unfolded_distillation | `unfolded_surface_no_bitflip_pz1e-3_rounds2` | 4 | 1 | 1.07 G | 24.4 M | 43.9× | | 2507.12511_unfolded_distillation | `unfolded_surface_no_bitflip_pz1e-3_rounds4` | 4 | 1 | 637 M | 15.5 M | 41.1× | | 2507.12511_unfolded_distillation | `unfolded_surface_no_bitflip_pz1e-3_rounds6` | 4 | 1 | 456 M | 11.2 M | 40.6× | | 2509.05212_fold_transversal_cultivation | `fold-d3_native_T_p0.001` | 6 | 0.75 | 219 M | 32.7 M | 6.69× | | 2509.05212_fold_transversal_cultivation | `fold-d5_native_T_p0.001` | 8 | 0.16 | 56.5 M | 5.23 M | 10.8× | | 2509.08658_few_clifford_terms | `d3_cultivation_T_p0.001` | 4 | 0.647 | 2.59 G | 40.7 M | 63.7× | | 2509.18294_transversal_star | `star_d=3` | 2 | 0.679 | 3.02 G | 42.1 M | 71.7× | | 2509.18294_transversal_star | `star_d=5` | 2 | 0.237 | 1.32 G | 22 M | 60× | | 2509.18294_transversal_star | `star_d=7` | 2 | 0.051 | 987 M | 12.4 M | 79.8× | | 2509.18294_transversal_star | `star_d=9` | 2 | 0.00791 | 761 M | 6.77 M | 112× | | 2509.18294_transversal_star | `star_d=11` | 2 | 0.000949 | 3.58 M | 1.36 M | 2.64× | | 2509.18294_transversal_star | `star_d=13` | 2 | 9.21e-05 | 2.34 M | 830 k | 2.82× | | 2509.18294_transversal_star | `star_d=15` | 2 | 7e-06 | 76.8 M | 1.65 M | 46.4× | | 2510.07269_transversal_dimension_jump | `tricycle_6_3_2` | 9 | 0.92 | 17.5 M | 5.16 M | 3.39× | | 2510.07269_transversal_dimension_jump | `tricycle_9_3_2` | 11 | 0.867 | 757 k | 378 k | 2× | | 2510.24615_lattice_surgery_cultivation | `inject-check-3_T` | 4 | 0.73 | 2.23 G | 45 M | 49.6× | | 2510.24615_lattice_surgery_cultivation | `lattice-surgery-complementary-gap-3-to-5-to-7_T` | 4 | 0.574 | 1.39 G | 19.6 M | 70.8× | | 2512.13908_google_cultivation_experiment | `fig2_cultivation_stim_X_T` | 4 | 1 | 5.04 G | 99.8 M | 50.5× | | 2512.13908_google_cultivation_experiment | `fig2_cultivation_stim_Z_T` | 4 | 1 | 5.04 G | 103 M | 48.7× | | 2512.13908_google_cultivation_experiment | `fig2_injection_stim_X_T` | 1 | 1 | 5.41 G | 117 M | 46.1× | | 2512.13908_google_cultivation_experiment | `fig2_injection_stim_Z_T` | 1 | 1 | 5.4 G | 117 M | 46.1× | | 2512.13908_google_cultivation_experiment | `fig2_simulation_stim_X_T` | 4 | 1 | 5.06 G | 100 M | 50.6× | | 2512.13908_google_cultivation_experiment | `fig2_simulation_stim_Z_T` | 4 | 1 | 5.07 G | 105 M | 48.4× | | 2512.13908_google_cultivation_experiment | `fig3_injection_stim_T` | 4 | 1 | 2.26 G | 102 M | 22.1× | | 2512.13908_google_cultivation_experiment | `fig3_simulation_stim_T` | 4 | 1 | 1.2 G | 98.6 M | 12.2× | | 2512.13908_google_cultivation_experiment | `fig3_simulation_zoom_stim_T` | 4 | 1 | 1.2 G | 99.8 M | 12× | | 2512.13908_google_cultivation_experiment | `fig3_with_idle_rounds_stim_T` | 4 | 1 | 1.2 G | 100 M | 11.9× | | 2512.13908_google_cultivation_experiment | `fig3_with_idle_rounds_zoom_stim_T` | 4 | 1 | 1.2 G | 98.9 M | 12.1× | | 2512.13908_google_cultivation_experiment | `fig3_without_idle_rounds_stim_T` | 4 | 1 | 3.27 G | 103 M | 31.9× | | 2512.13908_google_cultivation_experiment | `fig4_cultivation_N2_stim_X_T` | 4 | 1 | 2.45 G | 95.2 M | 25.7× | | 2512.13908_google_cultivation_experiment | `fig4_cultivation_N2_stim_Z_T` | 4 | 1 | 2.43 G | 96 M | 25.3× | | 2512.13908_google_cultivation_experiment | `fig4_cultivation_N3_stim_X_T` | 4 | 1 | 2.95 G | 95.3 M | 31× | | 2512.13908_google_cultivation_experiment | `fig4_cultivation_N3_stim_Z_T` | 4 | 1 | 3.05 G | 96.2 M | 31.7× | | 2512.13908_google_cultivation_experiment | `fig4_injection_N2_stim_X_T` | 1 | 1 | 4.65 G | 117 M | 39.7× | | 2512.13908_google_cultivation_experiment | `fig4_injection_N2_stim_Z_T` | 1 | 1 | 4.65 G | 118 M | 39.5× | | 2512.13908_google_cultivation_experiment | `fig4_simulation_injection_N2_stim_X_T` | 1 | 1 | 4.68 G | 117 M | 39.9× | | 2512.13908_google_cultivation_experiment | `fig4_simulation_injection_N2_stim_Z_T` | 1 | 1 | 4.67 G | 117 M | 39.9× | | 2512.23037_soft | `circuit_d3_p0.001` | 4 | 0.687 | 1.87 G | 44.8 M | 41.7× | | 2512.23037_soft | `circuit_d5_p0.001` | 6 | 0.144 | 976 M | 14.1 M | 69.1× | | 2603.05429_gauging_clifford_cultivation | `gauging_cultivation_d3_T_p0.001` | 4 | 0.461 | 1.89 G | 32 M | 59.1× | | 2603.05429_gauging_clifford_cultivation | `gauging_cultivation_d5_T_p0.001` | 6 | 0.0401 | 577 M | 10 M | 57.7× | | 2603.05429_gauging_clifford_cultivation | `gauging_cultivation_d7_T_p0.001` | 8 | 0.000225 | 435 M | 4.22 M | 103× | | 2604.01059_tsim | `distillation_color_d5_X` | 4 | 0.0797 | 2.73 G | 31.9 M | 85.6× | | 2604.01059_tsim | `distillation_steane_d3_X` | 4 | 0.353 | 3.13 G | 43.6 M | 71.7× | | 2604.01059_tsim | `distillation_unencoded_X` | 4 | - | 2.85 G | 58.5 M | 48.7× | | 2604.01059_tsim | `distillation_color_d5_Y` | 3 | 0.0874 | 2.78 G | 32.7 M | 84.9× | | 2604.01059_tsim | `distillation_steane_d3_Y` | 3 | 0.367 | 3.36 G | 43.3 M | 77.5× | | 2604.01059_tsim | `distillation_unencoded_Y` | 3 | - | 3.28 G | 58.7 M | 55.9× | | 2604.01059_tsim | `distillation_color_d5_Z` | 3 | 0.0798 | 2.55 G | 25 M | 102× | | 2604.01059_tsim | `distillation_steane_d3_Z` | 3 | 0.353 | 2.79 G | 33 M | 84.5× | | 2604.01059_tsim | `distillation_unencoded_Z` | 3 | - | 2.27 G | 41.6 M | 54.6× | | 2604.05126_qldpc_magic_state_injection | `bb144_cfg328_inj5_T_p0.001` | 1 | 0.069 | 1.23 G | 7.79 M | 158× | | 2604.05126_qldpc_magic_state_injection | `bb144_cfg342_inj3_T_p0.001` | 1 | 0.0687 | 1.3 G | 8.19 M | 159× | | 2604.05126_qldpc_magic_state_injection | `bb144_cfg369_inj1_T_p0.001` | 1 | 0.0703 | 1.32 G | 8.54 M | 155× | | 2604.05126_qldpc_magic_state_injection | `bb144_cfg422_inj2_T_p0.001` | 1 | 0.0687 | 1.32 G | 8.21 M | 161× | | 2604.05126_qldpc_magic_state_injection | `bb144_cfg491_inj4_T_p0.001` | 1 | 0.0684 | 1.31 G | 8.11 M | 162× | | 2604.05126_qldpc_magic_state_injection | `bb144_cfg88_inj6_T_p0.001` | 1 | 0.0721 | 1.29 G | 7.98 M | 162× | | 2604.27058_clifft | `coherent_d3_r1` | 0 | - | 8.19 G | 2.17 G | 3.78× | | 2604.27058_clifft | `coherent_d3_r3` | 4 | - | 1.16 G | 26.6 M | 43.5× | | 2604.27058_clifft | `coherent_d5_r1` | 0 | - | 7.79 G | 857 M | 9.09× | | 2604.27058_clifft | `coherent_d5_r5` | 11 | - | 467 k | 3 k | 156× | | 2604.27058_clifft | `cultivation_d3` | 4 | 0.687 | 2.04 G | 41.8 M | 48.8× | | 2604.27058_clifft | `cultivation_d5` | 6 | 0.144 | 750 M | 20.3 M | 37× | | 2604.27058_clifft | `distillation` | 3 | 0.0798 | 2.57 G | 24.7 M | 104× | | 2604.27058_clifft | `surface_d7_r7` | 0 | - | 757 M | 21.9 M | 34.5× | | 2607.28600_symft | `coherent_surface_d3_r1_p1e-3_rz0p02` | 0 | - | 8.19 G | 2.16 G | 3.8× | | 2607.28600_symft | `coherent_surface_d3_r3_p1e-3_rz0p02` | 4 | - | 1.17 G | 27 M | 43.3× | | 2607.28600_symft | `coherent_surface_d5_r1_p1e-3_rz0p02` | 0 | - | 7.78 G | 851 M | 9.14× | | 2607.28600_symft | `coherent_surface_d5_r5_p1e-3_rz0p02` | 11 | - | 466 k | 3.04 k | 154× | | 2607.28600_symft | `distillation` | 3 | 0.0798 | 2.56 G | 24.9 M | 103× | | 2607.28600_symft | `msc_d3_inject_cultivate_p1e-3` | 4 | 0.687 | 2.01 G | 41.2 M | 48.8× | | 2607.28600_symft | `msc_d5_inject_cultivate_p1e-3` | 6 | 0.144 | 767 M | 20.3 M | 37.9× | | 2607.28600_symft | `MSC_circuit_d7_p0.0005` | 8 | - | 31.4 M | 56.1 k | 560× | | 2607.28600_symft | `msc_proxy_d7_unverified_p1e-3` | 6 | - | 171 M | 673 k | 254× | | 2607.28600_symft | `pure_surface_d7_r7_p1e-3` | 0 | - | 758 M | 21.9 M | 34.5× | | 2607.28600_symft | `pure_surface_d9_r9_p1e-3` | 0 | - | 386 M | 9.88 M | 39.1× | | 2609.17706_chan_color_code_cultivation | `d3a6f2_inject_cultivate_T_p0.001` | 4 | 0.639 | 2.14 G | 40.5 M | 52.8× | | 2609.17706_chan_color_code_cultivation | `d3a6_inject_cultivate_T_p0.001` | 4 | 0.687 | 2.05 G | 41.7 M | 49.2× | | 2609.17706_chan_color_code_cultivation | `d5a19f13_inject_cultivate_T_p0.001` | 6 | 0.106 | 825 M | 19.5 M | 42.4× | | 2609.17706_chan_color_code_cultivation | `d5a19_inject_cultivate_T_p0.001` | 6 | 0.144 | 651 M | 20.1 M | 32.4× | | 2609.17706_chan_color_code_cultivation | `d5a19r4f13_inject_cultivate_T_p0.001` | 6 | 0.0811 | 909 M | 17 M | 53.5× | | 2609.17706_chan_color_code_cultivation | `d5a19r4_inject_cultivate_T_p0.001` | 6 | 0.111 | 808 M | 19.2 M | 42.1× | | 2609.18922_exact_ler_cultivation | `clifft_d3_p001` | 4 | 0.687 | 1.99 G | 40.9 M | 48.6× | | 2609.18922_exact_ler_cultivation | `soft_cultivation_d5_p0005` | 6 | 6.52e-05 | 868 M | 6.16 M | 141× | | 2610.03869_modular_neural_decoder | `stability_d3_X_t100_alpha0.5_p0.001` | 3 | - | 15.5 M | 63.5 k | 244× | | 2610.03869_modular_neural_decoder | `stability_d3_X_t20_alpha0.15_p0.001` | 3 | - | 145 M | 2.17 M | 66.8× | | 2610.03869_modular_neural_decoder | `stability_d3_X_t20_alpha0.5_p0.001` | 3 | - | 145 M | 469 k | 309× | | 2610.03869_modular_neural_decoder | `stability_d3_X_t20_alpha1.0_p0.001` | 3 | - | 133 M | 203 k | 656× | | 2610.06811_merlin | `cultivation_d3_t` | 4 | 0.687 | 1.88 G | 41.9 M | 44.9× | | 2610.06811_merlin | `cultivation_d5_t` | 6 | 0.144 | 750 M | 20.4 M | 36.8× | | 2610.06811_merlin | `distillation_15to1_p0.001` | 4 | 0.985 | 4.72 G | 69.8 M | 67.6× | | 2610.06811_merlin | `distillation_bh2_p0.001` | 4 | 0.986 | 4.74 G | 73.8 M | 64.2× | | 2610.06811_merlin | `distillation_bh4_p0.001` | 4 | 0.98 | 1.42 G | 63 M | 22.5× | | 2610.06811_merlin | `distillation_bh6_p0.001` | 4 | 0.974 | 2.13 G | 70.3 M | 30.3× | | 2610.06811_merlin | `distillation_bh8_p0.001` | 4 | 0.969 | 950 M | 60.5 M | 15.7× | | 2610.06811_merlin | `distillation_bh10_p0.001` | 4 | 0.963 | 1.17 G | 68.5 M | 17.1× | | 2610.06811_merlin | `distillation_bh12_p0.001` | 4 | 0.957 | 697 M | 58.7 M | 11.9× | | 2610.06811_merlin | `distillation_bh14_p0.001` | 4 | 0.951 | 790 M | 67.5 M | 11.7× | | 2610.06811_merlin | `distillation_bh16_p0.001` | 4 | 0.946 | 559 M | 57.5 M | 9.73× | | 2610.06811_merlin | `distillation_bh18_p0.001` | 4 | 0.94 | 657 M | 65.4 M | 10× | | 2610.06828_practical_cost_cultivation | `color_code_cultivation_f3_T` | 4 | 0.539 | 167 M | 1.66 M | 101× | | 2610.06828_practical_cost_cultivation | `color_code_cultivation_f5_T` | 6 | 0.0519 | 315 M | 1.4 M | 225× | | 2610.22315_magna | `bh2-T` | 3 | 0.986 | 3.92 G | 62.2 M | 63× | | 2610.22315_magna | `bh2-S` | 0 | 0.986 | 6.97 G | 990 M | 7.04× | | 2610.22315_magna | `bh4-T` | 3 | 0.98 | 3.29 G | 52.3 M | 62.9× | | 2610.22315_magna | `bh4-S` | 0 | 0.98 | 6.95 G | 913 M | 7.61× | | 2610.22315_magna | `bh6-T` | 3 | 0.974 | 2.85 G | 44 M | 64.8× | | 2610.22315_magna | `bh6-S` | 0 | 0.974 | 6.75 G | 859 M | 7.86× | | 2610.22315_magna | `bh10-T` | 3 | 0.963 | 2.21 G | 26.2 M | 84.5× | | 2610.22315_magna | `bh10-S` | 0 | 0.963 | 6.8 G | 809 M | 8.41× | | 2610.22315_magna | `bh20-T` | 3 | 0.935 | 1.32 G | 1.75 M | 756× | | 2610.22315_magna | `bh20-S` | 0 | 0.935 | 6.63 G | 612 M | 10.8× | | 2610.22315_magna | `bh40-T` | 3 | 0.882 | 757 M | 920 k | 823× | | 2610.22315_magna | `bh40-S` | 0 | 0.882 | 6.54 G | 491 M | 13.3× | | 2610.22315_magna | `bh60-T` | 3 | 0.833 | 507 M | 609 k | 832× | | 2610.22315_magna | `bh60-S` | 0 | 0.833 | 6.39 G | 419 M | 15.2× | | 2610.22315_magna | `bh80-T` | 3 | 0.788 | 379 M | 454 k | 835× | | 2610.22315_magna | `bh80-S` | 0 | 0.788 | 6.48 G | 370 M | 17.5× | | 2610.22315_magna | `bh100-T` | 3 | 0.747 | 290 M | 358 k | 809× | | 2610.22315_magna | `bh100-S` | 0 | 0.747 | 6.3 G | 317 M | 19.9× | | 2610.22315_magna | `color3` | 4 | 0.687 | 2.39 G | 43.5 M | 55× | | 2610.22315_magna | `color5` | 6 | 0.144 | 980 M | 12.8 M | 76.5× | | 2610.22315_magna | `rp2-d3` | 4 | 0.514 | 1.78 G | 26.3 M | 67.8× | | 2610.22315_magna | `rp2-d3-S` | 0 | 0.514 | 3.73 G | 41 M | 91× | | 2610.22315_magna | `rp2-d5` | 9 | 0.0945 | 130 M | 10.1 M | 12.8× | | 2610.22315_magna | `rp2-d5-S` | 0 | 0.0945 | 2.35 G | 21.5 M | 109× | | 2610.22315_magna | `dk` | 4 | 0.845 | 1.79 G | 42 M | 42.6× | | 2610.22315_magna | `wg-T` | 4 | 0.551 | 1.47 G | 14.4 M | 102× | | 2610.22315_magna | `wg-S` | 0 | 0.551 | 3.49 G | 78.5 M | 44.5× | </details> ## Testing - **Gate:** `uv run tsim-dev check` with a plain `uv sync`, no extra, on the M4 Pro: lint passes, 2757 tests pass, the Metal tests included. CI runs on Linux, where the Metal tests skip. - **Generator:** with the Metal dialect, #284's generator writes the same tapes as the Metal prototype (190 tapes compared), apart from the intended `draw_one` change. - **Against the CPU:** `test_metal_sampler.py` compares with the stabilizer-rank strategy and the CPU executor. It covers postselection, reference flips, column layouts, booleans against packed rows, deferred noise, wide rows, width-11 unrolled and looping tapes, and rare outcomes. - **Fixes:** a review found eight problems with sharing, interrupting, forking, pickling, limits and very large calls, and a second review found three more. A third review led to the SIMD-lane probe, the CPU fallback on a compile error, hashed noise streams per block, and a two-sided test of the 48-bit noise path. Each fix has a test, and I checked that each test fails without its fix, except the fork test, whose old behaviour is a hang, and the stream hashing, whose collisions are too rare to construct. ## Not verified - Other Apple GPUs than the M4 Pro, a real out-of-memory condition, and Windows (the fork hook is guarded, but nothing ran there). - `tsim-dev bench --suite full` on the CPU path. The CPU path is unchanged unless a GPU is used; the quick A/B against `origin/main` without Metal was at 0.99–1.02×. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
First of three PRs for the statevector executor on GPUs. It holds the host-side code that the CUDA backend (#219) and a Metal backend for Apple GPUs share, so that either can land first and the other rebases onto it. Nothing on
mainusesstatevector/gpu/until a backend does.Changes
statevector/gpu/noise.py: the noise kernels' host tables (channel groups cut into segments, 32-byte outcome records with alias thresholds, overflow flips),deferred_channelsandpass_rate. This is the host part of CUDA backend #219'scuda/noise.py. The fast-memory budget is now a parameter (CUDA has 46 KB of shared memory, Metal 14 KB is fastest). New:shared_words=Falsekeeps a block's words in device memory and gives the events the whole budget. Rows of more than about 47 words (over 3000 outputs) need that on Metal. A layout with no shot words raises aValueErrorinstead of dividing by zero.statevector/gpu/tape.py: CUDA backend #219's tape generator, with a frozenDialectfor what differs between CUDA C++ and the Metal Shading Language:make_float2/float2, literal suffixes;goto doneor ado { … break; } while (false)block, since Metal has nogoto;statevector/gpu/rows.py:piecesandselectfrom CUDA backend #219's sampler.shared.common_prefixand the deferred-measurement plans (schedule(defer=True),plan_component,_shot_work), unchanged from CUDA backend #219.Checks
gpu.tape.tape_functionwrites the same source, byte for byte, as CUDA backend #219'scuda/codegen.pyat 20b3eef. 202 tapes compared: the test circuits, random circuits up to width 11, cultivation d3 and tricycle 6-3-2, unrolled and looping, with and without postselection masks, at 1, 2 and 32 threads per shot. CUDA backend #219's CUDA tests therefore stay valid once it imports this module. I have no NVIDIA GPU, so nothing was run on CUDA.test/unit/statevector/test_gpu.py):ChannelSampler.sample_words: per-bit rates, and a four-word correlated fault, with words in fast and in device memory;pass_rateagainst Monte Carlo, anddeferred_channelsagainst the flip tables;piecesandselectagainst direct bit selection;do-block, amplitude chunks and paired projections.common_prefixtest intest_shared.py: against densereference_rotateon the test circuits and a noiseless 6-qubit circuit with a prefix of 20 or more actions.uv run tsim-dev checkon an Apple M4 Pro, macOS 15.7: 2630 passed, 2 failed. The two failures aretest_sampler.py::test_rotated_surface_code_single_noise_channel_matches_stim[*-before_round_data_depolarization](relative difference 0.0067 > 0.005, forcat5as well). They fail the same way on a cleanorigin/mainworktree.os.sched_getaffinityerror indev/tsim_dev/common.py, which also fails onmainon macOS. The commit skipped that one hook.Benchmark
uv run tsim-dev bench --ref origin/main, default suite, Apple M4 Pro, 1 thread, tolerance 5%: 8 circuits, 0 slower, 0 faster.Sampling is unchanged. Compile time goes up by up to 1.3× (coherent_d3_r3 +4 ms, qv_q10 +37 ms), which the harness reports as "same". The reason is that
plan_componentnow schedules and plans every component twice, to try the deferred order. Only a GPU backend gains from the deferred plan. If the CPU cost matters, the deferred plan can be computed only when a GPU sampler is built.Not verified
--suite fullwas not run.🤖 Generated with Claude Code