1use crate::collective::ReduceKind;
33use crate::symmetric::CollectiveError;
34use half::slice::HalfFloatSliceExt;
35use rlx_ir::DType;
36use std::sync::Arc;
37use std::time::{Duration, Instant};
38
39pub const TAG_RESERVED_BASE: u32 = 0xFFF0_0000;
43const TAG_BARRIER: u32 = TAG_RESERVED_BASE;
44const TAG_ALL_REDUCE: u32 = TAG_RESERVED_BASE + 1;
45const TAG_ALL_GATHER: u32 = TAG_RESERVED_BASE + 2;
46const TAG_BROADCAST: u32 = TAG_RESERVED_BASE + 3;
47const TAG_ALL_TO_ALL: u32 = TAG_RESERVED_BASE + 4;
48const TAG_REDUCE: u32 = TAG_RESERVED_BASE + 5;
49const TAG_PPERMUTE: u32 = TAG_RESERVED_BASE + 6;
50const TAG_ALL_TO_ALL_V: u32 = TAG_RESERVED_BASE + 7;
53
54pub trait Transport: Send + Sync {
63 fn rank(&self) -> u32;
65 fn world_size(&self) -> u32;
67
68 fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError>;
71
72 fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError>;
75
76 fn recv_bytes_timeout(
82 &self,
83 from: u32,
84 tag: u32,
85 _timeout: Duration,
86 ) -> Result<Option<Vec<u8>>, CollectiveError> {
87 Ok(Some(self.recv_bytes(from, tag)?))
88 }
89
90 fn barrier(&self) -> Result<(), CollectiveError> {
98 default_barrier(self)
99 }
100}
101
102pub fn default_barrier<T: Transport + ?Sized>(t: &T) -> Result<(), CollectiveError> {
106 let n = t.world_size();
107 if n <= 1 {
108 return Ok(());
109 }
110 let me = t.rank();
111 if me == 0 {
112 for r in 1..n {
113 t.recv_bytes(r, TAG_BARRIER)?;
114 }
115 for r in 1..n {
116 t.send_bytes(r, TAG_BARRIER, &[1u8])?;
117 }
118 } else {
119 t.send_bytes(0, TAG_BARRIER, &[1u8])?;
120 t.recv_bytes(0, TAG_BARRIER)?;
121 }
122 Ok(())
123}
124
125fn f32_to_le_bytes(data: &[f32]) -> Vec<u8> {
129 let mut out = Vec::with_capacity(data.len() * 4);
130 for &x in data {
131 out.extend_from_slice(&x.to_le_bytes());
132 }
133 out
134}
135
136fn le_bytes_to_f32(bytes: &[u8]) -> Result<Vec<f32>, CollectiveError> {
139 if !bytes.len().is_multiple_of(4) {
140 return Err(CollectiveError::TransportError {
141 reason: format!("recv payload {} bytes is not a multiple of 4", bytes.len()),
142 });
143 }
144 Ok(bytes
145 .as_chunks::<4>()
146 .0
147 .iter()
148 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
149 .collect())
150}
151
152fn combine(op: ReduceKind, a: f32, b: f32) -> f32 {
153 match op {
154 ReduceKind::Sum | ReduceKind::Mean => a + b,
155 ReduceKind::Max => a.max(b),
156 ReduceKind::Min => a.min(b),
157 }
158}
159
160fn finalize(op: ReduceKind, acc: f32, n: u32) -> f32 {
161 match op {
162 ReduceKind::Mean => acc / (n as f32),
163 _ => acc,
164 }
165}
166
167fn finalize64(op: ReduceKind, acc: f64, n: u32) -> f64 {
169 match op {
170 ReduceKind::Mean => acc / (n as f64),
171 _ => acc,
172 }
173}
174
175fn ring_chunk(len: usize, n: usize, i: usize) -> (usize, usize) {
181 let base = len / n;
182 let rem = len % n;
183 let bound = |j: usize| j * base + j.min(rem);
184 (bound(i), bound(i + 1))
185}
186
187fn expect_len(got: usize, want: usize) -> Result<(), CollectiveError> {
189 if got != want {
190 return Err(CollectiveError::LengthMismatch {
191 expected: want,
192 got,
193 });
194 }
195 Ok(())
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum ReduceMode {
202 Ring,
205 Deterministic,
216}
217
218pub fn env_reduce_mode() -> ReduceMode {
226 static MODE: std::sync::OnceLock<ReduceMode> = std::sync::OnceLock::new();
227 *MODE.get_or_init(|| {
228 let on = |k: &str| {
229 std::env::var(k)
230 .map(|v| v != "0" && !v.is_empty())
231 .unwrap_or(false)
232 };
233 if on("RLX_DETERMINISTIC_REDUCE") {
234 ReduceMode::Deterministic
235 } else {
236 ReduceMode::Ring
237 }
238 })
239}
240
241fn combine64(op: ReduceKind, a: f64, b: f64) -> f64 {
244 match op {
245 ReduceKind::Sum | ReduceKind::Mean => a + b,
246 ReduceKind::Max => a.max(b),
247 ReduceKind::Min => a.min(b),
248 }
249}
250
251fn is_reducible_float(d: DType) -> bool {
253 matches!(d, DType::F16 | DType::BF16 | DType::F32 | DType::F64)
254}
255
256fn is_reducible_int(d: DType) -> bool {
259 matches!(d, DType::I8 | DType::U8)
260}
261
262fn decode_to_i32(bytes: &[u8], dtype: DType) -> Vec<i32> {
263 match dtype {
264 DType::I8 => bytes.iter().map(|&b| b as i8 as i32).collect(),
265 DType::U8 => bytes.iter().map(|&b| b as i32).collect(),
266 _ => Vec::new(),
267 }
268}
269
270fn encode_into_i32(dst: &mut [u8], vals: &[i32], dtype: DType) {
271 match dtype {
272 DType::I8 => {
273 for (d, &v) in dst.iter_mut().zip(vals) {
274 *d = v.clamp(i8::MIN as i32, i8::MAX as i32) as i8 as u8;
275 }
276 }
277 DType::U8 => {
278 for (d, &v) in dst.iter_mut().zip(vals) {
279 *d = v.clamp(0, u8::MAX as i32) as u8;
280 }
281 }
282 _ => {}
283 }
284}
285
286fn combine_i32(op: ReduceKind, a: i32, b: i32) -> i32 {
287 match op {
288 ReduceKind::Sum | ReduceKind::Mean => a + b,
289 ReduceKind::Max => a.max(b),
290 ReduceKind::Min => a.min(b),
291 }
292}
293
294fn decode_to_f64(bytes: &[u8], dtype: DType) -> Vec<f64> {
296 match dtype {
297 DType::F32 => bytes
298 .as_chunks::<4>()
299 .0
300 .iter()
301 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f64)
302 .collect(),
303 DType::F64 => bytes
304 .as_chunks::<8>()
305 .0
306 .iter()
307 .map(|c| f64::from_le_bytes(*c))
308 .collect(),
309 DType::F16 => bytes
310 .as_chunks::<2>()
311 .0
312 .iter()
313 .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f64())
314 .collect(),
315 DType::BF16 => bytes
316 .as_chunks::<2>()
317 .0
318 .iter()
319 .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f64())
320 .collect(),
321 _ => Vec::new(),
322 }
323}
324
325fn encode_from_f64(vals: &[f64], dtype: DType) -> Vec<u8> {
327 let mut out = Vec::with_capacity(vals.len() * dtype.size_bytes().max(1));
328 match dtype {
329 DType::F32 => vals
330 .iter()
331 .for_each(|&v| out.extend_from_slice(&(v as f32).to_le_bytes())),
332 DType::F64 => vals
333 .iter()
334 .for_each(|&v| out.extend_from_slice(&v.to_le_bytes())),
335 DType::F16 => vals
336 .iter()
337 .for_each(|&v| out.extend_from_slice(&half::f16::from_f64(v).to_le_bytes())),
338 DType::BF16 => vals
339 .iter()
340 .for_each(|&v| out.extend_from_slice(&half::bf16::from_f64(v).to_le_bytes())),
341 _ => {}
342 }
343 out
344}
345
346fn combine32(op: ReduceKind, a: f32, b: f32) -> f32 {
349 match op {
350 ReduceKind::Sum | ReduceKind::Mean => a + b,
351 ReduceKind::Max => a.max(b),
352 ReduceKind::Min => a.min(b),
353 }
354}
355
356fn decode_to_f32(bytes: &[u8], dtype: DType) -> Vec<f32> {
362 match dtype {
363 DType::F32 => {
364 #[cfg(target_endian = "little")]
365 {
366 let (head, mid, tail) = unsafe { bytes.align_to::<f32>() };
367 if head.is_empty() && tail.is_empty() {
368 return mid.to_vec();
369 }
370 }
371 bytes
372 .as_chunks::<4>()
373 .0
374 .iter()
375 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
376 .collect()
377 }
378 DType::F16 => {
379 let mut out = vec![0f32; bytes.len() / 2];
380 #[cfg(target_endian = "little")]
381 {
382 let (head, mid, tail) = unsafe { bytes.align_to::<half::f16>() };
385 if head.is_empty() && tail.is_empty() {
386 mid.convert_to_f32_slice(&mut out);
387 return out;
388 }
389 }
390 for (o, c) in out.iter_mut().zip(bytes.as_chunks::<2>().0.iter()) {
391 *o = half::f16::from_le_bytes([c[0], c[1]]).to_f32();
392 }
393 out
394 }
395 DType::BF16 => {
396 let mut out = vec![0f32; bytes.len() / 2];
397 #[cfg(target_endian = "little")]
398 {
399 let (head, mid, tail) = unsafe { bytes.align_to::<half::bf16>() };
400 if head.is_empty() && tail.is_empty() {
401 mid.convert_to_f32_slice(&mut out);
402 return out;
403 }
404 }
405 for (o, c) in out.iter_mut().zip(bytes.as_chunks::<2>().0.iter()) {
406 *o = half::bf16::from_le_bytes([c[0], c[1]]).to_f32();
407 }
408 out
409 }
410 _ => Vec::new(),
411 }
412}
413
414fn encode_into(dst: &mut [u8], vals: &[f32], dtype: DType) {
420 match dtype {
421 DType::F32 => {
422 for (c, &v) in dst.as_chunks_mut::<4>().0.iter_mut().zip(vals) {
423 c.copy_from_slice(&v.to_le_bytes());
424 }
425 }
426 DType::F16 => {
427 #[cfg(target_endian = "little")]
428 {
429 let (head, mid, tail) = unsafe { dst.align_to_mut::<half::f16>() };
430 if head.is_empty() && tail.is_empty() {
431 mid.convert_from_f32_slice(vals);
432 return;
433 }
434 }
435 for (c, &v) in dst.as_chunks_mut::<2>().0.iter_mut().zip(vals) {
436 c.copy_from_slice(&half::f16::from_f32(v).to_le_bytes());
437 }
438 }
439 DType::BF16 => {
440 #[cfg(target_endian = "little")]
441 {
442 let (head, mid, tail) = unsafe { dst.align_to_mut::<half::bf16>() };
443 if head.is_empty() && tail.is_empty() {
444 mid.convert_from_f32_slice(vals);
445 return;
446 }
447 }
448 for (c, &v) in dst.as_chunks_mut::<2>().0.iter_mut().zip(vals) {
449 c.copy_from_slice(&half::bf16::from_f32(v).to_le_bytes());
450 }
451 }
452 _ => {}
453 }
454}
455
456fn reduce_into(dst: &mut [u8], incoming: &[u8], dtype: DType, op: ReduceKind) {
459 if is_reducible_int(dtype) {
460 let mut acc = decode_to_i32(dst, dtype);
461 let b = decode_to_i32(incoming, dtype);
462 for (x, &y) in acc.iter_mut().zip(&b) {
463 *x = combine_i32(op, *x, y);
464 }
465 encode_into_i32(dst, &acc, dtype);
466 } else if dtype == DType::F64 {
467 let a = decode_to_f64(dst, dtype);
468 let b = decode_to_f64(incoming, dtype);
469 let m: Vec<f64> = a
470 .iter()
471 .zip(&b)
472 .map(|(&x, &y)| combine64(op, x, y))
473 .collect();
474 dst.copy_from_slice(&encode_from_f64(&m, dtype));
475 } else {
476 let mut acc = decode_to_f32(dst, dtype);
477 let b = decode_to_f32(incoming, dtype);
478 for (x, &y) in acc.iter_mut().zip(&b) {
479 *x = combine32(op, *x, y);
480 }
481 encode_into(dst, &acc, dtype);
482 }
483}
484
485fn scale_mean(data: &mut [u8], dtype: DType, n: usize) {
488 if is_reducible_int(dtype) {
489 let half = n as i32 / 2;
491 let mut acc = decode_to_i32(data, dtype);
492 for v in acc.iter_mut() {
493 *v = (*v + if *v >= 0 { half } else { -half }) / n as i32;
494 }
495 encode_into_i32(data, &acc, dtype);
496 } else if dtype == DType::F64 {
497 let scaled: Vec<f64> = decode_to_f64(data, dtype)
498 .iter()
499 .map(|&v| v / n as f64)
500 .collect();
501 data.copy_from_slice(&encode_from_f64(&scaled, dtype));
502 } else {
503 let inv = 1.0 / n as f32;
504 let mut acc = decode_to_f32(data, dtype);
505 for v in acc.iter_mut() {
506 *v *= inv;
507 }
508 encode_into(data, &acc, dtype);
509 }
510}
511
512pub fn negotiate_reduce_dtype(dtypes: &[DType]) -> DType {
518 if dtypes.contains(&DType::F64) {
519 DType::F64
520 } else {
521 DType::F32
522 }
523}
524
525#[derive(Clone)]
532pub struct ProcessGroup {
533 transport: Arc<dyn Transport>,
534}
535
536impl ProcessGroup {
537 pub fn new(transport: Arc<dyn Transport>) -> Self {
538 Self { transport }
539 }
540
541 pub fn rank(&self) -> u32 {
542 self.transport.rank()
543 }
544
545 pub fn world_size(&self) -> u32 {
546 self.transport.world_size()
547 }
548
549 pub fn is_leader(&self) -> bool {
552 self.rank() == 0
553 }
554
555 pub fn transport(&self) -> &Arc<dyn Transport> {
556 &self.transport
557 }
558
559 pub fn barrier(&self) -> Result<(), CollectiveError> {
560 self.transport.barrier()
561 }
562
563 pub fn send_f32(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
568 debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
569 self.send_f32_tagged(to, tag, data)
570 }
571
572 pub fn recv_f32_timeout(
581 &self,
582 from: u32,
583 tag: u32,
584 timeout: Duration,
585 ) -> Result<Option<Vec<f32>>, CollectiveError> {
586 debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
587 match self.transport.recv_bytes_timeout(from, tag, timeout)? {
588 Some(b) => Ok(Some(le_bytes_to_f32(&b)?)),
589 None => Ok(None),
590 }
591 }
592
593 pub fn recv_f32(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
594 debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
595 self.recv_f32_tagged(from, tag)
596 }
597
598 fn send_f32_tagged(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
601 self.transport.send_bytes(to, tag, &f32_to_le_bytes(data))
602 }
603
604 fn recv_f32_tagged(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
605 le_bytes_to_f32(&self.transport.recv_bytes(from, tag)?)
606 }
607
608 pub fn all_reduce(&self, data: &mut [f32], op: ReduceKind) -> Result<(), CollectiveError> {
620 self.all_reduce_mode(data, op, env_reduce_mode())
621 }
622
623 pub fn all_reduce_mode(
626 &self,
627 data: &mut [f32],
628 op: ReduceKind,
629 mode: ReduceMode,
630 ) -> Result<(), CollectiveError> {
631 let n = self.world_size();
632 if n <= 1 {
633 for v in data.iter_mut() {
634 *v = finalize(op, *v, n.max(1));
635 }
636 return Ok(());
637 }
638 match mode {
639 ReduceMode::Ring => self.all_reduce_ring_f32(data, op),
640 ReduceMode::Deterministic => self.all_reduce_deterministic(data, op),
641 }
642 }
643
644 fn all_reduce_ring_f32(&self, data: &mut [f32], op: ReduceKind) -> Result<(), CollectiveError> {
646 let n = self.world_size() as usize;
647 let me = self.rank() as usize;
648 let next = ((me + 1) % n) as u32;
649 let prev = ((me + n - 1) % n) as u32;
650 let len = data.len();
651
652 for step in 0..n - 1 {
656 let (ss, se) = ring_chunk(len, n, (me + n - step) % n);
657 let (rs, re) = ring_chunk(len, n, (me + n - step - 1) % n);
658 self.send_f32_tagged(next, TAG_ALL_REDUCE, &data[ss..se])?;
659 let incoming = self.recv_f32_tagged(prev, TAG_ALL_REDUCE)?;
660 expect_len(incoming.len(), re - rs)?;
661 for (d, v) in data[rs..re].iter_mut().zip(incoming) {
662 *d = combine(op, *d, v);
663 }
664 }
665 let (os, oe) = ring_chunk(len, n, (me + 1) % n);
669 for v in data[os..oe].iter_mut() {
670 *v = finalize(op, *v, n as u32);
671 }
672 self.ring_all_gather(data, n, me)
673 }
674
675 fn ring_all_gather(
679 &self,
680 data: &mut [f32],
681 n: usize,
682 me: usize,
683 ) -> Result<(), CollectiveError> {
684 let next = ((me + 1) % n) as u32;
685 let prev = ((me + n - 1) % n) as u32;
686 let len = data.len();
687 for step in 0..n - 1 {
688 let (ss, se) = ring_chunk(len, n, (me + n - step + 1) % n);
689 let (rs, re) = ring_chunk(len, n, (me + n - step) % n);
690 self.send_f32_tagged(next, TAG_ALL_REDUCE, &data[ss..se])?;
691 let incoming = self.recv_f32_tagged(prev, TAG_ALL_REDUCE)?;
692 expect_len(incoming.len(), re - rs)?;
693 data[rs..re].copy_from_slice(&incoming);
694 }
695 Ok(())
696 }
697
698 fn all_reduce_deterministic(
708 &self,
709 data: &mut [f32],
710 op: ReduceKind,
711 ) -> Result<(), CollectiveError> {
712 let n = self.world_size() as usize;
713 let me = self.rank() as usize;
714 let next = ((me + 1) % n) as u32;
715 let prev = ((me + n - 1) % n) as u32;
716 let len = data.len();
717
718 let mut acc: Vec<f64> = data.iter().map(|&v| v as f64).collect();
720 let mut send = Vec::with_capacity((len / n + 1) * 8);
722
723 for step in 0..n - 1 {
726 let (ss, se) = ring_chunk(len, n, (me + n - step) % n);
727 let (rs, re) = ring_chunk(len, n, (me + n - step - 1) % n);
728 send.clear();
729 send.extend(acc[ss..se].iter().flat_map(|v| v.to_le_bytes()));
730 self.transport.send_bytes(next, TAG_ALL_REDUCE, &send)?;
731 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
732 expect_len(incoming.len(), (re - rs) * 8)?;
733 for (a, c) in acc[rs..re]
734 .iter_mut()
735 .zip(incoming.as_chunks::<8>().0.iter())
736 {
737 *a = combine64(op, *a, f64::from_le_bytes(*c));
738 }
739 }
740
741 let (os, oe) = ring_chunk(len, n, (me + 1) % n);
744 for i in os..oe {
745 data[i] = finalize64(op, acc[i], n as u32) as f32;
746 }
747 self.ring_all_gather(data, n, me)
748 }
749
750 pub fn all_reduce_typed(
758 &self,
759 data: &mut [u8],
760 dtype: DType,
761 op: ReduceKind,
762 ) -> Result<(), CollectiveError> {
763 if !is_reducible_float(dtype) && !is_reducible_int(dtype) {
764 return Err(CollectiveError::TransportError {
765 reason: format!("all_reduce_typed: unsupported dtype {dtype:?}"),
766 });
767 }
768 let esz = dtype.size_bytes();
769 if esz == 0 || !data.len().is_multiple_of(esz) {
770 return Err(CollectiveError::TransportError {
771 reason: format!(
772 "all_reduce_typed: {} bytes not a multiple of {esz}",
773 data.len()
774 ),
775 });
776 }
777 let n = self.world_size();
778 if n <= 1 {
779 return Ok(()); }
781 let n = n as usize;
782 let me = self.rank() as usize;
783 let next = ((me + 1) % n) as u32;
784 let prev = ((me + n - 1) % n) as u32;
785
786 let elems = data.len() / esz;
787 let base = elems / n;
788 let rem = elems % n;
789 let ebound = |i: usize| (i * base + i.min(rem)) * esz; let chunk = |i: usize| (ebound(i), ebound(i + 1));
791
792 for step in 0..n - 1 {
794 let (ss, se) = chunk((me + n - step) % n);
795 let (rs, re) = chunk((me + n - step - 1) % n);
796 self.transport
797 .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
798 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
799 if incoming.len() != re - rs {
800 return Err(CollectiveError::LengthMismatch {
801 expected: re - rs,
802 got: incoming.len(),
803 });
804 }
805 reduce_into(&mut data[rs..re], &incoming, dtype, op);
806 }
807 for step in 0..n - 1 {
809 let (ss, se) = chunk((me + n - step + 1) % n);
810 let (rs, re) = chunk((me + n - step) % n);
811 self.transport
812 .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
813 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
814 if incoming.len() != re - rs {
815 return Err(CollectiveError::LengthMismatch {
816 expected: re - rs,
817 got: incoming.len(),
818 });
819 }
820 data[rs..re].copy_from_slice(&incoming);
821 }
822 if matches!(op, ReduceKind::Mean) {
823 scale_mean(data, dtype, n);
824 }
825 Ok(())
826 }
827
828 pub fn spawn_all_reduce(
834 self: &Arc<Self>,
835 mut data: Vec<f32>,
836 op: ReduceKind,
837 ) -> std::thread::JoinHandle<Vec<f32>> {
838 let g = self.clone();
839 std::thread::spawn(move || {
840 g.all_reduce(&mut data, op).expect("background all_reduce");
841 data
842 })
843 }
844
845 pub fn all_gather(&self, local: &[f32]) -> Result<Vec<f32>, CollectiveError> {
849 let n = self.world_size();
850 let len = local.len();
851 if n <= 1 {
852 return Ok(local.to_vec());
853 }
854 if self.rank() == 0 {
855 let mut out = vec![0f32; n as usize * len];
856 out[..len].copy_from_slice(local);
857 for r in 1..n {
858 let chunk = self.recv_f32_tagged(r, TAG_ALL_GATHER)?;
859 if chunk.len() != len {
860 return Err(CollectiveError::LengthMismatch {
861 expected: len,
862 got: chunk.len(),
863 });
864 }
865 let start = r as usize * len;
866 out[start..start + len].copy_from_slice(&chunk);
867 }
868 for r in 1..n {
869 self.send_f32_tagged(r, TAG_ALL_GATHER, &out)?;
870 }
871 Ok(out)
872 } else {
873 self.send_f32_tagged(0, TAG_ALL_GATHER, local)?;
874 let out = self.recv_f32_tagged(0, TAG_ALL_GATHER)?;
875 if out.len() != n as usize * len {
876 return Err(CollectiveError::LengthMismatch {
877 expected: n as usize * len,
878 got: out.len(),
879 });
880 }
881 Ok(out)
882 }
883 }
884
885 pub fn broadcast(&self, root: u32, data: &mut [f32]) -> Result<(), CollectiveError> {
888 let n = self.world_size();
889 if n <= 1 {
890 return Ok(());
891 }
892 if self.rank() == root {
893 for r in 0..n {
894 if r != root {
895 self.send_f32_tagged(r, TAG_BROADCAST, data)?;
896 }
897 }
898 } else {
899 let res = self.recv_f32_tagged(root, TAG_BROADCAST)?;
900 if res.len() != data.len() {
901 return Err(CollectiveError::LengthMismatch {
902 expected: data.len(),
903 got: res.len(),
904 });
905 }
906 data.copy_from_slice(&res);
907 }
908 Ok(())
909 }
910
911 pub fn all_to_all(&self, data: &[f32]) -> Result<Vec<f32>, CollectiveError> {
921 let n = self.world_size() as usize;
922 if n <= 1 {
923 return Ok(data.to_vec());
924 }
925 if !data.len().is_multiple_of(n) {
926 return Err(CollectiveError::LengthMismatch {
927 expected: n,
928 got: data.len(),
929 });
930 }
931 let chunk = data.len() / n;
932 let rank = self.rank() as usize;
933 let mut out = vec![0f32; data.len()];
934 out[rank * chunk..(rank + 1) * chunk]
935 .copy_from_slice(&data[rank * chunk..(rank + 1) * chunk]);
936 for p in 0..n {
937 if p == rank {
938 continue;
939 }
940 let send = &data[p * chunk..(p + 1) * chunk];
941 if rank < p {
943 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL, send)?;
944 let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL)?;
945 out[p * chunk..(p + 1) * chunk].copy_from_slice(&r);
946 } else {
947 let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL)?;
948 out[p * chunk..(p + 1) * chunk].copy_from_slice(&r);
949 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL, send)?;
950 }
951 }
952 Ok(out)
953 }
954
955 pub fn all_to_all_v(
966 &self,
967 data: &[f32],
968 send_counts: &[usize],
969 ) -> Result<(Vec<f32>, Vec<usize>), CollectiveError> {
970 let n = self.world_size() as usize;
971 if send_counts.len() != n {
972 return Err(CollectiveError::LengthMismatch {
973 expected: n,
974 got: send_counts.len(),
975 });
976 }
977 let total_send: usize = send_counts.iter().sum();
978 if data.len() != total_send {
979 return Err(CollectiveError::LengthMismatch {
980 expected: total_send,
981 got: data.len(),
982 });
983 }
984 if n <= 1 {
985 return Ok((data.to_vec(), send_counts.to_vec()));
986 }
987
988 let rank = self.rank() as usize;
989 let mut send_off = vec![0usize; n + 1];
990 for i in 0..n {
991 send_off[i + 1] = send_off[i] + send_counts[i];
992 }
993
994 let mut recv_counts = vec![0usize; n];
996 recv_counts[rank] = send_counts[rank];
997 for p in 0..n {
998 if p == rank {
999 continue;
1000 }
1001 let my_count = (send_counts[p] as u64).to_le_bytes();
1002 let their = if rank < p {
1003 self.transport
1004 .send_bytes(p as u32, TAG_ALL_TO_ALL_V, &my_count)?;
1005 self.transport.recv_bytes(p as u32, TAG_ALL_TO_ALL_V)?
1006 } else {
1007 let r = self.transport.recv_bytes(p as u32, TAG_ALL_TO_ALL_V)?;
1008 self.transport
1009 .send_bytes(p as u32, TAG_ALL_TO_ALL_V, &my_count)?;
1010 r
1011 };
1012 if their.len() != 8 {
1013 return Err(CollectiveError::TransportError {
1014 reason: format!(
1015 "all_to_all_v: count from rank {p} has {} bytes, want 8",
1016 their.len()
1017 ),
1018 });
1019 }
1020 recv_counts[p] = u64::from_le_bytes(their[..].try_into().unwrap()) as usize;
1021 }
1022
1023 let mut recv_off = vec![0usize; n + 1];
1024 for i in 0..n {
1025 recv_off[i + 1] = recv_off[i] + recv_counts[i];
1026 }
1027 let mut out = vec![0f32; recv_off[n]];
1028 out[recv_off[rank]..recv_off[rank + 1]]
1029 .copy_from_slice(&data[send_off[rank]..send_off[rank + 1]]);
1030
1031 for p in 0..n {
1033 if p == rank {
1034 continue;
1035 }
1036 let send = &data[send_off[p]..send_off[p + 1]];
1037 let got = if rank < p {
1038 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL_V, send)?;
1039 self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL_V)?
1040 } else {
1041 let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL_V)?;
1042 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL_V, send)?;
1043 r
1044 };
1045 if got.len() != recv_counts[p] {
1046 return Err(CollectiveError::LengthMismatch {
1047 expected: recv_counts[p],
1048 got: got.len(),
1049 });
1050 }
1051 out[recv_off[p]..recv_off[p + 1]].copy_from_slice(&got);
1052 }
1053 Ok((out, recv_counts))
1054 }
1055
1056 pub fn reduce(
1060 &self,
1061 root: u32,
1062 data: &mut [f32],
1063 op: ReduceKind,
1064 ) -> Result<(), CollectiveError> {
1065 let n = self.world_size();
1066 if n <= 1 {
1067 return Ok(());
1068 }
1069 if self.rank() == root {
1070 let mut acc = data.to_vec();
1071 for r in 0..n {
1072 if r == root {
1073 continue;
1074 }
1075 let other = self.recv_f32_tagged(r, TAG_REDUCE)?;
1076 if other.len() != acc.len() {
1077 return Err(CollectiveError::LengthMismatch {
1078 expected: acc.len(),
1079 got: other.len(),
1080 });
1081 }
1082 for i in 0..acc.len() {
1083 acc[i] = op.fold(acc[i], other[i]);
1084 }
1085 }
1086 for v in acc.iter_mut() {
1087 *v = op.finalize(*v, n as usize);
1088 }
1089 data.copy_from_slice(&acc);
1090 } else {
1091 self.send_f32_tagged(root, TAG_REDUCE, data)?;
1092 }
1093 Ok(())
1094 }
1095
1096 pub fn ppermute(&self, data: &[f32], perm: &[(u32, u32)]) -> Result<Vec<f32>, CollectiveError> {
1100 let rank = self.rank();
1101 let my_dst = perm.iter().find(|&&(s, _)| s == rank).map(|&(_, d)| d);
1102 let my_src = perm.iter().find(|&&(_, d)| d == rank).map(|&(s, _)| s);
1103 if let Some(dst) = my_dst {
1105 if dst != rank {
1106 self.send_f32_tagged(dst, TAG_PPERMUTE, data)?;
1107 }
1108 }
1109 let out = match my_src {
1111 Some(src) if src == rank => data.to_vec(),
1112 Some(src) => self.recv_f32_tagged(src, TAG_PPERMUTE)?,
1113 None => vec![0f32; data.len()],
1114 };
1115 Ok(out)
1116 }
1117
1118 pub fn federated_average(
1125 &self,
1126 data: &mut [f32],
1127 deadline: Duration,
1128 ) -> Result<usize, CollectiveError> {
1129 let n = self.world_size();
1130 if n <= 1 {
1131 return Ok(1);
1132 }
1133 let elems = data.len();
1134 if self.rank() == 0 {
1135 let mut acc = data.to_vec();
1136 let mut present = 1usize;
1137 let end = Instant::now() + deadline;
1138 for r in 1..n {
1139 let remaining = end.saturating_duration_since(Instant::now());
1140 if let Some(bytes) =
1141 self.transport
1142 .recv_bytes_timeout(r, TAG_ALL_REDUCE, remaining)?
1143 {
1144 let other = le_bytes_to_f32(&bytes)?;
1145 if other.len() == elems {
1146 for i in 0..elems {
1147 acc[i] += other[i];
1148 }
1149 present += 1;
1150 }
1151 }
1152 }
1153 let inv = 1.0 / present as f32;
1154 for v in acc.iter_mut() {
1155 *v *= inv;
1156 }
1157 data.copy_from_slice(&acc);
1158 for r in 1..n {
1159 self.send_f32_tagged(r, TAG_ALL_REDUCE, &acc)?;
1160 }
1161 Ok(present)
1162 } else {
1163 self.send_f32_tagged(0, TAG_ALL_REDUCE, data)?;
1164 let res = self.recv_f32_tagged(0, TAG_ALL_REDUCE)?;
1165 if res.len() == elems {
1166 data.copy_from_slice(&res);
1167 }
1168 Ok(0)
1169 }
1170 }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 use super::*;
1176 use std::collections::{HashMap, VecDeque};
1177 use std::sync::{Condvar, Mutex};
1178
1179 struct ChannelTransport {
1183 rank: u32,
1184 world: u32,
1185 mailbox: Arc<(Mutex<HashMap<(u32, u32, u32), VecDeque<Vec<u8>>>>, Condvar)>,
1186 }
1187
1188 impl ChannelTransport {
1189 fn fan_out(world: u32) -> Vec<Arc<ChannelTransport>> {
1190 let mailbox = Arc::new((Mutex::new(HashMap::new()), Condvar::new()));
1191 (0..world)
1192 .map(|r| {
1193 Arc::new(ChannelTransport {
1194 rank: r,
1195 world,
1196 mailbox: mailbox.clone(),
1197 })
1198 })
1199 .collect()
1200 }
1201 }
1202
1203 impl Transport for ChannelTransport {
1204 fn rank(&self) -> u32 {
1205 self.rank
1206 }
1207 fn world_size(&self) -> u32 {
1208 self.world
1209 }
1210 fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError> {
1211 let (m, cv) = &*self.mailbox;
1212 m.lock()
1213 .unwrap()
1214 .entry((to, self.rank, tag))
1215 .or_default()
1216 .push_back(bytes.to_vec());
1217 cv.notify_all();
1218 Ok(())
1219 }
1220 fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError> {
1221 let (m, cv) = &*self.mailbox;
1222 let mut guard = m.lock().unwrap();
1223 loop {
1224 if let Some(q) = guard.get_mut(&(self.rank, from, tag))
1225 && let Some(v) = q.pop_front()
1226 {
1227 return Ok(v);
1228 }
1229 guard = cv.wait(guard).unwrap();
1230 }
1231 }
1232 }
1233
1234 fn run_ranks<F>(world: u32, body: F) -> Vec<()>
1235 where
1236 F: Fn(ProcessGroup) + Send + Sync + 'static,
1237 {
1238 let ts = ChannelTransport::fan_out(world);
1239 let body = Arc::new(body);
1240 let handles: Vec<_> = ts
1241 .into_iter()
1242 .map(|t| {
1243 let body = body.clone();
1244 std::thread::spawn(move || body(ProcessGroup::new(t)))
1245 })
1246 .collect();
1247 handles.into_iter().map(|h| h.join().unwrap()).collect()
1248 }
1249
1250 #[test]
1251 fn all_reduce_sum_matches_serial() {
1252 run_ranks(4, |g| {
1253 let r = g.rank() as f32;
1254 let mut data = vec![r + 1.0; 3]; g.all_reduce(&mut data, ReduceKind::Sum).unwrap();
1256 assert_eq!(data, vec![10.0; 3], "rank {}", g.rank());
1257 });
1258 }
1259
1260 #[test]
1261 fn all_reduce_modes_agree_on_the_sum() {
1262 for mode in [ReduceMode::Ring, ReduceMode::Deterministic] {
1264 run_ranks(4, move |g| {
1265 let r = g.rank() as f32 + 1.0; let mut data = vec![r, r * 2.0, r * 3.0];
1267 g.all_reduce_mode(&mut data, ReduceKind::Sum, mode).unwrap();
1268 assert_eq!(
1269 data,
1270 vec![10.0, 20.0, 30.0],
1271 "mode {mode:?} rank {}",
1272 g.rank()
1273 );
1274 });
1275 }
1276 }
1277
1278 #[test]
1279 fn deterministic_reduce_mean_divides() {
1280 run_ranks(4, |g| {
1281 let mut data = vec![g.rank() as f32 + 1.0; 2]; g.all_reduce_mode(&mut data, ReduceKind::Mean, ReduceMode::Deterministic)
1283 .unwrap();
1284 assert_eq!(data, vec![2.5, 2.5], "rank {}", g.rank());
1285 });
1286 }
1287
1288 #[test]
1289 fn deterministic_is_exact_where_the_f32_ring_would_lose_precision() {
1290 run_ranks(4, |g| {
1295 let v = match g.rank() {
1296 0 => 1e8f32,
1297 1 => -1e8f32,
1298 _ => 4.0f32,
1299 };
1300 let mut data = vec![v, v]; g.all_reduce_mode(&mut data, ReduceKind::Sum, ReduceMode::Deterministic)
1302 .unwrap();
1303 assert_eq!(data, vec![8.0, 8.0], "rank {}", g.rank());
1304 });
1305 }
1306
1307 #[test]
1308 fn deterministic_is_world_size_independent_for_replicated_input() {
1309 for &n in &[2u32, 3, 5, 7] {
1313 run_ranks(n, |g| {
1314 let mut data = vec![0.1f32, 0.2, 0.3, 0.7];
1315 g.all_reduce_mode(&mut data, ReduceKind::Mean, ReduceMode::Deterministic)
1316 .unwrap();
1317 assert_eq!(
1318 data,
1319 vec![0.1, 0.2, 0.3, 0.7],
1320 "n={} rank {}",
1321 g.world_size(),
1322 g.rank()
1323 );
1324 });
1325 }
1326 }
1327
1328 #[test]
1329 fn all_gather_concatenates_in_rank_order() {
1330 run_ranks(3, |g| {
1331 let r = g.rank() as f32;
1332 let out = g.all_gather(&[10.0 * r, 10.0 * r + 1.0]).unwrap();
1333 assert_eq!(
1334 out,
1335 vec![0.0, 1.0, 10.0, 11.0, 20.0, 21.0],
1336 "rank {}",
1337 g.rank()
1338 );
1339 });
1340 }
1341
1342 #[test]
1343 fn broadcast_from_root_overwrites() {
1344 run_ranks(4, |g| {
1345 let mut data = if g.is_leader() {
1346 vec![7.0, 8.0, 9.0]
1347 } else {
1348 vec![0.0, 0.0, 0.0]
1349 };
1350 g.broadcast(0, &mut data).unwrap();
1351 assert_eq!(data, vec![7.0, 8.0, 9.0], "rank {}", g.rank());
1352 });
1353 }
1354
1355 #[test]
1356 fn all_to_all_v_variable_chunks() {
1357 run_ranks(3, |g| {
1360 let n = g.world_size() as usize;
1361 let r = g.rank() as usize;
1362 let send_counts: Vec<usize> = (0..n).map(|j| j + 1).collect();
1363 let mut data = Vec::new();
1364 for (j, &c) in send_counts.iter().enumerate() {
1365 for _ in 0..c {
1366 data.push((100 * r + j) as f32);
1367 }
1368 }
1369 let (recv, recv_counts) = g.all_to_all_v(&data, &send_counts).unwrap();
1370 assert_eq!(recv_counts, vec![r + 1; n], "rank {r} recv_counts");
1371 let mut off = 0usize;
1372 for (j, &c) in recv_counts.iter().enumerate() {
1373 let chunk = &recv[off..off + c];
1374 assert!(
1375 chunk
1376 .iter()
1377 .all(|&v| (v - (100 * j + r) as f32).abs() < 1e-6),
1378 "rank {r} from {j}: {chunk:?}"
1379 );
1380 off += c;
1381 }
1382 });
1383 }
1384
1385 #[test]
1386 fn barrier_round_trips() {
1387 run_ranks(4, |g| {
1388 g.barrier().unwrap();
1389 g.barrier().unwrap(); });
1391 }
1392
1393 #[test]
1394 fn point_to_point_ring_handoff() {
1395 run_ranks(3, |g| {
1398 let n = g.world_size();
1399 let me = g.rank();
1400 let next = (me + 1) % n;
1401 let prev = (me + n - 1) % n;
1402 if me % 2 == 0 {
1405 g.send_f32(next, 1, &[me as f32]).unwrap();
1406 let got = g.recv_f32(prev, 1).unwrap();
1407 assert_eq!(got, vec![prev as f32]);
1408 } else {
1409 let got = g.recv_f32(prev, 1).unwrap();
1410 assert_eq!(got, vec![prev as f32]);
1411 g.send_f32(next, 1, &[me as f32]).unwrap();
1412 }
1413 });
1414 }
1415
1416 fn f16_bytes(vals: &[f32]) -> Vec<u8> {
1417 vals.iter()
1418 .flat_map(|&v| half::f16::from_f32(v).to_le_bytes())
1419 .collect()
1420 }
1421 fn f16_vals(bytes: &[u8]) -> Vec<f32> {
1422 bytes
1423 .as_chunks::<2>()
1424 .0
1425 .iter()
1426 .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f32())
1427 .collect()
1428 }
1429
1430 #[test]
1431 fn all_reduce_typed_f16_sums_across_ranks() {
1432 run_ranks(4, |g| {
1433 let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 3]); g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1435 .unwrap();
1436 assert_eq!(f16_vals(&bytes), vec![10.0; 3], "rank {}", g.rank());
1437 });
1438 }
1439
1440 #[test]
1441 fn all_reduce_typed_bf16_mean() {
1442 run_ranks(4, |g| {
1443 let mut bytes: Vec<u8> = [g.rank() as f32 + 1.0; 4]
1444 .iter()
1445 .flat_map(|&v| half::bf16::from_f32(v).to_le_bytes())
1446 .collect();
1447 g.all_reduce_typed(&mut bytes, DType::BF16, ReduceKind::Mean)
1448 .unwrap();
1449 let got: Vec<f32> = bytes
1450 .as_chunks::<2>()
1451 .0
1452 .iter()
1453 .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f32())
1454 .collect();
1455 assert_eq!(got, vec![2.5; 4], "rank {}", g.rank());
1457 });
1458 }
1459
1460 #[test]
1461 fn all_reduce_typed_uneven_length() {
1462 run_ranks(4, |g| {
1464 let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 5]);
1465 g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1466 .unwrap();
1467 assert_eq!(f16_vals(&bytes), vec![10.0; 5], "rank {}", g.rank());
1468 });
1469 }
1470
1471 #[test]
1472 fn all_reduce_typed_f16_large_buffer_simd_path() {
1473 run_ranks(3, |g| {
1477 let mut bytes = f16_bytes(&vec![g.rank() as f32 + 1.0; 1000]); g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1479 .unwrap();
1480 assert_eq!(f16_vals(&bytes), vec![6.0; 1000], "rank {}", g.rank());
1481 });
1482 }
1483
1484 #[test]
1485 fn all_reduce_typed_i8_federated_mean() {
1486 run_ranks(3, |g| {
1488 let mut bytes = vec![((g.rank() + 1) * 10) as i8 as u8; 4]; g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Mean)
1490 .unwrap();
1491 let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1492 assert_eq!(got, vec![20i8; 4], "rank {}", g.rank()); });
1494 }
1495
1496 #[test]
1497 fn all_reduce_typed_i8_sum_saturates() {
1498 run_ranks(2, |g| {
1499 let mut bytes = vec![100i8 as u8; 2];
1500 g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Sum)
1501 .unwrap();
1502 let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1503 assert_eq!(got, vec![127i8; 2], "rank {}", g.rank()); });
1505 }
1506
1507 #[test]
1508 fn negotiate_reduce_dtype_rules() {
1509 assert_eq!(
1510 negotiate_reduce_dtype(&[DType::F16, DType::BF16]),
1511 DType::F32
1512 );
1513 assert_eq!(
1514 negotiate_reduce_dtype(&[DType::F16, DType::F64]),
1515 DType::F64
1516 );
1517 assert_eq!(negotiate_reduce_dtype(&[]), DType::F32);
1518 }
1519}