Skip to content

Batch versions of all clifford gates - #116

Merged
Roger-luo merged 12 commits into
mainfrom
david/99-batch-gates
Jun 21, 2026
Merged

Roger-luo merged 12 commits into
mainfrom
david/99-batch-gates

Conversation

@david-pl

Copy link
Copy Markdown
Collaborator

Closes #99.

Makes batched versions proper traits and completes the implemented set. Also uses batched gates for STIM.

Copilot AI review requested due to automatic review settings June 12, 2026 10:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses issue #99 by promoting batched Clifford operations to first-class traits, filling out the batched gate set, and updating the STIM executor to dispatch through batched gate APIs (with added benchmarks and a Python demo).

Changes:

  • Introduces CliffordBatch / CliffordExtensionsBatch traits (with default loop implementations) and re-exports them in the runtime traits module.
  • Implements/extends batched gate implementations in ppvm-tableau (including removing the prior fixed 8-word mask limit via SmallVec) and adds coverage/benches.
  • Switches STIM execution to use batched single-qubit gate calls and batched pair-gate calls.

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
ppvm-python/demo/stim_demo.py Adds a small demo for loading/running STIM programs and sampling results.
crates/ppvm-tableau/src/gates/clifford.rs Adds batched implementations for the full Clifford set, uses SmallVec masks to remove the previous hard cap, and expands tests.
crates/ppvm-tableau/Cargo.toml Adds smallvec dependency and registers new benchmark targets.
crates/ppvm-tableau/benches/clifford_batch.rs Adds baseline and batched-vs-loop benchmarks across Clifford/extension gates.
crates/ppvm-tableau/benches/clifford_crossover.rs Adds a “crossover” benchmark to find batch-size break-even points.
crates/ppvm-stim/src/executor.rs Updates dispatch to use batched gate APIs; introduces per-instruction pair collection for 2-qubit gates.
crates/ppvm-runtime/src/traits/mod.rs Re-exports the new batch traits.
crates/ppvm-runtime/src/traits/clifford.rs Defines CliffordBatch and CliffordExtensionsBatch traits with default loop bodies.
Cargo.lock Records the added smallvec dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ppvm-runtime/src/traits/clifford.rs Outdated
Comment on lines +133 to +142
GateName::X => tab.x_batch(targets),
GateName::Y => tab.y_batch(targets),
GateName::Z => tab.z_batch(targets),
GateName::H | GateName::HXZ => tab.h_batch(targets),
GateName::S | GateName::SqrtZ => tab.s_batch(targets),
GateName::SDag | GateName::SqrtZDag => tab.s_adj_batch(targets),
GateName::SqrtX => tab.sqrt_x_batch(targets),
GateName::SqrtXDag => tab.sqrt_x_adj_batch(targets),
GateName::SqrtY => tab.sqrt_y_batch(targets),
GateName::SqrtYDag => tab.sqrt_y_adj_batch(targets),
Comment thread crates/ppvm-stim/src/executor.rs Outdated
Comment on lines +145 to +147
let pairs: Vec<(usize, usize)> =
targets.chunks_exact(2).map(|p| (p[0], p[1])).collect();
tab.cnot_batch(&pairs);
Comment thread crates/ppvm-stim/src/executor.rs Outdated
Comment on lines +150 to +152
let pairs: Vec<(usize, usize)> =
targets.chunks_exact(2).map(|p| (p[0], p[1])).collect();
tab.cy_batch(&pairs);
Comment thread crates/ppvm-stim/src/executor.rs Outdated
Comment on lines +155 to +157
let pairs: Vec<(usize, usize)> =
targets.chunks_exact(2).map(|p| (p[0], p[1])).collect();
tab.cz_batch(&pairs);
@github-actions

github-actions Bot commented Jun 12, 2026 •

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

Copilot AI review requested due to automatic review settings June 12, 2026 14:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 13 changed files in this pull request and generated 6 comments.

