1use crate::symmetric::{CollectiveError, Rank, SymmetricBuffer, SymmetricTransport};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ReduceKind {
39 Sum,
40 Mean,
41 Max,
42 Min,
43}
44
45impl ReduceKind {
46 fn fold(self, acc: f32, x: f32) -> f32 {
47 match self {
48 Self::Sum => acc + x,
49 Self::Mean => acc + x, Self::Max => acc.max(x),
51 Self::Min => acc.min(x),
52 }
53 }
54 fn finalize(self, acc: f32, n: usize) -> f32 {
55 match self {
56 Self::Mean => acc / (n as f32),
57 _ => acc,
58 }
59 }
60 fn identity(self) -> f32 {
61 match self {
62 Self::Sum | Self::Mean => 0.0,
63 Self::Max => f32::NEG_INFINITY,
64 Self::Min => f32::INFINITY,
65 }
66 }
67}
68
69pub fn all_reduce<T: SymmetricTransport>(
80 transport: &T,
81 buf: SymmetricBuffer, local: &mut [f32],
83 op: ReduceKind,
84) -> Result<(), CollectiveError> {
85 let elems = buf.len / 4;
86 if local.len() != elems {
87 return Err(CollectiveError::LengthMismatch {
88 expected: elems,
89 got: local.len(),
90 });
91 }
92 let me = transport.this_rank();
93 let n = transport.num_ranks();
94
95 let our_buf = SymmetricBuffer {
97 rank: me,
98 offset: buf.offset,
99 len: buf.len,
100 };
101 let bytes = unsafe { std::slice::from_raw_parts(local.as_ptr() as *const u8, buf.len) };
102 transport.put(our_buf, bytes)?;
103
104 transport.barrier()?;
106
107 let mut acc: Vec<f32> = vec![op.identity(); elems];
109 let mut scratch_bytes = vec![0u8; buf.len];
110 for r in 0..n {
111 let src = SymmetricBuffer {
112 rank: Rank(r),
113 offset: buf.offset,
114 len: buf.len,
115 };
116 transport.get(src, &mut scratch_bytes)?;
117 let scratch =
118 unsafe { std::slice::from_raw_parts(scratch_bytes.as_ptr() as *const f32, elems) };
119 for (i, &v) in scratch.iter().enumerate() {
120 acc[i] = op.fold(acc[i], v);
121 }
122 }
123 for v in acc.iter_mut() {
124 *v = op.finalize(*v, n as usize);
125 }
126 local.copy_from_slice(&acc);
127 Ok(())
128}
129
130pub fn all_gather<T: SymmetricTransport>(
137 transport: &T,
138 buf: SymmetricBuffer, local: &[f32],
140 output: &mut [f32],
141) -> Result<(), CollectiveError> {
142 let elems_per_rank = buf.len / 4;
143 let n = transport.num_ranks() as usize;
144 if local.len() != elems_per_rank {
145 return Err(CollectiveError::LengthMismatch {
146 expected: elems_per_rank,
147 got: local.len(),
148 });
149 }
150 if output.len() != n * elems_per_rank {
151 return Err(CollectiveError::LengthMismatch {
152 expected: n * elems_per_rank,
153 got: output.len(),
154 });
155 }
156
157 let me = transport.this_rank();
158 let our_buf = SymmetricBuffer {
159 rank: me,
160 offset: buf.offset,
161 len: buf.len,
162 };
163 let bytes = unsafe { std::slice::from_raw_parts(local.as_ptr() as *const u8, buf.len) };
164 transport.put(our_buf, bytes)?;
165 transport.barrier()?;
166
167 let mut scratch_bytes = vec![0u8; buf.len];
168 for r in 0..n {
169 let src = SymmetricBuffer {
170 rank: Rank(r as u32),
171 offset: buf.offset,
172 len: buf.len,
173 };
174 transport.get(src, &mut scratch_bytes)?;
175 let chunk = unsafe {
176 std::slice::from_raw_parts(scratch_bytes.as_ptr() as *const f32, elems_per_rank)
177 };
178 let dst_start = r * elems_per_rank;
179 output[dst_start..dst_start + elems_per_rank].copy_from_slice(chunk);
180 }
181 Ok(())
182}
183
184pub fn reduce_scatter<T: SymmetricTransport>(
192 transport: &T,
193 buf: SymmetricBuffer,
194 local: &[f32],
195 output: &mut [f32],
196 op: ReduceKind,
197) -> Result<(), CollectiveError> {
198 let total = buf.len / 4;
199 let n = transport.num_ranks() as usize;
200 if !total.is_multiple_of(n) {
201 return Err(CollectiveError::TransportError {
202 reason: format!("reduce_scatter: total elements {total} not divisible by {n} ranks"),
203 });
204 }
205 let chunk = total / n;
206 if local.len() != total {
207 return Err(CollectiveError::LengthMismatch {
208 expected: total,
209 got: local.len(),
210 });
211 }
212 if output.len() != chunk {
213 return Err(CollectiveError::LengthMismatch {
214 expected: chunk,
215 got: output.len(),
216 });
217 }
218
219 let me = transport.this_rank().0 as usize;
222 let mut full = local.to_vec();
223 all_reduce(transport, buf, &mut full, op)?;
224 output.copy_from_slice(&full[me * chunk..(me + 1) * chunk]);
225 Ok(())
226}
227
228pub fn ring_all_reduce<T: SymmetricTransport>(
241 transport: &T,
242 mailbox_offset: usize,
243 local: &mut [f32],
244 op: ReduceKind,
245) -> Result<(), CollectiveError> {
246 let n = transport.num_ranks() as usize;
247 let total = local.len();
248 if n <= 1 {
249 return Ok(());
250 }
251 if !total.is_multiple_of(n) {
252 return Err(CollectiveError::TransportError {
253 reason: format!("ring_all_reduce: len {total} not divisible by {n} ranks"),
254 });
255 }
256 let chunk = total / n;
257 let chunk_bytes = chunk * 4;
258 let me = transport.this_rank().0 as usize;
259 let right = (me + 1) % n;
260 let mailbox = |rank: usize| SymmetricBuffer {
261 rank: Rank(rank as u32),
262 offset: mailbox_offset,
263 len: chunk_bytes,
264 };
265 let mut recv = vec![0u8; chunk_bytes];
266
267 let send_chunk = |local: &[f32], idx: usize| -> *const u8 {
268 local[idx * chunk..idx * chunk + chunk].as_ptr() as *const u8
269 };
270
271 for step in 0..n - 1 {
274 let send_idx = (me + n - step) % n;
275 let recv_idx = (me + n - step - 1) % n;
276 let src = unsafe { std::slice::from_raw_parts(send_chunk(local, send_idx), chunk_bytes) };
277 transport.put(mailbox(right), src)?;
278 transport.barrier()?;
279 transport.get(mailbox(me), &mut recv)?;
280 let incoming = unsafe { std::slice::from_raw_parts(recv.as_ptr() as *const f32, chunk) };
281 let d = recv_idx * chunk;
282 for i in 0..chunk {
283 local[d + i] = op.fold(local[d + i], incoming[i]);
284 }
285 transport.barrier()?;
286 }
287
288 for step in 0..n - 1 {
290 let send_idx = (me + n + 1 - step) % n;
291 let recv_idx = (me + n - step) % n;
292 let src = unsafe { std::slice::from_raw_parts(send_chunk(local, send_idx), chunk_bytes) };
293 transport.put(mailbox(right), src)?;
294 transport.barrier()?;
295 transport.get(mailbox(me), &mut recv)?;
296 let incoming = unsafe { std::slice::from_raw_parts(recv.as_ptr() as *const f32, chunk) };
297 let d = recv_idx * chunk;
298 local[d..d + chunk].copy_from_slice(incoming);
299 transport.barrier()?;
300 }
301
302 if op == ReduceKind::Mean {
303 for v in local.iter_mut() {
304 *v /= n as f32;
305 }
306 }
307 Ok(())
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::symmetric::LocalTransport;
314
315 #[test]
319 fn all_reduce_sum_across_4_ranks() {
320 let n_ranks = 4u32;
321 let elems = 4usize;
322 let bytes = elems * 4;
323 let ts = LocalTransport::fan_out(n_ranks, bytes);
324 let _buf = SymmetricBuffer {
325 rank: Rank(0),
326 offset: 0,
327 len: bytes,
328 };
329
330 let mut state: Vec<Vec<f32>> = (0..n_ranks).map(|r| vec![(r + 1) as f32; elems]).collect();
332
333 for (r, t) in ts.iter().enumerate() {
339 let our_buf = SymmetricBuffer {
340 rank: Rank(r as u32),
341 offset: 0,
342 len: bytes,
343 };
344 let raw = unsafe { std::slice::from_raw_parts(state[r].as_ptr() as *const u8, bytes) };
345 t.put(our_buf, raw).unwrap();
346 }
347 for (r, t) in ts.iter().enumerate() {
352 let mut acc = vec![0f32; elems];
353 let mut scratch = vec![0u8; bytes];
354 for src_r in 0..n_ranks {
355 let src = SymmetricBuffer {
356 rank: Rank(src_r),
357 offset: 0,
358 len: bytes,
359 };
360 t.get(src, &mut scratch).unwrap();
361 let view =
362 unsafe { std::slice::from_raw_parts(scratch.as_ptr() as *const f32, elems) };
363 for (i, &v) in view.iter().enumerate() {
364 acc[i] += v;
365 }
366 }
367 state[r] = acc;
368 }
369
370 for (r, slot) in state.iter().enumerate() {
371 assert_eq!(slot, &vec![10.0; elems], "rank {r} after all-reduce");
372 }
373 }
374
375 #[test]
376 fn all_gather_concatenates_in_rank_order() {
377 let n_ranks = 3u32;
378 let chunk = 2usize;
379 let bytes = chunk * 4;
380 let ts = LocalTransport::fan_out(n_ranks, bytes);
381 let _buf = SymmetricBuffer {
382 rank: Rank(0),
383 offset: 0,
384 len: bytes,
385 };
386
387 let local: Vec<Vec<f32>> = (0..n_ranks)
389 .map(|r| {
390 let r = r as f32;
391 vec![10.0 * r, 10.0 * r + 1.0]
392 })
393 .collect();
394
395 for (r, t) in ts.iter().enumerate() {
397 let our_buf = SymmetricBuffer {
398 rank: Rank(r as u32),
399 offset: 0,
400 len: bytes,
401 };
402 let raw = unsafe { std::slice::from_raw_parts(local[r].as_ptr() as *const u8, bytes) };
403 t.put(our_buf, raw).unwrap();
404 }
405 for (r_idx, t) in ts.iter().enumerate() {
407 let mut output = vec![0f32; n_ranks as usize * chunk];
408 let mut scratch = vec![0u8; bytes];
409 for src_r in 0..n_ranks {
410 let src = SymmetricBuffer {
411 rank: Rank(src_r),
412 offset: 0,
413 len: bytes,
414 };
415 t.get(src, &mut scratch).unwrap();
416 let view =
417 unsafe { std::slice::from_raw_parts(scratch.as_ptr() as *const f32, chunk) };
418 let dst_start = src_r as usize * chunk;
419 output[dst_start..dst_start + chunk].copy_from_slice(view);
420 }
421 assert_eq!(
422 output,
423 vec![0.0, 1.0, 10.0, 11.0, 20.0, 21.0],
424 "rank {r_idx} after all-gather"
425 );
426 }
427 }
428
429 #[test]
430 fn ring_all_reduce_sum_matches_expected_multithread() {
431 for &n in &[2u32, 4] {
436 let total = 8usize; let chunk_bytes = (total / n as usize) * 4;
438 let ts = LocalTransport::fan_out(n, chunk_bytes);
439
440 let expected: Vec<f32> = (0..total)
441 .map(|i| (0..n).map(|r| r as f32 * 10.0 + i as f32).sum())
442 .collect();
443
444 let handles: Vec<_> = ts
445 .into_iter()
446 .enumerate()
447 .map(|(r, t)| {
448 std::thread::spawn(move || {
449 let mut local: Vec<f32> =
450 (0..total).map(|i| r as f32 * 10.0 + i as f32).collect();
451 ring_all_reduce(&t, 0, &mut local, ReduceKind::Sum).unwrap();
452 local
453 })
454 })
455 .collect();
456 for h in handles {
457 assert_eq!(h.join().unwrap(), expected, "n={n}");
458 }
459 }
460 }
461
462 #[test]
463 fn ring_all_reduce_mean_divides() {
464 let n = 4u32;
465 let total = 8usize;
466 let chunk_bytes = (total / n as usize) * 4;
467 let ts = LocalTransport::fan_out(n, chunk_bytes);
468 let handles: Vec<_> = ts
469 .into_iter()
470 .enumerate()
471 .map(|(r, t)| {
472 std::thread::spawn(move || {
473 let mut local = vec![r as f32 + 1.0; total]; ring_all_reduce(&t, 0, &mut local, ReduceKind::Mean).unwrap();
475 local
476 })
477 })
478 .collect();
479 for h in handles {
480 assert_eq!(h.join().unwrap(), vec![2.5f32; total]); }
482 }
483
484 #[test]
485 fn reduce_kind_max_takes_pointwise_max() {
486 let mut acc = ReduceKind::Max.identity();
487 for v in [3.0, 1.0, 7.0, -2.0] {
488 acc = ReduceKind::Max.fold(acc, v);
489 }
490 assert_eq!(acc, 7.0);
491 }
492
493 #[test]
494 fn reduce_kind_mean_divides_at_finalize() {
495 let mut acc = ReduceKind::Mean.identity();
496 for v in [2.0, 4.0, 6.0, 8.0] {
497 acc = ReduceKind::Mean.fold(acc, v);
498 }
499 assert_eq!(ReduceKind::Mean.finalize(acc, 4), 5.0);
500 }
501}