Skip to main content

mpi/
collective.rs

1//! Collective communication and reduction operations. Mirrors
2//! `mpi::collective` in rsmpi: the [`CommunicatorCollectives`] trait (rootless
3//! collectives, blanket-implemented for every [`Communicator`]), the [`Root`]
4//! trait (rooted collectives, implemented for [`Process`]), the [`Operation`]
5//! trait, and [`SystemOperation`] / [`UserOperation`].
6//!
7//! All algorithms are implemented in pure Rust on top of the point-to-point
8//! transport. They run on a communicator's dedicated *collective context*, so
9//! they never interfere with user point-to-point messages, and each collective
10//! uses its own message tag(s) so successive collectives cannot alias.
11
12use std::io;
13use std::ptr;
14
15use crate::datatype::{
16    ids, Buffer, BufferMut, Equivalence, PartitionedBuffer, PartitionedBufferMut,
17};
18use crate::point_to_point::{vec_from_bytes, Process};
19use crate::request::{Request, Scope, StaticScope};
20use crate::topology::{CommData, Communicator};
21use crate::transport;
22use crate::{Rank, Tag};
23
24// Distinct tags per collective phase (on the collective context).
25const T_BCAST: Tag = 3;
26const T_GATHER: Tag = 4;
27const T_SCATTER: Tag = 5;
28const T_ALLGATHER_UP: Tag = 6;
29const T_ALLTOALL: Tag = 8;
30const T_REDUCE: Tag = 9;
31const T_SCAN: Tag = 11;
32const T_EXSCAN: Tag = 12;
33const T_ALLGATHERV_UP: Tag = 13;
34const T_ALLGATHERV_DOWN: Tag = 14;
35const T_GATHERV: Tag = 15;
36/// Base tag for dissemination-barrier rounds (`+ round`).
37const T_BARRIER_ROUND: Tag = 200;
38
39/// Real rank of relative rank `rel` in a `root`-rooted, `n`-process tree.
40#[inline]
41fn real_rank(rel: Rank, root: Rank, n: Rank) -> Rank {
42    (rel + root) % n
43}
44const T_SCATTERV: Tag = 16;
45const T_ALLTOALLV: Tag = 17;
46
47const DT_BYTES: u32 = ids::U8;
48
49// =====================================================================
50// Reduction operations
51// =====================================================================
52
53/// The kind of built-in reduction.
54#[derive(Copy, Clone, Debug, PartialEq, Eq)]
55enum OpKind {
56    Sum,
57    Product,
58    Max,
59    Min,
60    LogicalAnd,
61    LogicalOr,
62    LogicalXor,
63    BitwiseAnd,
64    BitwiseOr,
65    BitwiseXor,
66}
67
68/// A reduction operation usable with `reduce`, `all_reduce`, `scan`, etc.
69/// (`MPI_Op`). Implemented by [`SystemOperation`] and [`UserOperation`].
70pub trait Operation {
71    /// Combine `val` into `acc` element-wise, interpreting the bytes as the
72    /// datatype identified by `dt_id`.
73    #[doc(hidden)]
74    fn reduce_bytes(&self, dt_id: u32, acc: &mut [u8], val: &[u8]);
75
76    /// Whether the operation is commutative (`MPI_Op_commutative`).
77    fn is_commutative(&self) -> bool {
78        true
79    }
80}
81
82/// A built-in reduction operation (`MPI_SUM`, `MPI_MAX`, …). Mirrors rsmpi's
83/// `SystemOperation`.
84#[derive(Copy, Clone, Debug, PartialEq, Eq)]
85pub struct SystemOperation {
86    kind: OpKind,
87}
88
89impl SystemOperation {
90    /// `MPI_SUM`.
91    pub fn sum() -> SystemOperation {
92        SystemOperation { kind: OpKind::Sum }
93    }
94    /// `MPI_PROD`.
95    pub fn product() -> SystemOperation {
96        SystemOperation {
97            kind: OpKind::Product,
98        }
99    }
100    /// `MPI_MAX`.
101    pub fn max() -> SystemOperation {
102        SystemOperation { kind: OpKind::Max }
103    }
104    /// `MPI_MIN`.
105    pub fn min() -> SystemOperation {
106        SystemOperation { kind: OpKind::Min }
107    }
108    /// `MPI_LAND`.
109    pub fn logical_and() -> SystemOperation {
110        SystemOperation {
111            kind: OpKind::LogicalAnd,
112        }
113    }
114    /// `MPI_LOR`.
115    pub fn logical_or() -> SystemOperation {
116        SystemOperation {
117            kind: OpKind::LogicalOr,
118        }
119    }
120    /// `MPI_LXOR`.
121    pub fn logical_xor() -> SystemOperation {
122        SystemOperation {
123            kind: OpKind::LogicalXor,
124        }
125    }
126    /// `MPI_BAND`.
127    pub fn bitwise_and() -> SystemOperation {
128        SystemOperation {
129            kind: OpKind::BitwiseAnd,
130        }
131    }
132    /// `MPI_BOR`.
133    pub fn bitwise_or() -> SystemOperation {
134        SystemOperation {
135            kind: OpKind::BitwiseOr,
136        }
137    }
138    /// `MPI_BXOR`.
139    pub fn bitwise_xor() -> SystemOperation {
140        SystemOperation {
141            kind: OpKind::BitwiseXor,
142        }
143    }
144
145    /// Serialize the operation kind to a byte (for RMA accumulate transport).
146    pub(crate) fn to_code(self) -> u8 {
147        match self.kind {
148            OpKind::Sum => 0,
149            OpKind::Product => 1,
150            OpKind::Max => 2,
151            OpKind::Min => 3,
152            OpKind::LogicalAnd => 4,
153            OpKind::LogicalOr => 5,
154            OpKind::LogicalXor => 6,
155            OpKind::BitwiseAnd => 7,
156            OpKind::BitwiseOr => 8,
157            OpKind::BitwiseXor => 9,
158        }
159    }
160
161    /// Reconstruct an operation from a byte produced by [`to_code`].
162    ///
163    /// [`to_code`]: SystemOperation::to_code
164    pub(crate) fn from_code(code: u8) -> SystemOperation {
165        let kind = match code {
166            0 => OpKind::Sum,
167            1 => OpKind::Product,
168            2 => OpKind::Max,
169            3 => OpKind::Min,
170            4 => OpKind::LogicalAnd,
171            5 => OpKind::LogicalOr,
172            6 => OpKind::LogicalXor,
173            7 => OpKind::BitwiseAnd,
174            8 => OpKind::BitwiseOr,
175            _ => OpKind::BitwiseXor,
176        };
177        SystemOperation { kind }
178    }
179}
180
181/// Combine two same-length byte runs element-wise as type `T` using `f`, using
182/// unaligned accesses (buffers may originate from unaligned byte vectors).
183fn combine<T: Copy>(acc: &mut [u8], val: &[u8], f: impl Fn(T, T) -> T) {
184    let size = std::mem::size_of::<T>();
185    if size == 0 {
186        return;
187    }
188    let n = acc.len() / size;
189    for i in 0..n {
190        // SAFETY: both slices hold at least `n * size` bytes; T is POD.
191        unsafe {
192            let ap = acc.as_mut_ptr().add(i * size) as *mut T;
193            let bp = val.as_ptr().add(i * size) as *const T;
194            let a = ptr::read_unaligned(ap);
195            let b = ptr::read_unaligned(bp);
196            ptr::write_unaligned(ap, f(a, b));
197        }
198    }
199}
200
201macro_rules! int_reduce {
202    ($kind:expr, $acc:expr, $val:expr, $t:ty) => {{
203        match $kind {
204            OpKind::Sum => combine::<$t>($acc, $val, |a, b| a.wrapping_add(b)),
205            OpKind::Product => combine::<$t>($acc, $val, |a, b| a.wrapping_mul(b)),
206            OpKind::Max => combine::<$t>($acc, $val, |a, b| if a >= b { a } else { b }),
207            OpKind::Min => combine::<$t>($acc, $val, |a, b| if a <= b { a } else { b }),
208            OpKind::LogicalAnd => combine::<$t>($acc, $val, |a, b| ((a != 0) && (b != 0)) as $t),
209            OpKind::LogicalOr => combine::<$t>($acc, $val, |a, b| ((a != 0) || (b != 0)) as $t),
210            OpKind::LogicalXor => combine::<$t>($acc, $val, |a, b| ((a != 0) ^ (b != 0)) as $t),
211            OpKind::BitwiseAnd => combine::<$t>($acc, $val, |a, b| a & b),
212            OpKind::BitwiseOr => combine::<$t>($acc, $val, |a, b| a | b),
213            OpKind::BitwiseXor => combine::<$t>($acc, $val, |a, b| a ^ b),
214        }
215    }};
216}
217
218macro_rules! float_reduce {
219    ($kind:expr, $acc:expr, $val:expr, $t:ty) => {{
220        match $kind {
221            OpKind::Sum => combine::<$t>($acc, $val, |a, b| a + b),
222            OpKind::Product => combine::<$t>($acc, $val, |a, b| a * b),
223            OpKind::Max => combine::<$t>($acc, $val, |a, b| a.max(b)),
224            OpKind::Min => combine::<$t>($acc, $val, |a, b| a.min(b)),
225            other => panic!(
226                "operation {:?} is not defined for floating-point data",
227                other
228            ),
229        }
230    }};
231}
232
233impl Operation for SystemOperation {
234    fn reduce_bytes(&self, dt_id: u32, acc: &mut [u8], val: &[u8]) {
235        debug_assert_eq!(acc.len(), val.len());
236        match dt_id {
237            ids::I8 => int_reduce!(self.kind, acc, val, i8),
238            ids::U8 => int_reduce!(self.kind, acc, val, u8),
239            ids::I16 => int_reduce!(self.kind, acc, val, i16),
240            ids::U16 => int_reduce!(self.kind, acc, val, u16),
241            ids::I32 => int_reduce!(self.kind, acc, val, i32),
242            ids::U32 => int_reduce!(self.kind, acc, val, u32),
243            ids::I64 => int_reduce!(self.kind, acc, val, i64),
244            ids::U64 => int_reduce!(self.kind, acc, val, u64),
245            ids::ISIZE => int_reduce!(self.kind, acc, val, isize),
246            ids::USIZE => int_reduce!(self.kind, acc, val, usize),
247            ids::I128 => int_reduce!(self.kind, acc, val, i128),
248            ids::U128 => int_reduce!(self.kind, acc, val, u128),
249            ids::F32 => float_reduce!(self.kind, acc, val, f32),
250            ids::F64 => float_reduce!(self.kind, acc, val, f64),
251            ids::BOOL => match self.kind {
252                OpKind::LogicalAnd | OpKind::BitwiseAnd | OpKind::Min => {
253                    combine::<u8>(acc, val, |a, b| ((a != 0) && (b != 0)) as u8)
254                }
255                OpKind::LogicalOr | OpKind::BitwiseOr | OpKind::Max => {
256                    combine::<u8>(acc, val, |a, b| ((a != 0) || (b != 0)) as u8)
257                }
258                OpKind::LogicalXor | OpKind::BitwiseXor => {
259                    combine::<u8>(acc, val, |a, b| ((a != 0) ^ (b != 0)) as u8)
260                }
261                other => panic!("operation {:?} is not defined for bool", other),
262            },
263            other => panic!("reduction not supported for datatype id {other}"),
264        }
265    }
266}
267
268/// A user-defined reduction operation wrapping a Rust closure. Mirrors rsmpi's
269/// `UserOperation`. The closure receives the incoming values and the
270/// accumulator (`inout`), matching MPI's `(invec, inoutvec)` convention.
271pub struct UserOperation<T: Equivalence> {
272    #[allow(clippy::type_complexity)]
273    f: Box<dyn Fn(&[T], &mut [T]) + Send + Sync>,
274    commutative: bool,
275}
276
277impl<T: Equivalence> UserOperation<T> {
278    /// A commutative user operation.
279    pub fn commutative<F>(f: F) -> UserOperation<T>
280    where
281        F: Fn(&[T], &mut [T]) + Send + Sync + 'static,
282    {
283        UserOperation {
284            f: Box::new(f),
285            commutative: true,
286        }
287    }
288
289    /// A non-commutative user operation.
290    pub fn non_commutative<F>(f: F) -> UserOperation<T>
291    where
292        F: Fn(&[T], &mut [T]) + Send + Sync + 'static,
293    {
294        UserOperation {
295            f: Box::new(f),
296            commutative: false,
297        }
298    }
299}
300
301impl<T: Equivalence> Operation for UserOperation<T> {
302    fn reduce_bytes(&self, _dt_id: u32, acc: &mut [u8], val: &[u8]) {
303        let mut a: Vec<T> = vec_from_bytes::<T>(acc);
304        let b: Vec<T> = vec_from_bytes::<T>(val);
305        (self.f)(&b, &mut a);
306        // Write the accumulator back into `acc`.
307        let size = std::mem::size_of::<T>();
308        // SAFETY: `a` has the same element count as `acc`; T is POD.
309        let a_bytes =
310            unsafe { std::slice::from_raw_parts(a.as_ptr() as *const u8, a.len() * size) };
311        acc[..a_bytes.len()].copy_from_slice(a_bytes);
312    }
313
314    fn is_commutative(&self) -> bool {
315        self.commutative
316    }
317}
318
319/// Perform a purely local reduction, combining `inbuf` into `inoutbuf`
320/// (`MPI_Reduce_local`).
321pub fn reduce_local_into<S, R, O>(inbuf: &S, inoutbuf: &mut R, op: O)
322where
323    S: Buffer + ?Sized,
324    R: BufferMut + ?Sized,
325    O: Operation,
326{
327    let dt = inbuf.as_datatype().id;
328    let inbytes = inbuf.as_bytes().to_vec();
329    op.reduce_bytes(dt, inoutbuf.as_bytes_mut(), &inbytes);
330}
331
332// =====================================================================
333// Byte-level collective algorithms (operate on the collective context)
334// =====================================================================
335
336fn rt() -> &'static transport::Runtime {
337    transport::runtime()
338}
339
340/// Dissemination barrier: `O(log n)` rounds, each rank exchanging with the peer
341/// `2^k` away.
342fn barrier_bytes(comm: &CommData) -> io::Result<()> {
343    let ctx = comm.coll_context();
344    let n = comm.size;
345    let me = comm.rank;
346    if n <= 1 {
347        return Ok(());
348    }
349    let mut dist = 1;
350    let mut round = 0;
351    while dist < n {
352        let dst = comm.world_rank((me + dist) % n);
353        let src = (me - dist + n) % n;
354        let tag = T_BARRIER_ROUND + round;
355        // Send first (eager, buffered on the receiver), then await our own.
356        rt().send(ctx, me, dst, tag, 1, DT_BYTES, &[0u8])?;
357        let _ = rt().recv(ctx, src, tag);
358        dist <<= 1;
359        round += 1;
360    }
361    Ok(())
362}
363
364/// Binomial-tree broadcast: `O(log n)` steps.
365pub(crate) fn bcast_bytes(comm: &CommData, root: Rank, buf: &mut [u8]) -> io::Result<()> {
366    let ctx = comm.coll_context();
367    let n = comm.size;
368    if n <= 1 {
369        return Ok(());
370    }
371    let me = comm.rank;
372    let rel = (me - root + n) % n;
373
374    // Receive phase: get the data from our parent (unless we are the root).
375    let mut mask = 1;
376    while mask < n {
377        if rel & mask != 0 {
378            let src = real_rank(rel - mask, root, n);
379            let (_s, _t, _c, _d, payload) = rt().recv(ctx, src, T_BCAST);
380            let k = buf.len().min(payload.len());
381            buf[..k].copy_from_slice(&payload[..k]);
382            break;
383        }
384        mask <<= 1;
385    }
386
387    // Send phase: forward to the subtree below us.
388    mask >>= 1;
389    while mask > 0 {
390        let child_rel = rel + mask;
391        if child_rel < n {
392            let dst = comm.world_rank(real_rank(child_rel, root, n));
393            rt().send(ctx, me, dst, T_BCAST, buf.len() as u64, DT_BYTES, buf)?;
394        }
395        mask >>= 1;
396    }
397    Ok(())
398}
399
400fn gather_bytes(
401    comm: &CommData,
402    root: Rank,
403    send: &[u8],
404    recv: Option<&mut [u8]>,
405) -> io::Result<()> {
406    let ctx = comm.coll_context();
407    let n = comm.size;
408    let blk = send.len();
409    if comm.rank == root {
410        let recv = recv.expect("root must provide a receive buffer");
411        recv[(root as usize) * blk..][..blk].copy_from_slice(send);
412        for peer in 0..n {
413            if peer != root {
414                let (_s, _t, _c, _d, payload) = rt().recv(ctx, peer, T_GATHER);
415                recv[(peer as usize) * blk..][..blk].copy_from_slice(&payload);
416            }
417        }
418    } else {
419        rt().send(
420            ctx,
421            comm.rank,
422            comm.world_rank(root),
423            T_GATHER,
424            blk as u64,
425            DT_BYTES,
426            send,
427        )?;
428    }
429    Ok(())
430}
431
432fn scatter_bytes(
433    comm: &CommData,
434    root: Rank,
435    send: Option<&[u8]>,
436    recv: &mut [u8],
437) -> io::Result<()> {
438    let ctx = comm.coll_context();
439    let n = comm.size;
440    let blk = recv.len();
441    if comm.rank == root {
442        let send = send.expect("root must provide a send buffer");
443        recv.copy_from_slice(&send[(root as usize) * blk..][..blk]);
444        for peer in 0..n {
445            if peer != root {
446                rt().send(
447                    ctx,
448                    comm.rank,
449                    comm.world_rank(peer),
450                    T_SCATTER,
451                    blk as u64,
452                    DT_BYTES,
453                    &send[(peer as usize) * blk..][..blk],
454                )?;
455            }
456        }
457    } else {
458        let (_s, _t, _c, _d, payload) = rt().recv(ctx, root, T_SCATTER);
459        let k = recv.len().min(payload.len());
460        recv[..k].copy_from_slice(&payload[..k]);
461    }
462    Ok(())
463}
464
465/// Ring allgather: `n-1` steps, each forwarding one block to the right
466/// neighbour and receiving one from the left (bandwidth-optimal).
467fn allgather_bytes(comm: &CommData, send: &[u8], recv: &mut [u8]) -> io::Result<()> {
468    let ctx = comm.coll_context();
469    let n = comm.size;
470    let me = comm.rank;
471    let blk = send.len();
472    // Seed our own block.
473    recv[(me as usize) * blk..][..blk].copy_from_slice(send);
474    if n <= 1 {
475        return Ok(());
476    }
477    let right = comm.world_rank((me + 1) % n);
478    let left = (me - 1 + n) % n;
479    for step in 0..n - 1 {
480        let send_idx = ((me - step + n) % n) as usize;
481        let recv_idx = ((me - step - 1 + n) % n) as usize;
482        rt().send(
483            ctx,
484            me,
485            right,
486            T_ALLGATHER_UP,
487            blk as u64,
488            DT_BYTES,
489            &recv[send_idx * blk..send_idx * blk + blk],
490        )?;
491        let (_s, _t, _c, _d, payload) = rt().recv(ctx, left, T_ALLGATHER_UP);
492        recv[recv_idx * blk..recv_idx * blk + blk].copy_from_slice(&payload);
493    }
494    Ok(())
495}
496
497fn all_to_all_bytes(comm: &CommData, send: &[u8], recv: &mut [u8]) -> io::Result<()> {
498    let ctx = comm.coll_context();
499    let n = comm.size as usize;
500    let blk = send.len().checked_div(n).unwrap_or(0);
501    let me = comm.rank as usize;
502    for peer in 0..n {
503        let block = &send[peer * blk..][..blk];
504        if peer == me {
505            recv[me * blk..][..blk].copy_from_slice(block);
506        } else {
507            rt().send(
508                ctx,
509                comm.rank,
510                comm.world_rank(peer as Rank),
511                T_ALLTOALL,
512                blk as u64,
513                DT_BYTES,
514                block,
515            )?;
516        }
517    }
518    for peer in 0..n {
519        if peer != me {
520            let (_s, _t, _c, _d, payload) = rt().recv(ctx, peer as Rank, T_ALLTOALL);
521            recv[peer * blk..][..blk].copy_from_slice(&payload);
522        }
523    }
524    Ok(())
525}
526
527/// Ordered linear reduce to `root`, folding contributions in rank order — used
528/// for non-commutative operations where combine order is significant.
529fn reduce_ordered<O: Operation>(
530    comm: &CommData,
531    root: Rank,
532    send: &[u8],
533    recv: Option<&mut [u8]>,
534    op: &O,
535    dt: u32,
536) -> io::Result<()> {
537    let ctx = comm.coll_context();
538    let n = comm.size;
539    if comm.rank == root {
540        let mut contribs: Vec<Vec<u8>> = vec![Vec::new(); n as usize];
541        contribs[root as usize] = send.to_vec();
542        for peer in 0..n {
543            if peer != root {
544                let (_s, _t, _c, _d, payload) = rt().recv(ctx, peer, T_REDUCE);
545                contribs[peer as usize] = payload;
546            }
547        }
548        let mut acc = contribs[0].clone();
549        for c in contribs.iter().skip(1) {
550            op.reduce_bytes(dt, &mut acc, c);
551        }
552        recv.expect("root must provide a receive buffer")
553            .copy_from_slice(&acc);
554    } else {
555        rt().send(
556            ctx,
557            comm.rank,
558            comm.world_rank(root),
559            T_REDUCE,
560            send.len() as u64,
561            dt,
562            send,
563        )?;
564    }
565    Ok(())
566}
567
568/// Reduce `send` from every rank into `recv` at `root`. Uses a binomial tree
569/// (`O(log n)`) for commutative operations and an ordered linear reduction for
570/// non-commutative ones (so combine order is well-defined).
571fn reduce_bytes_op<O: Operation>(
572    comm: &CommData,
573    root: Rank,
574    send: &[u8],
575    recv: Option<&mut [u8]>,
576    op: &O,
577    dt: u32,
578) -> io::Result<()> {
579    let n = comm.size;
580    if n <= 1 {
581        if let Some(r) = recv {
582            r.copy_from_slice(send);
583        }
584        return Ok(());
585    }
586    if !op.is_commutative() {
587        return reduce_ordered(comm, root, send, recv, op, dt);
588    }
589
590    let ctx = comm.coll_context();
591    let me = comm.rank;
592    let rel = (me - root + n) % n;
593    let mut acc = send.to_vec();
594    let mut mask = 1;
595    while mask < n {
596        if rel & mask != 0 {
597            // We are a leaf of this level: send our accumulator up and stop.
598            let parent = comm.world_rank(real_rank(rel - mask, root, n));
599            rt().send(ctx, me, parent, T_REDUCE, acc.len() as u64, dt, &acc)?;
600            break;
601        }
602        // Combine each child's contribution (commutative: order-independent).
603        let child_rel = rel + mask;
604        if child_rel < n {
605            let child = real_rank(child_rel, root, n);
606            let (_s, _t, _c, _d, payload) = rt().recv(ctx, child, T_REDUCE);
607            op.reduce_bytes(dt, &mut acc, &payload);
608        }
609        mask <<= 1;
610    }
611    if rel == 0 {
612        recv.expect("root must provide a receive buffer")
613            .copy_from_slice(&acc);
614    }
615    Ok(())
616}
617
618/// All-reduce = tree reduce to rank 0 followed by a binomial broadcast, both
619/// `O(log n)`.
620fn all_reduce_bytes<O: Operation>(
621    comm: &CommData,
622    send: &[u8],
623    recv: &mut [u8],
624    op: &O,
625    dt: u32,
626) -> io::Result<()> {
627    let n = comm.size;
628    if n <= 1 {
629        recv.copy_from_slice(send);
630        return Ok(());
631    }
632    let is_root = comm.rank == 0;
633    {
634        let recv_opt: Option<&mut [u8]> = if is_root { Some(&mut *recv) } else { None };
635        reduce_bytes_op(comm, 0, send, recv_opt, op, dt)?;
636    }
637    // The result lives in rank 0's `recv`; broadcast it to everyone.
638    bcast_bytes(comm, 0, recv)
639}
640
641fn scan_bytes<O: Operation>(
642    comm: &CommData,
643    send: &[u8],
644    recv: &mut [u8],
645    op: &O,
646    dt: u32,
647) -> io::Result<()> {
648    let ctx = comm.coll_context();
649    let n = comm.size;
650    let me = comm.rank;
651    recv.copy_from_slice(send);
652    if me > 0 {
653        let (_s, _t, _c, _d, prefix) = rt().recv(ctx, me - 1, T_SCAN);
654        let mut acc = prefix;
655        op.reduce_bytes(dt, &mut acc, send);
656        recv.copy_from_slice(&acc);
657    }
658    if me < n - 1 {
659        rt().send(
660            ctx,
661            me,
662            comm.world_rank(me + 1),
663            T_SCAN,
664            recv.len() as u64,
665            dt,
666            recv,
667        )?;
668    }
669    Ok(())
670}
671
672fn exclusive_scan_bytes<O: Operation>(
673    comm: &CommData,
674    send: &[u8],
675    recv: &mut [u8],
676    op: &O,
677    dt: u32,
678) -> io::Result<()> {
679    let ctx = comm.coll_context();
680    let n = comm.size;
681    let me = comm.rank;
682    if me > 0 {
683        let (_s, _t, _c, _d, prefix) = rt().recv(ctx, me - 1, T_EXSCAN);
684        recv.copy_from_slice(&prefix);
685    }
686    if me < n - 1 {
687        // Value forwarded to the next rank is the reduction over 0..=me.
688        let acc = if me == 0 {
689            send.to_vec()
690        } else {
691            let mut a = recv.to_vec();
692            op.reduce_bytes(dt, &mut a, send);
693            a
694        };
695        rt().send(
696            ctx,
697            me,
698            comm.world_rank(me + 1),
699            T_EXSCAN,
700            acc.len() as u64,
701            dt,
702            &acc,
703        )?;
704    }
705    Ok(())
706}
707
708// =====================================================================
709// Public traits
710// =====================================================================
711
712/// Map a transport error to the crate error type.
713fn cerr(e: io::Error) -> crate::MpiError {
714    crate::MpiError::Other(format!("MPI collective failed: {e}"))
715}
716
717/// A raw mutable byte pointer that is `Send`, so a background collective thread
718/// can write the receive buffer (kept alive for the operation by the returned
719/// [`Request`]'s borrow).
720struct SendMutPtr(*mut u8);
721// SAFETY: the pointed-to buffer is borrowed by the Request for `'a`, which
722// outlives the joined thread; there is a single writer.
723unsafe impl Send for SendMutPtr {}
724
725/// Collective operations that do not designate a root, available on every
726/// communicator. Mirrors rsmpi's `CommunicatorCollectives`.
727///
728/// Each blocking collective has a fallible `try_*` counterpart that returns a
729/// [`crate::MpiError`] instead of panicking on a transport failure.
730pub trait CommunicatorCollectives: Communicator {
731    /// Barrier synchronization (`MPI_Barrier`).
732    fn barrier(&self) {
733        self.try_barrier().expect("MPI_Barrier failed");
734    }
735
736    /// Fallible [`barrier`](CommunicatorCollectives::barrier).
737    fn try_barrier(&self) -> Result<(), crate::MpiError> {
738        barrier_bytes(self.comm_data()).map_err(cerr)
739    }
740
741    /// Gather equal-sized contributions from all ranks to all ranks
742    /// (`MPI_Allgather`).
743    fn all_gather_into<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
744    where
745        S: Buffer + ?Sized,
746        R: BufferMut + ?Sized,
747    {
748        self.try_all_gather_into(sendbuf, recvbuf)
749            .expect("MPI_Allgather failed");
750    }
751
752    /// Fallible [`all_gather_into`](CommunicatorCollectives::all_gather_into).
753    fn try_all_gather_into<S, R>(&self, sendbuf: &S, recvbuf: &mut R) -> Result<(), crate::MpiError>
754    where
755        S: Buffer + ?Sized,
756        R: BufferMut + ?Sized,
757    {
758        let send = sendbuf.as_bytes().to_vec();
759        allgather_bytes(self.comm_data(), &send, recvbuf.as_bytes_mut()).map_err(cerr)
760    }
761
762    /// All-to-all scatter/gather with equal block sizes (`MPI_Alltoall`).
763    fn all_to_all_into<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
764    where
765        S: Buffer + ?Sized,
766        R: BufferMut + ?Sized,
767    {
768        self.try_all_to_all_into(sendbuf, recvbuf)
769            .expect("MPI_Alltoall failed");
770    }
771
772    /// Fallible [`all_to_all_into`](CommunicatorCollectives::all_to_all_into).
773    fn try_all_to_all_into<S, R>(&self, sendbuf: &S, recvbuf: &mut R) -> Result<(), crate::MpiError>
774    where
775        S: Buffer + ?Sized,
776        R: BufferMut + ?Sized,
777    {
778        let send = sendbuf.as_bytes().to_vec();
779        all_to_all_bytes(self.comm_data(), &send, recvbuf.as_bytes_mut()).map_err(cerr)
780    }
781
782    /// Reduce contributions from all ranks and make the result available on all
783    /// ranks (`MPI_Allreduce`).
784    fn all_reduce_into<S, R, O>(&self, sendbuf: &S, recvbuf: &mut R, op: O)
785    where
786        S: Buffer + ?Sized,
787        R: BufferMut + ?Sized,
788        O: Operation,
789    {
790        self.try_all_reduce_into(sendbuf, recvbuf, op)
791            .expect("MPI_Allreduce failed");
792    }
793
794    /// Fallible [`all_reduce_into`](CommunicatorCollectives::all_reduce_into).
795    fn try_all_reduce_into<S, R, O>(
796        &self,
797        sendbuf: &S,
798        recvbuf: &mut R,
799        op: O,
800    ) -> Result<(), crate::MpiError>
801    where
802        S: Buffer + ?Sized,
803        R: BufferMut + ?Sized,
804        O: Operation,
805    {
806        let dt = sendbuf.as_datatype().id;
807        let send = sendbuf.as_bytes().to_vec();
808        all_reduce_bytes(self.comm_data(), &send, recvbuf.as_bytes_mut(), &op, dt).map_err(cerr)
809    }
810
811    /// In-place all-reduce: `buf` is both the input and the output
812    /// (`MPI_Allreduce` with `MPI_IN_PLACE`).
813    fn all_reduce_into_in_place<R, O>(&self, buf: &mut R, op: O)
814    where
815        R: BufferMut + ?Sized,
816        O: Operation,
817    {
818        let dt = buf.as_datatype().id;
819        let send = buf.as_bytes_mut().to_vec();
820        all_reduce_bytes(self.comm_data(), &send, buf.as_bytes_mut(), &op, dt)
821            .expect("MPI_Allreduce (in place) failed");
822    }
823
824    /// In-place all-gather: `buf` holds this rank's block at slot `rank` on
825    /// entry and every rank's block on return (`MPI_Allgather` with
826    /// `MPI_IN_PLACE`).
827    fn all_gather_into_in_place<R>(&self, buf: &mut R)
828    where
829        R: BufferMut + ?Sized,
830    {
831        let comm = self.comm_data();
832        let n = comm.size as usize;
833        let total = buf.as_bytes_mut().len();
834        let blk = total.checked_div(n).unwrap_or(0);
835        let me = comm.rank as usize;
836        let myblock = buf.as_bytes_mut()[me * blk..me * blk + blk].to_vec();
837        allgather_bytes(comm, &myblock, buf.as_bytes_mut())
838            .expect("MPI_Allgather (in place) failed");
839    }
840
841    /// Reduce, then scatter equal-sized blocks of the result
842    /// (`MPI_Reduce_scatter_block`).
843    fn reduce_scatter_block_into<S, R, O>(&self, sendbuf: &S, recvbuf: &mut R, op: O)
844    where
845        S: Buffer + ?Sized,
846        R: BufferMut + ?Sized,
847        O: Operation,
848    {
849        let comm = self.comm_data();
850        let dt = sendbuf.as_datatype().id;
851        let tmp = sendbuf.as_bytes().to_vec();
852        let mut reduced = tmp.clone();
853        all_reduce_bytes(comm, &tmp, &mut reduced, &op, dt)
854            .expect("MPI_Reduce_scatter_block failed");
855        let blk = recvbuf.as_bytes_mut().len();
856        let start = (comm.rank as usize) * blk;
857        recvbuf
858            .as_bytes_mut()
859            .copy_from_slice(&reduced[start..][..blk]);
860    }
861
862    /// Inclusive prefix reduction (`MPI_Scan`).
863    fn scan_into<S, R, O>(&self, sendbuf: &S, recvbuf: &mut R, op: O)
864    where
865        S: Buffer + ?Sized,
866        R: BufferMut + ?Sized,
867        O: Operation,
868    {
869        self.try_scan_into(sendbuf, recvbuf, op)
870            .expect("MPI_Scan failed");
871    }
872
873    /// Fallible [`scan_into`](CommunicatorCollectives::scan_into).
874    fn try_scan_into<S, R, O>(
875        &self,
876        sendbuf: &S,
877        recvbuf: &mut R,
878        op: O,
879    ) -> Result<(), crate::MpiError>
880    where
881        S: Buffer + ?Sized,
882        R: BufferMut + ?Sized,
883        O: Operation,
884    {
885        let dt = sendbuf.as_datatype().id;
886        let send = sendbuf.as_bytes().to_vec();
887        scan_bytes(self.comm_data(), &send, recvbuf.as_bytes_mut(), &op, dt).map_err(cerr)
888    }
889
890    /// Exclusive prefix reduction (`MPI_Exscan`).
891    fn exclusive_scan_into<S, R, O>(&self, sendbuf: &S, recvbuf: &mut R, op: O)
892    where
893        S: Buffer + ?Sized,
894        R: BufferMut + ?Sized,
895        O: Operation,
896    {
897        let dt = sendbuf.as_datatype().id;
898        let send = sendbuf.as_bytes().to_vec();
899        exclusive_scan_bytes(self.comm_data(), &send, recvbuf.as_bytes_mut(), &op, dt)
900            .expect("MPI_Exscan failed");
901    }
902
903    /// Varying-count all-gather (`MPI_Allgatherv`).
904    fn all_gather_varcount_into<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
905    where
906        S: Buffer + ?Sized,
907        R: PartitionedBufferMut + ?Sized,
908    {
909        let comm = self.comm_data();
910        let send = sendbuf.as_bytes().to_vec();
911        // Gather at rank 0 then broadcast the assembled buffer.
912        varcount_allgather(comm, &send, recvbuf);
913    }
914
915    /// Varying-count all-to-all (`MPI_Alltoallv`): rank *i* sends its *j*-th
916    /// block to rank *j*, with per-peer counts and displacements on both sides.
917    fn all_to_all_varcount_into<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
918    where
919        S: PartitionedBuffer + ?Sized,
920        R: PartitionedBufferMut + ?Sized,
921    {
922        let comm = self.comm_data();
923        let ctx = comm.coll_context();
924        let n = comm.size;
925        let me = comm.rank;
926        let selem = sendbuf.as_datatype().size;
927        let relem = recvbuf.as_datatype().size;
928        let sc = sendbuf.counts().to_vec();
929        let sd = sendbuf.displs().to_vec();
930        let rd = recvbuf.displs().to_vec();
931        let sbytes = sendbuf.as_bytes().to_vec();
932        for peer in 0..n {
933            let off = sd[peer as usize] as usize * selem;
934            let len = sc[peer as usize] as usize * selem;
935            let block = &sbytes[off..off + len];
936            if peer == me {
937                let ro = rd[me as usize] as usize * relem;
938                recvbuf.as_bytes_mut()[ro..ro + len].copy_from_slice(block);
939            } else {
940                rt().send(
941                    ctx,
942                    me,
943                    comm.world_rank(peer),
944                    T_ALLTOALLV,
945                    len as u64,
946                    DT_BYTES,
947                    block,
948                )
949                .expect("all_to_all_varcount");
950            }
951        }
952        for peer in 0..n {
953            if peer != me {
954                let (_s, _t, _c, _d, payload) = rt().recv(ctx, peer, T_ALLTOALLV);
955                let ro = rd[peer as usize] as usize * relem;
956                recvbuf.as_bytes_mut()[ro..ro + payload.len()].copy_from_slice(&payload);
957            }
958        }
959    }
960
961    /// Non-blocking barrier (`MPI_Ibarrier`). Progresses on a background thread
962    /// using a fresh context, so it genuinely overlaps subsequent work; `wait`
963    /// joins it.
964    fn immediate_barrier(&self) -> Request<'static, ()> {
965        let data = self.comm_data();
966        let comm = data.async_clone(data.derive_context(0x1BA5_1E11));
967        let handle = std::thread::spawn(move || {
968            let _ = barrier_bytes(&comm);
969        });
970        Request::from_join(StaticScope, handle)
971    }
972
973    /// Non-blocking all-gather (`MPI_Iallgather`).
974    fn immediate_all_gather_into<'a, S, R, Sc>(
975        &self,
976        scope: Sc,
977        sendbuf: &'a S,
978        recvbuf: &'a mut R,
979    ) -> Request<'a, R, Sc>
980    where
981        S: 'a + Buffer + ?Sized,
982        R: 'a + BufferMut + ?Sized,
983        Sc: Scope<'a>,
984    {
985        self.all_gather_into(sendbuf, recvbuf);
986        Request::completed(scope)
987    }
988
989    /// Non-blocking all-to-all (`MPI_Ialltoall`).
990    fn immediate_all_to_all_into<'a, S, R, Sc>(
991        &self,
992        scope: Sc,
993        sendbuf: &'a S,
994        recvbuf: &'a mut R,
995    ) -> Request<'a, R, Sc>
996    where
997        S: 'a + Buffer + ?Sized,
998        R: 'a + BufferMut + ?Sized,
999        Sc: Scope<'a>,
1000    {
1001        self.all_to_all_into(sendbuf, recvbuf);
1002        Request::completed(scope)
1003    }
1004
1005    /// Non-blocking all-reduce (`MPI_Iallreduce`). Progresses on a background
1006    /// thread using a fresh context so it overlaps computation; `wait` joins it.
1007    fn immediate_all_reduce_into<'a, S, R, O, Sc>(
1008        &self,
1009        scope: Sc,
1010        sendbuf: &'a S,
1011        recvbuf: &'a mut R,
1012        op: O,
1013    ) -> Request<'a, R, Sc>
1014    where
1015        S: 'a + Buffer + ?Sized,
1016        R: 'a + BufferMut + ?Sized,
1017        O: Operation + Send + 'static,
1018        Sc: Scope<'a>,
1019    {
1020        let data = self.comm_data();
1021        let comm = data.async_clone(data.derive_context(0x1A11_5EED));
1022        let dt = sendbuf.as_datatype().id;
1023        let send = sendbuf.as_bytes().to_vec();
1024        let rbytes = recvbuf.as_bytes_mut();
1025        let rptr = SendMutPtr(rbytes.as_mut_ptr());
1026        let rlen = rbytes.len();
1027        let handle = std::thread::spawn(move || {
1028            let rptr = rptr; // capture the whole (Send) wrapper, not `rptr.0`
1029                             // SAFETY: the receive buffer is borrowed by the returned Request for
1030                             // `'a`, which outlives this thread (joined on wait/drop).
1031            let recv = unsafe { std::slice::from_raw_parts_mut(rptr.0, rlen) };
1032            let _ = all_reduce_bytes(&comm, &send, recv, &op, dt);
1033        });
1034        Request::from_join(scope, handle)
1035    }
1036
1037    /// Non-blocking inclusive scan (`MPI_Iscan`).
1038    fn immediate_scan_into<'a, S, R, O, Sc>(
1039        &self,
1040        scope: Sc,
1041        sendbuf: &'a S,
1042        recvbuf: &'a mut R,
1043        op: O,
1044    ) -> Request<'a, R, Sc>
1045    where
1046        S: 'a + Buffer + ?Sized,
1047        R: 'a + BufferMut + ?Sized,
1048        O: Operation,
1049        Sc: Scope<'a>,
1050    {
1051        self.scan_into(sendbuf, recvbuf, op);
1052        Request::completed(scope)
1053    }
1054
1055    /// Non-blocking exclusive scan (`MPI_Iexscan`).
1056    fn immediate_exclusive_scan_into<'a, S, R, O, Sc>(
1057        &self,
1058        scope: Sc,
1059        sendbuf: &'a S,
1060        recvbuf: &'a mut R,
1061        op: O,
1062    ) -> Request<'a, R, Sc>
1063    where
1064        S: 'a + Buffer + ?Sized,
1065        R: 'a + BufferMut + ?Sized,
1066        O: Operation,
1067        Sc: Scope<'a>,
1068    {
1069        self.exclusive_scan_into(sendbuf, recvbuf, op);
1070        Request::completed(scope)
1071    }
1072
1073    /// Non-blocking reduce-scatter-block (`MPI_Ireduce_scatter_block`).
1074    fn immediate_reduce_scatter_block_into<'a, S, R, O, Sc>(
1075        &self,
1076        scope: Sc,
1077        sendbuf: &'a S,
1078        recvbuf: &'a mut R,
1079        op: O,
1080    ) -> Request<'a, R, Sc>
1081    where
1082        S: 'a + Buffer + ?Sized,
1083        R: 'a + BufferMut + ?Sized,
1084        O: Operation,
1085        Sc: Scope<'a>,
1086    {
1087        self.reduce_scatter_block_into(sendbuf, recvbuf, op);
1088        Request::completed(scope)
1089    }
1090}
1091
1092// Every communicator gets the rootless collectives.
1093impl<C: Communicator + ?Sized> CommunicatorCollectives for C {}
1094
1095fn varcount_allgather<R>(comm: &CommData, send: &[u8], recvbuf: &mut R)
1096where
1097    R: PartitionedBufferMut + ?Sized,
1098{
1099    let ctx = comm.coll_context();
1100    let n = comm.size;
1101    let root = 0;
1102    let elem = recvbuf.as_datatype().size;
1103    let counts: Vec<i32> = recvbuf.counts().to_vec();
1104    let displs: Vec<i32> = recvbuf.displs().to_vec();
1105    if comm.rank == root {
1106        let out = recvbuf.as_bytes_mut();
1107        // place own
1108        let off = displs[root as usize] as usize * elem;
1109        out[off..off + send.len()].copy_from_slice(send);
1110        for peer in 0..n {
1111            if peer != root {
1112                let (_s, _t, _c, _d, payload) = rt().recv(ctx, peer, T_ALLGATHERV_UP);
1113                let off = displs[peer as usize] as usize * elem;
1114                out[off..off + payload.len()].copy_from_slice(&payload);
1115            }
1116        }
1117        let full = out.to_vec();
1118        for peer in 0..n {
1119            if peer != root {
1120                rt().send(
1121                    ctx,
1122                    comm.rank,
1123                    comm.world_rank(peer),
1124                    T_ALLGATHERV_DOWN,
1125                    full.len() as u64,
1126                    DT_BYTES,
1127                    &full,
1128                )
1129                .expect("allgatherv");
1130            }
1131        }
1132    } else {
1133        let _ = &counts;
1134        rt().send(
1135            ctx,
1136            comm.rank,
1137            comm.world_rank(root),
1138            T_ALLGATHERV_UP,
1139            send.len() as u64,
1140            DT_BYTES,
1141            send,
1142        )
1143        .expect("allgatherv");
1144        let (_s, _t, _c, _d, payload) = rt().recv(ctx, root, T_ALLGATHERV_DOWN);
1145        let out = recvbuf.as_bytes_mut();
1146        let k = out.len().min(payload.len());
1147        out[..k].copy_from_slice(&payload[..k]);
1148    }
1149}
1150
1151/// Rooted collective operations, called on the [`Process`] that is the root
1152/// (and, by non-root ranks, on the same root process handle). Mirrors rsmpi's
1153/// `Root`.
1154pub trait Root {
1155    /// The rank of the root process.
1156    fn root_rank(&self) -> Rank;
1157
1158    #[doc(hidden)]
1159    fn root_comm(&self) -> &CommData;
1160
1161    /// Broadcast the root's buffer to every rank (`MPI_Bcast`).
1162    fn broadcast_into<Buf: BufferMut + ?Sized>(&self, buffer: &mut Buf) {
1163        bcast_bytes(self.root_comm(), self.root_rank(), buffer.as_bytes_mut())
1164            .expect("MPI_Bcast failed");
1165    }
1166
1167    /// (non-root) Contribute to a gather (`MPI_Gather`).
1168    fn gather_into<S: Buffer + ?Sized>(&self, sendbuf: &S) {
1169        let send = sendbuf.as_bytes().to_vec();
1170        gather_bytes(self.root_comm(), self.root_rank(), &send, None).expect("MPI_Gather failed");
1171    }
1172
1173    /// (root) Collect contributions from all ranks (`MPI_Gather`).
1174    fn gather_into_root<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
1175    where
1176        S: Buffer + ?Sized,
1177        R: BufferMut + ?Sized,
1178    {
1179        let send = sendbuf.as_bytes().to_vec();
1180        gather_bytes(
1181            self.root_comm(),
1182            self.root_rank(),
1183            &send,
1184            Some(recvbuf.as_bytes_mut()),
1185        )
1186        .expect("MPI_Gather failed");
1187    }
1188
1189    /// (non-root) Receive this rank's slice of a scatter (`MPI_Scatter`).
1190    fn scatter_into<R: BufferMut + ?Sized>(&self, recvbuf: &mut R) {
1191        scatter_bytes(
1192            self.root_comm(),
1193            self.root_rank(),
1194            None,
1195            recvbuf.as_bytes_mut(),
1196        )
1197        .expect("MPI_Scatter failed");
1198    }
1199
1200    /// (root) Distribute slices of the send buffer to all ranks
1201    /// (`MPI_Scatter`).
1202    fn scatter_into_root<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
1203    where
1204        S: Buffer + ?Sized,
1205        R: BufferMut + ?Sized,
1206    {
1207        let send = sendbuf.as_bytes().to_vec();
1208        scatter_bytes(
1209            self.root_comm(),
1210            self.root_rank(),
1211            Some(&send),
1212            recvbuf.as_bytes_mut(),
1213        )
1214        .expect("MPI_Scatter failed");
1215    }
1216
1217    /// (non-root) Contribute to a reduction whose result lands on the root
1218    /// (`MPI_Reduce`).
1219    fn reduce_into<S, O>(&self, sendbuf: &S, op: O)
1220    where
1221        S: Buffer + ?Sized,
1222        O: Operation,
1223    {
1224        let dt = sendbuf.as_datatype().id;
1225        let send = sendbuf.as_bytes().to_vec();
1226        reduce_bytes_op(self.root_comm(), self.root_rank(), &send, None, &op, dt)
1227            .expect("MPI_Reduce failed");
1228    }
1229
1230    /// (root) Reduce contributions from all ranks into `recvbuf`
1231    /// (`MPI_Reduce`).
1232    fn reduce_into_root<S, R, O>(&self, sendbuf: &S, recvbuf: &mut R, op: O)
1233    where
1234        S: Buffer + ?Sized,
1235        R: BufferMut + ?Sized,
1236        O: Operation,
1237    {
1238        let dt = sendbuf.as_datatype().id;
1239        let send = sendbuf.as_bytes().to_vec();
1240        reduce_bytes_op(
1241            self.root_comm(),
1242            self.root_rank(),
1243            &send,
1244            Some(recvbuf.as_bytes_mut()),
1245            &op,
1246            dt,
1247        )
1248        .expect("MPI_Reduce failed");
1249    }
1250
1251    /// (root) In-place reduce: the root's `buf` supplies its contribution and
1252    /// receives the result (`MPI_Reduce` with `MPI_IN_PLACE`). Non-root ranks
1253    /// call [`Root::reduce_into`] as usual.
1254    fn reduce_into_root_in_place<R, O>(&self, buf: &mut R, op: O)
1255    where
1256        R: BufferMut + ?Sized,
1257        O: Operation,
1258    {
1259        let dt = buf.as_datatype().id;
1260        let send = buf.as_bytes_mut().to_vec();
1261        reduce_bytes_op(
1262            self.root_comm(),
1263            self.root_rank(),
1264            &send,
1265            Some(buf.as_bytes_mut()),
1266            &op,
1267            dt,
1268        )
1269        .expect("MPI_Reduce (in place) failed");
1270    }
1271
1272    /// (root) In-place gather: the root's `buf` holds its own block at slot
1273    /// `root` on entry and every rank's block on return (`MPI_Gather` with
1274    /// `MPI_IN_PLACE`). Non-root ranks call [`Root::gather_into`].
1275    fn gather_into_root_in_place<R>(&self, buf: &mut R)
1276    where
1277        R: BufferMut + ?Sized,
1278    {
1279        let comm = self.root_comm();
1280        let root = self.root_rank();
1281        let n = comm.size as usize;
1282        let total = buf.as_bytes_mut().len();
1283        let blk = total.checked_div(n).unwrap_or(0);
1284        let myblock = buf.as_bytes_mut()[root as usize * blk..root as usize * blk + blk].to_vec();
1285        gather_bytes(comm, root, &myblock, Some(buf.as_bytes_mut()))
1286            .expect("MPI_Gather (in place) failed");
1287    }
1288
1289    // ---- varying-count rooted collectives ----
1290
1291    /// (non-root) Contribute to a varying-count gather (`MPI_Gatherv`).
1292    fn gather_varcount_into<S: Buffer + ?Sized>(&self, sendbuf: &S) {
1293        let comm = self.root_comm();
1294        let ctx = comm.coll_context();
1295        let send = sendbuf.as_bytes();
1296        rt().send(
1297            ctx,
1298            comm.rank,
1299            comm.world_rank(self.root_rank()),
1300            T_GATHERV,
1301            send.len() as u64,
1302            DT_BYTES,
1303            send,
1304        )
1305        .expect("gatherv");
1306    }
1307
1308    /// (root) Collect varying-count contributions (`MPI_Gatherv`).
1309    fn gather_varcount_into_root<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
1310    where
1311        S: Buffer + ?Sized,
1312        R: PartitionedBufferMut + ?Sized,
1313    {
1314        let comm = self.root_comm();
1315        let root = self.root_rank();
1316        let ctx = comm.coll_context();
1317        let n = comm.size;
1318        let elem = recvbuf.as_datatype().size;
1319        let displs = recvbuf.displs().to_vec();
1320        let send = sendbuf.as_bytes().to_vec();
1321        let off = displs[root as usize] as usize * elem;
1322        recvbuf.as_bytes_mut()[off..off + send.len()].copy_from_slice(&send);
1323        for peer in 0..n {
1324            if peer != root {
1325                let (_s, _t, _c, _d, payload) = rt().recv(ctx, peer, T_GATHERV);
1326                let off = displs[peer as usize] as usize * elem;
1327                recvbuf.as_bytes_mut()[off..off + payload.len()].copy_from_slice(&payload);
1328            }
1329        }
1330    }
1331
1332    /// (non-root) Receive this rank's slice of a varying-count scatter
1333    /// (`MPI_Scatterv`).
1334    fn scatter_varcount_into<R: BufferMut + ?Sized>(&self, recvbuf: &mut R) {
1335        let comm = self.root_comm();
1336        let ctx = comm.coll_context();
1337        let (_s, _t, _c, _d, payload) = rt().recv(ctx, self.root_rank(), T_SCATTERV);
1338        let out = recvbuf.as_bytes_mut();
1339        let k = out.len().min(payload.len());
1340        out[..k].copy_from_slice(&payload[..k]);
1341    }
1342
1343    /// (root) Distribute varying-count slices to all ranks (`MPI_Scatterv`).
1344    fn scatter_varcount_into_root<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
1345    where
1346        S: PartitionedBuffer + ?Sized,
1347        R: BufferMut + ?Sized,
1348    {
1349        let comm = self.root_comm();
1350        let root = self.root_rank();
1351        let ctx = comm.coll_context();
1352        let n = comm.size;
1353        let elem = sendbuf.as_datatype().size;
1354        let counts = sendbuf.counts().to_vec();
1355        let displs = sendbuf.displs().to_vec();
1356        let sbytes = sendbuf.as_bytes().to_vec();
1357        for peer in 0..n {
1358            let off = displs[peer as usize] as usize * elem;
1359            let len = counts[peer as usize] as usize * elem;
1360            let block = &sbytes[off..off + len];
1361            if peer == root {
1362                let out = recvbuf.as_bytes_mut();
1363                let k = out.len().min(block.len());
1364                out[..k].copy_from_slice(&block[..k]);
1365            } else {
1366                rt().send(
1367                    ctx,
1368                    comm.rank,
1369                    comm.world_rank(peer),
1370                    T_SCATTERV,
1371                    len as u64,
1372                    DT_BYTES,
1373                    block,
1374                )
1375                .expect("scatterv");
1376            }
1377        }
1378    }
1379
1380    // ---- non-blocking rooted collectives (complete synchronously here) ----
1381
1382    /// Non-blocking broadcast (`MPI_Ibcast`).
1383    fn immediate_broadcast_into<'a, Buf, Sc>(
1384        &self,
1385        scope: Sc,
1386        buffer: &'a mut Buf,
1387    ) -> Request<'a, Buf, Sc>
1388    where
1389        Buf: 'a + BufferMut + ?Sized,
1390        Sc: Scope<'a>,
1391    {
1392        self.broadcast_into(buffer);
1393        Request::completed(scope)
1394    }
1395
1396    /// (non-root) Non-blocking gather (`MPI_Igather`).
1397    fn immediate_gather_into<'a, S, Sc>(&self, scope: Sc, sendbuf: &'a S) -> Request<'a, S, Sc>
1398    where
1399        S: 'a + Buffer + ?Sized,
1400        Sc: Scope<'a>,
1401    {
1402        self.gather_into(sendbuf);
1403        Request::completed(scope)
1404    }
1405
1406    /// (root) Non-blocking gather (`MPI_Igather`).
1407    fn immediate_gather_into_root<'a, S, R, Sc>(
1408        &self,
1409        scope: Sc,
1410        sendbuf: &'a S,
1411        recvbuf: &'a mut R,
1412    ) -> Request<'a, R, Sc>
1413    where
1414        S: 'a + Buffer + ?Sized,
1415        R: 'a + BufferMut + ?Sized,
1416        Sc: Scope<'a>,
1417    {
1418        self.gather_into_root(sendbuf, recvbuf);
1419        Request::completed(scope)
1420    }
1421
1422    /// (non-root) Non-blocking scatter (`MPI_Iscatter`).
1423    fn immediate_scatter_into<'a, R, Sc>(&self, scope: Sc, recvbuf: &'a mut R) -> Request<'a, R, Sc>
1424    where
1425        R: 'a + BufferMut + ?Sized,
1426        Sc: Scope<'a>,
1427    {
1428        self.scatter_into(recvbuf);
1429        Request::completed(scope)
1430    }
1431
1432    /// (root) Non-blocking scatter (`MPI_Iscatter`).
1433    fn immediate_scatter_into_root<'a, S, R, Sc>(
1434        &self,
1435        scope: Sc,
1436        sendbuf: &'a S,
1437        recvbuf: &'a mut R,
1438    ) -> Request<'a, R, Sc>
1439    where
1440        S: 'a + Buffer + ?Sized,
1441        R: 'a + BufferMut + ?Sized,
1442        Sc: Scope<'a>,
1443    {
1444        self.scatter_into_root(sendbuf, recvbuf);
1445        Request::completed(scope)
1446    }
1447
1448    /// (non-root) Non-blocking reduce (`MPI_Ireduce`).
1449    fn immediate_reduce_into<'a, S, O, Sc>(
1450        &self,
1451        scope: Sc,
1452        sendbuf: &'a S,
1453        op: O,
1454    ) -> Request<'a, S, Sc>
1455    where
1456        S: 'a + Buffer + ?Sized,
1457        O: Operation,
1458        Sc: Scope<'a>,
1459    {
1460        self.reduce_into(sendbuf, op);
1461        Request::completed(scope)
1462    }
1463
1464    /// (root) Non-blocking reduce (`MPI_Ireduce`).
1465    fn immediate_reduce_into_root<'a, S, R, O, Sc>(
1466        &self,
1467        scope: Sc,
1468        sendbuf: &'a S,
1469        recvbuf: &'a mut R,
1470        op: O,
1471    ) -> Request<'a, R, Sc>
1472    where
1473        S: 'a + Buffer + ?Sized,
1474        R: 'a + BufferMut + ?Sized,
1475        O: Operation,
1476        Sc: Scope<'a>,
1477    {
1478        self.reduce_into_root(sendbuf, recvbuf, op);
1479        Request::completed(scope)
1480    }
1481
1482    /// Spawn `maxprocs` copies of `program` (with `args`) as a new MPI world and
1483    /// return an inter-communicator to them (`MPI_Comm_spawn`). Collective over
1484    /// the root's communicator; the spawned children obtain the matching parent
1485    /// inter-communicator via [`crate::topology::Communicator::parent`].
1486    fn spawn(
1487        &self,
1488        program: &str,
1489        args: &[String],
1490        maxprocs: Rank,
1491    ) -> Result<crate::topology::InterCommunicator, crate::MpiError> {
1492        crate::dpm::spawn_impl(self.root_comm(), self.root_rank(), program, args, maxprocs)
1493    }
1494}
1495
1496impl Root for Process<'_> {
1497    fn root_rank(&self) -> Rank {
1498        self.rank
1499    }
1500    fn root_comm(&self) -> &CommData {
1501        self.comm
1502    }
1503}
1504
1505#[cfg(test)]
1506mod tests {
1507    use super::*;
1508
1509    fn bytes_of<T: Copy>(v: &[T]) -> Vec<u8> {
1510        let mut out = vec![0u8; std::mem::size_of_val(v)];
1511        // SAFETY: T is Copy POD in these tests.
1512        unsafe {
1513            std::ptr::copy_nonoverlapping(v.as_ptr() as *const u8, out.as_mut_ptr(), out.len());
1514        }
1515        out
1516    }
1517
1518    #[test]
1519    fn sum_i32() {
1520        let mut acc = bytes_of(&[1i32, 2, 3]);
1521        let val = bytes_of(&[10i32, 20, 30]);
1522        SystemOperation::sum().reduce_bytes(ids::I32, &mut acc, &val);
1523        let result: Vec<i32> = vec_from_bytes(&acc);
1524        assert_eq!(result, vec![11, 22, 33]);
1525    }
1526
1527    #[test]
1528    fn max_f64() {
1529        let mut acc = bytes_of(&[1.0f64, 5.0]);
1530        let val = bytes_of(&[3.0f64, 2.0]);
1531        SystemOperation::max().reduce_bytes(ids::F64, &mut acc, &val);
1532        let result: Vec<f64> = vec_from_bytes(&acc);
1533        assert_eq!(result, vec![3.0, 5.0]);
1534    }
1535
1536    #[test]
1537    fn bitwise_and_u8() {
1538        let mut acc = bytes_of(&[0b1100u8, 0xFF]);
1539        let val = bytes_of(&[0b1010u8, 0x0F]);
1540        SystemOperation::bitwise_and().reduce_bytes(ids::U8, &mut acc, &val);
1541        assert_eq!(acc, vec![0b1000, 0x0F]);
1542    }
1543
1544    #[test]
1545    fn product_and_min() {
1546        let mut acc = bytes_of(&[2i64, 9]);
1547        SystemOperation::product().reduce_bytes(ids::I64, &mut acc, &bytes_of(&[3i64, 3]));
1548        assert_eq!(vec_from_bytes::<i64>(&acc), vec![6, 27]);
1549
1550        let mut acc = bytes_of(&[2i64, 9]);
1551        SystemOperation::min().reduce_bytes(ids::I64, &mut acc, &bytes_of(&[3i64, 3]));
1552        assert_eq!(vec_from_bytes::<i64>(&acc), vec![2, 3]);
1553    }
1554
1555    /// Tiny deterministic xorshift PRNG (no external deps, Miri-friendly).
1556    struct Rng(u64);
1557    impl Rng {
1558        fn next(&mut self) -> u64 {
1559            let mut x = self.0;
1560            x ^= x << 13;
1561            x ^= x >> 7;
1562            x ^= x << 17;
1563            self.0 = x;
1564            x
1565        }
1566        fn i32(&mut self) -> i32 {
1567            (self.next() >> 32) as i32 / 4
1568        }
1569    }
1570
1571    /// Property test: the byte-level reductions must match a plain-Rust
1572    /// reference over many random inputs and every commutative i32 op.
1573    type RefOp = fn(i32, i32) -> i32;
1574
1575    #[test]
1576    fn reduce_matches_reference_i32() {
1577        let mut rng = Rng(0x1234_5678_9abc_def0);
1578        let ops: &[(SystemOperation, RefOp)] = &[
1579            (SystemOperation::sum(), |a, b| a.wrapping_add(b)),
1580            (SystemOperation::product(), |a, b| a.wrapping_mul(b)),
1581            (SystemOperation::max(), |a, b| a.max(b)),
1582            (SystemOperation::min(), |a, b| a.min(b)),
1583            (SystemOperation::bitwise_and(), |a, b| a & b),
1584            (SystemOperation::bitwise_or(), |a, b| a | b),
1585            (SystemOperation::bitwise_xor(), |a, b| a ^ b),
1586        ];
1587        for _ in 0..500 {
1588            let len = (rng.next() % 8) as usize + 1;
1589            let a: Vec<i32> = (0..len).map(|_| rng.i32()).collect();
1590            let b: Vec<i32> = (0..len).map(|_| rng.i32()).collect();
1591            for (op, refn) in ops {
1592                let expected: Vec<i32> = a.iter().zip(&b).map(|(&x, &y)| refn(x, y)).collect();
1593                let mut acc = bytes_of(&a);
1594                op.reduce_bytes(ids::I32, &mut acc, &bytes_of(&b));
1595                assert_eq!(vec_from_bytes::<i32>(&acc), expected, "op mismatch");
1596            }
1597        }
1598    }
1599}