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;
58
59pub trait Transport: Send + Sync {
68 fn rank(&self) -> u32;
70 fn world_size(&self) -> u32;
72
73 fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError>;
76
77 fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError>;
80
81 fn recv_bytes_timeout(
87 &self,
88 from: u32,
89 tag: u32,
90 _timeout: Duration,
91 ) -> Result<Option<Vec<u8>>, CollectiveError> {
92 Ok(Some(self.recv_bytes(from, tag)?))
93 }
94
95 fn barrier(&self) -> Result<(), CollectiveError> {
103 default_barrier(self)
104 }
105}
106
107pub fn default_barrier<T: Transport + ?Sized>(t: &T) -> Result<(), CollectiveError> {
111 let n = t.world_size();
112 if n <= 1 {
113 return Ok(());
114 }
115 let me = t.rank();
116 if me == 0 {
117 for r in 1..n {
118 t.recv_bytes(r, TAG_BARRIER)?;
119 }
120 for r in 1..n {
121 t.send_bytes(r, TAG_BARRIER, &[1u8])?;
122 }
123 } else {
124 t.send_bytes(0, TAG_BARRIER, &[1u8])?;
125 t.recv_bytes(0, TAG_BARRIER)?;
126 }
127 Ok(())
128}
129
130fn f32_to_le_bytes(data: &[f32]) -> Vec<u8> {
134 let mut out = Vec::with_capacity(data.len() * 4);
135 for &x in data {
136 out.extend_from_slice(&x.to_le_bytes());
137 }
138 out
139}
140
141fn le_bytes_to_f32(bytes: &[u8]) -> Result<Vec<f32>, CollectiveError> {
144 if !bytes.len().is_multiple_of(4) {
145 return Err(CollectiveError::TransportError {
146 reason: format!("recv payload {} bytes is not a multiple of 4", bytes.len()),
147 });
148 }
149 Ok(bytes
150 .chunks_exact(4)
151 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
152 .collect())
153}
154
155fn combine(op: ReduceKind, a: f32, b: f32) -> f32 {
156 match op {
157 ReduceKind::Sum | ReduceKind::Mean => a + b,
158 ReduceKind::Max => a.max(b),
159 ReduceKind::Min => a.min(b),
160 }
161}
162
163fn finalize(op: ReduceKind, acc: f32, n: u32) -> f32 {
164 match op {
165 ReduceKind::Mean => acc / (n as f32),
166 _ => acc,
167 }
168}
169
170fn combine64(op: ReduceKind, a: f64, b: f64) -> f64 {
173 match op {
174 ReduceKind::Sum | ReduceKind::Mean => a + b,
175 ReduceKind::Max => a.max(b),
176 ReduceKind::Min => a.min(b),
177 }
178}
179
180fn is_reducible_float(d: DType) -> bool {
182 matches!(d, DType::F16 | DType::BF16 | DType::F32 | DType::F64)
183}
184
185fn is_reducible_int(d: DType) -> bool {
188 matches!(d, DType::I8 | DType::U8)
189}
190
191fn decode_to_i32(bytes: &[u8], dtype: DType) -> Vec<i32> {
192 match dtype {
193 DType::I8 => bytes.iter().map(|&b| b as i8 as i32).collect(),
194 DType::U8 => bytes.iter().map(|&b| b as i32).collect(),
195 _ => Vec::new(),
196 }
197}
198
199fn encode_into_i32(dst: &mut [u8], vals: &[i32], dtype: DType) {
200 match dtype {
201 DType::I8 => {
202 for (d, &v) in dst.iter_mut().zip(vals) {
203 *d = v.clamp(i8::MIN as i32, i8::MAX as i32) as i8 as u8;
204 }
205 }
206 DType::U8 => {
207 for (d, &v) in dst.iter_mut().zip(vals) {
208 *d = v.clamp(0, u8::MAX as i32) as u8;
209 }
210 }
211 _ => {}
212 }
213}
214
215fn combine_i32(op: ReduceKind, a: i32, b: i32) -> i32 {
216 match op {
217 ReduceKind::Sum | ReduceKind::Mean => a + b,
218 ReduceKind::Max => a.max(b),
219 ReduceKind::Min => a.min(b),
220 }
221}
222
223fn decode_to_f64(bytes: &[u8], dtype: DType) -> Vec<f64> {
225 match dtype {
226 DType::F32 => bytes
227 .chunks_exact(4)
228 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f64)
229 .collect(),
230 DType::F64 => bytes
231 .chunks_exact(8)
232 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
233 .collect(),
234 DType::F16 => bytes
235 .chunks_exact(2)
236 .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f64())
237 .collect(),
238 DType::BF16 => bytes
239 .chunks_exact(2)
240 .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f64())
241 .collect(),
242 _ => Vec::new(),
243 }
244}
245
246fn encode_from_f64(vals: &[f64], dtype: DType) -> Vec<u8> {
248 let mut out = Vec::with_capacity(vals.len() * dtype.size_bytes().max(1));
249 match dtype {
250 DType::F32 => vals
251 .iter()
252 .for_each(|&v| out.extend_from_slice(&(v as f32).to_le_bytes())),
253 DType::F64 => vals
254 .iter()
255 .for_each(|&v| out.extend_from_slice(&v.to_le_bytes())),
256 DType::F16 => vals
257 .iter()
258 .for_each(|&v| out.extend_from_slice(&half::f16::from_f64(v).to_le_bytes())),
259 DType::BF16 => vals
260 .iter()
261 .for_each(|&v| out.extend_from_slice(&half::bf16::from_f64(v).to_le_bytes())),
262 _ => {}
263 }
264 out
265}
266
267fn combine32(op: ReduceKind, a: f32, b: f32) -> f32 {
270 match op {
271 ReduceKind::Sum | ReduceKind::Mean => a + b,
272 ReduceKind::Max => a.max(b),
273 ReduceKind::Min => a.min(b),
274 }
275}
276
277fn decode_to_f32(bytes: &[u8], dtype: DType) -> Vec<f32> {
283 match dtype {
284 DType::F32 => {
285 #[cfg(target_endian = "little")]
286 {
287 let (head, mid, tail) = unsafe { bytes.align_to::<f32>() };
288 if head.is_empty() && tail.is_empty() {
289 return mid.to_vec();
290 }
291 }
292 bytes
293 .chunks_exact(4)
294 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
295 .collect()
296 }
297 DType::F16 => {
298 let mut out = vec![0f32; bytes.len() / 2];
299 #[cfg(target_endian = "little")]
300 {
301 let (head, mid, tail) = unsafe { bytes.align_to::<half::f16>() };
304 if head.is_empty() && tail.is_empty() {
305 mid.convert_to_f32_slice(&mut out);
306 return out;
307 }
308 }
309 for (o, c) in out.iter_mut().zip(bytes.chunks_exact(2)) {
310 *o = half::f16::from_le_bytes([c[0], c[1]]).to_f32();
311 }
312 out
313 }
314 DType::BF16 => {
315 let mut out = vec![0f32; bytes.len() / 2];
316 #[cfg(target_endian = "little")]
317 {
318 let (head, mid, tail) = unsafe { bytes.align_to::<half::bf16>() };
319 if head.is_empty() && tail.is_empty() {
320 mid.convert_to_f32_slice(&mut out);
321 return out;
322 }
323 }
324 for (o, c) in out.iter_mut().zip(bytes.chunks_exact(2)) {
325 *o = half::bf16::from_le_bytes([c[0], c[1]]).to_f32();
326 }
327 out
328 }
329 _ => Vec::new(),
330 }
331}
332
333fn encode_into(dst: &mut [u8], vals: &[f32], dtype: DType) {
339 match dtype {
340 DType::F32 => {
341 for (c, &v) in dst.chunks_exact_mut(4).zip(vals) {
342 c.copy_from_slice(&v.to_le_bytes());
343 }
344 }
345 DType::F16 => {
346 #[cfg(target_endian = "little")]
347 {
348 let (head, mid, tail) = unsafe { dst.align_to_mut::<half::f16>() };
349 if head.is_empty() && tail.is_empty() {
350 mid.convert_from_f32_slice(vals);
351 return;
352 }
353 }
354 for (c, &v) in dst.chunks_exact_mut(2).zip(vals) {
355 c.copy_from_slice(&half::f16::from_f32(v).to_le_bytes());
356 }
357 }
358 DType::BF16 => {
359 #[cfg(target_endian = "little")]
360 {
361 let (head, mid, tail) = unsafe { dst.align_to_mut::<half::bf16>() };
362 if head.is_empty() && tail.is_empty() {
363 mid.convert_from_f32_slice(vals);
364 return;
365 }
366 }
367 for (c, &v) in dst.chunks_exact_mut(2).zip(vals) {
368 c.copy_from_slice(&half::bf16::from_f32(v).to_le_bytes());
369 }
370 }
371 _ => {}
372 }
373}
374
375fn reduce_into(dst: &mut [u8], incoming: &[u8], dtype: DType, op: ReduceKind) {
378 if is_reducible_int(dtype) {
379 let mut acc = decode_to_i32(dst, dtype);
380 let b = decode_to_i32(incoming, dtype);
381 for (x, &y) in acc.iter_mut().zip(&b) {
382 *x = combine_i32(op, *x, y);
383 }
384 encode_into_i32(dst, &acc, dtype);
385 } else if dtype == DType::F64 {
386 let a = decode_to_f64(dst, dtype);
387 let b = decode_to_f64(incoming, dtype);
388 let m: Vec<f64> = a
389 .iter()
390 .zip(&b)
391 .map(|(&x, &y)| combine64(op, x, y))
392 .collect();
393 dst.copy_from_slice(&encode_from_f64(&m, dtype));
394 } else {
395 let mut acc = decode_to_f32(dst, dtype);
396 let b = decode_to_f32(incoming, dtype);
397 for (x, &y) in acc.iter_mut().zip(&b) {
398 *x = combine32(op, *x, y);
399 }
400 encode_into(dst, &acc, dtype);
401 }
402}
403
404fn scale_mean(data: &mut [u8], dtype: DType, n: usize) {
407 if is_reducible_int(dtype) {
408 let half = n as i32 / 2;
410 let mut acc = decode_to_i32(data, dtype);
411 for v in acc.iter_mut() {
412 *v = (*v + if *v >= 0 { half } else { -half }) / n as i32;
413 }
414 encode_into_i32(data, &acc, dtype);
415 } else if dtype == DType::F64 {
416 let scaled: Vec<f64> = decode_to_f64(data, dtype)
417 .iter()
418 .map(|&v| v / n as f64)
419 .collect();
420 data.copy_from_slice(&encode_from_f64(&scaled, dtype));
421 } else {
422 let inv = 1.0 / n as f32;
423 let mut acc = decode_to_f32(data, dtype);
424 for v in acc.iter_mut() {
425 *v *= inv;
426 }
427 encode_into(data, &acc, dtype);
428 }
429}
430
431pub fn negotiate_reduce_dtype(dtypes: &[DType]) -> DType {
437 if dtypes.contains(&DType::F64) {
438 DType::F64
439 } else {
440 DType::F32
441 }
442}
443
444#[derive(Clone)]
451pub struct ProcessGroup {
452 transport: Arc<dyn Transport>,
453}
454
455impl ProcessGroup {
456 pub fn new(transport: Arc<dyn Transport>) -> Self {
457 Self { transport }
458 }
459
460 pub fn rank(&self) -> u32 {
461 self.transport.rank()
462 }
463
464 pub fn world_size(&self) -> u32 {
465 self.transport.world_size()
466 }
467
468 pub fn is_leader(&self) -> bool {
471 self.rank() == 0
472 }
473
474 pub fn transport(&self) -> &Arc<dyn Transport> {
475 &self.transport
476 }
477
478 pub fn barrier(&self) -> Result<(), CollectiveError> {
479 self.transport.barrier()
480 }
481
482 pub fn send_f32(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
487 debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
488 self.send_f32_tagged(to, tag, data)
489 }
490
491 pub fn recv_f32(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
493 debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
494 self.recv_f32_tagged(from, tag)
495 }
496
497 fn send_f32_tagged(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
500 self.transport.send_bytes(to, tag, &f32_to_le_bytes(data))
501 }
502
503 fn recv_f32_tagged(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
504 le_bytes_to_f32(&self.transport.recv_bytes(from, tag)?)
505 }
506
507 pub fn all_reduce(&self, data: &mut [f32], op: ReduceKind) -> Result<(), CollectiveError> {
519 let n = self.world_size();
520 if n <= 1 {
521 for v in data.iter_mut() {
522 *v = finalize(op, *v, n.max(1));
523 }
524 return Ok(());
525 }
526 let n = n as usize;
527 let me = self.rank() as usize;
528 let next = ((me + 1) % n) as u32;
529 let prev = ((me + n - 1) % n) as u32;
530
531 let len = data.len();
533 let base = len / n;
534 let rem = len % n;
535 let bound = |i: usize| i * base + i.min(rem);
536 let chunk = |i: usize| (bound(i), bound(i + 1));
537
538 let expect = |incoming: &[f32], want: usize| -> Result<(), CollectiveError> {
542 if incoming.len() != want {
543 return Err(CollectiveError::LengthMismatch {
544 expected: want,
545 got: incoming.len(),
546 });
547 }
548 Ok(())
549 };
550
551 for step in 0..n - 1 {
553 let (ss, se) = chunk((me + n - step) % n);
554 let (rs, re) = chunk((me + n - step - 1) % n);
555 self.send_f32_tagged(next, TAG_ALL_REDUCE, &data[ss..se])?;
556 let incoming = self.recv_f32_tagged(prev, TAG_ALL_REDUCE)?;
557 expect(&incoming, re - rs)?;
558 for (d, v) in data[rs..re].iter_mut().zip(incoming) {
559 *d = combine(op, *d, v);
560 }
561 }
562 for step in 0..n - 1 {
564 let (ss, se) = chunk((me + n - step + 1) % n);
565 let (rs, re) = chunk((me + n - step) % n);
566 self.send_f32_tagged(next, TAG_ALL_REDUCE, &data[ss..se])?;
567 let incoming = self.recv_f32_tagged(prev, TAG_ALL_REDUCE)?;
568 expect(&incoming, re - rs)?;
569 data[rs..re].copy_from_slice(&incoming);
570 }
571 if matches!(op, ReduceKind::Mean) {
573 let inv = 1.0 / n as f32;
574 for v in data.iter_mut() {
575 *v *= inv;
576 }
577 }
578 Ok(())
579 }
580
581 pub fn all_reduce_typed(
589 &self,
590 data: &mut [u8],
591 dtype: DType,
592 op: ReduceKind,
593 ) -> Result<(), CollectiveError> {
594 if !is_reducible_float(dtype) && !is_reducible_int(dtype) {
595 return Err(CollectiveError::TransportError {
596 reason: format!("all_reduce_typed: unsupported dtype {dtype:?}"),
597 });
598 }
599 let esz = dtype.size_bytes();
600 if esz == 0 || !data.len().is_multiple_of(esz) {
601 return Err(CollectiveError::TransportError {
602 reason: format!(
603 "all_reduce_typed: {} bytes not a multiple of {esz}",
604 data.len()
605 ),
606 });
607 }
608 let n = self.world_size();
609 if n <= 1 {
610 return Ok(()); }
612 let n = n as usize;
613 let me = self.rank() as usize;
614 let next = ((me + 1) % n) as u32;
615 let prev = ((me + n - 1) % n) as u32;
616
617 let elems = data.len() / esz;
618 let base = elems / n;
619 let rem = elems % n;
620 let ebound = |i: usize| (i * base + i.min(rem)) * esz; let chunk = |i: usize| (ebound(i), ebound(i + 1));
622
623 for step in 0..n - 1 {
625 let (ss, se) = chunk((me + n - step) % n);
626 let (rs, re) = chunk((me + n - step - 1) % n);
627 self.transport
628 .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
629 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
630 if incoming.len() != re - rs {
631 return Err(CollectiveError::LengthMismatch {
632 expected: re - rs,
633 got: incoming.len(),
634 });
635 }
636 reduce_into(&mut data[rs..re], &incoming, dtype, op);
637 }
638 for step in 0..n - 1 {
640 let (ss, se) = chunk((me + n - step + 1) % n);
641 let (rs, re) = chunk((me + n - step) % n);
642 self.transport
643 .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
644 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
645 if incoming.len() != re - rs {
646 return Err(CollectiveError::LengthMismatch {
647 expected: re - rs,
648 got: incoming.len(),
649 });
650 }
651 data[rs..re].copy_from_slice(&incoming);
652 }
653 if matches!(op, ReduceKind::Mean) {
654 scale_mean(data, dtype, n);
655 }
656 Ok(())
657 }
658
659 pub fn spawn_all_reduce(
665 self: &Arc<Self>,
666 mut data: Vec<f32>,
667 op: ReduceKind,
668 ) -> std::thread::JoinHandle<Vec<f32>> {
669 let g = self.clone();
670 std::thread::spawn(move || {
671 g.all_reduce(&mut data, op).expect("background all_reduce");
672 data
673 })
674 }
675
676 pub fn all_gather(&self, local: &[f32]) -> Result<Vec<f32>, CollectiveError> {
680 let n = self.world_size();
681 let len = local.len();
682 if n <= 1 {
683 return Ok(local.to_vec());
684 }
685 if self.rank() == 0 {
686 let mut out = vec![0f32; n as usize * len];
687 out[..len].copy_from_slice(local);
688 for r in 1..n {
689 let chunk = self.recv_f32_tagged(r, TAG_ALL_GATHER)?;
690 if chunk.len() != len {
691 return Err(CollectiveError::LengthMismatch {
692 expected: len,
693 got: chunk.len(),
694 });
695 }
696 let start = r as usize * len;
697 out[start..start + len].copy_from_slice(&chunk);
698 }
699 for r in 1..n {
700 self.send_f32_tagged(r, TAG_ALL_GATHER, &out)?;
701 }
702 Ok(out)
703 } else {
704 self.send_f32_tagged(0, TAG_ALL_GATHER, local)?;
705 let out = self.recv_f32_tagged(0, TAG_ALL_GATHER)?;
706 if out.len() != n as usize * len {
707 return Err(CollectiveError::LengthMismatch {
708 expected: n as usize * len,
709 got: out.len(),
710 });
711 }
712 Ok(out)
713 }
714 }
715
716 pub fn broadcast(&self, root: u32, data: &mut [f32]) -> Result<(), CollectiveError> {
719 let n = self.world_size();
720 if n <= 1 {
721 return Ok(());
722 }
723 if self.rank() == root {
724 for r in 0..n {
725 if r != root {
726 self.send_f32_tagged(r, TAG_BROADCAST, data)?;
727 }
728 }
729 } else {
730 let res = self.recv_f32_tagged(root, TAG_BROADCAST)?;
731 if res.len() != data.len() {
732 return Err(CollectiveError::LengthMismatch {
733 expected: data.len(),
734 got: res.len(),
735 });
736 }
737 data.copy_from_slice(&res);
738 }
739 Ok(())
740 }
741
742 pub fn federated_average(
749 &self,
750 data: &mut [f32],
751 deadline: Duration,
752 ) -> Result<usize, CollectiveError> {
753 let n = self.world_size();
754 if n <= 1 {
755 return Ok(1);
756 }
757 let elems = data.len();
758 if self.rank() == 0 {
759 let mut acc = data.to_vec();
760 let mut present = 1usize;
761 let end = Instant::now() + deadline;
762 for r in 1..n {
763 let remaining = end.saturating_duration_since(Instant::now());
764 if let Some(bytes) =
765 self.transport
766 .recv_bytes_timeout(r, TAG_ALL_REDUCE, remaining)?
767 {
768 let other = le_bytes_to_f32(&bytes)?;
769 if other.len() == elems {
770 for i in 0..elems {
771 acc[i] += other[i];
772 }
773 present += 1;
774 }
775 }
776 }
777 let inv = 1.0 / present as f32;
778 for v in acc.iter_mut() {
779 *v *= inv;
780 }
781 data.copy_from_slice(&acc);
782 for r in 1..n {
783 self.send_f32_tagged(r, TAG_ALL_REDUCE, &acc)?;
784 }
785 Ok(present)
786 } else {
787 self.send_f32_tagged(0, TAG_ALL_REDUCE, data)?;
788 let res = self.recv_f32_tagged(0, TAG_ALL_REDUCE)?;
789 if res.len() == elems {
790 data.copy_from_slice(&res);
791 }
792 Ok(0)
793 }
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800 use std::collections::{HashMap, VecDeque};
801 use std::sync::{Condvar, Mutex};
802
803 struct ChannelTransport {
807 rank: u32,
808 world: u32,
809 mailbox: Arc<(Mutex<HashMap<(u32, u32, u32), VecDeque<Vec<u8>>>>, Condvar)>,
810 }
811
812 impl ChannelTransport {
813 fn fan_out(world: u32) -> Vec<Arc<ChannelTransport>> {
814 let mailbox = Arc::new((Mutex::new(HashMap::new()), Condvar::new()));
815 (0..world)
816 .map(|r| {
817 Arc::new(ChannelTransport {
818 rank: r,
819 world,
820 mailbox: mailbox.clone(),
821 })
822 })
823 .collect()
824 }
825 }
826
827 impl Transport for ChannelTransport {
828 fn rank(&self) -> u32 {
829 self.rank
830 }
831 fn world_size(&self) -> u32 {
832 self.world
833 }
834 fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError> {
835 let (m, cv) = &*self.mailbox;
836 m.lock()
837 .unwrap()
838 .entry((to, self.rank, tag))
839 .or_default()
840 .push_back(bytes.to_vec());
841 cv.notify_all();
842 Ok(())
843 }
844 fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError> {
845 let (m, cv) = &*self.mailbox;
846 let mut guard = m.lock().unwrap();
847 loop {
848 if let Some(q) = guard.get_mut(&(self.rank, from, tag))
849 && let Some(v) = q.pop_front()
850 {
851 return Ok(v);
852 }
853 guard = cv.wait(guard).unwrap();
854 }
855 }
856 }
857
858 fn run_ranks<F>(world: u32, body: F) -> Vec<()>
859 where
860 F: Fn(ProcessGroup) + Send + Sync + 'static,
861 {
862 let ts = ChannelTransport::fan_out(world);
863 let body = Arc::new(body);
864 let handles: Vec<_> = ts
865 .into_iter()
866 .map(|t| {
867 let body = body.clone();
868 std::thread::spawn(move || body(ProcessGroup::new(t)))
869 })
870 .collect();
871 handles.into_iter().map(|h| h.join().unwrap()).collect()
872 }
873
874 #[test]
875 fn all_reduce_sum_matches_serial() {
876 run_ranks(4, |g| {
877 let r = g.rank() as f32;
878 let mut data = vec![r + 1.0; 3]; g.all_reduce(&mut data, ReduceKind::Sum).unwrap();
880 assert_eq!(data, vec![10.0; 3], "rank {}", g.rank());
881 });
882 }
883
884 #[test]
885 fn all_gather_concatenates_in_rank_order() {
886 run_ranks(3, |g| {
887 let r = g.rank() as f32;
888 let out = g.all_gather(&[10.0 * r, 10.0 * r + 1.0]).unwrap();
889 assert_eq!(
890 out,
891 vec![0.0, 1.0, 10.0, 11.0, 20.0, 21.0],
892 "rank {}",
893 g.rank()
894 );
895 });
896 }
897
898 #[test]
899 fn broadcast_from_root_overwrites() {
900 run_ranks(4, |g| {
901 let mut data = if g.is_leader() {
902 vec![7.0, 8.0, 9.0]
903 } else {
904 vec![0.0, 0.0, 0.0]
905 };
906 g.broadcast(0, &mut data).unwrap();
907 assert_eq!(data, vec![7.0, 8.0, 9.0], "rank {}", g.rank());
908 });
909 }
910
911 #[test]
912 fn barrier_round_trips() {
913 run_ranks(4, |g| {
914 g.barrier().unwrap();
915 g.barrier().unwrap(); });
917 }
918
919 #[test]
920 fn point_to_point_ring_handoff() {
921 run_ranks(3, |g| {
924 let n = g.world_size();
925 let me = g.rank();
926 let next = (me + 1) % n;
927 let prev = (me + n - 1) % n;
928 if me % 2 == 0 {
931 g.send_f32(next, 1, &[me as f32]).unwrap();
932 let got = g.recv_f32(prev, 1).unwrap();
933 assert_eq!(got, vec![prev as f32]);
934 } else {
935 let got = g.recv_f32(prev, 1).unwrap();
936 assert_eq!(got, vec![prev as f32]);
937 g.send_f32(next, 1, &[me as f32]).unwrap();
938 }
939 });
940 }
941
942 fn f16_bytes(vals: &[f32]) -> Vec<u8> {
943 vals.iter()
944 .flat_map(|&v| half::f16::from_f32(v).to_le_bytes())
945 .collect()
946 }
947 fn f16_vals(bytes: &[u8]) -> Vec<f32> {
948 bytes
949 .chunks_exact(2)
950 .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f32())
951 .collect()
952 }
953
954 #[test]
955 fn all_reduce_typed_f16_sums_across_ranks() {
956 run_ranks(4, |g| {
957 let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 3]); g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
959 .unwrap();
960 assert_eq!(f16_vals(&bytes), vec![10.0; 3], "rank {}", g.rank());
961 });
962 }
963
964 #[test]
965 fn all_reduce_typed_bf16_mean() {
966 run_ranks(4, |g| {
967 let mut bytes: Vec<u8> = [g.rank() as f32 + 1.0; 4]
968 .iter()
969 .flat_map(|&v| half::bf16::from_f32(v).to_le_bytes())
970 .collect();
971 g.all_reduce_typed(&mut bytes, DType::BF16, ReduceKind::Mean)
972 .unwrap();
973 let got: Vec<f32> = bytes
974 .chunks_exact(2)
975 .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f32())
976 .collect();
977 assert_eq!(got, vec![2.5; 4], "rank {}", g.rank());
979 });
980 }
981
982 #[test]
983 fn all_reduce_typed_uneven_length() {
984 run_ranks(4, |g| {
986 let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 5]);
987 g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
988 .unwrap();
989 assert_eq!(f16_vals(&bytes), vec![10.0; 5], "rank {}", g.rank());
990 });
991 }
992
993 #[test]
994 fn all_reduce_typed_f16_large_buffer_simd_path() {
995 run_ranks(3, |g| {
999 let mut bytes = f16_bytes(&vec![g.rank() as f32 + 1.0; 1000]); g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1001 .unwrap();
1002 assert_eq!(f16_vals(&bytes), vec![6.0; 1000], "rank {}", g.rank());
1003 });
1004 }
1005
1006 #[test]
1007 fn all_reduce_typed_i8_federated_mean() {
1008 run_ranks(3, |g| {
1010 let mut bytes = vec![((g.rank() + 1) * 10) as i8 as u8; 4]; g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Mean)
1012 .unwrap();
1013 let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1014 assert_eq!(got, vec![20i8; 4], "rank {}", g.rank()); });
1016 }
1017
1018 #[test]
1019 fn all_reduce_typed_i8_sum_saturates() {
1020 run_ranks(2, |g| {
1021 let mut bytes = vec![100i8 as u8; 2];
1022 g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Sum)
1023 .unwrap();
1024 let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1025 assert_eq!(got, vec![127i8; 2], "rank {}", g.rank()); });
1027 }
1028
1029 #[test]
1030 fn negotiate_reduce_dtype_rules() {
1031 assert_eq!(
1032 negotiate_reduce_dtype(&[DType::F16, DType::BF16]),
1033 DType::F32
1034 );
1035 assert_eq!(
1036 negotiate_reduce_dtype(&[DType::F16, DType::F64]),
1037 DType::F64
1038 );
1039 assert_eq!(negotiate_reduce_dtype(&[]), DType::F32);
1040 }
1041}