Skip to content

perf(tableau): word-level Clifford gate delegation (autotune-found) - #149

Merged
Roger-luo merged 3 commits into
mainfrom
perf/word-level-clifford-gates
Jun 23, 2026
Merged

perf(tableau): word-level Clifford gate delegation (autotune-found)#149
Roger-luo merged 3 commits into
mainfrom
perf/word-level-clifford-gates

Conversation

@Roger-luo

@Roger-luo Roger-luo commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

What

Word-level Clifford gates in ppvm-tableau
(crates/ppvm-tableau/src/gates/clifford.rs): the per-row gate work operates
directly on the packed Pauli words (raw integer slices via
as_raw_slice/as_raw_mut_slice + a hoisted index/bits, index%bits, mask)
rather than going through bitvec's bounds-checked single-bit indexing inside
the 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() block
alone 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 _word methods
(x_word, y_word, …, cnot_word, cz_word, cy_word) on
impl Tableau<T>, wired only into the GeneralizedTableau delegation
macros. That left the canonical Clifford / CliffordExtensions impls for
bare Tableau<T> still delegating per-row through the slow bitvec path —
two independent implementations of every gate's phase/bit logic that could
silently diverge
, and the bare-Tableau path never actually got the speedup.

The latest commit folds the word-level loop into the canonical
Tableau<T> gate methods
and deletes the _word duplicates. The
GeneralizedTableau macros are reverted to delegate to the single canonical
method name (self.tableau.$name(...)). Net result:

  • One implementation per gate. Every caller — bare Tableau,
    GeneralizedTableau, and the fused batch path — now runs through the same
    canonical word-level code. Nothing can diverge.
  • The bare-Tableau direct path 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 (the
    GeneralizedTableau side 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 full workspace builds.
  • New tests cross-check each canonical single- and two-qubit gate against
    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 be
a real regression for sparse-vec:
the sparse-vec ops (add_or_insert, trim,
normalize) touch only SparseVector — they share no code path with the
Clifford gate methods this PR changes. They merely live in the same micro
criterion binary as the gate benches, so editing clifford.rs perturbs the
binary'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 main worktree; sparse-vec source is byte-identical on both, so any
delta is pure noise). Times in ns:

sparse-vec bench main (3 runs) this branch (3 runs)
usize/trim 12.15 / 12.22 / 13.67 11.21 / 11.33 / 11.34
u128/trim 13.57 / 12.78 / 14.51 13.15 / 14.82 / 13.94
usize/add_or_insert/new 63.17 / 64.31 / 64.71 63.29 / 62.86 / 64.32
u128/add_or_insert/new 78.31 / 76.35 / 80.89 74.48 / 74.32 / 77.20
U256/add_or_insert/new 127.9 / 123.6 / 167.1 119.0 / 123.7 / 124.2

The run-to-run spread on a single binary (e.g. main's
U256/add_or_insert/new swinging 123.6 → 167.1 ns, +35%, between identical
re-runs; u128/trim spanning 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 exercise
Clifford gates):

msd-fused bench main this branch
msd-fused-0 55.95 / 56.02 / 56.47 µs 57.21 / 57.37 / 57.76 µs
msd-fused-sample 24.43 / 24.32 / 24.47 µs 24.75 / 24.78 / 24.78 µs

~+2–3% and ~+1.5% respectively — small and stable. (Note: this main is the
PR'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 "⚠️" list that even shares a code path with the change.

Genuine wins (unchanged)

metric Δ
msd_mean (full MSD circuit) −14.6%
micro_cnot −21.9%
micro_measure_random −8.8% ✅

ppvm-tableau tests (287, incl. the new multi-word equivalence tests) and the
repo'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

… 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
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-23 18:12 UTC

Roger-luo and others added 2 commits June 23, 2026 13:43
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>
@Roger-luo
Roger-luo enabled auto-merge (squash) June 23, 2026 18:02
@Roger-luo
Roger-luo merged commit ad02c33 into main Jun 23, 2026
13 checks passed
@Roger-luo
Roger-luo deleted the perf/word-level-clifford-gates branch June 23, 2026 18:12
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