use super::exchange_block::{ExchangeBlock, PartnerPayload};
use super::export::{export_layer, ExportScratch};
use super::PLACEMENT_LOG_TARGET;
use crate::channel::prepared::Prepared;
use crate::engine::bucketed::{
apply_layer_bucketed, apply_layer_bucketed_with, ExtraRows, LayerKnobs, LayerScratch,
};
use crate::engine::coset::{CosetOrder, Gf2Span};
use crate::engine::knob::{parse_positive, Knob};
use crate::engine::merge::rest_rows_per_key;
use crate::engine::plan::PartitionPlan;
use crate::engine::timing::{lap, stamp, tally, timed};
use crate::engine::transport::{ChunkMap, ChunkWait, Transport};
use crate::pauli_sum::hash::PartitionRows;
use crate::pauli_sum::storage::{BucketColumns, PauliSum};
use crate::truncation::TruncationPolicy;
const DEFAULT_EXCHANGE_CHUNKS: usize = 8;
static EXCHANGE_CHUNKS: Knob<usize> =
Knob::new("PAULISTRINGS_EXCHANGE_CHUNKS", PLACEMENT_LOG_TARGET, parse_positive, || {
DEFAULT_EXCHANGE_CHUNKS
});
struct ReceivedRows<'a, const W: usize> {
blocks: Vec<Option<&'a ExchangeBlock<W>>>,
order: &'a CosetOrder,
map: &'a ChunkMap,
wait: &'a dyn ChunkWait,
#[cfg(feature = "phase-timing")]
append_ns: std::sync::atomic::AtomicU64,
#[cfg(feature = "phase-timing")]
chunk_wait_ns: std::sync::atomic::AtomicU64,
}
impl<'a, const W: usize> ReceivedRows<'a, W> {
fn new(
plan: &PartitionPlan,
received: &'a [Option<PartnerPayload<W>>],
order: &'a CosetOrder,
map: &'a ChunkMap,
wait: &'a dyn ChunkWait,
) -> Self {
let mut blocks = Vec::with_capacity(plan.remote.len());
for (k, remote) in plan.remote.iter().enumerate() {
let block_index = plan.received_block_index(k);
let block = received[remote.partner as usize]
.as_ref()
.and_then(|payload| payload.blocks.get(block_index));
debug_assert!(
block.is_some(),
"partition {} sent no block for remote delta {} (entry {}) — the partitions \
disagree about the layer's delta set",
remote.partner,
block_index,
remote.entry,
);
debug_assert!(
block.is_none_or(|block| block.header.entry == remote.entry as u32),
"partition {} sent entry {:?} where entry {} was expected",
remote.partner,
block.map(|block| block.header.entry),
remote.entry,
);
blocks.push(block);
}
Self {
blocks,
order,
map,
wait,
#[cfg(feature = "phase-timing")]
append_ns: std::sync::atomic::AtomicU64::new(0),
#[cfg(feature = "phase-timing")]
chunk_wait_ns: std::sync::atomic::AtomicU64::new(0),
}
}
}
impl<const W: usize> ExtraRows<W> for ReceivedRows<'_, W> {
const NEEDS_BETA: bool = true;
fn count(&self, beta: u32) -> usize {
let position = self.order.position_of(beta);
let mut rows = 0usize;
for block in self.blocks.iter().flatten() {
rows += block.segment(position).2.len();
}
rows
}
fn append_into(&self, beta: u32, rest: &mut BucketColumns<W>) {
timed!(let start = std::time::Instant::now());
let position = self.order.position_of(beta);
self.wait.wait_chunk(self.map.chunk_of_position(position));
timed!(self
.chunk_wait_ns
.fetch_add(start.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed,));
for block in &self.blocks {
let Some(block) = block else { continue };
let (segment_x, segment_z, segment_coeff) = block.segment(position);
rest.x.extend_from_slice(segment_x);
rest.z.extend_from_slice(segment_z);
rest.coeff.extend_from_slice(segment_coeff);
}
timed!(self
.append_ns
.fetch_add(start.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed,));
}
}
#[derive(Debug, Default)]
pub(crate) struct PartitionState<const W: usize> {
pub layer: LayerScratch<W>,
pub export: ExportScratch<W>,
pub chunks: ChunkMap,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct LayerExchangeCounts {
pub remote_deltas: usize,
pub rows_sent: Vec<u64>,
pub bytes_sent: Vec<u64>,
pub rows_received: u64,
pub receive_chunks: u32,
}
impl LayerExchangeCounts {
pub(crate) fn none(size: u32) -> Self {
Self {
remote_deltas: 0,
rows_sent: vec![0; size as usize],
bytes_sent: vec![0; size as usize],
rows_received: 0,
receive_chunks: 0,
}
}
}
pub(in crate::engine) fn apply_layer_partitioned_with_plan<const W: usize, T, X>(
local: &mut PauliSum<W>,
prepared: &Prepared<W>,
plan: &PartitionPlan,
#[cfg_attr(not(debug_assertions), allow(unused_variables))] rows: &PartitionRows<W>,
policy: &T,
state: &mut PartitionState<W>,
transport: &X,
) -> LayerExchangeCounts
where
T: TruncationPolicy<W> + ?Sized,
X: Transport,
{
let size = transport.size();
if !plan.has_remote() {
apply_layer_bucketed(local, prepared, policy, &mut state.layer);
return LayerExchangeCounts::default();
}
stamp!(stamp);
let span = Gf2Span::new(&plan.local_bucket_deltas, local.hash().bits());
let mut order = std::mem::take(&mut state.layer.order);
order.rebuild(&span, true);
state.chunks = ChunkMap::of_order(&order, *EXCHANGE_CHUNKS.get());
let (send, mut counts) = export_layer(local, prepared, plan, size, &order, &mut state.export);
lap!(stamp, state.layer.stats.export_ns);
tally!(state.layer.stats.rows_exported, counts.rows_sent.iter().sum::<u64>());
#[cfg(debug_assertions)]
{
super::export::debug_assert_exported_partitions(&send, rows);
for remote in &plan.remote {
debug_assert!(
send[remote.partner as usize].is_some(),
"no payload for partner {}, which the plan names",
remote.partner,
);
}
}
let retained;
let local_prepared: &Prepared<W> = match prepared {
Prepared::Local(ptm) => {
retained = Prepared::Local(ptm.retain_entries(&plan.local_entries));
&retained
}
Prepared::Rotation(_) => prepared,
};
let knobs = LayerKnobs {
bucket_deltas: Some(&plan.local_bucket_deltas),
rest_streams: Some(plan.rest_streams_total),
rows_per_key: match prepared {
Prepared::Local(ptm) => Some(rest_rows_per_key(ptm)),
Prepared::Rotation(_) => None,
},
generator_local: match prepared {
Prepared::Rotation(_) => plan.local_entries[1],
Prepared::Local(_) => true,
},
order: Some(&order),
};
let PartitionState { layer: layer_scratch, export: export_scratch, chunks: map } = state;
timed!(let body_ns = std::cell::Cell::new(0u64));
timed!(let exchange_start = std::time::Instant::now());
let (received, rows_received) =
transport.exchange_layer(send, &mut export_scratch.pool, map, |received, wait| {
timed!(let body_start = std::time::Instant::now());
#[cfg(debug_assertions)]
for block in received.iter().flatten().flat_map(|payload| &payload.blocks) {
debug_assert_eq!(
block.num_buckets() as usize,
local.num_buckets(),
"a partner sent a block indexed by {} buckets where this partition has {}: \
the partitions disagree about the bucket count",
block.num_buckets(),
local.num_buckets(),
);
}
let rows_received: u64 = received
.iter()
.flatten()
.flat_map(|payload| &payload.blocks)
.map(|block| block.rows() as u64)
.sum();
let received_rows = ReceivedRows::new(plan, received, &order, map, wait);
apply_layer_bucketed_with(
local,
local_prepared,
policy,
layer_scratch,
&received_rows,
knobs,
);
timed! {
layer_scratch.stats.recv_rows += rows_received;
layer_scratch.stats.append_ns += received_rows
.append_ns
.load(std::sync::atomic::Ordering::Relaxed);
layer_scratch.stats.chunk_wait_ns += received_rows
.chunk_wait_ns
.load(std::sync::atomic::Ordering::Relaxed);
body_ns.set(body_start.elapsed().as_nanos() as u64);
}
rows_received
});
timed! {
layer_scratch.stats.exchange_ns +=
exchange_start.elapsed().as_nanos() as u64 - body_ns.get();
stamp.rearm();
}
export_scratch.pool.extend(received.into_iter().flatten());
layer_scratch.order = order;
counts.remote_deltas = plan.remote.len();
counts.rows_received = rows_received;
counts.receive_chunks = map.chunks() as u32;
counts
}
#[cfg(test)]
mod tests;