mod peer;
use std::time::Duration;
use cudarc::driver::{CudaStream, CudaView, CudaViewMut, SyncOnDrop};
use super::error::GpuError;
use super::payload::DeviceBlock;
use super::GPU_LOG_TARGET;
use crate::engine::knob::Knob;
use crate::engine::partitioned::exchange_block::{chunk_rows_of, BlockHeader};
use crate::engine::transport::{ChunkMap, Payload};
#[cfg(test)]
pub(crate) use peer::PeerTally;
pub(crate) use peer::PeerWire;
#[cfg(any(test, feature = "test-utils"))]
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WireFault {
Post,
Wait,
}
const DEFAULT_WIRE_TIMEOUT: Duration = Duration::from_secs(300);
static WIRE_TIMEOUT: Knob<Duration> =
Knob::new("PAULISTRINGS_WIRE_TIMEOUT_S", GPU_LOG_TARGET, parse_timeout, || {
DEFAULT_WIRE_TIMEOUT
});
pub(crate) fn wire_timeout() -> Duration {
*WIRE_TIMEOUT.get()
}
fn parse_timeout(text: &str) -> Option<Duration> {
text.parse::<f64>().ok().filter(|s| s.is_finite() && *s > 0.0).map(Duration::from_secs_f64)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WireOpKind {
Send,
Receive,
}
#[derive(Clone, Copy)]
pub(crate) struct WireOp<'a> {
kind: WireOpKind,
peer: u32,
address: u64,
bytes: usize,
stream: &'a CudaStream,
}
impl<'a> WireOp<'a> {
pub(crate) fn kind(&self) -> WireOpKind {
self.kind
}
pub(crate) fn peer(&self) -> u32 {
self.peer
}
pub(crate) fn address(&self) -> u64 {
self.address
}
pub(crate) fn bytes(&self) -> usize {
self.bytes
}
pub(crate) fn stream(&self) -> &'a CudaStream {
self.stream
}
}
#[derive(Default)]
pub(crate) struct WireGroup<'a> {
ops: Vec<WireOp<'a>>,
guards: Vec<SyncOnDrop<'a>>,
}
impl<'a> WireGroup<'a> {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn send<T>(&mut self, source: CudaView<'a, T>, peer: u32, stream: &'a CudaStream) {
let bytes = source.len() * std::mem::size_of::<T>();
let (address, guard) = source.view_ptr(stream);
self.push(WireOpKind::Send, peer, address, bytes, stream, guard);
}
#[cfg(test)]
pub(crate) fn receive<T>(
&mut self,
destination: CudaViewMut<'a, T>,
peer: u32,
stream: &'a CudaStream,
) {
let bytes = destination.len() * std::mem::size_of::<T>();
let (address, guard) = destination.view_ptr(stream);
self.push(WireOpKind::Receive, peer, address, bytes, stream, guard);
}
pub(crate) fn receive_parts<T>(
&mut self,
destination: CudaViewMut<'a, T>,
parts: &[(usize, u32)],
stream: &'a CudaStream,
) {
let total: usize = parts.iter().map(|&(len, _)| len).sum();
assert!(
total <= destination.len(),
"receive_parts: {total} elements into a view of {}",
destination.len()
);
let elem_bytes = std::mem::size_of::<T>();
let (base, guard) = destination.view_ptr(stream);
let mut offset = 0usize;
for &(len, peer) in parts {
self.ops.push(WireOp {
kind: WireOpKind::Receive,
peer,
address: base + (offset * elem_bytes) as u64,
bytes: len * elem_bytes,
stream,
});
offset += len;
}
self.guards.push(guard);
}
fn push(
&mut self,
kind: WireOpKind,
peer: u32,
address: u64,
bytes: usize,
stream: &'a CudaStream,
guard: SyncOnDrop<'a>,
) {
self.ops.push(WireOp { kind, peer, address, bytes, stream });
self.guards.push(guard);
}
#[cfg(test)]
pub(crate) fn ops(&self) -> &[WireOp<'a>] {
&self.ops
}
pub(crate) fn post(self, wire: &dyn DeviceWire) -> Result<(), GpuError> {
self.post_with(|ops| wire.post(ops))
}
pub(crate) fn post_with(
self,
post: impl FnOnce(&[WireOp<'a>]) -> Result<(), GpuError>,
) -> Result<(), GpuError> {
let posted = post(&self.ops);
drop(self.guards);
posted
}
}
pub(crate) trait DeviceWire: Send + Sync {
#[cfg(test)]
fn rank(&self) -> u32;
#[cfg(test)]
fn size(&self) -> u32;
fn post(&self, ops: &[WireOp<'_>]) -> Result<(), GpuError>;
fn wait(&self, stream: &CudaStream) -> Result<(), GpuError>;
fn abort(&self);
fn is_healthy(&self) -> bool;
#[cfg(any(test, feature = "test-utils"))]
fn inject(&self, fault: WireFault);
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct Skeleton {
pub(crate) header: BlockHeader,
pub(crate) offsets: Vec<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct BlockSkeletons<const W: usize> {
pub(crate) blocks: Vec<Skeleton>,
}
impl<const W: usize> BlockSkeletons<W> {
pub(crate) fn fill_from(&mut self, blocks: &[DeviceBlock<W>]) {
self.blocks.truncate(blocks.len());
self.blocks.resize_with(blocks.len(), Skeleton::default);
for (skeleton, block) in self.blocks.iter_mut().zip(blocks) {
skeleton.header = block.header;
skeleton.offsets.clear();
skeleton.offsets.extend_from_slice(&block.offsets);
}
}
pub(crate) fn push_empty(&mut self, entry: u32, b: usize) {
self.blocks.push(Skeleton {
header: BlockHeader { num_buckets: b as u32, rows: 0, width: W as u32, entry },
offsets: vec![0; b + 1],
});
}
}
impl<const W: usize> Payload for BlockSkeletons<W> {
fn byte_parts(&self) -> Vec<&[u8]> {
let mut parts = Vec::with_capacity(2 * self.blocks.len());
for skeleton in &self.blocks {
parts.push(bytemuck::bytes_of(&skeleton.header));
parts.push(bytemuck::cast_slice(&skeleton.offsets));
}
parts
}
fn receive_into(&mut self, lens: &[usize]) -> Vec<&mut [u8]> {
assert_eq!(
lens.len() % 2,
0,
"block skeletons: {} parts is not a whole number of two-part blocks",
lens.len()
);
let num_blocks = lens.len() / 2;
self.blocks.truncate(num_blocks);
self.blocks.resize_with(num_blocks, Skeleton::default);
let mut parts = Vec::with_capacity(lens.len());
for (skeleton, lens) in self.blocks.iter_mut().zip(lens.chunks_exact(2)) {
assert_eq!(
lens[0],
std::mem::size_of::<BlockHeader>(),
"block skeletons: a header of {} bytes",
lens[0]
);
assert_eq!(
lens[1] % 4,
0,
"block skeletons: {} offset bytes is not a whole number of u32",
lens[1]
);
skeleton.offsets.clear();
skeleton.offsets.resize(lens[1] / 4, 0);
let Skeleton { header, offsets } = skeleton;
parts.push(bytemuck::bytes_of_mut(header));
parts.push(bytemuck::cast_slice_mut(&mut offsets[..]));
}
parts
}
fn finish_receive(&mut self) {
for skeleton in &self.blocks {
let header = &skeleton.header;
assert_eq!(
header.width as usize, W,
"block skeletons: a block encoded at W={} decoded at W={W}",
header.width
);
assert_eq!(
skeleton.offsets.len(),
header.num_buckets as usize + 1,
"block skeletons: a block of {} buckets arrived with {} offsets",
header.num_buckets,
skeleton.offsets.len()
);
assert!(
skeleton.offsets[0] == 0 && skeleton.offsets.windows(2).all(|w| w[0] <= w[1]),
"block skeletons: offsets that are not a CSR index"
);
assert_eq!(
skeleton.offsets[header.num_buckets as usize], header.rows,
"block skeletons: offsets end at {}, the header names {} rows",
skeleton.offsets[header.num_buckets as usize], header.rows
);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WireColumn {
X,
Z,
Coefficient,
}
impl WireColumn {
const ALL: [WireColumn; 3] = [WireColumn::X, WireColumn::Z, WireColumn::Coefficient];
pub(crate) fn elements_per_row<const W: usize>(self) -> usize {
match self {
WireColumn::X | WireColumn::Z => W,
WireColumn::Coefficient => 2,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ScheduledOp {
pub(crate) kind: WireOpKind,
pub(crate) peer: u32,
pub(crate) chunk: usize,
pub(crate) column: WireColumn,
pub(crate) delta: usize,
pub(crate) rows: (usize, usize),
}
pub(crate) fn schedule(
partners: &[u32],
own: &[&[u32]],
received: &[&[u32]],
map: &ChunkMap,
) -> Vec<ScheduledOp> {
assert_eq!(partners.len(), own.len());
assert_eq!(partners.len(), received.len());
let own_rows: Vec<Vec<usize>> = own.iter().map(|offsets| chunk_rows_of(offsets, map)).collect();
let received_rows: Vec<Vec<usize>> =
received.iter().map(|offsets| chunk_rows_of(offsets, map)).collect();
let mut peers: Vec<u32> = partners.to_vec();
peers.sort_unstable();
peers.dedup();
let mut ops = Vec::new();
for chunk in 0..map.chunks() {
for &peer in &peers {
for column in WireColumn::ALL {
for (k, _) in partners.iter().enumerate().filter(|&(_, &partner)| partner == peer) {
let rows = (own_rows[k][chunk], own_rows[k][chunk + 1]);
if rows.1 > rows.0 {
ops.push(ScheduledOp {
kind: WireOpKind::Send,
peer,
chunk,
column,
delta: k,
rows,
});
}
}
}
}
}
for chunk in 0..map.chunks() {
for column in WireColumn::ALL {
for (k, &peer) in partners.iter().enumerate() {
let rows = (received_rows[k][chunk], received_rows[k][chunk + 1]);
if rows.1 > rows.0 {
ops.push(ScheduledOp {
kind: WireOpKind::Receive,
peer,
chunk,
column,
delta: k,
rows,
});
}
}
}
}
ops
}
#[cfg(test)]
mod tests;