Skip to content

feat(statevector): add the host code that GPU backends share - #284

Merged
rafaelha merged 12 commits into
mainfrom
feat/gpu-shared
Nov 12, 2026
Merged

rafaelha merged 12 commits into
mainfrom
feat/gpu-shared

Conversation

@rafaelha

Copy link
Copy Markdown
Collaborator

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 main uses statevector/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_channels and pass_rate. This is the host part of CUDA backend #219's cuda/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=False keeps 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 a ValueError instead of dividing by zero.
  • statevector/gpu/tape.py: CUDA backend #219's tape generator, with a frozen Dialect for what differs between CUDA C++ and the Metal Shading Language:
    • spelling: qualifiers, address spaces, make_float2/float2, literal suffixes;
    • the shuffle templates and the lane mask;
    • control flow: goto done or a do { … break; } while (false) block, since Metal has no goto;
    • two options that Metal needs and CUDA keeps off: splitting an unrolled tape's amplitudes into arrays of 32, and projecting partner pairs together.
  • statevector/gpu/rows.py: pieces and select from CUDA backend #219's sampler.
  • shared.common_prefix and the deferred-measurement plans (schedule(defer=True), plan_component, _shot_work), unchanged from CUDA backend #219.

Checks

  • CUDA source identical: with the CUDA dialect, gpu.tape.tape_function writes the same source, byte for byte, as CUDA backend #219's cuda/codegen.py at 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.
  • New CPU tests (test/unit/statevector/test_gpu.py):
    • a NumPy model of the noise kernel draws from the tables, against ChannelSampler.sample_words: per-bit rates, and a four-word correlated fault, with words in fast and in device memory;
    • pass_rate against Monte Carlo, and deferred_channels against the flip tables;
    • pieces and select against direct bit selection;
    • the generator's do-block, amplitude chunks and paired projections.
  • common_prefix test in test_shared.py: against dense reference_rotate on the test circuits and a noiseless 6-qubit circuit with a prefix of 20 or more actions.
  • uv run tsim-dev check on an Apple M4 Pro, macOS 15.7: 2630 passed, 2 failed. The two failures are test_sampler.py::test_rotated_surface_code_single_noise_channel_matches_stim[*-before_round_data_depolarization] (relative difference 0.0067 > 0.005, for cat5 as well). They fail the same way on a clean origin/main worktree.
  • Lint passes except pyright's os.sched_getaffinity error in dev/tsim_dev/common.py, which also fails on main on 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.

circuit k this branch (shots/s) main (shots/s) rate ratio compile ms (branch / main)
inject_cultivate_d3 4 3.43e7 3.42e7 1.00 24 / 21
inject_cultivate_d5 6 1.89e7 1.89e7 1.00 141 / 130
end2end_d3_dsurface15 4 1.37e6 1.37e6 0.99 1732 / 1644
hook_inject_T_d15_pd5 1 1.15e6 1.16e6 0.99 1952 / 1932
distillation_color_d5_Z 3 2.42e7 2.41e7 1.00 24 / 23
coherent_d3_r3 4 2.41e7 2.41e7 1.00 17 / 13
surface_d7_r7 0 1.40e7 1.40e7 1.00 102 / 102
qv_q10 10 1.27e7 1.25e7 1.00 186 / 149

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_component now 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

  • Nothing ran on a CUDA GPU or, in this PR, on Metal.
  • --suite full was not run.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

☂️ Code Coverage

current status: ✅

Overall Coverage

Statements Covered Coverage Threshold Status
5616 4969 88% 0% 🟢

New Files

File Coverage Status
src/tsim/statevector/gpu/_init_.py 100% 🟢
src/tsim/statevector/gpu/noise.py 93% 🟢
src/tsim/statevector/gpu/rows.py 100% 🟢
src/tsim/statevector/gpu/tape.py 99% 🟢
TOTAL 98% 🟢

Modified Files

File Coverage Status
src/tsim/statevector/program.py 100% 🟢
src/tsim/statevector/schedule.py 100% 🟢
src/tsim/statevector/shared.py 38% 🟢
TOTAL 79% 🟢

updated for commit: 1da9703 by action🐍

rafaelha and others added 2 commits November 10, 2026 20:17
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>
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>
rafaelha added a commit that referenced this pull request Nov 10, 2026
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>
@rafaelha rafaelha mentioned this pull request Nov 10, 2026
rafaelha and others added 5 commits November 11, 2026 03:56
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>
rafaelha added a commit that referenced this pull request Nov 11, 2026
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

codecov Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.79006% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/tsim/statevector/gpu/noise.py 92.94% 12 Missing ⚠️
src/tsim/statevector/gpu/tape.py 99.19% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@rafaelha
rafaelha marked this pull request as ready for review November 11, 2026 22:01
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-11T22:08:37.995131Z 8a7f4cf Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/tsim/statevector/gpu/noise.py Outdated
keep = (
1 << 24
if hi - lo == 1
else min(round(float(fire.alias_prob[k]) * (1 << 24)), 1 << 24)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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;"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

rafaelha and others added 3 commits November 11, 2026 15:13
… 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>
@rafaelha
rafaelha merged commit 2e738ac into main Nov 12, 2026
11 checks passed
@rafaelha
rafaelha deleted the feat/gpu-shared branch November 12, 2026 04:42
rafaelha added a commit that referenced this pull request Nov 12, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant