1use crate::collective::ReduceKind;
44use crate::symmetric::CollectiveError;
45use half::slice::HalfFloatSliceExt;
46use rlx_ir::DType;
47use std::sync::Arc;
48use std::time::{Duration, Instant};
49
50pub const TAG_RESERVED_BASE: u32 = 0xFFF0_0000;
54const TAG_BARRIER: u32 = TAG_RESERVED_BASE;
55const TAG_ALL_REDUCE: u32 = TAG_RESERVED_BASE + 1;
56const TAG_ALL_GATHER: u32 = TAG_RESERVED_BASE + 2;
57const TAG_BROADCAST: u32 = TAG_RESERVED_BASE + 3;
58const TAG_ALL_TO_ALL: u32 = TAG_RESERVED_BASE + 4;
59const TAG_REDUCE: u32 = TAG_RESERVED_BASE + 5;
60const TAG_PPERMUTE: u32 = TAG_RESERVED_BASE + 6;
61
62pub trait Transport: Send + Sync {
71 fn rank(&self) -> u32;
73 fn world_size(&self) -> u32;
75
76 fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError>;
79
80 fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError>;
83
84 fn recv_bytes_timeout(
90 &self,
91 from: u32,
92 tag: u32,
93 _timeout: Duration,
94 ) -> Result<Option<Vec<u8>>, CollectiveError> {
95 Ok(Some(self.recv_bytes(from, tag)?))
96 }
97
98 fn barrier(&self) -> Result<(), CollectiveError> {
106 default_barrier(self)
107 }
108}
109
110pub fn default_barrier<T: Transport + ?Sized>(t: &T) -> Result<(), CollectiveError> {
114 let n = t.world_size();
115 if n <= 1 {
116 return Ok(());
117 }
118 let me = t.rank();
119 if me == 0 {
120 for r in 1..n {
121 t.recv_bytes(r, TAG_BARRIER)?;
122 }
123 for r in 1..n {
124 t.send_bytes(r, TAG_BARRIER, &[1u8])?;
125 }
126 } else {
127 t.send_bytes(0, TAG_BARRIER, &[1u8])?;
128 t.recv_bytes(0, TAG_BARRIER)?;
129 }
130 Ok(())
131}
132
133fn f32_to_le_bytes(data: &[f32]) -> Vec<u8> {
137 let mut out = Vec::with_capacity(data.len() * 4);
138 for &x in data {
139 out.extend_from_slice(&x.to_le_bytes());
140 }
141 out
142}
143
144fn le_bytes_to_f32(bytes: &[u8]) -> Result<Vec<f32>, CollectiveError> {
147 if !bytes.len().is_multiple_of(4) {
148 return Err(CollectiveError::TransportError {
149 reason: format!("recv payload {} bytes is not a multiple of 4", bytes.len()),
150 });
151 }
152 Ok(bytes
153 .chunks_exact(4)
154 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
155 .collect())
156}
157
158fn combine(op: ReduceKind, a: f32, b: f32) -> f32 {
159 match op {
160 ReduceKind::Sum | ReduceKind::Mean => a + b,
161 ReduceKind::Max => a.max(b),
162 ReduceKind::Min => a.min(b),
163 }
164}
165
166fn finalize(op: ReduceKind, acc: f32, n: u32) -> f32 {
167 match op {
168 ReduceKind::Mean => acc / (n as f32),
169 _ => acc,
170 }
171}
172
173fn finalize64(op: ReduceKind, acc: f64, n: u32) -> f64 {
175 match op {
176 ReduceKind::Mean => acc / (n as f64),
177 _ => acc,
178 }
179}
180
181fn ring_chunk(len: usize, n: usize, i: usize) -> (usize, usize) {
187 let base = len / n;
188 let rem = len % n;
189 let bound = |j: usize| j * base + j.min(rem);
190 (bound(i), bound(i + 1))
191}
192
193fn expect_len(got: usize, want: usize) -> Result<(), CollectiveError> {
195 if got != want {
196 return Err(CollectiveError::LengthMismatch {
197 expected: want,
198 got,
199 });
200 }
201 Ok(())
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum ReduceMode {
208 Ring,
211 Deterministic,
222}
223
224pub fn env_reduce_mode() -> ReduceMode {
232 static MODE: std::sync::OnceLock<ReduceMode> = std::sync::OnceLock::new();
233 *MODE.get_or_init(|| {
234 let on = |k: &str| {
235 std::env::var(k)
236 .map(|v| v != "0" && !v.is_empty())
237 .unwrap_or(false)
238 };
239 if on("RLX_DETERMINISTIC_REDUCE") {
240 ReduceMode::Deterministic
241 } else {
242 ReduceMode::Ring
243 }
244 })
245}
246
247fn combine64(op: ReduceKind, a: f64, b: f64) -> f64 {
250 match op {
251 ReduceKind::Sum | ReduceKind::Mean => a + b,
252 ReduceKind::Max => a.max(b),
253 ReduceKind::Min => a.min(b),
254 }
255}
256
257fn is_reducible_float(d: DType) -> bool {
259 matches!(d, DType::F16 | DType::BF16 | DType::F32 | DType::F64)
260}
261
262fn is_reducible_int(d: DType) -> bool {
265 matches!(d, DType::I8 | DType::U8)
266}
267
268fn decode_to_i32(bytes: &[u8], dtype: DType) -> Vec<i32> {
269 match dtype {
270 DType::I8 => bytes.iter().map(|&b| b as i8 as i32).collect(),
271 DType::U8 => bytes.iter().map(|&b| b as i32).collect(),
272 _ => Vec::new(),
273 }
274}
275
276fn encode_into_i32(dst: &mut [u8], vals: &[i32], dtype: DType) {
277 match dtype {
278 DType::I8 => {
279 for (d, &v) in dst.iter_mut().zip(vals) {
280 *d = v.clamp(i8::MIN as i32, i8::MAX as i32) as i8 as u8;
281 }
282 }
283 DType::U8 => {
284 for (d, &v) in dst.iter_mut().zip(vals) {
285 *d = v.clamp(0, u8::MAX as i32) as u8;
286 }
287 }
288 _ => {}
289 }
290}
291
292fn combine_i32(op: ReduceKind, a: i32, b: i32) -> i32 {
293 match op {
294 ReduceKind::Sum | ReduceKind::Mean => a + b,
295 ReduceKind::Max => a.max(b),
296 ReduceKind::Min => a.min(b),
297 }
298}
299
300fn decode_to_f64(bytes: &[u8], dtype: DType) -> Vec<f64> {
302 match dtype {
303 DType::F32 => bytes
304 .chunks_exact(4)
305 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f64)
306 .collect(),
307 DType::F64 => bytes
308 .chunks_exact(8)
309 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
310 .collect(),
311 DType::F16 => bytes
312 .chunks_exact(2)
313 .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f64())
314 .collect(),
315 DType::BF16 => bytes
316 .chunks_exact(2)
317 .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f64())
318 .collect(),
319 _ => Vec::new(),
320 }
321}
322
323fn encode_from_f64(vals: &[f64], dtype: DType) -> Vec<u8> {
325 let mut out = Vec::with_capacity(vals.len() * dtype.size_bytes().max(1));
326 match dtype {
327 DType::F32 => vals
328 .iter()
329 .for_each(|&v| out.extend_from_slice(&(v as f32).to_le_bytes())),
330 DType::F64 => vals
331 .iter()
332 .for_each(|&v| out.extend_from_slice(&v.to_le_bytes())),
333 DType::F16 => vals
334 .iter()
335 .for_each(|&v| out.extend_from_slice(&half::f16::from_f64(v).to_le_bytes())),
336 DType::BF16 => vals
337 .iter()
338 .for_each(|&v| out.extend_from_slice(&half::bf16::from_f64(v).to_le_bytes())),
339 _ => {}
340 }
341 out
342}
343
344fn combine32(op: ReduceKind, a: f32, b: f32) -> f32 {
347 match op {
348 ReduceKind::Sum | ReduceKind::Mean => a + b,
349 ReduceKind::Max => a.max(b),
350 ReduceKind::Min => a.min(b),
351 }
352}
353
354fn decode_to_f32(bytes: &[u8], dtype: DType) -> Vec<f32> {
360 match dtype {
361 DType::F32 => {
362 #[cfg(target_endian = "little")]
363 {
364 let (head, mid, tail) = unsafe { bytes.align_to::<f32>() };
365 if head.is_empty() && tail.is_empty() {
366 return mid.to_vec();
367 }
368 }
369 bytes
370 .chunks_exact(4)
371 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
372 .collect()
373 }
374 DType::F16 => {
375 let mut out = vec![0f32; bytes.len() / 2];
376 #[cfg(target_endian = "little")]
377 {
378 let (head, mid, tail) = unsafe { bytes.align_to::<half::f16>() };
381 if head.is_empty() && tail.is_empty() {
382 mid.convert_to_f32_slice(&mut out);
383 return out;
384 }
385 }
386 for (o, c) in out.iter_mut().zip(bytes.chunks_exact(2)) {
387 *o = half::f16::from_le_bytes([c[0], c[1]]).to_f32();
388 }
389 out
390 }
391 DType::BF16 => {
392 let mut out = vec![0f32; bytes.len() / 2];
393 #[cfg(target_endian = "little")]
394 {
395 let (head, mid, tail) = unsafe { bytes.align_to::<half::bf16>() };
396 if head.is_empty() && tail.is_empty() {
397 mid.convert_to_f32_slice(&mut out);
398 return out;
399 }
400 }
401 for (o, c) in out.iter_mut().zip(bytes.chunks_exact(2)) {
402 *o = half::bf16::from_le_bytes([c[0], c[1]]).to_f32();
403 }
404 out
405 }
406 _ => Vec::new(),
407 }
408}
409
410fn encode_into(dst: &mut [u8], vals: &[f32], dtype: DType) {
416 match dtype {
417 DType::F32 => {
418 for (c, &v) in dst.chunks_exact_mut(4).zip(vals) {
419 c.copy_from_slice(&v.to_le_bytes());
420 }
421 }
422 DType::F16 => {
423 #[cfg(target_endian = "little")]
424 {
425 let (head, mid, tail) = unsafe { dst.align_to_mut::<half::f16>() };
426 if head.is_empty() && tail.is_empty() {
427 mid.convert_from_f32_slice(vals);
428 return;
429 }
430 }
431 for (c, &v) in dst.chunks_exact_mut(2).zip(vals) {
432 c.copy_from_slice(&half::f16::from_f32(v).to_le_bytes());
433 }
434 }
435 DType::BF16 => {
436 #[cfg(target_endian = "little")]
437 {
438 let (head, mid, tail) = unsafe { dst.align_to_mut::<half::bf16>() };
439 if head.is_empty() && tail.is_empty() {
440 mid.convert_from_f32_slice(vals);
441 return;
442 }
443 }
444 for (c, &v) in dst.chunks_exact_mut(2).zip(vals) {
445 c.copy_from_slice(&half::bf16::from_f32(v).to_le_bytes());
446 }
447 }
448 _ => {}
449 }
450}
451
452fn reduce_into(dst: &mut [u8], incoming: &[u8], dtype: DType, op: ReduceKind) {
455 if is_reducible_int(dtype) {
456 let mut acc = decode_to_i32(dst, dtype);
457 let b = decode_to_i32(incoming, dtype);
458 for (x, &y) in acc.iter_mut().zip(&b) {
459 *x = combine_i32(op, *x, y);
460 }
461 encode_into_i32(dst, &acc, dtype);
462 } else if dtype == DType::F64 {
463 let a = decode_to_f64(dst, dtype);
464 let b = decode_to_f64(incoming, dtype);
465 let m: Vec<f64> = a
466 .iter()
467 .zip(&b)
468 .map(|(&x, &y)| combine64(op, x, y))
469 .collect();
470 dst.copy_from_slice(&encode_from_f64(&m, dtype));
471 } else {
472 let mut acc = decode_to_f32(dst, dtype);
473 let b = decode_to_f32(incoming, dtype);
474 for (x, &y) in acc.iter_mut().zip(&b) {
475 *x = combine32(op, *x, y);
476 }
477 encode_into(dst, &acc, dtype);
478 }
479}
480
481fn scale_mean(data: &mut [u8], dtype: DType, n: usize) {
484 if is_reducible_int(dtype) {
485 let half = n as i32 / 2;
487 let mut acc = decode_to_i32(data, dtype);
488 for v in acc.iter_mut() {
489 *v = (*v + if *v >= 0 { half } else { -half }) / n as i32;
490 }
491 encode_into_i32(data, &acc, dtype);
492 } else if dtype == DType::F64 {
493 let scaled: Vec<f64> = decode_to_f64(data, dtype)
494 .iter()
495 .map(|&v| v / n as f64)
496 .collect();
497 data.copy_from_slice(&encode_from_f64(&scaled, dtype));
498 } else {
499 let inv = 1.0 / n as f32;
500 let mut acc = decode_to_f32(data, dtype);
501 for v in acc.iter_mut() {
502 *v *= inv;
503 }
504 encode_into(data, &acc, dtype);
505 }
506}
507
508pub fn negotiate_reduce_dtype(dtypes: &[DType]) -> DType {
514 if dtypes.contains(&DType::F64) {
515 DType::F64
516 } else {
517 DType::F32
518 }
519}
520
521#[derive(Clone)]
528pub struct ProcessGroup {
529 transport: Arc<dyn Transport>,
530}
531
532impl ProcessGroup {
533 pub fn new(transport: Arc<dyn Transport>) -> Self {
534 Self { transport }
535 }
536
537 pub fn rank(&self) -> u32 {
538 self.transport.rank()
539 }
540
541 pub fn world_size(&self) -> u32 {
542 self.transport.world_size()
543 }
544
545 pub fn is_leader(&self) -> bool {
548 self.rank() == 0
549 }
550
551 pub fn transport(&self) -> &Arc<dyn Transport> {
552 &self.transport
553 }
554
555 pub fn barrier(&self) -> Result<(), CollectiveError> {
556 self.transport.barrier()
557 }
558
559 pub fn send_f32(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
564 debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
565 self.send_f32_tagged(to, tag, data)
566 }
567
568 pub fn recv_f32(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
570 debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
571 self.recv_f32_tagged(from, tag)
572 }
573
574 fn send_f32_tagged(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
577 self.transport.send_bytes(to, tag, &f32_to_le_bytes(data))
578 }
579
580 fn recv_f32_tagged(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
581 le_bytes_to_f32(&self.transport.recv_bytes(from, tag)?)
582 }
583
584 pub fn all_reduce(&self, data: &mut [f32], op: ReduceKind) -> Result<(), CollectiveError> {
596 self.all_reduce_mode(data, op, env_reduce_mode())
597 }
598
599 pub fn all_reduce_mode(
602 &self,
603 data: &mut [f32],
604 op: ReduceKind,
605 mode: ReduceMode,
606 ) -> Result<(), CollectiveError> {
607 let n = self.world_size();
608 if n <= 1 {
609 for v in data.iter_mut() {
610 *v = finalize(op, *v, n.max(1));
611 }
612 return Ok(());
613 }
614 match mode {
615 ReduceMode::Ring => self.all_reduce_ring_f32(data, op),
616 ReduceMode::Deterministic => self.all_reduce_deterministic(data, op),
617 }
618 }
619
620 fn all_reduce_ring_f32(&self, data: &mut [f32], op: ReduceKind) -> Result<(), CollectiveError> {
622 let n = self.world_size() as usize;
623 let me = self.rank() as usize;
624 let next = ((me + 1) % n) as u32;
625 let prev = ((me + n - 1) % n) as u32;
626 let len = data.len();
627
628 for step in 0..n - 1 {
632 let (ss, se) = ring_chunk(len, n, (me + n - step) % n);
633 let (rs, re) = ring_chunk(len, n, (me + n - step - 1) % n);
634 self.send_f32_tagged(next, TAG_ALL_REDUCE, &data[ss..se])?;
635 let incoming = self.recv_f32_tagged(prev, TAG_ALL_REDUCE)?;
636 expect_len(incoming.len(), re - rs)?;
637 for (d, v) in data[rs..re].iter_mut().zip(incoming) {
638 *d = combine(op, *d, v);
639 }
640 }
641 let (os, oe) = ring_chunk(len, n, (me + 1) % n);
645 for v in data[os..oe].iter_mut() {
646 *v = finalize(op, *v, n as u32);
647 }
648 self.ring_all_gather(data, n, me)
649 }
650
651 fn ring_all_gather(
655 &self,
656 data: &mut [f32],
657 n: usize,
658 me: usize,
659 ) -> Result<(), CollectiveError> {
660 let next = ((me + 1) % n) as u32;
661 let prev = ((me + n - 1) % n) as u32;
662 let len = data.len();
663 for step in 0..n - 1 {
664 let (ss, se) = ring_chunk(len, n, (me + n - step + 1) % n);
665 let (rs, re) = ring_chunk(len, n, (me + n - step) % n);
666 self.send_f32_tagged(next, TAG_ALL_REDUCE, &data[ss..se])?;
667 let incoming = self.recv_f32_tagged(prev, TAG_ALL_REDUCE)?;
668 expect_len(incoming.len(), re - rs)?;
669 data[rs..re].copy_from_slice(&incoming);
670 }
671 Ok(())
672 }
673
674 fn all_reduce_deterministic(
684 &self,
685 data: &mut [f32],
686 op: ReduceKind,
687 ) -> Result<(), CollectiveError> {
688 let n = self.world_size() as usize;
689 let me = self.rank() as usize;
690 let next = ((me + 1) % n) as u32;
691 let prev = ((me + n - 1) % n) as u32;
692 let len = data.len();
693
694 let mut acc: Vec<f64> = data.iter().map(|&v| v as f64).collect();
696 let mut send = Vec::with_capacity((len / n + 1) * 8);
698
699 for step in 0..n - 1 {
702 let (ss, se) = ring_chunk(len, n, (me + n - step) % n);
703 let (rs, re) = ring_chunk(len, n, (me + n - step - 1) % n);
704 send.clear();
705 send.extend(acc[ss..se].iter().flat_map(|v| v.to_le_bytes()));
706 self.transport.send_bytes(next, TAG_ALL_REDUCE, &send)?;
707 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
708 expect_len(incoming.len(), (re - rs) * 8)?;
709 for (a, c) in acc[rs..re].iter_mut().zip(incoming.chunks_exact(8)) {
710 *a = combine64(op, *a, f64::from_le_bytes(c.try_into().unwrap()));
711 }
712 }
713
714 let (os, oe) = ring_chunk(len, n, (me + 1) % n);
717 for i in os..oe {
718 data[i] = finalize64(op, acc[i], n as u32) as f32;
719 }
720 self.ring_all_gather(data, n, me)
721 }
722
723 pub fn all_reduce_typed(
731 &self,
732 data: &mut [u8],
733 dtype: DType,
734 op: ReduceKind,
735 ) -> Result<(), CollectiveError> {
736 if !is_reducible_float(dtype) && !is_reducible_int(dtype) {
737 return Err(CollectiveError::TransportError {
738 reason: format!("all_reduce_typed: unsupported dtype {dtype:?}"),
739 });
740 }
741 let esz = dtype.size_bytes();
742 if esz == 0 || !data.len().is_multiple_of(esz) {
743 return Err(CollectiveError::TransportError {
744 reason: format!(
745 "all_reduce_typed: {} bytes not a multiple of {esz}",
746 data.len()
747 ),
748 });
749 }
750 let n = self.world_size();
751 if n <= 1 {
752 return Ok(()); }
754 let n = n as usize;
755 let me = self.rank() as usize;
756 let next = ((me + 1) % n) as u32;
757 let prev = ((me + n - 1) % n) as u32;
758
759 let elems = data.len() / esz;
760 let base = elems / n;
761 let rem = elems % n;
762 let ebound = |i: usize| (i * base + i.min(rem)) * esz; let chunk = |i: usize| (ebound(i), ebound(i + 1));
764
765 for step in 0..n - 1 {
767 let (ss, se) = chunk((me + n - step) % n);
768 let (rs, re) = chunk((me + n - step - 1) % n);
769 self.transport
770 .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
771 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
772 if incoming.len() != re - rs {
773 return Err(CollectiveError::LengthMismatch {
774 expected: re - rs,
775 got: incoming.len(),
776 });
777 }
778 reduce_into(&mut data[rs..re], &incoming, dtype, op);
779 }
780 for step in 0..n - 1 {
782 let (ss, se) = chunk((me + n - step + 1) % n);
783 let (rs, re) = chunk((me + n - step) % n);
784 self.transport
785 .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
786 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
787 if incoming.len() != re - rs {
788 return Err(CollectiveError::LengthMismatch {
789 expected: re - rs,
790 got: incoming.len(),
791 });
792 }
793 data[rs..re].copy_from_slice(&incoming);
794 }
795 if matches!(op, ReduceKind::Mean) {
796 scale_mean(data, dtype, n);
797 }
798 Ok(())
799 }
800
801 pub fn spawn_all_reduce(
807 self: &Arc<Self>,
808 mut data: Vec<f32>,
809 op: ReduceKind,
810 ) -> std::thread::JoinHandle<Vec<f32>> {
811 let g = self.clone();
812 std::thread::spawn(move || {
813 g.all_reduce(&mut data, op).expect("background all_reduce");
814 data
815 })
816 }
817
818 pub fn all_gather(&self, local: &[f32]) -> Result<Vec<f32>, CollectiveError> {
822 let n = self.world_size();
823 let len = local.len();
824 if n <= 1 {
825 return Ok(local.to_vec());
826 }
827 if self.rank() == 0 {
828 let mut out = vec![0f32; n as usize * len];
829 out[..len].copy_from_slice(local);
830 for r in 1..n {
831 let chunk = self.recv_f32_tagged(r, TAG_ALL_GATHER)?;
832 if chunk.len() != len {
833 return Err(CollectiveError::LengthMismatch {
834 expected: len,
835 got: chunk.len(),
836 });
837 }
838 let start = r as usize * len;
839 out[start..start + len].copy_from_slice(&chunk);
840 }
841 for r in 1..n {
842 self.send_f32_tagged(r, TAG_ALL_GATHER, &out)?;
843 }
844 Ok(out)
845 } else {
846 self.send_f32_tagged(0, TAG_ALL_GATHER, local)?;
847 let out = self.recv_f32_tagged(0, TAG_ALL_GATHER)?;
848 if out.len() != n as usize * len {
849 return Err(CollectiveError::LengthMismatch {
850 expected: n as usize * len,
851 got: out.len(),
852 });
853 }
854 Ok(out)
855 }
856 }
857
858 pub fn broadcast(&self, root: u32, data: &mut [f32]) -> Result<(), CollectiveError> {
861 let n = self.world_size();
862 if n <= 1 {
863 return Ok(());
864 }
865 if self.rank() == root {
866 for r in 0..n {
867 if r != root {
868 self.send_f32_tagged(r, TAG_BROADCAST, data)?;
869 }
870 }
871 } else {
872 let res = self.recv_f32_tagged(root, TAG_BROADCAST)?;
873 if res.len() != data.len() {
874 return Err(CollectiveError::LengthMismatch {
875 expected: data.len(),
876 got: res.len(),
877 });
878 }
879 data.copy_from_slice(&res);
880 }
881 Ok(())
882 }
883
884 pub fn all_to_all(&self, data: &[f32]) -> Result<Vec<f32>, CollectiveError> {
890 let n = self.world_size() as usize;
891 if n <= 1 {
892 return Ok(data.to_vec());
893 }
894 if !data.len().is_multiple_of(n) {
895 return Err(CollectiveError::LengthMismatch {
896 expected: n,
897 got: data.len(),
898 });
899 }
900 let chunk = data.len() / n;
901 let rank = self.rank() as usize;
902 let mut out = vec![0f32; data.len()];
903 out[rank * chunk..(rank + 1) * chunk]
904 .copy_from_slice(&data[rank * chunk..(rank + 1) * chunk]);
905 for p in 0..n {
906 if p == rank {
907 continue;
908 }
909 let send = &data[p * chunk..(p + 1) * chunk];
910 if rank < p {
912 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL, send)?;
913 let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL)?;
914 out[p * chunk..(p + 1) * chunk].copy_from_slice(&r);
915 } else {
916 let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL)?;
917 out[p * chunk..(p + 1) * chunk].copy_from_slice(&r);
918 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL, send)?;
919 }
920 }
921 Ok(out)
922 }
923
924 pub fn reduce(
928 &self,
929 root: u32,
930 data: &mut [f32],
931 op: ReduceKind,
932 ) -> Result<(), CollectiveError> {
933 let n = self.world_size();
934 if n <= 1 {
935 return Ok(());
936 }
937 if self.rank() == root {
938 let mut acc = data.to_vec();
939 for r in 0..n {
940 if r == root {
941 continue;
942 }
943 let other = self.recv_f32_tagged(r, TAG_REDUCE)?;
944 if other.len() != acc.len() {
945 return Err(CollectiveError::LengthMismatch {
946 expected: acc.len(),
947 got: other.len(),
948 });
949 }
950 for i in 0..acc.len() {
951 acc[i] = op.fold(acc[i], other[i]);
952 }
953 }
954 for v in acc.iter_mut() {
955 *v = op.finalize(*v, n as usize);
956 }
957 data.copy_from_slice(&acc);
958 } else {
959 self.send_f32_tagged(root, TAG_REDUCE, data)?;
960 }
961 Ok(())
962 }
963
964 pub fn ppermute(&self, data: &[f32], perm: &[(u32, u32)]) -> Result<Vec<f32>, CollectiveError> {
968 let rank = self.rank();
969 let my_dst = perm.iter().find(|&&(s, _)| s == rank).map(|&(_, d)| d);
970 let my_src = perm.iter().find(|&&(_, d)| d == rank).map(|&(s, _)| s);
971 if let Some(dst) = my_dst {
973 if dst != rank {
974 self.send_f32_tagged(dst, TAG_PPERMUTE, data)?;
975 }
976 }
977 let out = match my_src {
979 Some(src) if src == rank => data.to_vec(),
980 Some(src) => self.recv_f32_tagged(src, TAG_PPERMUTE)?,
981 None => vec![0f32; data.len()],
982 };
983 Ok(out)
984 }
985
986 pub fn federated_average(
993 &self,
994 data: &mut [f32],
995 deadline: Duration,
996 ) -> Result<usize, CollectiveError> {
997 let n = self.world_size();
998 if n <= 1 {
999 return Ok(1);
1000 }
1001 let elems = data.len();
1002 if self.rank() == 0 {
1003 let mut acc = data.to_vec();
1004 let mut present = 1usize;
1005 let end = Instant::now() + deadline;
1006 for r in 1..n {
1007 let remaining = end.saturating_duration_since(Instant::now());
1008 if let Some(bytes) =
1009 self.transport
1010 .recv_bytes_timeout(r, TAG_ALL_REDUCE, remaining)?
1011 {
1012 let other = le_bytes_to_f32(&bytes)?;
1013 if other.len() == elems {
1014 for i in 0..elems {
1015 acc[i] += other[i];
1016 }
1017 present += 1;
1018 }
1019 }
1020 }
1021 let inv = 1.0 / present as f32;
1022 for v in acc.iter_mut() {
1023 *v *= inv;
1024 }
1025 data.copy_from_slice(&acc);
1026 for r in 1..n {
1027 self.send_f32_tagged(r, TAG_ALL_REDUCE, &acc)?;
1028 }
1029 Ok(present)
1030 } else {
1031 self.send_f32_tagged(0, TAG_ALL_REDUCE, data)?;
1032 let res = self.recv_f32_tagged(0, TAG_ALL_REDUCE)?;
1033 if res.len() == elems {
1034 data.copy_from_slice(&res);
1035 }
1036 Ok(0)
1037 }
1038 }
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043 use super::*;
1044 use std::collections::{HashMap, VecDeque};
1045 use std::sync::{Condvar, Mutex};
1046
1047 struct ChannelTransport {
1051 rank: u32,
1052 world: u32,
1053 mailbox: Arc<(Mutex<HashMap<(u32, u32, u32), VecDeque<Vec<u8>>>>, Condvar)>,
1054 }
1055
1056 impl ChannelTransport {
1057 fn fan_out(world: u32) -> Vec<Arc<ChannelTransport>> {
1058 let mailbox = Arc::new((Mutex::new(HashMap::new()), Condvar::new()));
1059 (0..world)
1060 .map(|r| {
1061 Arc::new(ChannelTransport {
1062 rank: r,
1063 world,
1064 mailbox: mailbox.clone(),
1065 })
1066 })
1067 .collect()
1068 }
1069 }
1070
1071 impl Transport for ChannelTransport {
1072 fn rank(&self) -> u32 {
1073 self.rank
1074 }
1075 fn world_size(&self) -> u32 {
1076 self.world
1077 }
1078 fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError> {
1079 let (m, cv) = &*self.mailbox;
1080 m.lock()
1081 .unwrap()
1082 .entry((to, self.rank, tag))
1083 .or_default()
1084 .push_back(bytes.to_vec());
1085 cv.notify_all();
1086 Ok(())
1087 }
1088 fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError> {
1089 let (m, cv) = &*self.mailbox;
1090 let mut guard = m.lock().unwrap();
1091 loop {
1092 if let Some(q) = guard.get_mut(&(self.rank, from, tag))
1093 && let Some(v) = q.pop_front()
1094 {
1095 return Ok(v);
1096 }
1097 guard = cv.wait(guard).unwrap();
1098 }
1099 }
1100 }
1101
1102 fn run_ranks<F>(world: u32, body: F) -> Vec<()>
1103 where
1104 F: Fn(ProcessGroup) + Send + Sync + 'static,
1105 {
1106 let ts = ChannelTransport::fan_out(world);
1107 let body = Arc::new(body);
1108 let handles: Vec<_> = ts
1109 .into_iter()
1110 .map(|t| {
1111 let body = body.clone();
1112 std::thread::spawn(move || body(ProcessGroup::new(t)))
1113 })
1114 .collect();
1115 handles.into_iter().map(|h| h.join().unwrap()).collect()
1116 }
1117
1118 #[test]
1119 fn all_reduce_sum_matches_serial() {
1120 run_ranks(4, |g| {
1121 let r = g.rank() as f32;
1122 let mut data = vec![r + 1.0; 3]; g.all_reduce(&mut data, ReduceKind::Sum).unwrap();
1124 assert_eq!(data, vec![10.0; 3], "rank {}", g.rank());
1125 });
1126 }
1127
1128 #[test]
1129 fn all_reduce_modes_agree_on_the_sum() {
1130 for mode in [ReduceMode::Ring, ReduceMode::Deterministic] {
1132 run_ranks(4, move |g| {
1133 let r = g.rank() as f32 + 1.0; let mut data = vec![r, r * 2.0, r * 3.0];
1135 g.all_reduce_mode(&mut data, ReduceKind::Sum, mode).unwrap();
1136 assert_eq!(
1137 data,
1138 vec![10.0, 20.0, 30.0],
1139 "mode {mode:?} rank {}",
1140 g.rank()
1141 );
1142 });
1143 }
1144 }
1145
1146 #[test]
1147 fn deterministic_reduce_mean_divides() {
1148 run_ranks(4, |g| {
1149 let mut data = vec![g.rank() as f32 + 1.0; 2]; g.all_reduce_mode(&mut data, ReduceKind::Mean, ReduceMode::Deterministic)
1151 .unwrap();
1152 assert_eq!(data, vec![2.5, 2.5], "rank {}", g.rank());
1153 });
1154 }
1155
1156 #[test]
1157 fn deterministic_is_exact_where_the_f32_ring_would_lose_precision() {
1158 run_ranks(4, |g| {
1163 let v = match g.rank() {
1164 0 => 1e8f32,
1165 1 => -1e8f32,
1166 _ => 4.0f32,
1167 };
1168 let mut data = vec![v, v]; g.all_reduce_mode(&mut data, ReduceKind::Sum, ReduceMode::Deterministic)
1170 .unwrap();
1171 assert_eq!(data, vec![8.0, 8.0], "rank {}", g.rank());
1172 });
1173 }
1174
1175 #[test]
1176 fn deterministic_is_world_size_independent_for_replicated_input() {
1177 for &n in &[2u32, 3, 5, 7] {
1181 run_ranks(n, |g| {
1182 let mut data = vec![0.1f32, 0.2, 0.3, 0.7];
1183 g.all_reduce_mode(&mut data, ReduceKind::Mean, ReduceMode::Deterministic)
1184 .unwrap();
1185 assert_eq!(
1186 data,
1187 vec![0.1, 0.2, 0.3, 0.7],
1188 "n={} rank {}",
1189 g.world_size(),
1190 g.rank()
1191 );
1192 });
1193 }
1194 }
1195
1196 #[test]
1197 fn all_gather_concatenates_in_rank_order() {
1198 run_ranks(3, |g| {
1199 let r = g.rank() as f32;
1200 let out = g.all_gather(&[10.0 * r, 10.0 * r + 1.0]).unwrap();
1201 assert_eq!(
1202 out,
1203 vec![0.0, 1.0, 10.0, 11.0, 20.0, 21.0],
1204 "rank {}",
1205 g.rank()
1206 );
1207 });
1208 }
1209
1210 #[test]
1211 fn broadcast_from_root_overwrites() {
1212 run_ranks(4, |g| {
1213 let mut data = if g.is_leader() {
1214 vec![7.0, 8.0, 9.0]
1215 } else {
1216 vec![0.0, 0.0, 0.0]
1217 };
1218 g.broadcast(0, &mut data).unwrap();
1219 assert_eq!(data, vec![7.0, 8.0, 9.0], "rank {}", g.rank());
1220 });
1221 }
1222
1223 #[test]
1224 fn barrier_round_trips() {
1225 run_ranks(4, |g| {
1226 g.barrier().unwrap();
1227 g.barrier().unwrap(); });
1229 }
1230
1231 #[test]
1232 fn point_to_point_ring_handoff() {
1233 run_ranks(3, |g| {
1236 let n = g.world_size();
1237 let me = g.rank();
1238 let next = (me + 1) % n;
1239 let prev = (me + n - 1) % n;
1240 if me % 2 == 0 {
1243 g.send_f32(next, 1, &[me as f32]).unwrap();
1244 let got = g.recv_f32(prev, 1).unwrap();
1245 assert_eq!(got, vec![prev as f32]);
1246 } else {
1247 let got = g.recv_f32(prev, 1).unwrap();
1248 assert_eq!(got, vec![prev as f32]);
1249 g.send_f32(next, 1, &[me as f32]).unwrap();
1250 }
1251 });
1252 }
1253
1254 fn f16_bytes(vals: &[f32]) -> Vec<u8> {
1255 vals.iter()
1256 .flat_map(|&v| half::f16::from_f32(v).to_le_bytes())
1257 .collect()
1258 }
1259 fn f16_vals(bytes: &[u8]) -> Vec<f32> {
1260 bytes
1261 .chunks_exact(2)
1262 .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f32())
1263 .collect()
1264 }
1265
1266 #[test]
1267 fn all_reduce_typed_f16_sums_across_ranks() {
1268 run_ranks(4, |g| {
1269 let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 3]); g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1271 .unwrap();
1272 assert_eq!(f16_vals(&bytes), vec![10.0; 3], "rank {}", g.rank());
1273 });
1274 }
1275
1276 #[test]
1277 fn all_reduce_typed_bf16_mean() {
1278 run_ranks(4, |g| {
1279 let mut bytes: Vec<u8> = [g.rank() as f32 + 1.0; 4]
1280 .iter()
1281 .flat_map(|&v| half::bf16::from_f32(v).to_le_bytes())
1282 .collect();
1283 g.all_reduce_typed(&mut bytes, DType::BF16, ReduceKind::Mean)
1284 .unwrap();
1285 let got: Vec<f32> = bytes
1286 .chunks_exact(2)
1287 .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f32())
1288 .collect();
1289 assert_eq!(got, vec![2.5; 4], "rank {}", g.rank());
1291 });
1292 }
1293
1294 #[test]
1295 fn all_reduce_typed_uneven_length() {
1296 run_ranks(4, |g| {
1298 let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 5]);
1299 g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1300 .unwrap();
1301 assert_eq!(f16_vals(&bytes), vec![10.0; 5], "rank {}", g.rank());
1302 });
1303 }
1304
1305 #[test]
1306 fn all_reduce_typed_f16_large_buffer_simd_path() {
1307 run_ranks(3, |g| {
1311 let mut bytes = f16_bytes(&vec![g.rank() as f32 + 1.0; 1000]); g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1313 .unwrap();
1314 assert_eq!(f16_vals(&bytes), vec![6.0; 1000], "rank {}", g.rank());
1315 });
1316 }
1317
1318 #[test]
1319 fn all_reduce_typed_i8_federated_mean() {
1320 run_ranks(3, |g| {
1322 let mut bytes = vec![((g.rank() + 1) * 10) as i8 as u8; 4]; g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Mean)
1324 .unwrap();
1325 let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1326 assert_eq!(got, vec![20i8; 4], "rank {}", g.rank()); });
1328 }
1329
1330 #[test]
1331 fn all_reduce_typed_i8_sum_saturates() {
1332 run_ranks(2, |g| {
1333 let mut bytes = vec![100i8 as u8; 2];
1334 g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Sum)
1335 .unwrap();
1336 let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1337 assert_eq!(got, vec![127i8; 2], "rank {}", g.rank()); });
1339 }
1340
1341 #[test]
1342 fn negotiate_reduce_dtype_rules() {
1343 assert_eq!(
1344 negotiate_reduce_dtype(&[DType::F16, DType::BF16]),
1345 DType::F32
1346 );
1347 assert_eq!(
1348 negotiate_reduce_dtype(&[DType::F16, DType::F64]),
1349 DType::F64
1350 );
1351 assert_eq!(negotiate_reduce_dtype(&[]), DType::F32);
1352 }
1353}