Comment thread crates/ppvm-stim/src/executor.rs
Comment on lines 144 to +147
GateName::CX | GateName::ZCX | GateName::CNot => {
targets.chunks_exact(2).for_each(|p| tab.cnot(p[0], p[1]));
let pairs: Vec<(usize, usize)> =
targets.chunks_exact(2).map(|p| (p[0], p[1])).collect();
tab.cnot_batch(&pairs);
Comment on lines 149 to +152
GateName::CY | GateName::ZCY => {
targets.chunks_exact(2).for_each(|p| tab.cy(p[0], p[1]));
let pairs: Vec<(usize, usize)> =
targets.chunks_exact(2).map(|p| (p[0], p[1])).collect();
tab.cy_batch(&pairs);
Comment on lines 154 to +157
GateName::CZ | GateName::ZCZ => {
targets.chunks_exact(2).for_each(|p| tab.cz(p[0], p[1]));
let pairs: Vec<(usize, usize)> =
targets.chunks_exact(2).map(|p| (p[0], p[1])).collect();
tab.cz_batch(&pairs);
Comment on lines +151 to +157
/// Batched Clifford gates: apply the same gate to many qubits in one call.
///
/// Default implementations loop over the corresponding single-qubit
/// (or single-pair) method on [`Clifford`]. Types like the stabilizer
/// `Tableau` override the defaults with a fused inner-loop or bitmask
/// implementation; types without a faster path inherit the loop and
/// still benefit from the trait's uniform API.
Comment on lines +179 to +187
fn build_masks(&self, indices: &[usize]) -> Option<(MaskBuf<T>, usize)> {
if self.data.is_empty() {
return None;
}
let n_words = self.data[0].word.xbits.data.as_raw_slice().len();
debug_assert!(n_words <= 8, "Storage exceeds 8 words");
let bits_per_word = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
let one = <T::Storage as BitView>::Store::one();
let zero = <T::Storage as BitView>::Store::zero();
let mut masks = [zero; 8];
let mut masks: MaskBuf<T> = smallvec![zero; n_words];
Roger-luo and others added 2 commits June 13, 2026 10:48
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 13, 2026 14:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 13 changed files in this pull request and generated 4 comments.

Comment on lines +25 to +26
const N: usize = 128;
const BATCH_SIZES: &[usize] = &[1, 2, 4, 8];
Comment thread crates/ppvm-stim/src/executor.rs Outdated
Comment on lines +145 to +147
let pairs: Vec<(usize, usize)> =
targets.chunks_exact(2).map(|p| (p[0], p[1])).collect();
tab.cnot_batch(&pairs);
Comment thread crates/ppvm-stim/src/executor.rs Outdated
Comment on lines +150 to +152
let pairs: Vec<(usize, usize)> =
targets.chunks_exact(2).map(|p| (p[0], p[1])).collect();
tab.cy_batch(&pairs);
Comment thread crates/ppvm-stim/src/executor.rs Outdated
Comment on lines +155 to +157
let pairs: Vec<(usize, usize)> =
targets.chunks_exact(2).map(|p| (p[0], p[1])).collect();
tab.cz_batch(&pairs);

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 13 changed files in this pull request and generated 2 comments.

Comment on lines 144 to +157
GateName::CX | GateName::ZCX | GateName::CNot => {
targets.chunks_exact(2).for_each(|p| tab.cnot(p[0], p[1]));
let mut pairs: Vec<(usize, usize)> = Vec::with_capacity(targets.len() / 2);
pairs.extend(targets.chunks_exact(2).map(|p| (p[0], p[1])));
tab.cnot_batch(&pairs);
}
GateName::CY | GateName::ZCY => {
targets.chunks_exact(2).for_each(|p| tab.cy(p[0], p[1]));
let mut pairs: Vec<(usize, usize)> = Vec::with_capacity(targets.len() / 2);
pairs.extend(targets.chunks_exact(2).map(|p| (p[0], p[1])));
tab.cy_batch(&pairs);
}
GateName::CZ | GateName::ZCZ => {
targets.chunks_exact(2).for_each(|p| tab.cz(p[0], p[1]));
let mut pairs: Vec<(usize, usize)> = Vec::with_capacity(targets.len() / 2);
pairs.extend(targets.chunks_exact(2).map(|p| (p[0], p[1])));
tab.cz_batch(&pairs);
Comment on lines +4 to +10
//! Crossover bench: at fixed tableau size, sweep the *batch size* `k` and
//! compare a flat loop of `k` single-gate calls against the batched call.
//!
//! Goal is to find the smallest `k` at which the bitmask / fused batch pays
//! off versus the loop. The fixed tableau size is 128 (two `u64` words, so
//! batches up to `k = 64` stay in one storage word; `k > 64` spans both).
//!
# Conflicts:
#	Cargo.lock
#	crates/ppvm-tableau/Cargo.toml
@Roger-luo

Copy link
Copy Markdown
Collaborator

Benchmark: batched vs. non-batched (loop) Clifford gates

Measuring what this PR actually buys, on top of the latest main (merged into the branch). loop = the previous per-target single-gate dispatch; batch = this PR's batched methods. Higher speedup = batch faster.

Environment: Apple M4 Pro (14 cores), rustc 1.96.0, --release, branch tip 61f5646a. Micro-benchmarks via the PR's own clifford_batch / clifford_crossover criterion benches (median of 50 samples). End-to-end via the surface_d30 example, serial.

TL;DR

  • End-to-end STIM sampling of a distance-30 surface code (1889 qubits): ~2.05× faster (1.22 s/shot → 0.59 s/shot). This is the headline number for "uses batched gates for STIM".
  • Single-qubit gates are the big win: 11–39× faster when applied across many qubits (the mask-fusion collapses N per-qubit passes into O(n_words) per Pauli row).
  • Two-qubit gates (cnot/cz/cy) are not a uniform win. They regress 2–3× for small, L1-resident tableaux and only pay off (≈1.1–1.3×, cy ≈ neutral) for large out-of-L1 tableaux. The net end-to-end gain is positive because surface-code circuits are single-qubit/reset/measure heavy and run at large qubit counts.

End-to-end: surface_d30 (distance-30 surface code, 1889 qubits, 12 shots, serial)

Executor per shot 12 shots speedup
loop (non-batch / pre-PR dispatch) 1.22 s 14.67 s 1.00×
batch (this PR) 0.59 s 7.1 s 2.05×

Measured by surgically reverting only the Clifford-gate arms of execute_validated to the pre-PR single-gate loops; everything else (parsing, resets, measurement, noise sampling) is identical. Both numbers were stable across two runs.

Single-qubit gates — Tableau<Byte8F64<2>>, n=128 (L1-resident)

Gate Targets loop batch speedup
x all 128 11.23 µs 590 ns 19.0×
y all 128 13.29 µs 644 ns 20.6×
z all 128 11.00 µs 590 ns 18.6×
h all 128 21.49 µs 630 ns 34.1×
s all 128 18.36 µs 667 ns 27.5×
s_adj all 128 19.91 µs 679 ns 29.3×
sqrt_x all 128 18.51 µs 591 ns 31.3×
sqrt_x_adj all 128 18.34 µs 475 ns 38.6×
sqrt_y all 128 19.86 µs 623 ns 31.9×
sqrt_y_adj all 128 19.88 µs 625 ns 31.8×

For the "every-other" pattern (64 targets) the speedups are ~11–25× (same gates, fewer qubits per word).

Two-qubit gates

Gate Config Targets loop batch speedup
cnot n=128 (L1) 64 pairs 15.98 µs 36.41 µs 0.4× (regress)
cz n=128 (L1) 64 pairs 15.78 µs 44.21 µs 0.4× (regress)
cy n=128 (L1) 64 pairs 17.84 µs 55.51 µs 0.3× (regress)
cnot n=2048 (out of L1) 1024 pairs 9.03 ms 6.85 ms 1.3×
cz n=2048 (out of L1) 1024 pairs 8.35 ms 7.60 ms 1.1×
cy n=2048 (out of L1) 1024 pairs 9.68 ms 11.48 ms 0.8× (regress)

The raw-word two-qubit batch only beats the loop once the tableau no longer fits in cache; cy doesn't reach break-even even at 2048 qubits. Worth a look before relying on cy_batch in hot paths.

Crossover — smallest batch size k where batch overtakes loop (n=128)

Gate k=1 k=2 k=4 k=8
h 0.95× 1.52× 2.61× 4.83×
x 0.75× 0.93× 1.46× 2.51×
s 0.75× 1.07× 1.76× 3.19×

Batch cost is roughly flat in k (one fused pass per word), so it wins from k≈2 (h, s) or k≈4 (x); at k=1 there's a small fixed mask-building overhead.


Takeaway: the PR delivers a solid, real ~2× end-to-end speedup on a realistic surface-code STIM workload, driven almost entirely by single-qubit batching. The two-qubit batch paths are the weak spot — net-neutral-to-negative except for large tableaux — and cy_batch in particular may be worth revisiting.

Numbers reproducible with cargo bench -p ppvm-tableau --bench clifford_batch / --bench clifford_crossover and cargo run --release --example surface_d30 -p ppvm-stim.

@Roger-luo
Roger-luo merged commit ddd8c9c into main Jun 21, 2026
9 checks passed
@Roger-luo
Roger-luo deleted the david/99-batch-gates branch June 21, 2026 21:24
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.

Properly batch all gates

3 participants