use crate::engine::gpu::columns::grow;
use crate::engine::gpu::error::GpuError;
use crate::engine::gpu::layer::{timed_transfer, DeviceStats, LayerScratch, Transfer};
use crate::engine::gpu::sum::{launch_fingerprint, GpuSum};
use crate::engine::gpu::wire::{ScheduledOp, Skeleton, WireColumn, WireGroup, WireOpKind};
use crate::engine::timing::{tally, timed};
use crate::engine::transport::ChunkMap;
use super::{received_row_bytes, DeviceExport, PendingReceive, MAX_RECEIVED_SEGMENT};
pub(super) fn stage_receive<const W: usize>(
sum: &GpuSum<W>,
export: &mut DeviceExport<W>,
blocks: &[&Skeleton],
cap_rows: usize,
stats: &mut DeviceStats,
) -> Result<u8, GpuError> {
let buckets = sum.hash.num_buckets();
lay_out(
export,
buckets,
blocks.iter().map(|block| {
(block.header.num_buckets, &block.offsets[..], block.header.rows as usize)
}),
);
if export.received_max_segment > MAX_RECEIVED_SEGMENT {
return Err(GpuError::Unsupported(
"a fused-layer block or a received segment exceeds the record cap at the agreed bucket count",
));
}
#[cfg(any(test, feature = "test-utils"))]
if std::mem::take(&mut export.fail_receive_growth) {
return Err(GpuError::OutOfMemory {
device: Some(sum.device()),
bytes: Some((export.received_rows * received_row_bytes::<W>()) as u64),
});
}
let (log2, most) = receive_chunking(&export.offsets_host, buckets, cap_rows);
size_receive(sum, export, most, stats)?;
Ok(log2)
}
pub(super) fn wire_ready<const W: usize>(export: &DeviceExport<W>) -> Result<(), GpuError> {
match &export.wire {
None => Err(GpuError::Unsupported("a remote layer on a partition without a device wire")),
Some(wire) if !wire.is_healthy() => {
Err(GpuError::Wire("an exchange on a failed or aborted device wire"))
}
Some(_) => Ok(()),
}
}
pub(super) fn discard_received<const W: usize>(
export: &mut DeviceExport<W>,
) -> Result<u64, GpuError> {
let n = export.offsets_host.len();
export.stream.memset_zeros(&mut export.received_offsets.slice_mut(0..n))?;
export.received_rows = 0;
export.received_max_segment = 0;
Ok(0)
}
pub(super) fn receive_cap_rows<const W: usize>(bytes: usize) -> usize {
(bytes / received_row_bytes::<W>()).max(1)
}
fn lay_out<'a, const W: usize>(
export: &mut DeviceExport<W>,
buckets: usize,
blocks: impl Iterator<Item = (u32, &'a [u32], usize)>,
) {
let mut total = 0usize;
let mut max_segment = 0usize;
export.offsets_host.clear();
for (num_buckets, offsets, rows) in blocks {
assert_eq!(
num_buckets as usize, buckets,
"a partner sent a block indexed by {num_buckets} buckets where this partition has {buckets}"
);
export.offsets_host.extend_from_slice(&offsets[..buckets + 1]);
max_segment = offsets[..buckets + 1]
.windows(2)
.map(|w| (w[1] - w[0]) as usize)
.max()
.unwrap_or(0)
.max(max_segment);
total += rows;
}
export.received_max_segment = max_segment;
export.received_rows = total;
}
pub(super) fn rows_between(offsets: &[u32], buckets: usize, lo: usize, hi: usize) -> usize {
offsets.chunks_exact(buckets + 1).map(|block| (block[hi] - block[lo]) as usize).sum()
}
pub(super) fn chunk_max(offsets: &[u32], buckets: usize, log2: u8) -> usize {
let step = buckets >> log2;
(0..1usize << log2)
.map(|chunk| rows_between(offsets, buckets, chunk * step, (chunk + 1) * step))
.max()
.unwrap_or(0)
}
pub(super) fn receive_chunking(offsets: &[u32], buckets: usize, cap_rows: usize) -> (u8, usize) {
let mut log2 = 0u8;
loop {
let most = chunk_max(offsets, buckets, log2);
if most <= cap_rows || 1usize << log2 >= buckets {
return (log2, most);
}
log2 += 1;
}
}
pub(super) fn position_chunks(buckets: usize, log2: u8) -> ChunkMap {
let map = ChunkMap::new(buckets, 0, 1 << log2);
debug_assert_eq!(map.chunks(), 1 << log2);
map
}
fn size_receive<const W: usize>(
sum: &GpuSum<W>,
export: &mut DeviceExport<W>,
rows: usize,
stats: &mut DeviceStats,
) -> Result<(), GpuError> {
let stream = &sum.stream;
let ordinal = sum.device();
let DeviceExport {
received_offsets,
received_x,
received_z,
received_coefficient,
received_fingerprint,
offsets_host,
..
} = export;
grow(stream, received_offsets, offsets_host.len().max(2), ordinal)?;
grow(stream, received_x, rows.max(1) * W, ordinal)?;
grow(stream, received_z, rows.max(1) * W, ordinal)?;
grow(stream, received_coefficient, 2 * rows.max(1), ordinal)?;
grow(stream, received_fingerprint, rows.max(1), ordinal)?;
timed_transfer(stream, stats, Transfer::H2d, || {
stream.memcpy_htod(
&offsets_host[..],
&mut received_offsets.slice_mut(0..offsets_host.len()),
)?;
Ok(())
})
}
pub(super) fn chunk_layout(
offsets: &[u32],
buckets: usize,
lo: usize,
hi: usize,
base: &mut Vec<u32>,
) -> usize {
base.clear();
base.resize(16, 0);
let mut at = 0usize;
for (k, block) in offsets.chunks_exact(buckets + 1).enumerate() {
base[k] = (at as u32).wrapping_sub(block[lo]);
at += (block[hi] - block[lo]) as usize;
}
at
}
pub(crate) fn receive_chunk<const W: usize>(
sum: &GpuSum<W>,
scratch: &mut LayerScratch<W>,
chunk: usize,
) -> Result<usize, GpuError> {
timed!(let t = std::time::Instant::now());
let export = &mut scratch.export;
let mut pending = export.pending.take().expect("a pending receive");
debug_assert_eq!(pending.next, chunk);
let r = move_chunk(sum, export, &mut pending, chunk, true, &mut scratch.stats);
export.pending = Some(pending);
tally!(scratch.stats.exchange_ns, t.elapsed().as_nanos());
#[cfg(any(test, feature = "test-utils"))]
if r.is_ok() && scratch.export.fail_after_chunk == Some(chunk) {
scratch.export.fail_after_chunk = None;
return Err(GpuError::OutOfMemory { device: Some(sum.device()), bytes: None });
}
r
}
fn move_chunk<const W: usize>(
sum: &GpuSum<W>,
export: &mut DeviceExport<W>,
pending: &mut PendingReceive<W>,
chunk: usize,
keep: bool,
stats: &mut DeviceStats,
) -> Result<usize, GpuError> {
let buckets = sum.hash.num_buckets();
let stream = &sum.stream;
let (lo, hi) = (pending.map.bound(chunk) as usize, pending.map.bound(chunk + 1) as usize);
let n = chunk_layout(&export.offsets_host, buckets, lo, hi, &mut export.base_host);
let wire = export
.wire
.clone()
.ok_or(GpuError::Unsupported("a remote layer on a partition without a device wire"))?;
let DeviceExport {
received_base,
received_x,
received_z,
received_coefficient,
received_fingerprint,
base_host,
..
} = export;
let PendingReceive { send, own, ops, .. } = &*pending;
let (sends, receives) = ops.split_at(ops.partition_point(|op| op.kind == WireOpKind::Send));
let of_chunk = |ops: &'_ [ScheduledOp]| -> std::ops::Range<usize> {
ops.partition_point(|op| op.chunk < chunk)..ops.partition_point(|op| op.chunk <= chunk)
};
let (sends, receives) = (&sends[of_chunk(sends)], &receives[of_chunk(receives)]);
let mut group = WireGroup::new();
for op in sends {
let (q, j) = own[op.delta];
let block = &send[q].as_ref().expect("a payload for every partner").blocks[j];
let (r0, r1) = op.rows;
let elements = op.column.elements_per_row::<W>();
match op.column {
WireColumn::X => {
group.send(block.x.slice(r0 * elements..r1 * elements), op.peer, stream)
}
WireColumn::Z => {
group.send(block.z.slice(r0 * elements..r1 * elements), op.peer, stream)
}
WireColumn::Coefficient => {
group.send(block.coefficient.slice(r0 * elements..r1 * elements), op.peer, stream)
}
}
}
let receive_parts = |column: WireColumn| -> Vec<(usize, u32)> {
let elements = column.elements_per_row::<W>();
receives
.iter()
.filter(|op| op.column == column)
.map(|op| ((op.rows.1 - op.rows.0) * elements, op.peer))
.collect()
};
group.receive_parts(received_x.slice_mut(0..n * W), &receive_parts(WireColumn::X), stream);
group.receive_parts(received_z.slice_mut(0..n * W), &receive_parts(WireColumn::Z), stream);
group.receive_parts(
received_coefficient.slice_mut(0..2 * n),
&receive_parts(WireColumn::Coefficient),
stream,
);
if let Err(error) = group.post(&*wire).and_then(|()| wire.wait(stream)) {
pending.failed = true;
return Err(error);
}
pending.next = chunk + 1;
if !keep {
return Ok(n);
}
launch_fingerprint(
stream,
&sum.kernels,
&sum.fingerprint_rows,
(&*received_x, &*received_z),
received_fingerprint,
n,
)?;
timed_transfer(stream, stats, Transfer::H2d, || {
stream.memcpy_htod(&base_host[..], received_base)?;
Ok(())
})?;
Ok(n)
}
pub(crate) fn finish_receive<const W: usize>(
sum: &GpuSum<W>,
scratch: &mut LayerScratch<W>,
) -> Result<(), GpuError> {
let export = &mut scratch.export;
let Some(mut pending) = export.pending.take() else {
return Ok(());
};
let mut drained = Ok(());
for chunk in pending.next..pending.map.chunks() {
if pending.failed {
break;
}
if let Err(error) = move_chunk(sum, export, &mut pending, chunk, false, &mut scratch.stats)
{
drained = Err(error);
break;
}
}
let synced = sum.stream.synchronize().map_err(GpuError::from);
export.sends.extend(pending.send.into_iter().flatten());
drained.and(synced)
}