Repository navigation
Gather postselected shots in parallel and stop finishing discarded ones - #259
Conversation
#252 made CompiledDetectorSampler.sample return only the shots that pass postselection. On the statevector path it found them with one NumPy pass over every shot's words after sampling, on one thread, and still cleared and finished discarded shots as #222 and #223 did. On B850 against main 5af2731 (before #252), postselected cells fell to 0.25x (surface d=7, 10 threads), 0.53x (cultivation d=5) and 0.17-0.31x (coherent r=1). - Each chunk now returns whether its shots passed (the screen and every component's pruning, i.e. `alive`) and compacts its survivors in place on its own thread; a numba pass joins the chunks. - A program without components screens each noise block inside the noise kernel, while it is in cache, and copies the survivors in parallel out of scratch words the sampler reuses (at most 32 MiB). - Discarded shots are no longer finished: _prune drops their lanes and clears `alive` without drawing coins or writing outputs, and the `kept`/`pruned_kept` clearing is gone. Nothing reads them any more. Samples are bit-for-bit those of main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis 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.
🟡 Changes recommended
Postselection with zero or one survivor can leave bit-packed outputs referencing the original allocation, retaining discarded shot data and component keys instead of releasing them.
1 open issue
What changed in this PR
This PR addresses postselection slowdowns in the active_statevector backend, following the change to return only accepted shots.
Changes:
- Gathers surviving shots through chunk-local compaction and compiled kernels instead of a single NumPy filtering pass.
- Screens direct-only programs inside noise blocks and reuses scratch memory.
- Stops finishing discarded shots and updates pruning tests to check survivor identity and sample equality.
| File | Description |
|---|---|
test/unit/statevector/test_shared.py |
Checks pruning through alive and compares survivors with unpruned samples. |
test/helpers/statevector.py |
Exposes survivor flags in sampling statistics. |
src/tsim/statevector/shots.py |
Adds survivor gathering, buffer shrinking, and direct-only scratch reuse. |
src/tsim/statevector/shared.py |
Removes output completion and clearing for pruned shots. |
src/tsim/noise/channels.py |
Adds per-block screening and survivor counts. |
CHANGELOG.md |
Documents the postselection optimizations. |
Review effort: Balanced
- Take and return the scratch words under a lock, so that two threads on a free-threaded build cannot share them, and pickle a ShotSampler without the lock and the scratch, as main pickles. - Keep at most 16 MiB of scratch per sampler, noted in the changelog. - Warn in _shrink's docstring that no other view of the words may remain. - Unit tests for _copy_kept's branches, _join, _compact, scratch reuse across shrinking and growing calls, and pickling after a screened call. - run_lanes' docstring and the end-of-run NOTE no longer mention clearing pruned shots' outputs or _finish_shot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review (independent agent pass) and follow-ups The reviewer found no bug that changes samples. Byte-identical output with It also confirmed that
Addressed in a9af140:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
🟡 Changes recommended
Zero- or one-survivor bit-packed results can retain the full original shot/key allocation, while the packed postselection test checks padding bits instead of masked detectors due to incorrect bit order.
2 open issues
Review effort: Balanced
☂️ Code Coverage
Overall Coverage
New FilesNo new covered files... Modified Files
|
…ittle-endian Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With strategy="active_statevector", the samplers run on a CUDA GPU when CuPy sees one and every component fits in 11 active qubits, as the stabilizer-rank strategies do with JAX; otherwise on the CPU. This drops device="cuda" and CompiledDetectorSampler.sample_postselected: sample keeps main's columns and packing, and with a postselection_mask copies only the passing shots off the GPU. Observable expectations stay available through the private sample_accepted. Also follow main's seeding (#263) and ShotPlan (#259), read the multiprocessor count of the current device, and keep the rest of the test suite on the CPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With strategy="active_statevector", the samplers run on a CUDA GPU when CuPy sees one and every component fits in 11 active qubits, as the stabilizer-rank strategies do with JAX; otherwise on the CPU. This drops device="cuda" and CompiledDetectorSampler.sample_postselected: sample keeps main's columns and packing, and with a postselection_mask copies only the passing shots off the GPU. Observable expectations stay available through the private sample_accepted. Also follow main's seeding (#263) and ShotPlan (#259), read the multiprocessor count of the current device, and keep the rest of the test suite on the CPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With strategy="active_statevector", the samplers run on a CUDA GPU when CuPy sees one and every component fits in 11 active qubits, as the stabilizer-rank strategies do with JAX; otherwise on the CPU. This drops device="cuda" and CompiledDetectorSampler.sample_postselected: sample keeps main's columns and packing, and with a postselection_mask copies only the passing shots off the GPU. Observable expectations stay available through the private sample_accepted. Also follow main's seeding (#263) and ShotPlan (#259), read the multiprocessor count of the current device, and keep the rest of the test suite on the CPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Summary
Fixes the postselection slowdown that #252, #222 and #223 brought to
strategy="active_statevector"before 1.0.0. Samples are bit-for-bit those ofmain.#252 made
CompiledDetectorSampler.sample(postselection_mask=...)return only the shots that pass. The statevector path found them with one NumPy pass over every shot's words after sampling, on one thread. It also still cleared (#222) and finished (#223) the discarded shots, though nothing reads them any more. Againstmainat 5af2731 (before these PRs), on B850 (Ryzen 9 9900X), the postselected cells fell to:surface_d7_r7cultivation_d5coherent_d3_r1coherent_d5_r1coherent_d3_r3Changes
alive. Each chunk's_run_chunkalready knows which shots passed: the screen of postselected direct detectors, then each component's pruning. It now hands that back, compacts its survivors in place on its own thread (_compact), and a numba_joinputs the chunks together._shrinkgives the rest of the buffer back, so a view of a few survivors doesn't keep a whole call's words alive.ShotPlan.rejectedis now a bool.ChannelSampler.sample_screened_wordsscreens each noise block inside the noise kernel, while the block is still in cache. A parallel_copy_keptthen copies the survivors out of scratch words that the sampler reuses (at most 32 MiB). Fresh scratch memory each call cost about 0.4 ms of page faults per 2 MB on these 0.2 ms calls. Both loops have branchless paths for 1-word rows and for blocks that keep most of their shots._prunedrops the dying lanes' shots fromliveandalive, without_finish_shot's pattern XOR, coin draws and hidden clearing._screen's&= keptand thepruned_keptclearing are gone too. This is the follow-up Return only postselected shots from CompiledDetectorSampler.sample #252 listed. Return only postselected shots from CompiledDetectorSampler.sample #252 kept the pattern XOR because its filter read the words; this filter readsalive.Benchmarks
near-clifford-benchmarksharness on B850, sharing on, bit-packed. Shot data is the simulator's call alone. "Counting included" adds the harness's counting, which for 5af2731 includes dropping the rejected shots itself.surface_d7_r7distillation_d3distillation_d5cultivation_d3cultivation_d5cultivation_d5_end2endcultivation_d7coherent_d3_r3coherent_d5_r5coherent_d3_r1coherent_d5_r1On the two K=0 coherent r=1 circuits, a call is 0.1–0.2 ms of noise sampling. Returning only the survivors costs one extra read and write of the call's words there. 5af2731 skipped that work and left it to the caller, so the shot-data rate can't match. Counting included, these cells are at 0.74–1.34x of 5af2731. Unmasked calls take the same paths as before.
Testing
main(aa9d6a2): 880 configurations, at 1 and 10 threads, and again with the part size forced to 4096 (the multi-part path).batch_size, the reference sample, and appended and separate observables.test_shared.pytests read discards off discarded rows' contents.test_pruning_discards_exactly_the_postselected_failuresnow readsalive.test_pruned_shots_report_false_after_their_pruning_actionis replaced bytest_pruning_keeps_exactly_the_shots_an_unpruned_run_accepts, an exact check: with the same seed, pruning keeps exactly the shots that an unpruned run accepts, with identical rows._copy_kept(every branch, serial and parallel),_join,_compact, scratch reuse across shrinking and growing calls, and pickling a sampler after a screened call (as onmain; the scratch lock is left out).uv run pytest: 2465 passed.test_kernels.py::test_the_calling_thread_keeps_parallel_kernels_after_run_tasksfails on this machine on unmodifiedmaintoo, and was deselected.pre-commit run --all-files, including pyright, is clean.🤖 Generated with Claude Code