use std::ops::Range;
use std::time::Duration;
use bytemuck::Pod;
use silvic_core::{Collective, Result, SilvicError};
use crate::link::{Incoming, Link, OP_ALLGATHER};
use crate::reduce::{chunk_range, fold_slots, gathers, MaxF64, Reduce, SumF64, SumI64};
use crate::rendezvous::{form_ring, Listener};
#[derive(Debug)]
pub struct TcpCollective {
rank: usize,
world: usize,
ring: Option<Ring>,
seq: u32,
timeout: Duration,
broken: Option<SilvicError>,
}
impl TcpCollective {
pub fn bind(host: &str) -> Result<Listener> {
Listener::bind(host, host)
}
pub fn bind_advertised(host: &str, node_ip: &str) -> Result<Listener> {
Listener::bind(host, node_ip)
}
pub fn connect(
listener: Listener,
rank: usize,
addresses: &[String],
job_token: u64,
timeout: Duration,
) -> Result<Self> {
let world = addresses.len();
let ring = form_ring(listener, rank, addresses, job_token, timeout)?.map(|(next, prev)| Ring {
link: Link::new(next, prev, rank, world, timeout),
rank,
world,
scratch: Vec::new(),
});
Ok(Self {
rank,
world,
ring,
seq: 0,
timeout,
broken: None,
})
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
if let Some(ring) = &mut self.ring {
ring.link.set_timeout(timeout);
}
}
fn begin(&mut self) -> Result<u32> {
if let Some(error) = &self.broken {
return Err(error.clone());
}
let seq = self.seq;
self.seq = seq.wrapping_add(1);
Ok(seq)
}
fn settle<T>(&mut self, result: Result<T>) -> Result<T> {
if let Err(error) = &result {
tracing::error!(rank = self.rank, %error, "collective failed; shutting the ring down");
if let Some(ring) = &self.ring {
ring.link.shutdown();
}
self.broken = Some(error.clone());
}
result
}
fn allreduce<R: Reduce>(&mut self, data: &mut [R::Elem]) -> Result<()> {
let seq = self.begin()?;
match self.ring.as_mut() {
Some(ring) if !data.is_empty() => {
let result = ring.allreduce::<R>(data, seq);
self.settle(result)
}
_ => Ok(()),
}
}
}
impl Collective for TcpCollective {
fn rank(&self) -> usize {
self.rank
}
fn world_size(&self) -> usize {
self.world
}
fn allreduce_sum_f64(&mut self, data: &mut [f64]) -> Result<()> {
self.allreduce::<SumF64>(data)
}
fn allreduce_sum_i64(&mut self, data: &mut [i64]) -> Result<()> {
self.allreduce::<SumI64>(data)
}
fn allreduce_max_f64(&mut self, data: &mut [f64]) -> Result<()> {
self.allreduce::<MaxF64>(data)
}
fn allgather_bytes(&mut self, local: &[u8]) -> Result<Vec<Vec<u8>>> {
let seq = self.begin()?;
match self.ring.as_mut() {
Some(ring) => {
let result = ring.allgather_bytes(local, seq);
self.settle(result)
}
None => Ok(vec![local.to_vec()]),
}
}
}
#[derive(Debug)]
struct Ring {
link: Link,
rank: usize,
world: usize,
scratch: Vec<u64>,
}
impl Ring {
fn allreduce<R: Reduce>(&mut self, data: &mut [R::Elem], seq: u32) -> Result<()> {
if gathers::<R::Elem>(data.len(), self.world) {
self.gather_and_fold::<R>(data, seq)
} else {
self.reduce_scatter::<R>(data, seq)?;
self.allgather_chunks::<R>(data, seq)
}
}
fn gather_and_fold<R: Reduce>(&mut self, data: &mut [R::Elem], seq: u32) -> Result<()> {
let (rank, world, len) = (self.rank, self.world, data.len());
let slots = typed::<R::Elem>(&mut self.scratch, len * world);
slots[rank * len..][..len].copy_from_slice(data);
for step in 0..world - 1 {
let send = (rank + world - step) % world;
let recv = (rank + world - step - 1) % world;
let (outgoing, incoming) = split(slots, send * len..(send + 1) * len, recv * len..(recv + 1) * len);
let incoming = Incoming::Exact(bytemuck::cast_slice_mut(incoming));
self.link
.exchange(R::OP, seq, bytemuck::cast_slice(outgoing), incoming)?;
}
fold_slots::<R>(slots, data, world);
Ok(())
}
fn reduce_scatter<R: Reduce>(&mut self, data: &mut [R::Elem], seq: u32) -> Result<()> {
let (rank, world, len) = (self.rank, self.world, data.len());
let buffer = typed::<R::Elem>(&mut self.scratch, chunk_range(len, world, 0).len());
for step in 0..world - 1 {
let send = chunk_range(len, world, (rank + world - step) % world);
let recv = chunk_range(len, world, (rank + world - step - 1) % world);
let partial = &mut buffer[..recv.len()];
let incoming = Incoming::Exact(bytemuck::cast_slice_mut(partial));
self.link
.exchange(R::OP, seq, bytemuck::cast_slice(&data[send]), incoming)?;
for (local, &acc) in data[recv].iter_mut().zip(partial.iter()) {
*local = R::combine(acc, *local);
}
}
Ok(())
}
fn allgather_chunks<R: Reduce>(&mut self, data: &mut [R::Elem], seq: u32) -> Result<()> {
let (rank, world, len) = (self.rank, self.world, data.len());
for step in 0..world - 1 {
let send = chunk_range(len, world, (rank + 1 + world - step) % world);
let recv = chunk_range(len, world, (rank + world - step) % world);
let (outgoing, incoming) = split(data, send, recv);
let incoming = Incoming::Exact(bytemuck::cast_slice_mut(incoming));
self.link
.exchange(R::OP, seq, bytemuck::cast_slice(outgoing), incoming)?;
}
Ok(())
}
fn allgather_bytes(&mut self, local: &[u8], seq: u32) -> Result<Vec<Vec<u8>>> {
let (rank, world) = (self.rank, self.world);
let mut blocks = vec![Vec::new(); world];
blocks[rank] = local.to_vec();
for step in 0..world - 1 {
let send = (rank + world - step) % world;
let recv = (rank + world - step - 1) % world;
let (outgoing, incoming) = split(&mut blocks, send..send + 1, recv..recv + 1);
self.link
.exchange(OP_ALLGATHER, seq, &outgoing[0], Incoming::Any(&mut incoming[0]))?;
}
Ok(blocks)
}
}
fn typed<T: Pod>(scratch: &mut Vec<u64>, len: usize) -> &mut [T] {
const { assert!(size_of::<T>() == size_of::<u64>() && align_of::<T>() <= align_of::<u64>()) };
if scratch.len() < len {
scratch.resize(len, 0);
}
bytemuck::cast_slice_mut(&mut scratch[..len])
}
fn split<T>(data: &mut [T], read: Range<usize>, write: Range<usize>) -> (&[T], &mut [T]) {
if read.end <= write.start {
let (head, tail) = data.split_at_mut(write.start);
(&head[read], &mut tail[..write.len()])
} else {
debug_assert!(write.end <= read.start, "ranges {read:?} and {write:?} overlap");
let (head, tail) = data.split_at_mut(read.start);
(&tail[..read.len()], &mut head[write])
}
}