use std::sync::Arc;
use std::time::Instant;
use num_complex::Complex64;
#[cfg(feature = "phase-timing")]
use super::driver::DriverLaps;
use super::driver::{scatter_local, DriverFrame, LOG_TARGET};
use super::runtime::PartitionRuntime;
use super::topology::{PartitionConfig, TopologyError};
use crate::circuit::Circuit;
use crate::engine::backend::{HostPartition, PartitionBackend, PartitionStorage};
use crate::engine::timing::timed;
use crate::engine::trace::PartitionTrace;
use crate::engine::transport::Transport;
use crate::engine::{progress_scope, Direction, PropagateOptions};
use crate::pauli_string::qubit_mask;
use crate::pauli_sum::hash::PartitionRows;
use crate::pauli_sum::PauliSum;
use crate::truncation::TruncationPolicy;
#[cfg(feature = "phase-timing")]
use crate::engine::stats::PartitionPhaseStats;
const GATHER_PARTS: usize = 4;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PartitionRowPolicy {
Seeded(Option<u64>),
Cut(Vec<Vec<usize>>),
SeededExcluding {
seed: Option<u64>,
exclude_x: Vec<usize>,
exclude_z: Vec<usize>,
},
}
impl PartitionRowPolicy {
pub fn rows<const W: usize>(
&self,
num_qubits: usize,
bits: u8,
default_seed: u64,
) -> PartitionRows<W> {
let mask = |qubits: &[usize]| qubit_mask(qubits.iter().copied(), num_qubits);
match self {
Self::Seeded(seed) => {
PartitionRows::from_seed(num_qubits, bits, seed.unwrap_or(default_seed))
}
Self::Cut(blocks) => PartitionRows::cut(num_qubits, blocks),
Self::SeededExcluding { seed, exclude_x, exclude_z } => {
PartitionRows::from_seed_excluding(
num_qubits,
bits,
seed.unwrap_or(default_seed),
&mask(exclude_x),
&mask(exclude_z),
)
}
}
}
}
#[derive(Clone)]
pub struct ScatterOptions<const W: usize> {
pub runtime: Arc<PartitionRuntime>,
pub rows: ScatterRows<W>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ScatterRows<const W: usize> {
Policy(PartitionRowPolicy),
Explicit(PartitionRows<W>),
}
impl<const W: usize> ScatterRows<W> {
pub(crate) fn resolve(self, sum: &PauliSum<W>, bits: u8) -> PartitionRows<W> {
match self {
Self::Policy(policy) => policy.rows(sum.num_qubits(), bits, sum.hash().seed()),
Self::Explicit(rows) => rows,
}
}
}
pub(crate) fn group_bits(size: u32) -> u8 {
assert!(
size.is_power_of_two(),
"a group of {size} ranks cannot be a partitioning: a partition is named by log2(P) \
GF(2) rows, so the rank count must be a power of two",
);
size.trailing_zeros() as u8
}
pub struct DistributedSum<const W: usize, X: Transport, B = HostPartition<W>> {
local: B,
rows: PartitionRows<W>,
runtime: Arc<PartitionRuntime>,
transport: X,
trace: Option<PartitionTrace>,
#[cfg(feature = "phase-timing")]
laps: DriverLaps,
}
impl<const W: usize, X: Transport, B> DistributedSum<W, X, B> {
pub fn rank(&self) -> u32 {
self.transport.rank()
}
pub fn size(&self) -> u32 {
self.transport.size()
}
pub fn transport(&self) -> &X {
&self.transport
}
pub fn rows(&self) -> &PartitionRows<W> {
&self.rows
}
pub fn runtime(&self) -> &Arc<PartitionRuntime> {
&self.runtime
}
pub fn enable_trace(&mut self) {
self.trace.get_or_insert_with(PartitionTrace::default);
}
pub fn take_trace(&mut self) -> Option<PartitionTrace> {
self.trace.as_mut().map(std::mem::take)
}
#[cfg(feature = "cuda")]
pub(crate) fn from_backend(
local: B,
rows: PartitionRows<W>,
runtime: Arc<PartitionRuntime>,
transport: X,
scatter_ns: u64,
) -> Self {
#[cfg(not(feature = "phase-timing"))]
let _ = scatter_ns;
Self {
local,
rows,
runtime,
transport,
trace: None,
#[cfg(feature = "phase-timing")]
laps: DriverLaps::new(scatter_ns),
}
}
#[cfg(feature = "cuda")]
pub(crate) fn backend(&self) -> &B {
&self.local
}
#[cfg(feature = "cuda")]
pub(crate) fn backend_mut(&mut self) -> &mut B {
&mut self.local
}
#[cfg(all(feature = "cuda", feature = "phase-timing"))]
pub(crate) fn take_driver_laps(&mut self) -> (u64, u64, u64) {
self.laps.take()
}
#[cfg(feature = "phase-timing")]
pub(crate) fn lap_gather(&self, ns: u64) {
self.laps.lap_gather(ns);
}
pub(crate) fn propagate_on_backend<T>(
&mut self,
circuit: &Circuit<W>,
policy: &T,
direction: Direction,
options: PropagateOptions,
) where
T: TruncationPolicy<W> + ?Sized,
B: PartitionBackend<W, T>,
{
let runtime = &*self.runtime;
let transport = &self.transport;
let rank = transport.rank() as usize;
let fingerprint =
run_fingerprint(circuit.channels.len(), direction, options, self.rows.num_qubits(), W);
let frame = DriverFrame {
parts: std::slice::from_mut(&mut self.local),
rows: &self.rows,
runtime,
trace: &mut self.trace,
#[cfg(feature = "phase-timing")]
laps: &mut self.laps,
rank: Some(rank),
size: transport.size() as usize,
};
frame.propagate(
circuit,
policy,
direction,
options,
|| transport.check_consistency(fingerprint),
|mut items, layers| {
let work = &mut items[0];
runtime.install(move || layers.run(rank, work, transport));
items
},
);
}
}
impl<const W: usize, X: Transport, B: PartitionStorage<W>> DistributedSum<W, X, B> {
pub fn len(&self) -> usize {
let mut total = [self.local.len() as u64];
self.transport.allreduce_sum_u64(&mut total);
total[0] as usize
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn bits(&self) -> u8 {
self.local.hash().bits()
}
pub fn num_qubits(&self) -> usize {
self.rows.num_qubits()
}
}
impl<const W: usize, X: Transport> DistributedSum<W, X> {
pub fn scatter(
sum: PauliSum<W>,
transport: X,
config: &PartitionConfig,
) -> Result<Self, TopologyError> {
let options = ScatterOptions {
runtime: PartitionRuntime::new(config)?,
rows: ScatterRows::Policy(PartitionRowPolicy::Seeded(None)),
};
Ok(Self::scatter_with(sum, transport, options))
}
pub fn scatter_with(sum: PauliSum<W>, transport: X, options: ScatterOptions<W>) -> Self {
let ScatterOptions { runtime, rows } = options;
let rows = rows.resolve(&sum, group_bits(transport.size()));
assert_eq!(
runtime.num_partitions(),
1,
"a distributed run holds one partition per process, but the runtime has {}; \
domains-per-rank hybrids are not implemented",
runtime.num_partitions(),
);
rows.assert_splits(sum.hash(), sum.num_qubits(), 1usize << group_bits(transport.size()));
let started = Instant::now();
let local = {
let sum = ∑
let rows = &rows;
let transport = &transport;
runtime.install(move || scatter_local(sum, rows, transport.rank(), transport))
};
log::info!(
target: LOG_TARGET,
"scatter: {} terms -> {} held, {} bucket bits, {:.3} s, {}",
sum.len(),
local.len(),
local.hash().bits(),
started.elapsed().as_secs_f64(),
progress_scope(
Some(transport.rank() as usize),
transport.size() as usize,
Some(&runtime.placement_summary()),
),
);
Self {
local: HostPartition::new(local),
rows,
runtime,
transport,
trace: None,
#[cfg(feature = "phase-timing")]
laps: DriverLaps::new(started.elapsed().as_nanos() as u64),
}
}
pub fn propagate<T>(&mut self, circuit: &Circuit<W>, policy: &T, direction: Direction)
where
T: TruncationPolicy<W> + ?Sized,
{
self.propagate_with(circuit, policy, direction, PropagateOptions::default())
}
pub fn propagate_with<T>(
&mut self,
circuit: &Circuit<W>,
policy: &T,
direction: Direction,
options: PropagateOptions,
) where
T: TruncationPolicy<W> + ?Sized,
{
self.propagate_on_backend(circuit, policy, direction, options);
}
pub fn gather(&self) -> Option<PauliSum<W>> {
timed!(let started = Instant::now());
let out = gather_share(&self.local.sum, &self.transport);
timed!(self.lap_gather(started.elapsed().as_nanos() as u64));
out
}
pub fn local(&self) -> &PauliSum<W> {
&self.local.sum
}
#[cfg(feature = "phase-timing")]
pub fn take_stats(&mut self) -> PartitionPhaseStats {
let (scatter_ns, gather_ns, layers) = self.laps.take();
PartitionPhaseStats {
per_partition: vec![self.local.state.layer.take_phase_stats()],
scatter_ns,
gather_ns,
layers,
}
}
pub fn assert_invariants(&self) {
#[cfg(any(test, debug_assertions))]
self.local.sum.assert_invariants();
let held = self.local.sum.partition_rank_of_all(&self.rows);
assert!(
held.is_none() || held == Some(self.rank()),
"rank {} holds keys of partition {held:?}",
self.rank(),
);
}
}
pub(crate) fn gather_share<const W: usize, X: Transport>(
local: &PauliSum<W>,
transport: &X,
) -> Option<PauliSum<W>> {
let lens: Vec<u64> = (0..local.num_buckets()).map(|b| local.bucket_len(b) as u64).collect();
let (x, z, coeff) = local.to_arrays();
let parts: Vec<&[u8]> = vec![
bytemuck::cast_slice(&lens),
bytemuck::cast_slice(x.as_flattened()),
bytemuck::cast_slice(z.as_flattened()),
bytemuck::cast_slice(&coeff),
];
let all = transport.gather_to_root(parts);
drop((x, z, coeff));
all.map(|all| {
let hash = local.hash().clone();
let num_qubits = local.num_qubits();
let parts: Vec<PauliSum<W>> = all
.into_iter()
.enumerate()
.map(|(rank, parts)| decode_rank(&parts, hash.clone(), num_qubits, rank))
.collect();
PauliSum::merge_partitions(parts)
})
}
fn run_fingerprint(
channels: usize,
direction: Direction,
options: PropagateOptions,
num_qubits: usize,
w: usize,
) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let mut mix = |v: u64| {
hash ^= v;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
};
mix(channels as u64);
mix(matches!(direction, Direction::Heisenberg) as u64);
mix(options.target_bucket_len as u64);
mix(options.min_buckets as u64);
mix(num_qubits as u64);
mix(w as u64);
hash
}
fn decode_column<T: bytemuck::Pod>(bytes: &[u8], len: usize, what: &str) -> Vec<T> {
let stride = std::mem::size_of::<T>();
assert_eq!(
bytes.len(),
len * stride,
"gather {what}: expected {} bytes for {len} entries, got {}",
len * stride,
bytes.len(),
);
bytes.chunks_exact(stride).map(bytemuck::pod_read_unaligned).collect()
}
fn decode_rows<const W: usize>(bytes: &[u8], rows: usize, what: &str) -> Vec<[u64; W]> {
let stride = W * std::mem::size_of::<u64>();
assert_eq!(
bytes.len(),
rows * stride,
"gather {what}: expected {} bytes for {rows} rows, got {}",
rows * stride,
bytes.len(),
);
bytes
.chunks_exact(stride)
.map(|row| std::array::from_fn(|i| bytemuck::pod_read_unaligned(&row[i * 8..i * 8 + 8])))
.collect()
}
fn decode_rank<const W: usize>(
parts: &[Vec<u8>],
hash: crate::pauli_sum::hash::Gf2Hash<W>,
num_qubits: usize,
rank: usize,
) -> PauliSum<W> {
assert_eq!(
parts.len(),
GATHER_PARTS,
"gather: rank {rank} sent {} parts, expected {GATHER_PARTS}",
parts.len(),
);
let num_buckets = hash.num_buckets();
let lens: Vec<u64> = decode_column(&parts[0], num_buckets, "bucket lengths");
let lens: Vec<usize> = lens.iter().map(|&l| l as usize).collect();
let terms: usize = lens.iter().sum();
let x = decode_rows::<W>(&parts[1], terms, "x column");
let z = decode_rows::<W>(&parts[2], terms, "z column");
let coeff = decode_column::<Complex64>(&parts[3], terms, "coeff column");
PauliSum::from_bucket_columns(&lens, x, z, coeff, hash, num_qubits)
}
#[cfg(test)]
mod tests;