use std::panic::Location;
use std::sync::{Arc, Mutex, PoisonError, RwLock};
use crate::array::Data;
use crate::buffer::Buffer;
use crate::kernels::Operand;
use crate::primitive::Primitive;
use crate::{Array, DType, Error, IntoShape, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AllReduce {
Sum,
Max,
Min,
}
pub trait Communicator: Send + Sync {
fn rank(&self) -> usize;
fn size(&self) -> usize;
fn all_reduce(&self, op: AllReduce, dtype: DType, data: &mut [u8]) -> Result<()>;
fn all_gather(&self, input: &[u8], output: &mut [u8]) -> Result<()>;
fn send(&self, dst: usize, data: &[u8]) -> Result<()>;
fn recv(&self, src: usize, data: &mut [u8]) -> Result<()>;
fn split(&self, color: i32, key: i32) -> Result<Arc<dyn Communicator>> {
let _ = (color, key);
Err(Error::InvalidArgument(
"distributed: this backend does not split groups (as MLX's ring)".into(),
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CollectiveOp {
Reduce(AllReduce),
Gather,
Send {
dst: usize,
},
Recv {
src: usize,
shape: Vec<usize>,
dtype: DType,
},
}
impl CollectiveOp {
pub fn name(&self) -> &'static str {
match self {
Self::Reduce(AllReduce::Sum) => "all_sum",
Self::Reduce(AllReduce::Max) => "all_max",
Self::Reduce(AllReduce::Min) => "all_min",
Self::Gather => "all_gather",
Self::Send { .. } => "send",
Self::Recv { .. } => "recv",
}
}
}
struct Registered {
comm: Arc<dyn Communicator>,
order: Mutex<()>,
}
static GROUPS: Mutex<Vec<Arc<Registered>>> = Mutex::new(Vec::new());
static DEFAULT: RwLock<Option<Group>> = RwLock::new(None);
fn registered(id: usize) -> Result<Arc<Registered>> {
let groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
id.checked_sub(1)
.and_then(|i| groups.get(i).cloned())
.ok_or_else(|| Error::InvalidArgument(format!("distributed: no group {id}")))
}
#[derive(Clone)]
pub struct Group {
id: usize,
rank: usize,
size: usize,
}
impl std::fmt::Debug for Group {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Group(rank {} of {})", self.rank, self.size)
}
}
impl Group {
pub fn single() -> Self {
Self {
id: 0,
rank: 0,
size: 1,
}
}
pub fn new(comm: Arc<dyn Communicator>) -> Self {
let (rank, size) = (comm.rank(), comm.size());
if size <= 1 {
return Self::single();
}
let mut groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
groups.push(Arc::new(Registered {
comm,
order: Mutex::new(()),
}));
Self {
id: groups.len(),
rank,
size,
}
}
pub fn rank(&self) -> usize {
self.rank
}
pub fn size(&self) -> usize {
self.size
}
pub fn split(&self, color: i32, key: i32) -> Result<Group> {
if self.size == 1 {
return Ok(self.clone());
}
Ok(Group::new(registered(self.id)?.comm.split(color, key)?))
}
}
pub fn set_default_group(group: Group) {
*DEFAULT.write().unwrap_or_else(PoisonError::into_inner) = Some(group);
}
pub fn default_group() -> Group {
DEFAULT
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone()
.unwrap_or_else(Group::single)
}
fn group_or_default(group: Option<&Group>) -> Group {
group.cloned().unwrap_or_else(default_group)
}
pub(crate) fn group_size(id: usize) -> Result<usize> {
match id {
0 => Ok(1),
id => Ok(registered(id)?.comm.size()),
}
}
pub(crate) fn group_rank(id: usize) -> Result<usize> {
match id {
0 => Ok(0),
id => Ok(registered(id)?.comm.rank()),
}
}
fn record(op: CollectiveOp, group: &Group, inputs: Vec<Array>) -> Result<Array> {
let mut out = Array::from_op(
Primitive::Collective {
op,
group: group.id,
},
inputs,
)?;
Ok(out.swap_remove(0))
}
fn reduce(x: &Array, op: AllReduce, group: Option<&Group>) -> Result<Array> {
let group = group_or_default(group);
if group.size == 1 {
return Ok(x.clone());
}
record(CollectiveOp::Reduce(op), &group, vec![x.clone()])
}
fn gather(x: &Array, group: Option<&Group>) -> Result<Array> {
let group = group_or_default(group);
if group.size == 1 {
return Ok(x.clone());
}
record(CollectiveOp::Gather, &group, vec![x.clone()])
}
fn peer(op: &str, group: &Group, peer: usize) -> Result<()> {
if group.size == 1 || peer >= group.size || peer == group.rank {
return Err(Error::InvalidArgument(format!(
"{op}: rank {peer} is not another rank of a group of {}",
group.size
)));
}
Ok(())
}
fn send_op(x: &Array, dst: usize, group: Option<&Group>) -> Result<Array> {
let group = group_or_default(group);
peer("send", &group, dst)?;
record(CollectiveOp::Send { dst }, &group, vec![x.clone()])
}
fn recv_op(shape: Vec<usize>, dtype: DType, src: usize, group: Option<&Group>) -> Result<Array> {
let group = group_or_default(group);
peer("recv", &group, src)?;
record(CollectiveOp::Recv { src, shape, dtype }, &group, Vec::new())
}
fn header(dtype: DType, shape: &[usize]) -> Vec<u8> {
let name = dtype.name().as_bytes();
let mut h = Vec::with_capacity(32 + name.len() + 8 * shape.len());
h.extend_from_slice(b"nuvecsnd");
h.extend_from_slice(&(name.len() as u64).to_le_bytes());
h.extend_from_slice(name);
h.extend_from_slice(&(shape.len() as u64).to_le_bytes());
for &d in shape {
h.extend_from_slice(&(d as u64).to_le_bytes());
}
h
}
pub(crate) fn eval_cpu(
op: &CollectiveOp,
group: usize,
operands: &[Operand<'_>],
) -> Result<Vec<Data>> {
let entry = registered(group)?;
let _order = entry.order.lock().unwrap_or_else(PoisonError::into_inner);
let comm = &entry.comm;
let data = match op {
CollectiveOp::Reduce(reduce) => {
let (meta, data) = operands[0];
let mut out = crate::kernels::contiguous(meta, data)?;
comm.all_reduce(*reduce, meta.dtype, out.bytes_mut())?;
out
}
CollectiveOp::Gather => {
let (meta, data) = operands[0];
let input = crate::kernels::contiguous(meta, data)?;
let mut out = Buffer::try_alloc(input.bytes().len() * comm.size())?;
comm.all_gather(input.bytes(), out.bytes_mut())?;
out
}
CollectiveOp::Send { dst } => {
let (meta, data) = operands[0];
let input = crate::kernels::contiguous(meta, data)?;
let h = header(meta.dtype, &meta.shape);
comm.send(*dst, &(h.len() as u64).to_le_bytes())?;
comm.send(*dst, &h)?;
comm.send(*dst, &(input.bytes().len() as u64).to_le_bytes())?;
comm.send(*dst, input.bytes())?;
input
}
CollectiveOp::Recv { src, shape, dtype } => {
let want = header(*dtype, shape);
let mut len = [0u8; 8];
comm.recv(*src, &mut len)?;
let len = u64::from_le_bytes(len) as usize;
if len > (1 << 20) {
return Err(Error::InvalidArgument(format!(
"recv: rank {src} sent no array header"
)));
}
let mut got = vec![0u8; len];
comm.recv(*src, &mut got)?;
let mut bytes = [0u8; 8];
comm.recv(*src, &mut bytes)?;
let bytes = u64::from_le_bytes(bytes) as usize;
if got != want {
let mut sink = vec![0u8; bytes.min(1 << 20)];
let mut left = bytes;
while left > 0 {
let n = left.min(sink.len());
comm.recv(*src, &mut sink[..n])?;
left -= n;
}
return Err(Error::InvalidArgument(format!(
"recv: rank {src} sent another dtype or shape than {} {shape:?}",
dtype.name()
)));
}
let n: usize = shape.iter().product();
let mut out = Buffer::try_alloc(n * dtype.size())?;
comm.recv(*src, out.bytes_mut())?;
out
}
};
Ok(vec![Data::new(data)])
}
#[track_caller]
pub fn all_sum(x: &Array, group: Option<&Group>) -> Array {
Array::sticky("all_sum", Location::caller(), &[x], || {
reduce(x, AllReduce::Sum, group)
})
}
#[track_caller]
pub fn all_max(x: &Array, group: Option<&Group>) -> Array {
Array::sticky("all_max", Location::caller(), &[x], || {
reduce(x, AllReduce::Max, group)
})
}
#[track_caller]
pub fn all_min(x: &Array, group: Option<&Group>) -> Array {
Array::sticky("all_min", Location::caller(), &[x], || {
reduce(x, AllReduce::Min, group)
})
}
#[track_caller]
pub fn all_gather(x: &Array, group: Option<&Group>) -> Array {
Array::sticky("all_gather", Location::caller(), &[x], || gather(x, group))
}
#[track_caller]
pub fn send(x: &Array, dst: usize, group: Option<&Group>) -> Array {
Array::sticky("send", Location::caller(), &[x], || send_op(x, dst, group))
}
#[track_caller]
pub fn recv(shape: impl IntoShape, dtype: DType, src: usize, group: Option<&Group>) -> Array {
let spec = shape.shape_spec();
Array::constructor("recv", Location::caller(), dtype, &spec, |shape| {
recv_op(shape.to_vec(), dtype, src, group)
})
}
#[track_caller]
pub fn recv_like(x: &Array, src: usize, group: Option<&Group>) -> Array {
let at = Location::caller();
Array::sticky("recv_like", at, &[x], || {
recv_op(x.dims().to_vec(), x.dtype(), src, group)
})
}
#[track_caller]
pub fn sum_gradients(x: &Array, group: Option<&Group>) -> Array {
let group = group_or_default(group);
if group.size == 1 {
return x.clone();
}
Array::sticky("sum_gradients", Location::caller(), &[x], || {
let f = crate::custom_function_flat(|xs| Ok(xs.to_vec())).with_vjp(
move |_primals, _outputs, cotangents| {
cotangents
.iter()
.map(|c| reduce(c, AllReduce::Sum, Some(&group)))
.collect()
},
);
Ok(f.call(std::slice::from_ref(x))?.remove(0))
})
}
impl crate::tree::Static for Group {
fn hash_static(&self, mut h: &mut dyn std::hash::Hasher) {
std::hash::Hash::hash(&(self.id, self.rank, self.size), &mut h);
}
}