perf(tableau): word-level Clifford gate delegation (autotune-found) - #149
Merged
Conversation
… full turbofish `<T::Storage as BitView>::Store` paths in all `_word` methods SUMMARY: fix E0401 by replacing local `type Store` aliases with full turbofish `<T::Storage as BitView>::Store` paths in all `_word` methods
|
PR #149 added the packed-word optimization as a parallel set of `_word` methods (`x_word`, ..., `cy_word`) wired only into the `GeneralizedTableau` delegation macros. The canonical `Clifford` / `CliffordExtensions` impls for `Tableau<T>` kept delegating per-row through the slow `bitvec` single-bit path, leaving two independent copies of every gate's phase/bit logic that could silently diverge. Fold the word-level loop into the canonical `Tableau<T>` gate methods (single source of truth), delete the `_word` duplicates, and revert the `impl_generalized_tableau_clifford!` / `_pair!` macros to delegate to the single canonical name. Now every caller — bare `Tableau`, `GeneralizedTableau`, and the fused batch path — runs through one implementation. The bare-`Tableau` direct path also picks up the optimization it was previously missing. The word-level loop needs `Store: PrimInt`, so the canonical `Clifford for Tableau<T>` impl now carries that bound (matching the `GeneralizedTableau` side, which already required it). The bound propagates to the two `Tableau<T>` impls in noise.rs whose `TableauLike: Clifford` supertrait depends on it; the ripple stops there and the whole workspace builds. Add multi-word equivalence tests: each canonical single- and two-qubit gate is cross-checked against the independent batch (`_many`) implementation on a 2-storage-word tableau, covering qubit indices in both words and word boundaries, same-word vs cross-word two-qubit pairs, plus hand-derived known-transform checks for a high-index single gate and a cross-word CNOT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Word-level Clifford gates in
ppvm-tableau(
crates/ppvm-tableau/src/gates/clifford.rs): the per-row gate work operatesdirectly on the packed Pauli words (raw integer slices via
as_raw_slice/as_raw_mut_slice+ a hoistedindex/bits,index%bits, mask)rather than going through
bitvec's bounds-checked single-bit indexing insidethe per-row loop.
Why / how it was found
This was discovered by autotune running against
tableau-msd/micro.The research agent profiled the 85-qubit MSD circuit (samply + flamegraph),
found ~72% of time in the Clifford gate row-loops (the
encode()blockalone is ~42%), and targeted that hot path.
Single source of truth (design fix in the latest commit)
The optimization originally landed as a parallel set of
_wordmethods(
x_word,y_word, …,cnot_word,cz_word,cy_word) onimpl Tableau<T>, wired only into theGeneralizedTableaudelegationmacros. That left the canonical
Clifford/CliffordExtensionsimpls forbare
Tableau<T>still delegating per-row through the slowbitvecpath —two independent implementations of every gate's phase/bit logic that could
silently diverge, and the bare-
Tableaupath never actually got the speedup.The latest commit folds the word-level loop into the canonical
Tableau<T>gate methods and deletes the_wordduplicates. TheGeneralizedTableaumacros are reverted to delegate to the single canonicalmethod name (
self.tableau.$name(...)). Net result:Tableau,GeneralizedTableau, and the fused batch path — now runs through the samecanonical word-level code. Nothing can diverge.
Tableaudirect path picks up the optimization it was previouslymissing.
Store: PrimInt, so the canonicalClifford for Tableau<T>impl now carries that bound (theGeneralizedTableauside already required it). The bound propagates to thetwo
Tableau<T>impls innoise.rswhoseTableauLike: Cliffordsupertraitdepends on it; the ripple stops there and the full workspace builds.
the independent batch (
_many) implementation on a multi-word tableau(qubit indices spanning two storage words and the word boundary; same-word vs
cross-word two-qubit pairs), plus hand-derived known-transform checks for a
high-index single gate and a cross-word CNOT.
Re-measurement: the "regressions" were measurement artifacts
The original table flagged
sparse-vec/trim(+31%),sparse-vec/add_or_insert(+11.8%) and
msd_fused(+3–5%) as a "tradeoff." Structurally, this can't bea real regression for sparse-vec: the sparse-vec ops (
add_or_insert,trim,normalize) touch onlySparseVector— they share no code path with theClifford gate methods this PR changes. They merely live in the same
microcriterion binary as the gate benches, so editing
clifford.rsperturbs thebinary's codegen/layout (inlining, function placement, i-cache) and shifts the
co-located numbers — classic same-binary measurement noise, not a real slowdown.
Empirical check (criterion median, 3 back-to-back runs each on this branch vs a
fresh
mainworktree; sparse-vec source is byte-identical on both, so anydelta is pure noise). Times in ns:
usize/trimu128/trimusize/add_or_insert/newu128/add_or_insert/newU256/add_or_insert/newThe run-to-run spread on a single binary (e.g.
main'sU256/add_or_insert/newswinging 123.6 → 167.1 ns, +35%, between identicalre-runs;
u128/trimspanning 12.8 → 14.5 ns) dwarfs the originally-reported+11.8% / +31%. Branch vs main is flat within that noise band for every
sparse-vec metric — if anything this branch is marginally faster on
trim. The"tradeoff" framing for sparse-vec was a measurement artifact.
tableau-msd-fused(criterion median, 3 runs each; this bench does exerciseClifford gates):
msd-fused-0msd-fused-sample~+2–3% and ~+1.5% respectively — small and stable. (Note: this⚠️ " list that even shares a code path with the change.
mainis thePR's merge-base, i.e. the pre-PR per-row path, so this compares the new
word-level fold against the original slow path, not against the PR's own earlier
state.) This is within codegen/layout perturbation territory and is the only
bench in the "
Genuine wins (unchanged)
msd_mean(full MSD circuit)micro_cnotmicro_measure_randomppvm-tableautests (287, incl. the new multi-word equivalence tests) and therepo's pre-commit hooks (rustfmt / clippy
-D warnings/ cargo check / hawkeye)pass; the full workspace builds and all 756 workspace tests pass.
🤖 Found by autotune + Claude Code