1use std::panic::Location;
15use std::sync::{Arc, Mutex, PoisonError, RwLock};
16
17use crate::array::Data;
18use crate::buffer::Buffer;
19use crate::kernels::Operand;
20use crate::primitive::Primitive;
21use crate::{Array, DType, Error, IntoShape, Result};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25#[non_exhaustive]
26pub enum AllReduce {
27 Sum,
29 Max,
31 Min,
33}
34
35pub trait Communicator: Send + Sync {
38 fn rank(&self) -> usize;
40 fn size(&self) -> usize;
42 fn all_reduce(&self, op: AllReduce, dtype: DType, data: &mut [u8]) -> Result<()>;
45 fn all_gather(&self, input: &[u8], output: &mut [u8]) -> Result<()>;
47 fn send(&self, dst: usize, data: &[u8]) -> Result<()>;
49 fn recv(&self, src: usize, data: &mut [u8]) -> Result<()>;
51 fn split(&self, color: i32, key: i32) -> Result<Arc<dyn Communicator>> {
53 let _ = (color, key);
54 Err(Error::InvalidArgument(
55 "distributed: this backend does not split groups (as MLX's ring)".into(),
56 ))
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62#[non_exhaustive]
63pub enum CollectiveOp {
64 Reduce(AllReduce),
66 Gather,
68 Send {
70 dst: usize,
72 },
73 Recv {
75 src: usize,
77 shape: Vec<usize>,
79 dtype: DType,
81 },
82}
83
84impl CollectiveOp {
85 pub fn name(&self) -> &'static str {
87 match self {
88 Self::Reduce(AllReduce::Sum) => "all_sum",
89 Self::Reduce(AllReduce::Max) => "all_max",
90 Self::Reduce(AllReduce::Min) => "all_min",
91 Self::Gather => "all_gather",
92 Self::Send { .. } => "send",
93 Self::Recv { .. } => "recv",
94 }
95 }
96}
97
98struct Registered {
101 comm: Arc<dyn Communicator>,
102 order: Mutex<()>,
103}
104
105static GROUPS: Mutex<Vec<Arc<Registered>>> = Mutex::new(Vec::new());
107static DEFAULT: RwLock<Option<Group>> = RwLock::new(None);
108
109fn registered(id: usize) -> Result<Arc<Registered>> {
110 let groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
111 id.checked_sub(1)
112 .and_then(|i| groups.get(i).cloned())
113 .ok_or_else(|| Error::InvalidArgument(format!("distributed: no group {id}")))
114}
115
116#[derive(Clone)]
118pub struct Group {
119 id: usize,
120 rank: usize,
121 size: usize,
122}
123
124impl std::fmt::Debug for Group {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "Group(rank {} of {})", self.rank, self.size)
127 }
128}
129
130impl Group {
131 pub fn single() -> Self {
133 Self {
134 id: 0,
135 rank: 0,
136 size: 1,
137 }
138 }
139
140 pub fn new(comm: Arc<dyn Communicator>) -> Self {
142 let (rank, size) = (comm.rank(), comm.size());
143 if size <= 1 {
144 return Self::single();
145 }
146 let mut groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
147 groups.push(Arc::new(Registered {
148 comm,
149 order: Mutex::new(()),
150 }));
151 Self {
152 id: groups.len(),
153 rank,
154 size,
155 }
156 }
157
158 pub fn rank(&self) -> usize {
160 self.rank
161 }
162
163 pub fn size(&self) -> usize {
165 self.size
166 }
167
168 pub fn split(&self, color: i32, key: i32) -> Result<Group> {
171 if self.size == 1 {
172 return Ok(self.clone());
173 }
174 Ok(Group::new(registered(self.id)?.comm.split(color, key)?))
175 }
176}
177
178pub fn set_default_group(group: Group) {
181 *DEFAULT.write().unwrap_or_else(PoisonError::into_inner) = Some(group);
182}
183
184pub fn default_group() -> Group {
186 DEFAULT
187 .read()
188 .unwrap_or_else(PoisonError::into_inner)
189 .clone()
190 .unwrap_or_else(Group::single)
191}
192
193fn group_or_default(group: Option<&Group>) -> Group {
194 group.cloned().unwrap_or_else(default_group)
195}
196
197pub(crate) fn group_size(id: usize) -> Result<usize> {
199 match id {
200 0 => Ok(1),
201 id => Ok(registered(id)?.comm.size()),
202 }
203}
204
205pub(crate) fn group_rank(id: usize) -> Result<usize> {
207 match id {
208 0 => Ok(0),
209 id => Ok(registered(id)?.comm.rank()),
210 }
211}
212
213fn record(op: CollectiveOp, group: &Group, inputs: Vec<Array>) -> Result<Array> {
214 let mut out = Array::from_op(
215 Primitive::Collective {
216 op,
217 group: group.id,
218 },
219 inputs,
220 )?;
221 Ok(out.swap_remove(0))
222}
223
224fn reduce(x: &Array, op: AllReduce, group: Option<&Group>) -> Result<Array> {
225 let group = group_or_default(group);
226 if group.size == 1 {
227 return Ok(x.clone());
228 }
229 record(CollectiveOp::Reduce(op), &group, vec![x.clone()])
230}
231
232fn gather(x: &Array, group: Option<&Group>) -> Result<Array> {
233 let group = group_or_default(group);
234 if group.size == 1 {
235 return Ok(x.clone());
236 }
237 record(CollectiveOp::Gather, &group, vec![x.clone()])
238}
239
240fn peer(op: &str, group: &Group, peer: usize) -> Result<()> {
241 if group.size == 1 || peer >= group.size || peer == group.rank {
242 return Err(Error::InvalidArgument(format!(
243 "{op}: rank {peer} is not another rank of a group of {}",
244 group.size
245 )));
246 }
247 Ok(())
248}
249
250fn send_op(x: &Array, dst: usize, group: Option<&Group>) -> Result<Array> {
251 let group = group_or_default(group);
252 peer("send", &group, dst)?;
253 record(CollectiveOp::Send { dst }, &group, vec![x.clone()])
254}
255
256fn recv_op(shape: Vec<usize>, dtype: DType, src: usize, group: Option<&Group>) -> Result<Array> {
257 let group = group_or_default(group);
258 peer("recv", &group, src)?;
259 record(CollectiveOp::Recv { src, shape, dtype }, &group, Vec::new())
260}
261
262fn header(dtype: DType, shape: &[usize]) -> Vec<u8> {
266 let name = dtype.name().as_bytes();
267 let mut h = Vec::with_capacity(32 + name.len() + 8 * shape.len());
268 h.extend_from_slice(b"nuvecsnd");
269 h.extend_from_slice(&(name.len() as u64).to_le_bytes());
270 h.extend_from_slice(name);
271 h.extend_from_slice(&(shape.len() as u64).to_le_bytes());
272 for &d in shape {
273 h.extend_from_slice(&(d as u64).to_le_bytes());
274 }
275 h
276}
277
278pub(crate) fn eval_cpu(
280 op: &CollectiveOp,
281 group: usize,
282 operands: &[Operand<'_>],
283) -> Result<Vec<Data>> {
284 let entry = registered(group)?;
285 let _order = entry.order.lock().unwrap_or_else(PoisonError::into_inner);
286 let comm = &entry.comm;
287 let data = match op {
288 CollectiveOp::Reduce(reduce) => {
289 let (meta, data) = operands[0];
290 let mut out = crate::kernels::contiguous(meta, data)?;
291 comm.all_reduce(*reduce, meta.dtype, out.bytes_mut())?;
292 out
293 }
294 CollectiveOp::Gather => {
295 let (meta, data) = operands[0];
296 let input = crate::kernels::contiguous(meta, data)?;
297 let mut out = Buffer::try_alloc(input.bytes().len() * comm.size())?;
298 comm.all_gather(input.bytes(), out.bytes_mut())?;
299 out
300 }
301 CollectiveOp::Send { dst } => {
302 let (meta, data) = operands[0];
303 let input = crate::kernels::contiguous(meta, data)?;
304 let h = header(meta.dtype, &meta.shape);
305 comm.send(*dst, &(h.len() as u64).to_le_bytes())?;
306 comm.send(*dst, &h)?;
307 comm.send(*dst, &(input.bytes().len() as u64).to_le_bytes())?;
308 comm.send(*dst, input.bytes())?;
309 input
310 }
311 CollectiveOp::Recv { src, shape, dtype } => {
312 let want = header(*dtype, shape);
313 let mut len = [0u8; 8];
314 comm.recv(*src, &mut len)?;
315 let len = u64::from_le_bytes(len) as usize;
316 if len > (1 << 20) {
317 return Err(Error::InvalidArgument(format!(
318 "recv: rank {src} sent no array header"
319 )));
320 }
321 let mut got = vec![0u8; len];
322 comm.recv(*src, &mut got)?;
323 let mut bytes = [0u8; 8];
324 comm.recv(*src, &mut bytes)?;
325 let bytes = u64::from_le_bytes(bytes) as usize;
326 if got != want {
327 let mut sink = vec![0u8; bytes.min(1 << 20)];
329 let mut left = bytes;
330 while left > 0 {
331 let n = left.min(sink.len());
332 comm.recv(*src, &mut sink[..n])?;
333 left -= n;
334 }
335 return Err(Error::InvalidArgument(format!(
336 "recv: rank {src} sent another dtype or shape than {} {shape:?}",
337 dtype.name()
338 )));
339 }
340 let n: usize = shape.iter().product();
341 let mut out = Buffer::try_alloc(n * dtype.size())?;
342 comm.recv(*src, out.bytes_mut())?;
343 out
344 }
345 };
346 Ok(vec![Data::new(data)])
347}
348
349#[track_caller]
360pub fn all_sum(x: &Array, group: Option<&Group>) -> Array {
361 Array::sticky("all_sum", Location::caller(), &[x], || {
362 reduce(x, AllReduce::Sum, group)
363 })
364}
365
366#[track_caller]
368pub fn all_max(x: &Array, group: Option<&Group>) -> Array {
369 Array::sticky("all_max", Location::caller(), &[x], || {
370 reduce(x, AllReduce::Max, group)
371 })
372}
373
374#[track_caller]
376pub fn all_min(x: &Array, group: Option<&Group>) -> Array {
377 Array::sticky("all_min", Location::caller(), &[x], || {
378 reduce(x, AllReduce::Min, group)
379 })
380}
381
382#[track_caller]
385pub fn all_gather(x: &Array, group: Option<&Group>) -> Array {
386 Array::sticky("all_gather", Location::caller(), &[x], || gather(x, group))
387}
388
389#[track_caller]
392pub fn send(x: &Array, dst: usize, group: Option<&Group>) -> Array {
393 Array::sticky("send", Location::caller(), &[x], || send_op(x, dst, group))
394}
395
396#[track_caller]
399pub fn recv(shape: impl IntoShape, dtype: DType, src: usize, group: Option<&Group>) -> Array {
400 let spec = shape.shape_spec();
401 Array::constructor("recv", Location::caller(), dtype, &spec, |shape| {
402 recv_op(shape.to_vec(), dtype, src, group)
403 })
404}
405
406#[track_caller]
408pub fn recv_like(x: &Array, src: usize, group: Option<&Group>) -> Array {
409 let at = Location::caller();
410 Array::sticky("recv_like", at, &[x], || {
411 recv_op(x.dims().to_vec(), x.dtype(), src, group)
412 })
413}
414
415#[track_caller]
419pub fn sum_gradients(x: &Array, group: Option<&Group>) -> Array {
420 let group = group_or_default(group);
421 if group.size == 1 {
422 return x.clone();
423 }
424 Array::sticky("sum_gradients", Location::caller(), &[x], || {
425 let f = crate::custom_function_flat(|xs| Ok(xs.to_vec())).with_vjp(
426 move |_primals, _outputs, cotangents| {
427 cotangents
428 .iter()
429 .map(|c| reduce(c, AllReduce::Sum, Some(&group)))
430 .collect()
431 },
432 );
433 Ok(f.call(std::slice::from_ref(x))?.remove(0))
434 })
435}
436
437impl crate::tree::Static for Group {
438 fn hash_static(&self, mut h: &mut dyn std::hash::Hasher) {
439 std::hash::Hash::hash(&(self.id, self.rank, self.size), &mut h);
440 }
441}