1use crate::symmetric::{CollectiveError, Rank, SymmetricBuffer, SymmetricTransport};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ReduceKind {
28 Sum,
29 Mean,
30 Max,
31 Min,
32}
33
34impl ReduceKind {
35 pub(crate) fn fold(self, acc: f32, x: f32) -> f32 {
36 match self {
37 Self::Sum => acc + x,
38 Self::Mean => acc + x, Self::Max => acc.max(x),
40 Self::Min => acc.min(x),
41 }
42 }
43 pub(crate) fn finalize(self, acc: f32, n: usize) -> f32 {
44 match self {
45 Self::Mean => acc / (n as f32),
46 _ => acc,
47 }
48 }
49 fn identity(self) -> f32 {
50 match self {
51 Self::Sum | Self::Mean => 0.0,
52 Self::Max => f32::NEG_INFINITY,
53 Self::Min => f32::INFINITY,
54 }
55 }
56}
57
58pub fn all_reduce<T: SymmetricTransport>(
69 transport: &T,
70 buf: SymmetricBuffer, local: &mut [f32],
72 op: ReduceKind,
73) -> Result<(), CollectiveError> {
74 let elems = buf.len / 4;
75 if local.len() != elems {
76 return Err(CollectiveError::LengthMismatch {
77 expected: elems,
78 got: local.len(),
79 });
80 }
81 let me = transport.this_rank();
82 let n = transport.num_ranks();
83
84 let our_buf = SymmetricBuffer {
86 rank: me,
87 offset: buf.offset,
88 len: buf.len,
89 };
90 let bytes = unsafe { std::slice::from_raw_parts(local.as_ptr() as *const u8, buf.len) };
91 transport.put(our_buf, bytes)?;
92
93 transport.barrier()?;
95
96 let mut acc: Vec<f32> = vec![op.identity(); elems];
98 let mut scratch_bytes = vec![0u8; buf.len];
99 for r in 0..n {
100 let src = SymmetricBuffer {
101 rank: Rank(r),
102 offset: buf.offset,
103 len: buf.len,
104 };
105 transport.get(src, &mut scratch_bytes)?;
106 let scratch =
107 unsafe { std::slice::from_raw_parts(scratch_bytes.as_ptr() as *const f32, elems) };
108 for (i, &v) in scratch.iter().enumerate() {
109 acc[i] = op.fold(acc[i], v);
110 }
111 }
112 for v in acc.iter_mut() {
113 *v = op.finalize(*v, n as usize);
114 }
115 local.copy_from_slice(&acc);
116 Ok(())
117}
118
119pub fn all_gather<T: SymmetricTransport>(
126 transport: &T,
127 buf: SymmetricBuffer, local: &[f32],
129 output: &mut [f32],
130) -> Result<(), CollectiveError> {
131 let elems_per_rank = buf.len / 4;
132 let n = transport.num_ranks() as usize;
133 if local.len() != elems_per_rank {
134 return Err(CollectiveError::LengthMismatch {
135 expected: elems_per_rank,
136 got: local.len(),
137 });
138 }
139 if output.len() != n * elems_per_rank {
140 return Err(CollectiveError::LengthMismatch {
141 expected: n * elems_per_rank,
142 got: output.len(),
143 });
144 }
145
146 let me = transport.this_rank();
147 let our_buf = SymmetricBuffer {
148 rank: me,
149 offset: buf.offset,
150 len: buf.len,
151 };
152 let bytes = unsafe { std::slice::from_raw_parts(local.as_ptr() as *const u8, buf.len) };
153 transport.put(our_buf, bytes)?;
154 transport.barrier()?;
155
156 let mut scratch_bytes = vec![0u8; buf.len];
157 for r in 0..n {
158 let src = SymmetricBuffer {
159 rank: Rank(r as u32),
160 offset: buf.offset,
161 len: buf.len,
162 };
163 transport.get(src, &mut scratch_bytes)?;
164 let chunk = unsafe {
165 std::slice::from_raw_parts(scratch_bytes.as_ptr() as *const f32, elems_per_rank)
166 };
167 let dst_start = r * elems_per_rank;
168 output[dst_start..dst_start + elems_per_rank].copy_from_slice(chunk);
169 }
170 Ok(())
171}
172
173pub fn reduce_scatter<T: SymmetricTransport>(
181 transport: &T,
182 buf: SymmetricBuffer,
183 local: &[f32],
184 output: &mut [f32],
185 op: ReduceKind,
186) -> Result<(), CollectiveError> {
187 let total = buf.len / 4;
188 let n = transport.num_ranks() as usize;
189 if !total.is_multiple_of(n) {
190 return Err(CollectiveError::TransportError {
191 reason: format!("reduce_scatter: total elements {total} not divisible by {n} ranks"),
192 });
193 }
194 let chunk = total / n;
195 if local.len() != total {
196 return Err(CollectiveError::LengthMismatch {
197 expected: total,
198 got: local.len(),
199 });
200 }
201 if output.len() != chunk {
202 return Err(CollectiveError::LengthMismatch {
203 expected: chunk,
204 got: output.len(),
205 });
206 }
207
208 let me = transport.this_rank().0 as usize;
211 let mut full = local.to_vec();
212 all_reduce(transport, buf, &mut full, op)?;
213 output.copy_from_slice(&full[me * chunk..(me + 1) * chunk]);
214 Ok(())
215}
216
217pub fn ring_all_reduce<T: SymmetricTransport>(
230 transport: &T,
231 mailbox_offset: usize,
232 local: &mut [f32],
233 op: ReduceKind,
234) -> Result<(), CollectiveError> {
235 let n = transport.num_ranks() as usize;
236 let total = local.len();
237 if n <= 1 {
238 return Ok(());
239 }
240 if !total.is_multiple_of(n) {
241 return Err(CollectiveError::TransportError {
242 reason: format!("ring_all_reduce: len {total} not divisible by {n} ranks"),
243 });
244 }
245 let chunk = total / n;
246 let chunk_bytes = chunk * 4;
247 let me = transport.this_rank().0 as usize;
248 let right = (me + 1) % n;
249 let mailbox = |rank: usize| SymmetricBuffer {
250 rank: Rank(rank as u32),
251 offset: mailbox_offset,
252 len: chunk_bytes,
253 };
254 let mut recv = vec![0u8; chunk_bytes];
255
256 let send_chunk = |local: &[f32], idx: usize| -> *const u8 {
257 local[idx * chunk..idx * chunk + chunk].as_ptr() as *const u8
258 };
259
260 for step in 0..n - 1 {
263 let send_idx = (me + n - step) % n;
264 let recv_idx = (me + n - step - 1) % n;
265 let src = unsafe { std::slice::from_raw_parts(send_chunk(local, send_idx), chunk_bytes) };
266 transport.put(mailbox(right), src)?;
267 transport.barrier()?;
268 transport.get(mailbox(me), &mut recv)?;
269 let incoming = unsafe { std::slice::from_raw_parts(recv.as_ptr() as *const f32, chunk) };
270 let d = recv_idx * chunk;
271 for i in 0..chunk {
272 local[d + i] = op.fold(local[d + i], incoming[i]);
273 }
274 transport.barrier()?;
275 }
276
277 for step in 0..n - 1 {
279 let send_idx = (me + n + 1 - step) % n;
280 let recv_idx = (me + n - step) % n;
281 let src = unsafe { std::slice::from_raw_parts(send_chunk(local, send_idx), chunk_bytes) };
282 transport.put(mailbox(right), src)?;
283 transport.barrier()?;
284 transport.get(mailbox(me), &mut recv)?;
285 let incoming = unsafe { std::slice::from_raw_parts(recv.as_ptr() as *const f32, chunk) };
286 let d = recv_idx * chunk;
287 local[d..d + chunk].copy_from_slice(incoming);
288 transport.barrier()?;
289 }
290
291 if op == ReduceKind::Mean {
292 for v in local.iter_mut() {
293 *v /= n as f32;
294 }
295 }
296 Ok(())
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::symmetric::LocalTransport;
303
304 #[test]
308 fn all_reduce_sum_across_4_ranks() {
309 let n_ranks = 4u32;
310 let elems = 4usize;
311 let bytes = elems * 4;
312 let ts = LocalTransport::fan_out(n_ranks, bytes);
313 let _buf = SymmetricBuffer {
314 rank: Rank(0),
315 offset: 0,
316 len: bytes,
317 };
318
319 let mut state: Vec<Vec<f32>> = (0..n_ranks).map(|r| vec![(r + 1) as f32; elems]).collect();
321
322 for (r, t) in ts.iter().enumerate() {
328 let our_buf = SymmetricBuffer {
329 rank: Rank(r as u32),
330 offset: 0,
331 len: bytes,
332 };
333 let raw = unsafe { std::slice::from_raw_parts(state[r].as_ptr() as *const u8, bytes) };
334 t.put(our_buf, raw).unwrap();
335 }
336 for (r, t) in ts.iter().enumerate() {
341 let mut acc = vec![0f32; elems];
342 let mut scratch = vec![0u8; bytes];
343 for src_r in 0..n_ranks {
344 let src = SymmetricBuffer {
345 rank: Rank(src_r),
346 offset: 0,
347 len: bytes,
348 };
349 t.get(src, &mut scratch).unwrap();
350 let view =
351 unsafe { std::slice::from_raw_parts(scratch.as_ptr() as *const f32, elems) };
352 for (i, &v) in view.iter().enumerate() {
353 acc[i] += v;
354 }
355 }
356 state[r] = acc;
357 }
358
359 for (r, slot) in state.iter().enumerate() {
360 assert_eq!(slot, &vec![10.0; elems], "rank {r} after all-reduce");
361 }
362 }
363
364 #[test]
365 fn all_gather_concatenates_in_rank_order() {
366 let n_ranks = 3u32;
367 let chunk = 2usize;
368 let bytes = chunk * 4;
369 let ts = LocalTransport::fan_out(n_ranks, bytes);
370 let _buf = SymmetricBuffer {
371 rank: Rank(0),
372 offset: 0,
373 len: bytes,
374 };
375
376 let local: Vec<Vec<f32>> = (0..n_ranks)
378 .map(|r| {
379 let r = r as f32;
380 vec![10.0 * r, 10.0 * r + 1.0]
381 })
382 .collect();
383
384 for (r, t) in ts.iter().enumerate() {
386 let our_buf = SymmetricBuffer {
387 rank: Rank(r as u32),
388 offset: 0,
389 len: bytes,
390 };
391 let raw = unsafe { std::slice::from_raw_parts(local[r].as_ptr() as *const u8, bytes) };
392 t.put(our_buf, raw).unwrap();
393 }
394 for (r_idx, t) in ts.iter().enumerate() {
396 let mut output = vec![0f32; n_ranks as usize * chunk];
397 let mut scratch = vec![0u8; bytes];
398 for src_r in 0..n_ranks {
399 let src = SymmetricBuffer {
400 rank: Rank(src_r),
401 offset: 0,
402 len: bytes,
403 };
404 t.get(src, &mut scratch).unwrap();
405 let view =
406 unsafe { std::slice::from_raw_parts(scratch.as_ptr() as *const f32, chunk) };
407 let dst_start = src_r as usize * chunk;
408 output[dst_start..dst_start + chunk].copy_from_slice(view);
409 }
410 assert_eq!(
411 output,
412 vec![0.0, 1.0, 10.0, 11.0, 20.0, 21.0],
413 "rank {r_idx} after all-gather"
414 );
415 }
416 }
417
418 #[test]
419 fn ring_all_reduce_sum_matches_expected_multithread() {
420 for &n in &[2u32, 4] {
425 let total = 8usize; let chunk_bytes = (total / n as usize) * 4;
427 let ts = LocalTransport::fan_out(n, chunk_bytes);
428
429 let expected: Vec<f32> = (0..total)
430 .map(|i| (0..n).map(|r| r as f32 * 10.0 + i as f32).sum())
431 .collect();
432
433 let handles: Vec<_> = ts
434 .into_iter()
435 .enumerate()
436 .map(|(r, t)| {
437 std::thread::spawn(move || {
438 let mut local: Vec<f32> =
439 (0..total).map(|i| r as f32 * 10.0 + i as f32).collect();
440 ring_all_reduce(&t, 0, &mut local, ReduceKind::Sum).unwrap();
441 local
442 })
443 })
444 .collect();
445 for h in handles {
446 assert_eq!(h.join().unwrap(), expected, "n={n}");
447 }
448 }
449 }
450
451 #[test]
452 fn ring_all_reduce_mean_divides() {
453 let n = 4u32;
454 let total = 8usize;
455 let chunk_bytes = (total / n as usize) * 4;
456 let ts = LocalTransport::fan_out(n, chunk_bytes);
457 let handles: Vec<_> = ts
458 .into_iter()
459 .enumerate()
460 .map(|(r, t)| {
461 std::thread::spawn(move || {
462 let mut local = vec![r as f32 + 1.0; total]; ring_all_reduce(&t, 0, &mut local, ReduceKind::Mean).unwrap();
464 local
465 })
466 })
467 .collect();
468 for h in handles {
469 assert_eq!(h.join().unwrap(), vec![2.5f32; total]); }
471 }
472
473 #[test]
474 fn reduce_kind_max_takes_pointwise_max() {
475 let mut acc = ReduceKind::Max.identity();
476 for v in [3.0, 1.0, 7.0, -2.0] {
477 acc = ReduceKind::Max.fold(acc, v);
478 }
479 assert_eq!(acc, 7.0);
480 }
481
482 #[test]
483 fn reduce_kind_mean_divides_at_finalize() {
484 let mut acc = ReduceKind::Mean.identity();
485 for v in [2.0, 4.0, 6.0, 8.0] {
486 acc = ReduceKind::Mean.fold(acc, v);
487 }
488 assert_eq!(ReduceKind::Mean.finalize(acc, 4), 5.0);
489 }
490}