wai-quantum: the branching test pins its reference run and forces the state vector's split paths #393

Merged
dcharlot merged 1 commit from wai/quantum-branching-threads-pinned into main 2026-10-21 12:11:03 -04:00
2 changed files with 35 additions and 1 deletions

View file

@ -1213,21 +1213,45 @@ mod tests {
/// Threads, the state limit and the order branches run in change nothing:
/// below 16 qubits (many trees) and above (one tree, replays).
///
/// The reference run is pinned by its bits, the same on every target, so a
/// failure says whether the reference itself moved or only a variant. From
/// 16 qubits the run is one tree on the state vector's own threads, which
/// split a pass only from 2¹⁸ amplitudes a thread; the 16-qubit case forces
/// the split paths too, where it ran them before that threshold.
#[test]
fn branching_runs_depend_on_neither_threads_nor_the_state_limit() {
use crate::quantum_sv::with_forced_threads;
let noise = NoiseModel { depol1: 0.02, depol2: 0.05, damping: 0.01, dephasing: 0.01, readout: 0.02 };
for (n, shots) in [(6u32, 300u64), (16, 40)] {
for (n, shots, pin) in [(6u32, 300u64, 0xf1f4_2a95_7ae8_7a7e_u64), (16, 40, 0x5e90_2d74_edf2_7819)] {
let g = random_circuit(n, 3, 9);
let a = branching_trajectories(&g, n, &noise, &observables(n), shots, 3, 64, 1).unwrap();
assert_eq!(run_digest(&a), pin, "n {n}: the reference run is not the pinned one: {:?}", a.expectations);
for (states, threads) in [(1usize, 1usize), (2, 5), (64, 5)] {
let b = branching_trajectories(&g, n, &noise, &observables(n), shots, 3, states, threads).unwrap();
// Replays redo gates, so only the work differs.
assert_eq!((&b.expectations, &b.counts, b.shots, b.histories), (&a.expectations, &a.counts, a.shots, a.histories), "n {n}: {states} states, {threads} threads");
assert!(b.gate_applications >= a.gate_applications);
}
// (A target without threads has no split paths to force.)
if n >= 16 && !cfg!(target_arch = "wasm32") {
for (states, sv_threads) in [(1usize, 2usize), (64, 4)] {
let b = with_forced_threads(Some(sv_threads), || branching_trajectories(&g, n, &noise, &observables(n), shots, 3, states, 1).unwrap());
assert_eq!(run_digest(&b), pin, "n {n}: {states} states, the state vector on {sv_threads} threads: {:?}", b.expectations);
}
}
}
}
/// A run's estimates, counts, shots and histories, as one FNV-1a digest of
/// their bits.
fn run_digest(r: &BranchingRun) -> u64 {
let mut v: Vec<u64> = r.expectations.iter().flat_map(|e| [e.mean.to_bits(), e.stderr.to_bits()]).collect();
v.extend(r.counts.iter().flat_map(|&(x, c)| [x, c]));
v.extend([r.shots, r.histories]);
v.iter().fold(0xcbf2_9ce4_8422_2325u64, |h, &w| (h ^ w).wrapping_mul(0x0000_0100_0000_01b3))
}
/// With rare errors most shots share the error-free history.
#[test]
fn rare_errors_share_the_work() {

View file

@ -287,6 +287,16 @@ thread_local! {
static ONE_THREAD: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
}
/// Tests: run `f` with this thread's passes split `threads` ways (`None`: as
/// the machine would), so callers' tests can take the split paths at small sizes.
#[cfg(test)]
pub(crate) fn with_forced_threads<R>(threads: Option<usize>, f: impl FnOnce() -> R) -> R {
let before = FORCE_THREADS.with(|t| t.replace(threads));
let r = f();
FORCE_THREADS.with(|t| t.set(before));
r
}
/// Run `f` with this thread's state-vector kernels on this thread alone. For
/// callers that run many small states in parallel; the result is the same
/// bits either way.