diff --git a/crates/ppvm-pauli-word/src/loss/data.rs b/crates/ppvm-pauli-word/src/loss/data.rs index 68b53fbf..1fee9677 100644 --- a/crates/ppvm-pauli-word/src/loss/data.rs +++ b/crates/ppvm-pauli-word/src/loss/data.rs @@ -230,58 +230,6 @@ impl PauliWord } } - #[inline(always)] - fn get_multiple(&self, indices: [usize; Q]) -> Self { - let mut result = Self::new(Q); - for (i, &idx) in indices.iter().enumerate() { - result.set(i, self.get(idx)); - } - result - } - - #[inline(always)] - fn set_multiple( - &mut self, - indices: [usize; Q], - values: &Self, - ) { - if values.nqubits != Q { - panic!("Values must have the same number of qubits as indices"); - } - for (i, &idx) in indices.iter().enumerate() { - self.xbits.set(idx, values.xbits[i]); - self.zbits.set(idx, values.zbits[i]); - self.lbits.set(idx, values.lbits[i]); - } - self.rehash(); - } - - #[inline(always)] - fn get_slice(&self, slice: std::ops::Range) -> Self { - if slice.end > self.nqubits { - panic!("Slice out of bounds"); - } - let n_qubits = slice.len(); - let mut xbits = BitArray::ZERO; - let mut zbits = BitArray::ZERO; - let mut lbits = BitArray::ZERO; - for (i, idx) in slice.into_iter().enumerate() { - xbits.set(i, self.xbits[idx]); - zbits.set(i, self.zbits[idx]); - lbits.set(i, self.lbits[idx]); - } - let mut ret = Self { - xbits, - zbits, - lbits, - nqubits: n_qubits, - hash_cache: 0, - _phantom: std::marker::PhantomData, - }; - ret.rehash(); - ret - } - #[inline(always)] fn is(&self, index: usize, pauli: Pauli) -> bool { if index >= self.nqubits { diff --git a/crates/ppvm-pauli-word/src/word/data.rs b/crates/ppvm-pauli-word/src/word/data.rs index 27e0e02b..3411b196 100644 --- a/crates/ppvm-pauli-word/src/word/data.rs +++ b/crates/ppvm-pauli-word/src/word/data.rs @@ -212,54 +212,6 @@ impl(&self, indices: [usize; Q]) -> Self { - let mut result = Self::new(Q); - for (i, &idx) in indices.iter().enumerate() { - result.set(i, self.get(idx)); - } - result - } - - #[inline(always)] - fn set_multiple( - &mut self, - indices: [usize; Q], - values: &Self, - ) { - if values.nqubits != Q { - panic!("Values must have the same number of qubits as indices"); - } - for (i, &idx) in indices.iter().enumerate() { - self.xbits.set(idx, values.xbits[i]); - self.zbits.set(idx, values.zbits[i]); - } - self.rehash(); - } - - #[inline(always)] - fn get_slice(&self, slice: std::ops::Range) -> Self { - if slice.end > self.nqubits { - panic!("Slice out of bounds"); - } - let n_qubits = slice.len(); - let mut xbits = BitArray::ZERO; - let mut zbits = BitArray::ZERO; - for (i, idx) in slice.into_iter().enumerate() { - xbits.set(i, self.xbits[idx]); - zbits.set(i, self.zbits[idx]); - } - let mut ret = Self { - xbits, - zbits, - nqubits: n_qubits, - hash_cache: 0, - _phantom: std::marker::PhantomData, - }; - ret.rehash(); - ret - } - #[inline(always)] fn is(&self, index: usize, pauli: Pauli) -> bool { if index >= self.nqubits { diff --git a/crates/ppvm-tableau/src/data.rs b/crates/ppvm-tableau/src/data.rs index 7b8d008d..c737814b 100644 --- a/crates/ppvm-tableau/src/data.rs +++ b/crates/ppvm-tableau/src/data.rs @@ -807,6 +807,9 @@ where /// word) or [`Self::cz_block_pairs_cross_word`] (straddling two words), so /// callers never need to reason about the `u64` packing. CZ is symmetric, /// so the two bases may be passed in either order. + /// + /// This is a whole-block primitive over the POD raw-word planes — see the + /// crate-level "Data-parallel / GPU-offloadable surface" section. pub fn cz_block(&mut self, control_base: usize, target_base: usize, count: usize) where <::Store as TryFrom>::Error: Debug, @@ -1003,6 +1006,15 @@ where + Copy, I: TableauIndex + Send + Sync, { + /// Split every coefficient into a branch and non-branch term for a + /// single-qubit Pauli measurement/rotation, in one pass over the whole + /// coefficient array. + /// + /// This is a whole-batch primitive over the coefficient array — see the + /// crate-level "Data-parallel / GPU-offloadable surface" section. The + /// `#[cfg(feature = "rayon")]` path computes each entry's branch/non-branch + /// coefficients in parallel; only the final accumulation into the + /// coefficient map is sequential. pub(crate) fn branch_with_coefficients( &mut self, addr0: usize, diff --git a/crates/ppvm-tableau/src/lib.rs b/crates/ppvm-tableau/src/lib.rs index 76f51a88..d2d8f091 100644 --- a/crates/ppvm-tableau/src/lib.rs +++ b/crates/ppvm-tableau/src/lib.rs @@ -28,6 +28,38 @@ //! let r1 = LossyMeasure::measure(&mut tab, 1); //! assert_eq!(r0, r1); //! ``` +//! +//! ## Data-parallel / GPU-offloadable surface +//! +//! A handful of primitives in this crate operate on whole contiguous arrays +//! at once, with no per-element host closures — the shape a thread pool or a +//! CUDA kernel would want: +//! +//! - Every tableau row ([`PhasedPauliWord`](ppvm_pauli_word::phase::PhasedPauliWord)) +//! stores its Pauli word as fixed-size `bytemuck::Pod` integer bit-planes +//! (`xbits`/`zbits`, backed by `[u8; N]` or `[u64; N]`), reachable as +//! contiguous raw integer slices via `as_raw_slice`/`as_raw_mut_slice` — +//! plain-old-data, not a bit-addressed abstraction. +//! - The Clifford single-/two-qubit gates +//! (`crates/ppvm-tableau/src/gates/clifford.rs`) all run through one shared +//! implementation per gate that loops over rows operating directly on +//! those raw integer slices with a hoisted word-index/bit/mask, bypassing +//! `bitvec`'s per-bit bounds checks inside the loop. Every caller — +//! [`data::Tableau`], [`data::GeneralizedTableau`] (via the +//! `impl_generalized_tableau_clifford*` macros), and the fused batch path +//! below — funnels through that one implementation. +//! - [`CliffordBatch`](ppvm_traits::traits::CliffordBatch) methods (`x_many`, +//! `cz_many`, …) and [`data::GeneralizedTableau::cz_block`] / +//! `cz_block_pairs` / `cz_block_pairs_cross_word` apply a gate to a whole +//! contiguous block of qubits as bulk masked slice operations — the entry +//! points a thread pool or CUDA kernel would implement over the row +//! planes. +//! - `GeneralizedTableau::branch_with_coefficients` (crate-private) transforms +//! the whole coefficient array in one pass. Its `#[cfg(feature = "rayon")]` +//! path (`branch_coefficients_parallel`, gated by `RAYON_COEFF_THRESHOLD`) +//! computes the per-entry branch/non-branch coefficient math in parallel, +//! with no shared mutable state — the accumulation of those results into +//! the coefficient map afterwards remains sequential. /// Core [`Tableau`](data::Tableau) and /// [`GeneralizedTableau`](data::GeneralizedTableau) types. diff --git a/crates/ppvm-traits/src/map/dashmap.rs b/crates/ppvm-traits/src/map/dashmap.rs index c973ac7a..4e9749fe 100644 --- a/crates/ppvm-traits/src/map/dashmap.rs +++ b/crates/ppvm-traits/src/map/dashmap.rs @@ -78,21 +78,6 @@ where } } -// impl<'a, S, V, State> ACMapIterMut<'a, S, V> for DashMap, V, State> -// where -// S: PauliStorage + 'a, -// V: Coefficient + 'a, -// State: Clone + BuildHasher + 'a, -// { -// type Item = dashmap::mapref::multiple::RefMutMulti<'a, PauliWord, V>; -// type IterMut = -// dashmap::iter::IterMut<'a, PauliWord, V, State, DashMap, V, State>>; - -// fn iter_mut(&'a mut self) -> Self::IterMut { -// DashMap::iter_mut(self) -// } -// } - impl<'a, P, C, Hasher, W> Trace<'a, P> for DashMap where P: Sync + 'a, @@ -123,11 +108,6 @@ where .or_insert_with(|| entry.value().clone()); }); - // // FIXME: clone is not very efficient when T::Coeff is an expression - // self.par_extend( - // dest.par_iter() - // .map(|entry| (entry.key().clone(), entry.value().clone())), - // ); dest.clear(); } } @@ -139,20 +119,6 @@ where H: Default + Clone + BuildHasher + Send + Sync, W: PauliWordTrait + Send + Sync, { - fn map_insert(&mut self, dest: &mut Self, f: F) - where - F: Fn(&W, &mut C) -> Option<(W, C)> + Sync + Send, - { - self.par_iter_mut().for_each(|mut entry| { - let (k, v) = entry.pair_mut(); - if let Some((new_k, new_v)) = f(k, v) { - dest.entry(new_k) - .and_modify(|v| *v += new_v.clone()) - .or_insert(new_v); - } - }) - } - fn map_insert_vec(&mut self, dest: &mut Vec<(W, C)>, f: F) where F: Fn(&W, &mut C) -> Option<(W, C)> + Sync + Send, diff --git a/crates/ppvm-traits/src/map/hashmap.rs b/crates/ppvm-traits/src/map/hashmap.rs index 17dcd1c4..a511d6a1 100644 --- a/crates/ppvm-traits/src/map/hashmap.rs +++ b/crates/ppvm-traits/src/map/hashmap.rs @@ -88,20 +88,6 @@ macro_rules! impl_acmap_iter { Self::iter(self) } } - - // impl<'a, S, V, State> crate::traits::ACMapIterMut<'a, S, V> - // for $name, V, State> - // where - // S: PauliStorage + 'a, - // V: Coefficient + 'a, - // State: Clone + BuildHasher + 'a, - // { - // type Item = (&'a PauliWord, &'a mut V); - // type IterMut = std::collections::hash_map::IterMut<'a, PauliWord, V>; - // fn iter_mut(&'a mut self) -> Self::IterMut { - // Self::iter_mut(self) - // } - // } }; } @@ -154,19 +140,6 @@ macro_rules! impl_acmap_insert { Hasher: Default + Clone + BuildHasher + 'a, W: PauliWordTrait + 'a, { - fn map_insert(&mut self, dest: &mut Self, f: F) - where - F: Fn(&W, &mut C) -> Option<(W, C)> + Sync + Send, - { - for (k, v) in self.iter_mut() { - if let Some((new_k, new_v)) = f(k, v) { - dest.entry(new_k) - .and_modify(|val| *val += new_v.clone()) - .or_insert(new_v); - } - } - } - fn map_insert_vec(&mut self, dest: &mut Vec<(W, C)>, f: F) where F: Fn(&W, &mut C) -> Option<(W, C)> + Sync + Send, diff --git a/crates/ppvm-traits/src/traits/map.rs b/crates/ppvm-traits/src/traits/map.rs index 98c81183..179c504c 100644 --- a/crates/ppvm-traits/src/traits/map.rs +++ b/crates/ppvm-traits/src/traits/map.rs @@ -59,8 +59,9 @@ pub trait ACMapMulAssign { fn mul_assign(&mut self, value: V); } -/// Combined in-place modify + insert pattern used to express branching -/// gates (where one input entry can produce several output entries). +/// In-place modify + insert pattern used to express branching gates +/// (where one input entry can produce zero, one, or several output +/// entries). pub trait ACMapInsert< S: PauliStorage, V: Coefficient, @@ -69,21 +70,17 @@ pub trait ACMapInsert< > { /// Modify each existing entry in place; if `f` returns `Some((k', v'))`, - /// also insert that new entry into `dest`. - fn map_insert(&mut self, dest: &mut Self, f: F) - where - F: Fn(&W, &mut V) -> Option<(W, V)> + Sync + Send; - - /// Like [`map_insert`](Self::map_insert) but appends produced entries - /// into a plain `Vec` (a push per entry, no hashing) instead of a map. - /// The caller merges the buffer into the destination map afterwards, - /// avoiding a second hashmap probe per produced entry. + /// append that new entry into `dest`, a plain `Vec` (a push per entry, + /// no hashing). This is the primary hot-path entry point: the caller + /// merges the buffer into the destination map afterwards, avoiding a + /// second hashmap probe per produced entry. fn map_insert_vec(&mut self, dest: &mut Vec<(W, V)>, f: F) where F: Fn(&W, &mut V) -> Option<(W, V)> + Sync + Send; - /// Same as [`map_insert`](Self::map_insert) but `f` may return a - /// `Vec` of new entries to be inserted into `dest`. + /// Like [`map_insert_vec`](Self::map_insert_vec) but `f` may return a + /// `Vec` of new entries per existing entry, inserted directly into the + /// destination map `dest`. fn map_insert_multiple(&mut self, dest: &mut Self, f: F) where F: Fn(&W, &mut V) -> Option> + Sync + Send; @@ -149,6 +146,33 @@ pub trait ACMapRetain< /// /// You don't normally implement `ACMap` directly: the blanket impl below /// covers any type that implements all the constituent traits. +/// +/// # Map-backend implementor's guide +/// +/// A new backing map (a GPU-resident map, a sharded map, anything beyond +/// the existing `HashMap` / `IndexMap` / `AHashMap` / `DashMap` backends) +/// must implement exactly these traits — nothing more: +/// +/// * [`ACMapBase`] — construction, length, clear. +/// * [`ACMapIter`] — borrowing iteration over `(key, value)`. +/// * [`ACMapAddAssign`] — `+=` insert-or-accumulate. +/// * [`ACMapMulAssign`] — scalar `*=`. +/// * [`ACMapInsert`] — just `map_insert_vec` and `map_insert_multiple` +/// (there is no third variant to implement). +/// * [`ACMapContains`] — membership queries. +/// * [`ACMapScale`] — in-place per-entry transform. +/// * [`ACMapRetain`] — predicate-based entry removal. +/// * [`ACMapConsume`] — drain-and-accumulate merge of two maps. +/// +/// Of these, `map_insert_vec`, `map_insert_multiple`, `scale`, +/// `mul_assign`, `retain`, and `consume` are whole-collection batch entry +/// points: a concurrent or GPU backend can dispatch each of them as one +/// parallel pass over every entry. `add_assign` is the exception — it +/// operates on a single key at a time, so a thread-pool backend typically +/// only parallelizes its batch sibling, `map_add_assign`. See the +/// `DashMap` impl in `crates/ppvm-traits/src/map/dashmap.rs` for a working +/// concurrent precedent: it implements every batch method with rayon +/// `par_iter`. pub trait ACMap< S: PauliStorage, V: Coefficient, diff --git a/crates/ppvm-traits/src/traits/mod.rs b/crates/ppvm-traits/src/traits/mod.rs index 57520129..ffabeef8 100644 --- a/crates/ppvm-traits/src/traits/mod.rs +++ b/crates/ppvm-traits/src/traits/mod.rs @@ -8,7 +8,6 @@ mod hash; mod map; mod measure; mod noise; -mod ptm; mod reset; mod storage; mod strategy; @@ -26,7 +25,7 @@ pub use map::{ pub use measure::{LossyMeasure, Measure}; pub use noise::{ AmplitudeDamping, AsymmetricLossChannel, CorrelatedLossChannel, Depolarizing, Depolarizing2, - LossChannel, PauliError, PauliErrorAll, ResetLossChannel, TwoQubitPauliError, + LossChannel, PauliError, ResetLossChannel, TwoQubitPauliError, }; pub use reset::Reset; pub use storage::PauliStorage; diff --git a/crates/ppvm-traits/src/traits/noise.rs b/crates/ppvm-traits/src/traits/noise.rs index 80544ecb..5d2ba671 100644 --- a/crates/ppvm-traits/src/traits/noise.rs +++ b/crates/ppvm-traits/src/traits/noise.rs @@ -59,14 +59,6 @@ pub trait PauliError { } } -/// Apply the same single-qubit Pauli error channel uniformly to every -/// qubit in the system. -pub trait PauliErrorAll { - /// Apply `Pauli error` with probabilities `p = [p_x, p_y, p_z]` to - /// every qubit. - fn pauli_error_all(&mut self, p: [T::Coeff; 3]); -} - /// Two-qubit Pauli error channel. pub trait TwoQubitPauliError { /// Apply a two-qubit Pauli-error channel to one pair. Probabilities are given in the order: diff --git a/crates/ppvm-traits/src/traits/word_trait.rs b/crates/ppvm-traits/src/traits/word_trait.rs index 1afe5ba6..3d145ed3 100644 --- a/crates/ppvm-traits/src/traits/word_trait.rs +++ b/crates/ppvm-traits/src/traits/word_trait.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::char::Pauli; -use crate::traits::PauliStorage; use std::fmt::Display; use std::hash::Hash; @@ -65,21 +64,6 @@ pub trait PauliWordTrait: Clone + Hash + Eq + PauliIter + From + Display /// [`Pauli`] symbol at `index`. fn get(&self, index: usize) -> Pauli; - /// Build a new word containing only the slots at `indices`. - fn get_multiple(&self, indices: [usize; Q]) -> Self { - let mut result = Self::new(Q); - for (i, &idx) in indices.iter().enumerate() { - result.set(i, self.get(idx)); - } - result - } - - /// Overwrite the slots at `indices` with the slots from `values`. - fn set_multiple(&mut self, indices: [usize; Q], values: &Self); - - /// Build a new word from the contiguous slice `slice` of this one. - fn get_slice(&self, slice: std::ops::Range) -> Self; - /// Quick check: is the slot at `index` equal to `pauli`? fn is(&self, index: usize, pauli: Pauli) -> bool; @@ -98,15 +82,6 @@ pub trait PauliWordTrait: Clone + Hash + Eq + PauliIter + From + Display new } - /// Return a clone with two slots overwritten. - #[inline(always)] - fn set_new_2(&self, index_0: usize, pauli_0: Pauli, index_1: usize, pauli_1: Pauli) -> Self { - let mut new = self.clone(); - new.set(index_0, pauli_0); - new.set(index_1, pauli_1); - new - } - /// Check if this word anticommutes with a single-qubit Pauli at `addr0`, /// where `pauli = (xbit, zbit)`. #[inline(always)]