Repository navigation
Conversation
The survivors of each chunk were appended to a Python list one row at a time and stacked back into a batch with np.stack, which cost more than the statevector simulation on cultivation d3 (about 60 ms of a 110 ms call of 262,144 shots). They now go into a two-batch array: a chunk adds at most one batch of survivors to fewer than one pending batch. Samples are bit-identical for a fixed seed. Quick benchmark, all threads: cultivation d3 2.61M -> 5.81M shots/s with trajectory sharing and 1.66M -> 2.56M with one lane per shot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ChannelSampler.sample looped over every channel in Python and XOR-ed a dense uint8 row of all f-variables into each shot an error fired on; on surface d7r7 an error flips 2.7 of 337 f-variables, and the sampler took 91% of the time. A numba kernel now walks each channel's geometric skips and flips only the f-variables of the fired outcome. Blocks of 4096 shots run in parallel, each from its own SplitMix64 stream seeded with 64 bits from the sampler's Generator, so samples depend on the seed and the number of shots but not on the number of threads. The output layout is unchanged. The stream differs from NumPy's, so one test that pinned 4 of 10 events at p=0.3 now checks the rate over 100,000 shots. New tests cover a 2-bit channel's joint distribution across blocks, a certain error reaching every row of a partial last block, and reproducibility. Quick benchmark, all threads: surface d7r7 1.55M -> 22.4M shots/s, distillation (shared) 3.6M -> 9.0M, cultivation d5 (shared) 1.5M -> 2.7M. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With postselection, every chunk gathered the direct outputs into a zeroed array, copied its detector columns into the result and tested the postselected ones in three NumPy passes: 80 ms per 262,144 shots on cultivation d5 (47% of a call) and 466 ms on SymFT's d=7 circuit. One parallel numba pass now writes each direct detector into its result row and returns the discard mask (3.7 ms and 13 ms). Survivors are now buffered with one np.concatenate per chunk, as the review of the preallocated buffer suggested: as fast, without the two-batch invariant, and JAX never reads a buffer that is overwritten later. A new test checks that every survivor keeps its own direct detectors; the old tests could not catch misattributed rows, because their only direct detector was postselected. Samples are bit-identical for a fixed seed. Quick benchmark, all threads: distillation (shared) 9.0M -> 16.3M shots/s, cultivation d5 (shared) 2.7M -> 3.2M, SymFT d7 postselected (shared) 202k -> 241k. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A channel whose identity probability is rounded below zero (e.g. PAULI_CHANNEL_1(0.33, 0.56, 0.11)) had a firing probability above 1, so log1p(-p) was NaN: undefined samples, a segfault on x86-64. The firing probability is now the clipped total of the non-identity outcomes, which is also exact for small probabilities. - Numba's default threading layer aborts the process when two Python threads run parallel code at once. tsim.utils.kernels.kernel compiles each kernel serially and in parallel, runs it in parallel only above a work threshold (waking the threads costs about 0.1 ms) and holds a lock around parallel calls. The parallel compilation gets its own name, since numba names cache files after the function and the two would share one entry. - Numba refuses to define a cached function where no cache directory is writable, which made `import tsim` fail on read-only installs; jit falls back to compiling without a cache. - SplitMix64 helpers move to tsim.utils.rng; geometric skips use a 64-bit uniform and return a float, inf when the uniform rounds to 1. - The direct-detector kernel takes named arguments built once per sampler. - Tests: blocks draw independent streams, a rounded identity probability, concurrent Python threads (in a subprocess), and a stronger survivor test with reference bits, flips, a shared f-bit, reordered outputs and a direct observable. A test whose circuit never reached the postselection path is removed; the reference test no longer counts the reference sample's row. - uv.lock adds only the numba lines (the earlier lock had been rewritten with a user-level index configuration). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The JAX shared executor kept static lane tables: kernels ran over every slot of a power-of-two table (26-40% live), the loop ran in segments with host checks, resized and resumed when a table filled, and compiled again for every new table size (about a second, randomly inside sample calls). On wide components that work dominated: 88% of a coherent d5r5 call. The executor is now a Python loop over the tape that keeps the lanes in growable NumPy arrays and calls small numba kernels: each action touches only the live lanes and the 2^extent amplitudes it can reach. extent comes from the plan (lower records one plus the highest active axis), and the planner now promotes onto the lowest free axis (a free SWAP of two dormant axes), so the extent tracks the active width: 1.8x less array work on coherent d5r5, 2.0x on SymFT's d=7 circuit. Rare outcomes are drawn as binomials by geometric skips with a 64-bit uniform, so the chance of a first hit stays exact below float32 resolution, as before. Grouping, splitting, pruning and reconstruction keep their semantics, and the exact distribution tests run unchanged against the new executor. The JAX executor stays for trajectory_sharing=False. shared_breakdown.py timed stages of the JAX executor and is removed. Quick benchmark, all threads, trajectory sharing: coherent d5r5 109k -> 436k shots/s, SymFT d7 postselected 241k -> 1.35M, cultivation d5 3.2M -> 8.6M, cultivation d3 5.1M -> 8.9M, distillation 16.3M -> 17.5M. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one-thread Tsim run set only NPROC=1, which pins XLA; numba (noise sampling, trajectory sharing) now also gets NUMBA_NUM_THREADS=1. Shared samplers no longer recompile for new lane tables, so every configuration gets a single warmup call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Once postselection had pruned every lane, the next active measurement took np.max of an empty array and raised; tiny batches, high rejection and zero shots hit it. The tape now stops when no lanes are left, and the norm deviation is a NaN-propagating max with an initial value, which the sampler now also warns about. - The lane table allocated at least 16 rows of 2^width amplitudes even for a few shots and copied full rows when growing. A lane holds at least one shot, so the table is capped at the shot count, and growing copies only the amplitudes the current extent reaches. - The JAX executor's per-lane shot count and choose_outcome's count argument existed only so that a future sharing executor could split lanes; both are gone. - Tests: a component whose extent drops and rises again (a generated d5r3 surface code with coherent over-rotations), sampled with NaN in every amplitude outside the tracked region, matches a run at full width bitwise; every kernel in parallel matches the serial run bitwise; shots whose lanes all die, and zero shots; every action stays within its extent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The extent test missed a clearing bug after the first rise: the generated circuit's low-extent stretches hold only rotations, so no lane moved while the extent was low. It now also runs with every fifth extent raised to the full width (an extent is only an upper bound), poisons the lane table with NaN after every split, prune and growth, and compares the norm deviations bitwise. - The parallel-vs-serial test's baseline ran the wide calls in parallel too; it now forces every kernel serial. - The all-dead test checks that some shot's lanes did all die. - A test checks that a NaN normalization deviation warns. - The lane table's shot cap no longer needs max(1, ...), and the module docstring is reflowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the d=5 end-to-end cultivation circuit (463 qubits, 184,039 error bits) building a sampler took 101 s, 80 s of it in ChannelSampler: - np.unique(error_transform, axis=1) sorted all 184,039 columns of the 2,821-row error transform, comparing whole columns: 66 s. unique_columns finds the 15,148 distinct columns with a dict over the bit-packed columns and sorts only those; big-endian packing keeps the lexicographic order, so the result equals np.unique's. - absorb_subset_channels compared every pair of the 21,341 channels: 14 s. A channel of at most max_bits signatures has at most 15 strict subsets, so they are looked up in a dict, and absorbed in the original order. Both give the same noise model as before, bitwise, and the same samples for a fixed seed (checked on distillation, cultivation d3/d5, coherent d3r3 and the end-to-end circuit). Building the end-to-end sampler now takes 24 s, 17 s of it in zxparam's full_reduce. Tests compare unique_columns with np.unique and the indexed absorption with the pairwise one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- unique_columns' docstring states the 1-D inverse (np.unique returned a 2-D one on numpy 2.0) and that nonzero entries count as 1, and drops benchmark figures that would go stale; `seen` is now `column_ids`. - The absorption test also draws channels without signatures and runs with max_bits 2 and 4; dropping the empty subset now fails it. - absorb_subset_channels' docstring and sort comment say what holds and why the order matters, and ChannelSampler's simplification steps are numbered 1 to 4. 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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e394e8404a
ℹ️ 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".
| _, order = np.unique(packed[first], axis=0, return_index=True) | ||
| rank = np.empty_like(order) | ||
| rank[order] = np.arange(len(order)) | ||
| return matrix[:, first[order]], rank[column_ids] |
There was a problem hiding this comment.
Normalize columns before returning packed representatives
When matrix contains noncanonical nonzero values, which the docstring explicitly says count as 1, np.packbits coalesces them with literal ones but this return keeps the original representative. For example, columns containing 2 and 1 receive one ID, and if the 2 column comes first, ChannelSampler stores 2 in signature_matrix; the later matrix multiplication modulo 2 then treats both columns as zero, so sampled errors stop flipping their outputs. Normalize the matrix to boolean/0-or-1 before both packing and returning it (or reject values outside {0, 1}) to avoid order-dependent noise samples.
Useful? React with 👍 / 👎.
☂️ Code Coverage
Overall Coverage
New Files
Modified Files
|
Summary
Stacked on #198 (base
trajectory-sharing), which is stacked on #196. On that branch, two costs dominated:This PR moves both onto numba kernels on the CPU. It also makes building a sampler for large circuits 4× faster. numba is now a runtime dependency (
numba>=0.61).Noise sampling (
ChannelSampler.sample):Direct detectors and postselection:
np.concatenateper chunk instead of row by row in a Python list.Trajectory-sharing executor:
extentcomes from the plan. The planner now promotes onto the lowest free axis, so the extent tracks the active width. This does 1.8× less array work on coherent d5r5 and 2.0× less on SymFT's d=7.trajectory_sharing=False. Its per-lanecountand thecountargument ofchoose_outcomeexisted only for sharing, and both are removed.Sampler setup on large circuits:
unique_columnsbuilds a dict over the bit-packed columns. Its result equalsnp.unique(axis=1).absorb_subset_channelslooks up the at most 15 strict subsets of each channel instead of comparing all pairs.full_reduce. The noise model is bitwise unchanged.tsim.utils.kernels: this module handles three numba pitfalls:tsim.utils.rngholds the SplitMix64 helpers.Fixed along the way: a channel whose identity probability rounds below zero, such as
PAULI_CHANNEL_1(0.33, 0.56, 0.11), had a firing probability above 1, which made the geometric skips NaN. The firing probability is now the clipped total of the non-identity outcomes.Performance (same machine, Apple M4 Pro)
How to read the table:
scripts/clifft_comparison/run_all.sh, with Clifft 0.11.0.NPROC=1 NUMBA_NUM_THREADS=1.trajectory-sharing.What the numbers show:
One thread: Tsim now beats Clifft on every circuit in the table. Clifft is still faster on 20-qubit quantum volume (5 against 2 shots/s).
All threads:
d=5 end-to-end cultivation, postselected on the 107 cultivation-stage detectors:
How this was verified
unique_columnsequalsnp.unique.Test plan
pytest: 1641 passedpyright src test: 0 errorsKnown limitations, left for follow-ups:
libomp.trajectory_sharing=False.full_reduce. 187k of its 225k vertices are noise spiders.🤖 Generated with Claude Code