use crate::channel::Prepare;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Instant;
use super::backend::PartitionBackend;
use super::plan::PartitionPlan;
use super::transport::Transport;
use crate::channel::prepared::MAX_LOCAL_SUPPORT;
use crate::channel::Channel;
use crate::circuit::Circuit;
use crate::collectives::Collectives;
use crate::engine::timing::{lap, stamp, tally, timed};
use crate::engine::trace::{record_layer_row, PartitionLayerRow};
use crate::engine::{Direction, PropagateOptions, LOG_TARGET};
use crate::pauli_sum::hash::{Gf2Hash, PartitionRows};
use crate::truncation::TruncationPolicy;
pub(crate) const BITS_AGREE_EVERY: usize = 16;
fn agrees_bucket_bits(application_index: usize) -> bool {
application_index < BITS_AGREE_EVERY || application_index.is_multiple_of(BITS_AGREE_EVERY)
}
#[cfg(debug_assertions)]
fn assert_bucket_bits_agree(collectives: &impl Collectives, bits: u8, application_index: usize) {
let highest = collectives.allreduce_max_u8(bits);
let lowest = u8::MAX - collectives.allreduce_max_u8(u8::MAX - bits);
assert_eq!(
lowest,
highest,
"partition {}, layer {application_index}: the partitions hold between {lowest} and {highest} \
bucket bits (this one {bits}), where the bucket-count agreement keeps them equal",
collectives.rank(),
);
}
struct CountingCollectives<'a> {
inner: &'a dyn Collectives,
calls: &'a AtomicU32,
}
impl crate::collectives::sealed::Sealed for CountingCollectives<'_> {}
impl Collectives for CountingCollectives<'_> {
fn rank(&self) -> u32 {
self.inner.rank()
}
fn size(&self) -> u32 {
self.inner.size()
}
fn allreduce_max_u8(&self, v: u8) -> u8 {
self.calls.fetch_add(1, Ordering::Relaxed);
self.inner.allreduce_max_u8(v)
}
fn allreduce_sum_u64(&self, buffer: &mut [u64]) {
self.calls.fetch_add(1, Ordering::Relaxed);
self.inner.allreduce_sum_u64(buffer)
}
fn allreduce_sum_f64(&self, buffer: &mut [f64]) {
self.calls.fetch_add(1, Ordering::Relaxed);
self.inner.allreduce_sum_f64(buffer)
}
fn barrier(&self) {
self.calls.fetch_add(1, Ordering::Relaxed);
self.inner.barrier()
}
}
pub(crate) struct PartitionWork<B> {
pub(crate) local: B,
pub(crate) rows: Vec<PartitionLayerRow>,
}
pub(crate) struct PartitionContext<'a, const W: usize> {
pub(crate) rows: &'a PartitionRows<W>,
pub(crate) rank: usize,
pub(crate) size: usize,
pub(crate) tracing: bool,
}
pub(crate) const DECLINED_PREPARATION: &str = "support above MAX_LOCAL_SUPPORT (2) qubits is \
unsupported except for PauliRotation, and a channel must not write outside its declared support";
const _: () = assert!(MAX_LOCAL_SUPPORT == 2, "DECLINED_PREPARATION names the value");
fn prepare_or_panic<const W: usize>(
channel: &dyn Channel<W>,
hash: &Gf2Hash<W>,
adjoint: bool,
rank: usize,
circuit_index: usize,
) -> crate::channel::prepared::Prepared<W> {
channel.prepare(hash, adjoint).unwrap_or_else(|| {
let weight: u32 = channel.support().iter().map(|w| w.count_ones()).sum();
panic!(
"partition {rank}, layer {circuit_index}: channel preparation declined for a \
channel of support {weight}: {DECLINED_PREPARATION}",
)
})
}
pub(crate) fn run_layers<const W: usize, T, X, B>(
circuit: &Circuit<W>,
policy: &T,
direction: Direction,
options: PropagateOptions,
context: PartitionContext<'_, W>,
work: &mut PartitionWork<B>,
transport: &X,
) where
T: TruncationPolicy<W> + ?Sized,
X: Transport,
B: PartitionBackend<W, T>,
{
let PartitionContext { rows, rank, size, tracing } = context;
let num_layers = circuit.channels.len();
let adjoint = matches!(direction, Direction::Heisenberg);
let group_size = transport.size();
let finalizes = policy.finalizes_layer();
let policy_calls = AtomicU32::new(0);
let solo = group_size == 1;
let local = &mut work.local;
let mut plan = PartitionPlan::default();
for application_index in 0..num_layers {
let circuit_index = match direction {
Direction::Forward => application_index,
Direction::Heisenberg => num_layers - 1 - application_index,
};
let channel: &dyn Channel<W> = circuit.channels[circuit_index].as_ref();
let debug_enabled = log::log_enabled!(target: LOG_TARGET, log::Level::Debug);
let timed_layer = tracing || debug_enabled;
let layer_start = timed_layer.then(Instant::now);
let terms_before = local.len();
let bits_before = local.hash().bits();
stamp!(stamp);
tally!(local.stats().layers, 1);
tally!(local.stats().terms_in, terms_before);
let refined_early =
solo && local.refine_unprepared(options.target_bucket_len, options.min_buckets);
if refined_early {
lap!(stamp, local.stats().rebucket_ns);
}
let mut prepared = prepare_or_panic(channel, local.hash(), adjoint, rank, circuit_index);
plan.rebuild(&prepared, rows, rank as u32);
lap!(stamp, local.stats().prepare_ns);
let mut collectives = 0u32;
if !refined_early && (solo || plan.has_remote() || agrees_bucket_bits(application_index)) {
let mut wanted_bits =
local.proposed_bits(&prepared, options.target_bucket_len, options.min_buckets);
if !solo {
wanted_bits = transport.allreduce_max_u8(wanted_bits);
collectives += 1;
}
lap!(stamp, local.stats().collective_ns);
if wanted_bits > local.hash().bits() {
while local.hash().bits() < wanted_bits {
local.refine();
}
lap!(stamp, local.stats().rebucket_ns);
prepared = prepare_or_panic(channel, local.hash(), adjoint, rank, circuit_index);
plan.rebuild(&prepared, rows, rank as u32);
lap!(stamp, local.stats().prepare_ns);
}
}
#[cfg(debug_assertions)]
if !solo {
assert_bucket_bits_agree(transport, local.hash().bits(), application_index);
}
let counts = local.apply_layer(&prepared, &plan, rows, policy, transport);
timed!(stamp.rearm());
if finalizes {
policy_calls.store(0, Ordering::Relaxed);
let counting = CountingCollectives { inner: transport, calls: &policy_calls };
local.finalize_layer(policy, &counting);
collectives += policy_calls.load(Ordering::Relaxed);
}
lap!(stamp, local.stats().finalize_ns);
tally!(local.stats().terms_out, local.len());
let remote_deltas = counts.remote_deltas;
let rows_received = counts.rows_received;
let elapsed = layer_start.map(|start| start.elapsed());
if tracing {
let mut row = PartitionLayerRow {
bits: local.hash().bits(),
remote_deltas: counts.remote_deltas as u32,
collectives,
circuit_index: circuit_index as u32,
application_index: application_index as u32,
gate_name: channel.debug_name(),
terms_in: terms_before,
terms_out: local.len(),
rows_sent: counts.rows_sent,
bytes_sent: counts.bytes_sent,
rows_received: counts.rows_received,
nanos: elapsed.unwrap_or_default().as_nanos() as u64,
refine_passes: u32::from(local.hash().bits() - bits_before),
receive_chunks: counts.receive_chunks,
device: None,
};
local.trace_layer(&mut row);
record_layer_row(&mut work.rows, row);
}
if debug_enabled {
let Some(elapsed) = elapsed else { continue };
log::debug!(
target: LOG_TARGET,
"layer {}/{} [{}]: {} -> {} terms, {:.1} ms (partition {}/{}, {} remote deltas, \
{} rows in)",
application_index + 1,
num_layers,
channel.debug_name(),
terms_before,
local.len(),
elapsed.as_secs_f64() * 1e3,
rank,
size,
remote_deltas,
rows_received,
);
}
}
}
#[cfg(test)]
mod tests;