Repository navigation
feat(statevector): sample on Apple GPUs through Metal - #290
Conversation
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>
☂️ Code Coverage
Overall Coverage
New Files
Modified Files
|
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>
fccbace to
335c1d8
Compare
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Calls on one sampler run one at a time (they share its batch buffers), and the buffer pool is locked. - A failed or interrupted call waits for the batches it committed, through further interrupts, and drops the batch buffers, so the next call does not return its rows. - A sampler used in a process forked after Metal's setup samples on the CPU, with a warning, instead of hanging. - Samplers pickle: the CPU sampler and the inputs, with the GPU sampler rebuilt on load (or the CPU one where Metal is missing). - Noise beyond the GPU tables' limits, and running out of GPU memory for programs without components, fall back to the CPU with a warning. - Calls of more than 2^31 shots run in parts (the GPU counters are 32-bit). - `SamplerStats.device` says where a sampler samples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… samples - On Apple silicon, Tsim depends on pyobjc-framework-Metal (about 7 MB), so the plain install samples on the GPU; the `metal` extra is gone. - The samplers' repr says where they sample (`samples on metal`). - The docs, the user skill and AGENTS.md say that the GPU computes outcome probabilities in float32, that its seeds depend on `batch_size`, and that `TSIM_DEVICE=cpu` keeps the samplers on the CPU. - Review fixes: `os.register_at_fork` is guarded (Windows has none), and batches of wide rows stay below 512 MB, so the kernels' 32-bit offsets do not wrap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sampler always returns every output in order, so noise_kept's pack_rows copies output word k and clears the bits past the last output, instead of reading a piece table per word. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing read compile_seconds, gpu_seconds or _Slot.shots; the tape kernel template never used OW or S, and NE always equalled NO; FAST_MATH was never toggled. runtime.PROFILE stays: the benchmarking skill uses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tape kernel's glue, the noise split, the screen's pass rate, the shared-memory fit test, the deviation, the width limit and the fork warning now come from tsim.statevector.gpu; the generated kernels and dispatches are unchanged. codegen's tape_function and hex64 pass-throughs go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The kernels' counts buffer was addressed by bare byte offsets and indices, and 256, 1024, 1 << 20 and stacklevel=5 were spelled out at each use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both noise kernels and noise_kept repeated the segment buffers, and both noise kernels the same ballot, bitmap and queue block; those are now one signature string and an enqueue function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 170-line MSL template and the source assembly leave sampler.py, so _module only plans, compiles and uploads. sample and direct_bools share one fallback to the CPU (_or_cpu) for forked processes and GPU out-of-memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_run_accepted took 10 parameters, _encode_pack 9 and _encode_batch 8, most of them the same per call; they now take a _Call and the batch's own values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tsim/sampler.py knew Metal's width limit, its fork state, its error types and the no-words rule; metal.shot_sampler now decides, and sampler.py keeps only the TSIM_DEVICE check and one call, so a second GPU backend adds one line there. ShotSampler says device = "cpu" itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The detector, measurement and looping comparisons with cat5 are one test, as are the postselection checks in threadgroup and device memory, and the two out-of-memory fallbacks; the same cases run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changelog entry described the kernels, and three docstrings repeated a seven-line paragraph; the install page keeps the details. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b9b58be to
82d3ed1
Compare
The screen ran one thread per shot over every output word, so 32 of a block's 256 threads walked 44 words in sequence on end-to-end cultivation d=5. A thread per word and shot now clears the shot's flag, and the constants go into the rows as they are written. The noise kernel of end2end_d5_dsurface15 takes 2.27 instead of 2.95 ms per million shots on an M4 Pro (1.2x the throughput). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From 8 qubits on, a shot's threads hold at most 16 amplitudes instead of 32: fewer registers per thread pay for the extra shuffles. On an M4 Pro tricycle_6_3_2 and unrotated_H_injection (9 qubits) run 1.4x, the 8-qubit circuits 1.1-1.2x faster; 7 qubits tie and 6 lose 10%, so narrower tapes keep 32. CUDA keeps tape.threads_per_shot as it was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov deactivated this repository and now rejects every upload with HTTP 400, which failed the 3.12 job and cancelled the others. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e GPU The kernels take a thread's lane and SIMD group from its index in its threadgroup: a shot's threads exchange amplitudes by lane, and the scans list shots in lane order. The device check only checked the SIMD width, so a GPU that laid lanes out otherwise would have sampled wrongly without an error. A probe kernel now records every thread's lane and group in threadgroups of 128, 256 and 1024 threads (the kernels' sizes); where they are out of order, Metal counts as unavailable and Tsim samples on the CPU. check_simd_width becomes check_device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A kernel compiles on a sampler's first call with each postselection mask; a compile error raised out of sample(). Library now raises CompileError, on which the sampler warns and samples on the CPU, as out of GPU memory, and stays there: it neither recompiles nor warns on each call, and its stats say cpu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A segment's stream was mix64(seed ^ bid * C ^ sg * GOLDEN): two (block, segment) pairs with equal XORs shared their draws. The block's stream is now hashed first, so streams of different blocks are unrelated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test only bounded the faults from above, so it passed if the 48-bit path never fired. Probabilities near 3e-10 keep the channels on that path and give ~83 expected faults, which the test now checks both ways: with the 48-bit path off, 24-bit skips add ~110 faults and it fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov accepts uploads again, so a failed upload fails the build again. This reverts commit c7a825d. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
… 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>
CI has no Apple GPU, so the Metal tests do not run there and the executor's lines always count as uncovered. 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>
A fault draws its slot and a 40-bit fraction from the low 40 bits of its draw and compares the fraction with the 40-bit keep thresholds of the shared host tables, so an outcome below about 1e-8 of its channel is no longer impossible. The event keeps the fraction's high 24 bits; the low 16 only matter when those tie with the threshold's (a chance of 2^-24) and come from a hash of the fault then, so the noise kernel costs the same (tsim-dev bench --device gpu, quick suite: no circuit slower than 5%). A circuit with a rotation branch that float32 cannot resolve samples on the CPU, with a warning that says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # src/tsim/statevector/gpu/tape.py
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: 8e4167f7cf
ℹ️ 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".
| def direct_bools(self, shots: int, flip: np.ndarray | None = None) -> np.ndarray: | ||
| """Return every output bit of a program without components, as booleans.""" | ||
| return self._or_cpu( | ||
| lambda: self.sample_accepted(shots, flip=flip, bools=True)[0], |
There was a problem hiding this comment.
Forward batch_size through direct boolean sampling
For a component-free sampler with bit_packed=False, the public fast path calls direct_bools, but this call to sample_accepted drops the user-supplied batch_size. When shots exceeds that requested size, boolean sampling therefore uses the default GPU batch partition while bit_packed=True honors the requested partition; because Metal draws a new stream per batch, two same-seeded calls with the same explicit batch_size can return different underlying bits solely due to output formatting, and the requested memory bound is also lost. Pass batch_size through this fast path.
AGENTS.md reference: AGENTS.md:L69-L72
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 02e24ac: direct_bools takes batch_size and the Metal sampler passes it to sample_accepted. A new test (test_direct_booleans_take_the_batch_size_as_packed_rows_do) fails without the fix.
A sampler without components returns booleans through direct_bools, which dropped batch_size; on the GPU each batch draws its own stream, so booleans and packed rows of one seed and batch size differed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI ran only on `ubuntu-latest`, although the package claims to run on any operating system, and the Metal backend (#290) was never installed or imported in CI. - Adds `macos-latest` (arm64, so `pyobjc-framework-Metal` is installed) and `windows-latest` to the test matrix, for Python 3.14 only. Version-specific failures already show up on the four Linux jobs. - `fail-fast: false`, so one OS failing does not cancel the others. - Steps run in `bash` on every OS (Git Bash on Windows) so the existing commands work unchanged. - On macOS, a step prints `runtime.unavailable()`. On the standard runner it reports `the Metal executor needs an Apple GPU (Apple silicon)` (the runner has an Apple Paravirtual device), so `test/integration/test_metal_sampler.py` still skips there. Fixes that Windows needed: - `tesseract-decoder` has no Windows wheels; the dev dependency is now limited to `sys_platform != 'win32'` (uv.lock updated). `test_skills.py` already skips decoders it cannot import. - `test/unit/test_dev_cli.py` skips on Windows: the `tsim-dev` CLI imports `fcntl` and is POSIX-only. - `test_skills.py` reads the skill's Markdown as UTF-8; Windows defaulted to cp1252 and failed on non-ASCII characters. Coverage upload is unchanged (Linux, 3.12). No user-visible change, so no changelog entry. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- 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 onpyobjc-framework-Metal(about 7 MB) on Apple silicon only, so other platforms install nothing new.What users get
samples on metal), and so doessampler.stats.device.TSIM_DEVICE=cpukeeps the samplers on the CPU, in double precision. Tsim reads it each time it compiles a sampler.batch_size. The install page, the docstrings, the user skill, AGENTS.md and the CHANGELOG say so.How it works
goto, SIMD-group shuffles, and amplitudes in arrays of 32. Metal moves an array of 64 to the stack, which costs 1.8×.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=1at58471a37, 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.qv_q10is below 1×.bh*-Tdistillation, stability experiments, end-to-end cultivation): those parallelize across the GPU.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.All 179 circuits
qv_q10hook_inject_T_d15_pd5_p0.001hook_inject_T_d15_pd7_p0.001li_inject_T_d15_pd5_p0.001li_inject_T_d15_pd7_p0.001pregrown_hook_inject_T_d15_pd5_p0.001pregrown_hook_inject_T_d15_pd7_p0.001zz_inject_T_d15_pd5_p0.001zz_inject_T_d15_pd7_p0.001s_injection_T_d3_p0.001_pinj0.1s_injection_T_d5_p0.001_pinj0.1s_injection_T_d7_p0.001_pinj0.1s_injection_T_d9_p0.001_pinj0.1cult_growing_T_d3_to_d11_p0.001cult_growing_T_d3_to_d7_p0.001cult_growing_T_d5_to_d13_p0.001cult_growing_T_d5_to_d9_p0.001end2end_d3_dsurface15_p0.001end2end_d5_dsurface15_p0.001inject_cultivate_d3_p0.001inject_cultivate_d5_p0.001expanded-rotated-init-x-injection-double-HXY_d1=3_d2=11_p=0.001expanded-rotated-injection-double-ghz-HXY_d1=3_d2=11_p=0.001expanded-rotated-injection-syndrome-double-ghz-HXY_d1=3_d2=11_p=0.001expanded-unrotated_H_injection-ghz_d1=3_d2=11_p=0.001expanded-unrotated_H_injection-syndrome-ghz_d1=3_d2=11_p=0.001rotated-init-x-injection-double-HXY_d1=3_p=0.001rotated-init-x-injection-syndrome-double-ghz-HXY_d1=3_p=0.001rotated-injection-double-ghz-HXY_d1=3_p=0.001rotated-injection-ghz-HXY_d1=3_p=0.001rotated-injection-syndrome-double-ghz-HXY_d1=3_p=0.001unrotated_HXY_injection-ghz_d1=3_p=0.001unrotated_HXY_injection-syndrome-ghz_d1=3_p=0.001unrotated_H_injection-ghz_d1=3_p=0.001unrotated_H_injection-syndrome-ghz_d1=3_p=0.001rp_3_T_cultrp_3_rp_5_T_cultrp_3_sc_7_end2end_4_full_rdsrp_3_rp_5_sc_11_end2end_6_full_rdshook_inject_T_d3_p0.001inject_cultivate_T_d3_p0.001distillation_15to1_T_d5_p0.001distillation_15to1_T_d9_p0.001distillation_15to1_T_d13_p0.001distillation_15to1_T_d17_p0.001distillation_15to1_T_d21_p0.001distillation_15to1_T_d25_p0.001unfolded_repetition_no_preselection_pz1e-3_rounds2unfolded_repetition_no_preselection_pz1e-3_rounds4unfolded_repetition_no_preselection_pz1e-3_rounds6unfolded_surface_no_bitflip_pz1e-3_rounds2unfolded_surface_no_bitflip_pz1e-3_rounds4unfolded_surface_no_bitflip_pz1e-3_rounds6fold-d3_native_T_p0.001fold-d5_native_T_p0.001d3_cultivation_T_p0.001star_d=3star_d=5star_d=7star_d=9star_d=11star_d=13star_d=15tricycle_6_3_2tricycle_9_3_2inject-check-3_Tlattice-surgery-complementary-gap-3-to-5-to-7_Tfig2_cultivation_stim_X_Tfig2_cultivation_stim_Z_Tfig2_injection_stim_X_Tfig2_injection_stim_Z_Tfig2_simulation_stim_X_Tfig2_simulation_stim_Z_Tfig3_injection_stim_Tfig3_simulation_stim_Tfig3_simulation_zoom_stim_Tfig3_with_idle_rounds_stim_Tfig3_with_idle_rounds_zoom_stim_Tfig3_without_idle_rounds_stim_Tfig4_cultivation_N2_stim_X_Tfig4_cultivation_N2_stim_Z_Tfig4_cultivation_N3_stim_X_Tfig4_cultivation_N3_stim_Z_Tfig4_injection_N2_stim_X_Tfig4_injection_N2_stim_Z_Tfig4_simulation_injection_N2_stim_X_Tfig4_simulation_injection_N2_stim_Z_Tcircuit_d3_p0.001circuit_d5_p0.001gauging_cultivation_d3_T_p0.001gauging_cultivation_d5_T_p0.001gauging_cultivation_d7_T_p0.001distillation_color_d5_Xdistillation_steane_d3_Xdistillation_unencoded_Xdistillation_color_d5_Ydistillation_steane_d3_Ydistillation_unencoded_Ydistillation_color_d5_Zdistillation_steane_d3_Zdistillation_unencoded_Zbb144_cfg328_inj5_T_p0.001bb144_cfg342_inj3_T_p0.001bb144_cfg369_inj1_T_p0.001bb144_cfg422_inj2_T_p0.001bb144_cfg491_inj4_T_p0.001bb144_cfg88_inj6_T_p0.001coherent_d3_r1coherent_d3_r3coherent_d5_r1coherent_d5_r5cultivation_d3cultivation_d5distillationsurface_d7_r7coherent_surface_d3_r1_p1e-3_rz0p02coherent_surface_d3_r3_p1e-3_rz0p02coherent_surface_d5_r1_p1e-3_rz0p02coherent_surface_d5_r5_p1e-3_rz0p02distillationmsc_d3_inject_cultivate_p1e-3msc_d5_inject_cultivate_p1e-3MSC_circuit_d7_p0.0005msc_proxy_d7_unverified_p1e-3pure_surface_d7_r7_p1e-3pure_surface_d9_r9_p1e-3d3a6f2_inject_cultivate_T_p0.001d3a6_inject_cultivate_T_p0.001d5a19f13_inject_cultivate_T_p0.001d5a19_inject_cultivate_T_p0.001d5a19r4f13_inject_cultivate_T_p0.001d5a19r4_inject_cultivate_T_p0.001clifft_d3_p001soft_cultivation_d5_p0005stability_d3_X_t100_alpha0.5_p0.001stability_d3_X_t20_alpha0.15_p0.001stability_d3_X_t20_alpha0.5_p0.001stability_d3_X_t20_alpha1.0_p0.001cultivation_d3_tcultivation_d5_tdistillation_15to1_p0.001distillation_bh2_p0.001distillation_bh4_p0.001distillation_bh6_p0.001distillation_bh8_p0.001distillation_bh10_p0.001distillation_bh12_p0.001distillation_bh14_p0.001distillation_bh16_p0.001distillation_bh18_p0.001color_code_cultivation_f3_Tcolor_code_cultivation_f5_Tbh2-Tbh2-Sbh4-Tbh4-Sbh6-Tbh6-Sbh10-Tbh10-Sbh20-Tbh20-Sbh40-Tbh40-Sbh60-Tbh60-Sbh80-Tbh80-Sbh100-Tbh100-Scolor3color5rp2-d3rp2-d3-Srp2-d5rp2-d5-Sdkwg-Twg-STesting
uv run tsim-dev checkwith a plainuv 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.draw_onechange.test_metal_sampler.pycompares 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.Not verified
tsim-dev bench --suite fullon the CPU path. The CPU path is unchanged unless a GPU is used; the quick A/B againstorigin/mainwithout Metal was at 0.99–1.02×.🤖 Generated with Claude Code