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 = rlx_ir::bytes::decode_le::<f32>(&scratch_bytes[..elems * 4]);
107 for (i, &v) in scratch.iter().enumerate() {
108 acc[i] = op.fold(acc[i], v);
109 }
110 }
111 for v in acc.iter_mut() {
112 *v = op.finalize(*v, n as usize);
113 }
114 local.copy_from_slice(&acc);
115 Ok(())
116}
117
118pub fn all_gather<T: SymmetricTransport>(
125 transport: &T,
126 buf: SymmetricBuffer, local: &[f32],
128 output: &mut [f32],
129) -> Result<(), CollectiveError> {
130 let elems_per_rank = buf.len / 4;
131 let n = transport.num_ranks() as usize;
132 if local.len() != elems_per_rank {
133 return Err(CollectiveError::LengthMismatch {
134 expected: elems_per_rank,
135 got: local.len(),
136 });
137 }
138 if output.len() != n * elems_per_rank {
139 return Err(CollectiveError::LengthMismatch {
140 expected: n * elems_per_rank,
141 got: output.len(),
142 });
143 }
144
145 let me = transport.this_rank();
146 let our_buf = SymmetricBuffer {
147 rank: me,
148 offset: buf.offset,
149 len: buf.len,
150 };
151 let bytes = unsafe { std::slice::from_raw_parts(local.as_ptr() as *const u8, buf.len) };
152 transport.put(our_buf, bytes)?;
153 transport.barrier()?;
154
155 let mut scratch_bytes = vec![0u8; buf.len];
156 for r in 0..n {
157 let src = SymmetricBuffer {
158 rank: Rank(r as u32),
159 offset: buf.offset,
160 len: buf.len,
161 };
162 transport.get(src, &mut scratch_bytes)?;
163 let chunk = rlx_ir::bytes::decode_le::<f32>(&scratch_bytes[..elems_per_rank * 4]);
164 let dst_start = r * elems_per_rank;
165 output[dst_start..dst_start + elems_per_rank].copy_from_slice(chunk.as_ref());
166 }
167 Ok(())
168}
169
170pub fn reduce_scatter<T: SymmetricTransport>(
178 transport: &T,
179 buf: SymmetricBuffer,
180 local: &[f32],
181 output: &mut [f32],
182 op: ReduceKind,
183) -> Result<(), CollectiveError> {
184 let total = buf.len / 4;
185 let n = transport.num_ranks() as usize;
186 if !total.is_multiple_of(n) {
187 return Err(CollectiveError::TransportError {
188 reason: format!("reduce_scatter: total elements {total} not divisible by {n} ranks"),
189 });
190 }
191 let chunk = total / n;
192 if local.len() != total {
193 return Err(CollectiveError::LengthMismatch {
194 expected: total,
195 got: local.len(),
196 });
197 }
198 if output.len() != chunk {
199 return Err(CollectiveError::LengthMismatch {
200 expected: chunk,
201 got: output.len(),
202 });
203 }
204
205 let me = transport.this_rank().0 as usize;
208 let mut full = local.to_vec();
209 all_reduce(transport, buf, &mut full, op)?;
210 output.copy_from_slice(&full[me * chunk..(me + 1) * chunk]);
211 Ok(())
212}
213
214pub fn ring_all_reduce<T: SymmetricTransport>(
227 transport: &T,
228 mailbox_offset: usize,
229 local: &mut [f32],
230 op: ReduceKind,
231) -> Result<(), CollectiveError> {
232 let n = transport.num_ranks() as usize;
233 let total = local.len();
234 if n <= 1 {
235 return Ok(());
236 }
237 if !total.is_multiple_of(n) {
238 return Err(CollectiveError::TransportError {
239 reason: format!("ring_all_reduce: len {total} not divisible by {n} ranks"),
240 });
241 }
242 let chunk = total / n;
243 let chunk_bytes = chunk * 4;
244 let me = transport.this_rank().0 as usize;
245 let right = (me + 1) % n;
246 let mailbox = |rank: usize| SymmetricBuffer {
247 rank: Rank(rank as u32),
248 offset: mailbox_offset,
249 len: chunk_bytes,
250 };
251 let mut recv = vec![0u8; chunk_bytes];
252
253 let send_chunk = |local: &[f32], idx: usize| -> *const u8 {
254 local[idx * chunk..idx * chunk + chunk].as_ptr() as *const u8
255 };
256
257 for step in 0..n - 1 {
260 let send_idx = (me + n - step) % n;
261 let recv_idx = (me + n - step - 1) % n;
262 let src = unsafe { std::slice::from_raw_parts(send_chunk(local, send_idx), chunk_bytes) };
263 transport.put(mailbox(right), src)?;
264 transport.barrier()?;
265 transport.get(mailbox(me), &mut recv)?;
266 let incoming = rlx_ir::bytes::decode_le::<f32>(&recv[..chunk * 4]);
267 let d = recv_idx * chunk;
268 for i in 0..chunk {
269 local[d + i] = op.fold(local[d + i], incoming[i]);
270 }
271 transport.barrier()?;
272 }
273
274 for step in 0..n - 1 {
276 let send_idx = (me + n + 1 - step) % n;
277 let recv_idx = (me + n - step) % n;
278 let src = unsafe { std::slice::from_raw_parts(send_chunk(local, send_idx), chunk_bytes) };
279 transport.put(mailbox(right), src)?;
280 transport.barrier()?;
281 transport.get(mailbox(me), &mut recv)?;
282 let incoming = rlx_ir::bytes::decode_le::<f32>(&recv[..chunk * 4]);
283 let d = recv_idx * chunk;
284 local[d..d + chunk].copy_from_slice(incoming.as_ref());
285 transport.barrier()?;
286 }
287
288 if op == ReduceKind::Mean {
289 for v in local.iter_mut() {
290 *v /= n as f32;
291 }
292 }
293 Ok(())
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::symmetric::LocalTransport;
300
301 #[test]
305 fn all_reduce_sum_across_4_ranks() {
306 let n_ranks = 4u32;
307 let elems = 4usize;
308 let bytes = elems * 4;
309 let ts = LocalTransport::fan_out(n_ranks, bytes);
310 let _buf = SymmetricBuffer {
311 rank: Rank(0),
312 offset: 0,
313 len: bytes,
314 };
315
316 let mut state: Vec<Vec<f32>> = (0..n_ranks).map(|r| vec![(r + 1) as f32; elems]).collect();
318
319 for (r, t) in ts.iter().enumerate() {
325 let our_buf = SymmetricBuffer {
326 rank: Rank(r as u32),
327 offset: 0,
328 len: bytes,
329 };
330 let raw = unsafe { std::slice::from_raw_parts(state[r].as_ptr() as *const u8, bytes) };
331 t.put(our_buf, raw).unwrap();
332 }
333 for (r, t) in ts.iter().enumerate() {
338 let mut acc = vec![0f32; elems];
339 let mut scratch = vec![0u8; bytes];
340 for src_r in 0..n_ranks {
341 let src = SymmetricBuffer {
342 rank: Rank(src_r),
343 offset: 0,
344 len: bytes,
345 };
346 t.get(src, &mut scratch).unwrap();
347 let view = rlx_ir::bytes::decode_le::<f32>(&scratch[..elems * 4]);
348 for (i, &v) in view.iter().enumerate() {
349 acc[i] += v;
350 }
351 }
352 state[r] = acc;
353 }
354
355 for (r, slot) in state.iter().enumerate() {
356 assert_eq!(slot, &vec![10.0; elems], "rank {r} after all-reduce");
357 }
358 }
359
360 #[test]
361 fn all_gather_concatenates_in_rank_order() {
362 let n_ranks = 3u32;
363 let chunk = 2usize;
364 let bytes = chunk * 4;
365 let ts = LocalTransport::fan_out(n_ranks, bytes);
366 let _buf = SymmetricBuffer {
367 rank: Rank(0),
368 offset: 0,
369 len: bytes,
370 };
371
372 let local: Vec<Vec<f32>> = (0..n_ranks)
374 .map(|r| {
375 let r = r as f32;
376 vec![10.0 * r, 10.0 * r + 1.0]
377 })
378 .collect();
379
380 for (r, t) in ts.iter().enumerate() {
382 let our_buf = SymmetricBuffer {
383 rank: Rank(r as u32),
384 offset: 0,
385 len: bytes,
386 };
387 let raw = unsafe { std::slice::from_raw_parts(local[r].as_ptr() as *const u8, bytes) };
388 t.put(our_buf, raw).unwrap();
389 }
390 for (r_idx, t) in ts.iter().enumerate() {
392 let mut output = vec![0f32; n_ranks as usize * chunk];
393 let mut scratch = vec![0u8; bytes];
394 for src_r in 0..n_ranks {
395 let src = SymmetricBuffer {
396 rank: Rank(src_r),
397 offset: 0,
398 len: bytes,
399 };
400 t.get(src, &mut scratch).unwrap();
401 let view = rlx_ir::bytes::decode_le::<f32>(&scratch[..chunk * 4]);
402 let dst_start = src_r as usize * chunk;
403 output[dst_start..dst_start + chunk].copy_from_slice(view.as_ref());
404 }
405 assert_eq!(
406 output,
407 vec![0.0, 1.0, 10.0, 11.0, 20.0, 21.0],
408 "rank {r_idx} after all-gather"
409 );
410 }
411 }
412
413 #[test]
414 fn ring_all_reduce_sum_matches_expected_multithread() {
415 for &n in &[2u32, 4] {
420 let total = 8usize; let chunk_bytes = (total / n as usize) * 4;
422 let ts = LocalTransport::fan_out(n, chunk_bytes);
423
424 let expected: Vec<f32> = (0..total)
425 .map(|i| (0..n).map(|r| r as f32 * 10.0 + i as f32).sum())
426 .collect();
427
428 let handles: Vec<_> = ts
429 .into_iter()
430 .enumerate()
431 .map(|(r, t)| {
432 std::thread::spawn(move || {
433 let mut local: Vec<f32> =
434 (0..total).map(|i| r as f32 * 10.0 + i as f32).collect();
435 ring_all_reduce(&t, 0, &mut local, ReduceKind::Sum).unwrap();
436 local
437 })
438 })
439 .collect();
440 for h in handles {
441 assert_eq!(h.join().unwrap(), expected, "n={n}");
442 }
443 }
444 }
445
446 #[test]
447 fn ring_all_reduce_mean_divides() {
448 let n = 4u32;
449 let total = 8usize;
450 let chunk_bytes = (total / n as usize) * 4;
451 let ts = LocalTransport::fan_out(n, chunk_bytes);
452 let handles: Vec<_> = ts
453 .into_iter()
454 .enumerate()
455 .map(|(r, t)| {
456 std::thread::spawn(move || {
457 let mut local = vec![r as f32 + 1.0; total]; ring_all_reduce(&t, 0, &mut local, ReduceKind::Mean).unwrap();
459 local
460 })
461 })
462 .collect();
463 for h in handles {
464 assert_eq!(h.join().unwrap(), vec![2.5f32; total]); }
466 }
467
468 #[test]
469 fn reduce_kind_max_takes_pointwise_max() {
470 let mut acc = ReduceKind::Max.identity();
471 for v in [3.0, 1.0, 7.0, -2.0] {
472 acc = ReduceKind::Max.fold(acc, v);
473 }
474 assert_eq!(acc, 7.0);
475 }
476
477 #[test]
478 fn reduce_kind_mean_divides_at_finalize() {
479 let mut acc = ReduceKind::Mean.identity();
480 for v in [2.0, 4.0, 6.0, 8.0] {
481 acc = ReduceKind::Mean.fold(acc, v);
482 }
483 assert_eq!(ReduceKind::Mean.finalize(acc, 4), 5.0);
484 }
485}