Skip to main content

mesh_sieve/algs/
communicator.rs

1//! Communication abstraction for intra-process (Rayon) and inter-process (MPI) message passing.
2//!
3//! Wire format conventions (for higher-level protocols):
4//! - All integers are LE fixed width (u32 counts/tags/ranks, u64 IDs).
5//! - Structs are #[repr(C)] and bytemuck::Pod-safe; no #[repr(packed)].
6//! - Receivers may truncate to their provided buffer length; higher layers must
7//!   exchange sizes first if exact lengths are required.
8
9use once_cell::sync::Lazy;
10use std::collections::{HashMap, VecDeque};
11use std::sync::atomic::{AtomicU32, Ordering};
12use std::sync::{Arc, Condvar, Mutex};
13
14use crate::mesh_error::{CommError, MeshSieveError};
15
16/// Anything that can be waited on.
17pub trait Wait {
18    /// Wait for completion and return the received data (if any).
19    fn wait(self) -> Option<Vec<u8>>;
20}
21
22/// Non-blocking completion test.
23pub trait PollWait {
24    /// Return `Some(bytes)` if the operation has completed, otherwise `None`.
25    fn try_wait(&mut self) -> Option<Vec<u8>>;
26}
27
28/// Non-blocking communication interface (minimal by design).
29///
30/// Implementors provide asynchronous send/receive operations and waitable handles.
31pub trait Communicator: Send + Sync + 'static {
32    /// Handle returned by `isend`.
33    type SendHandle: Wait;
34    /// Handle returned by `irecv`.
35    type RecvHandle: Wait;
36
37    fn isend(&self, peer: usize, tag: u16, buf: &[u8]) -> Self::SendHandle;
38    fn irecv(&self, peer: usize, tag: u16, buf: &mut [u8]) -> Self::RecvHandle;
39
40    /// Fallible send initiation for backends that can error.
41    fn isend_result(
42        &self,
43        peer: usize,
44        tag: u16,
45        buf: &[u8],
46    ) -> Result<Self::SendHandle, MeshSieveError> {
47        Ok(self.isend(peer, tag, buf))
48    }
49
50    /// Fallible receive initiation for backends that can error.
51    fn irecv_result(
52        &self,
53        peer: usize,
54        tag: u16,
55        buf: &mut [u8],
56    ) -> Result<Self::RecvHandle, MeshSieveError> {
57        Ok(self.irecv(peer, tag, buf))
58    }
59
60    /// Returns true if this communicator is NoComm (for test logic)
61    fn is_no_comm(&self) -> bool {
62        false
63    }
64
65    /// Rank of this process (0..size-1)
66    fn rank(&self) -> usize;
67    /// Total number of ranks
68    fn size(&self) -> usize;
69
70    /// Synchronization barrier (default: no-op for non-MPI comms)
71    fn barrier(&self) {}
72
73    /// Fallible barrier for backends that can error.
74    fn barrier_result(&self) -> Result<(), MeshSieveError> {
75        self.barrier();
76        Ok(())
77    }
78
79    /// Broadcast a byte buffer from `root` to all ranks.
80    fn broadcast(&self, root: usize, buf: &mut [u8]) {
81        if self.size() <= 1 {
82            return;
83        }
84        if self.rank() == root {
85            let mut sends = Vec::with_capacity(self.size().saturating_sub(1));
86            for peer in 0..self.size() {
87                if peer != root {
88                    sends.push(self.isend(peer, COLLECTIVE_TAG_BROADCAST, buf));
89                }
90            }
91            for send in sends {
92                let _ = send.wait();
93            }
94        } else {
95            let mut tmp = vec![0u8; buf.len()];
96            let recv = self.irecv(root, COLLECTIVE_TAG_BROADCAST, &mut tmp);
97            if let Some(data) = recv.wait() {
98                let copy_len = buf.len().min(data.len());
99                buf[..copy_len].copy_from_slice(&data[..copy_len]);
100            }
101        }
102    }
103
104    /// Fallible broadcast for backends that can error.
105    fn broadcast_result(&self, root: usize, buf: &mut [u8]) -> Result<(), MeshSieveError> {
106        self.broadcast(root, buf);
107        Ok(())
108    }
109
110    /// All-reduce sum for `u64` buffers.
111    fn allreduce_sum(&self, values: &mut [u64]) {
112        let size = self.size();
113        if size <= 1 {
114            return;
115        }
116        let root = 0;
117        let encoded = encode_u64_le(values);
118        if self.rank() == root {
119            let mut accum = values.to_vec();
120            let mut recvs = Vec::with_capacity(size.saturating_sub(1));
121            for peer in 0..size {
122                if peer != root {
123                    let mut tmp = vec![0u8; encoded.len()];
124                    let handle = self.irecv(peer, COLLECTIVE_TAG_ALLREDUCE_GATHER, &mut tmp);
125                    recvs.push(handle);
126                }
127            }
128            for recv in recvs {
129                if let Some(data) = recv.wait() {
130                    add_u64_le(&data, &mut accum);
131                }
132            }
133            values.copy_from_slice(&accum);
134            let out_bytes = encode_u64_le(values);
135            let mut sends = Vec::with_capacity(size.saturating_sub(1));
136            for peer in 0..size {
137                if peer != root {
138                    sends.push(self.isend(peer, COLLECTIVE_TAG_ALLREDUCE_BROADCAST, &out_bytes));
139                }
140            }
141            for send in sends {
142                let _ = send.wait();
143            }
144        } else {
145            let send = self.isend(root, COLLECTIVE_TAG_ALLREDUCE_GATHER, &encoded);
146            let mut tmp = vec![0u8; encoded.len()];
147            let recv = self.irecv(root, COLLECTIVE_TAG_ALLREDUCE_BROADCAST, &mut tmp);
148            let _ = send.wait();
149            if let Some(data) = recv.wait() {
150                decode_u64_le(&data, values);
151            }
152        }
153    }
154
155    /// Fallible all-reduce for backends that can error.
156    fn allreduce_sum_result(&self, values: &mut [u64]) -> Result<(), MeshSieveError> {
157        self.allreduce_sum(values);
158        Ok(())
159    }
160
161    /// All-gather fixed-size byte buffers into `recvbuf` (rank-major order).
162    fn allgather(&self, sendbuf: &[u8], recvbuf: &mut [u8]) {
163        let size = self.size();
164        let chunk = sendbuf.len();
165        if size == 0 || chunk == 0 {
166            return;
167        }
168        assert_eq!(
169            recvbuf.len(),
170            size * chunk,
171            "recvbuf must be size * sendbuf.len()"
172        );
173        let rank = self.rank();
174        let start = rank * chunk;
175        recvbuf[start..start + chunk].copy_from_slice(sendbuf);
176        if size <= 1 {
177            return;
178        }
179        let mut sends = Vec::with_capacity(size.saturating_sub(1));
180        let mut recvs = Vec::with_capacity(size.saturating_sub(1));
181        for peer in 0..size {
182            if peer == rank {
183                continue;
184            }
185            sends.push(self.isend(peer, COLLECTIVE_TAG_ALLGATHER, sendbuf));
186            let mut tmp = vec![0u8; chunk];
187            let recv = self.irecv(peer, COLLECTIVE_TAG_ALLGATHER, &mut tmp);
188            recvs.push((peer, recv));
189        }
190        for send in sends {
191            let _ = send.wait();
192        }
193        for (peer, recv) in recvs {
194            if let Some(data) = recv.wait() {
195                let offset = peer * chunk;
196                assert_eq!(
197                    data.len(),
198                    chunk,
199                    "allgather received unexpected buffer length"
200                );
201                recvbuf[offset..offset + chunk].copy_from_slice(&data);
202            }
203        }
204    }
205
206    /// Fallible all-gather for backends that can error.
207    fn allgather_result(&self, sendbuf: &[u8], recvbuf: &mut [u8]) -> Result<(), MeshSieveError> {
208        self.allgather(sendbuf, recvbuf);
209        Ok(())
210    }
211
212    /// Reserve a contiguous range of tags for this communicator.
213    ///
214    /// Tags in `[RESERVED_TAGS_START, u16::MAX]` are reserved for collectives and
215    /// must not be used by higher-level protocols.
216    fn reserve_tag_range(&self, n: u16) -> Result<CommTag, MeshSieveError> {
217        reserve_tag_range(n)
218    }
219}
220
221/// Tags in this range are reserved for collectives and internal protocols.
222pub const RESERVED_TAGS_START: u16 = COLLECTIVE_TAG_ALLREDUCE_BROADCAST;
223
224const COLLECTIVE_TAG_BROADCAST: u16 = u16::MAX - 1;
225const COLLECTIVE_TAG_ALLGATHER: u16 = u16::MAX - 2;
226const COLLECTIVE_TAG_ALLREDUCE_GATHER: u16 = u16::MAX - 3;
227const COLLECTIVE_TAG_ALLREDUCE_BROADCAST: u16 = u16::MAX - 4;
228
229static NEXT_TAG: AtomicU32 = AtomicU32::new(0);
230
231fn reserve_tag_range(n: u16) -> Result<CommTag, MeshSieveError> {
232    if n == 0 {
233        return Err(MeshSieveError::Communication(CommError(
234            "tag allocation requires n > 0".into(),
235        )));
236    }
237    let n = n as u32;
238    let limit = RESERVED_TAGS_START as u32;
239    let mut current = NEXT_TAG.load(Ordering::Relaxed);
240    loop {
241        let end = current + n - 1;
242        if end >= limit {
243            return Err(MeshSieveError::Communication(CommError(
244                "tag allocation exhausted available user tag range".into(),
245            )));
246        }
247        match NEXT_TAG.compare_exchange(current, end + 1, Ordering::SeqCst, Ordering::Relaxed) {
248            Ok(_) => return Ok(CommTag::new(current as u16)),
249            Err(next) => current = next,
250        }
251    }
252}
253
254fn encode_u64_le(values: &[u64]) -> Vec<u8> {
255    let mut out = Vec::with_capacity(std::mem::size_of_val(values));
256    for value in values {
257        out.extend_from_slice(&value.to_le_bytes());
258    }
259    out
260}
261
262fn decode_u64_le(bytes: &[u8], out: &mut [u64]) {
263    debug_assert_eq!(bytes.len(), std::mem::size_of_val(out));
264    for (chunk, slot) in bytes
265        .chunks_exact(core::mem::size_of::<u64>())
266        .zip(out.iter_mut())
267    {
268        let mut raw = [0u8; 8];
269        raw.copy_from_slice(chunk);
270        *slot = u64::from_le_bytes(raw);
271    }
272}
273
274fn add_u64_le(bytes: &[u8], accum: &mut [u64]) {
275    debug_assert_eq!(bytes.len(), std::mem::size_of_val(accum));
276    for (chunk, slot) in bytes
277        .chunks_exact(core::mem::size_of::<u64>())
278        .zip(accum.iter_mut())
279    {
280        let mut raw = [0u8; 8];
281        raw.copy_from_slice(chunk);
282        *slot += u64::from_le_bytes(raw);
283    }
284}
285
286/// Tag newtype for safer tag arithmetic.
287#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
288pub struct CommTag(u16);
289
290impl CommTag {
291    /// Create a new tag from a raw `u16`.
292    #[inline]
293    pub const fn new(tag: u16) -> Self {
294        Self(tag)
295    }
296
297    /// Return the underlying `u16` value.
298    #[inline]
299    pub const fn as_u16(self) -> u16 {
300        self.0
301    }
302
303    /// Safely offset the tag by `dx`, wrapping on overflow.
304    #[inline]
305    pub const fn offset(self, dx: u16) -> Self {
306        Self(self.0.wrapping_add(dx))
307    }
308}
309
310impl From<u16> for CommTag {
311    #[inline]
312    fn from(x: u16) -> Self {
313        CommTag::new(x)
314    }
315}
316
317/// Convenience bundle of tags for the multi-phase section completion.
318#[derive(Copy, Clone, Debug)]
319pub struct SectionCommTags {
320    /// Tag used during the size-exchange phase.
321    pub sizes: CommTag,
322    /// Tag used during the data-exchange phase.
323    pub data: CommTag,
324}
325
326impl SectionCommTags {
327    /// Construct tags from a base, assigning deterministic offsets per phase.
328    #[inline]
329    pub const fn from_base(base: CommTag) -> Self {
330        Self {
331            sizes: base,
332            data: base.offset(1),
333        }
334    }
335}
336
337/// Convenience bundle of tags for sieve completion.
338#[derive(Copy, Clone, Debug)]
339pub struct SieveCommTags {
340    /// Tag used during the size exchange phase.
341    pub sizes: CommTag,
342    /// Tag used during the data exchange phase.
343    pub data: CommTag,
344}
345
346impl SieveCommTags {
347    /// Construct tags from a base, assigning deterministic offsets per phase.
348    #[inline]
349    pub const fn from_base(base: CommTag) -> Self {
350        Self {
351            sizes: base,
352            data: base.offset(1),
353        }
354    }
355}
356
357/// Convenience bundle of tags for stack completion.
358#[derive(Copy, Clone, Debug)]
359pub struct StackCommTags {
360    /// Tag used during the size exchange phase.
361    pub sizes: CommTag,
362    /// Tag used during the data exchange phase.
363    pub data: CommTag,
364}
365
366impl StackCommTags {
367    /// Construct tags from a base, assigning deterministic offsets per phase.
368    #[inline]
369    pub const fn from_base(base: CommTag) -> Self {
370        Self {
371            sizes: base,
372            data: base.offset(1),
373        }
374    }
375}
376
377/// Compile-time no-op comm for pure serial unit tests.
378#[derive(Clone, Debug, Default)]
379pub struct NoComm;
380
381impl Wait for () {
382    fn wait(self) -> Option<Vec<u8>> {
383        None
384    }
385}
386
387impl PollWait for () {
388    fn try_wait(&mut self) -> Option<Vec<u8>> {
389        None
390    }
391}
392
393impl Communicator for NoComm {
394    type SendHandle = ();
395    type RecvHandle = ();
396
397    fn isend(&self, _peer: usize, _tag: u16, _buf: &[u8]) {}
398
399    fn irecv(&self, _peer: usize, _tag: u16, _buf: &mut [u8]) {}
400
401    fn is_no_comm(&self) -> bool {
402        true
403    }
404
405    fn rank(&self) -> usize {
406        0
407    }
408
409    fn size(&self) -> usize {
410        1
411    }
412}
413
414// --- RayonComm: intra-process / multi-thread ---
415
416type Key = (usize, usize, u16); // (src, dst, tag)
417
418#[derive(Default)]
419struct Slot {
420    q: VecDeque<Vec<u8>>,
421}
422
423struct Mailbox {
424    map: Mutex<HashMap<Key, Arc<(Mutex<Slot>, Condvar)>>>,
425}
426
427static MAILBOX: Lazy<Mailbox> = Lazy::new(|| Mailbox {
428    map: Mutex::new(HashMap::new()),
429});
430
431fn mailbox_entry(key: Key) -> Arc<(Mutex<Slot>, Condvar)> {
432    let mut g = MAILBOX.map.lock().expect("MAILBOX poisoned");
433    g.entry(key)
434        .or_insert_with(|| Arc::new((Mutex::new(Slot::default()), Condvar::new())))
435        .clone()
436}
437
438pub struct LocalSendHandle;
439
440impl Wait for LocalSendHandle {
441    fn wait(self) -> Option<Vec<u8>> {
442        None
443    }
444}
445
446impl PollWait for LocalSendHandle {
447    fn try_wait(&mut self) -> Option<Vec<u8>> {
448        None
449    }
450}
451
452pub struct LocalRecvHandle {
453    cell: Arc<(Mutex<Slot>, Condvar)>,
454    want_len: usize,
455}
456
457impl Wait for LocalRecvHandle {
458    fn wait(self) -> Option<Vec<u8>> {
459        let (lock, cv) = &*self.cell;
460        let mut slot = lock.lock().expect("Slot poisoned");
461        while slot.q.is_empty() {
462            slot = cv.wait(slot).expect("Condvar poisoned");
463        }
464        let mut msg = slot.q.pop_front().expect("q non-empty");
465        msg.truncate(self.want_len.min(msg.len()));
466        Some(msg)
467    }
468}
469
470impl PollWait for LocalRecvHandle {
471    fn try_wait(&mut self) -> Option<Vec<u8>> {
472        let (lock, _cv) = &*self.cell;
473        let mut slot = lock.lock().expect("Slot poisoned");
474        if slot.q.is_empty() {
475            None
476        } else {
477            let mut msg = slot.q.pop_front().expect("q non-empty");
478            msg.truncate(self.want_len.min(msg.len()));
479            Some(msg)
480        }
481    }
482}
483
484#[derive(Clone, Debug)]
485pub struct RayonComm {
486    rank: usize,
487    size: usize,
488}
489
490impl RayonComm {
491    pub fn new(rank: usize, size: usize) -> Self {
492        Self { rank, size }
493    }
494}
495
496impl Communicator for RayonComm {
497    type SendHandle = LocalSendHandle;
498    type RecvHandle = LocalRecvHandle;
499
500    fn isend(&self, peer: usize, tag: u16, buf: &[u8]) -> Self::SendHandle {
501        let key = (self.rank, peer, tag);
502        let entry = mailbox_entry(key);
503        let (lock, cv) = &*entry;
504        {
505            let mut slot = lock.lock().expect("Slot poisoned");
506            slot.q.push_back(buf.to_vec());
507        }
508        cv.notify_all();
509        LocalSendHandle
510    }
511
512    fn irecv(&self, peer: usize, tag: u16, buf: &mut [u8]) -> Self::RecvHandle {
513        let key = (peer, self.rank, tag);
514        LocalRecvHandle {
515            cell: mailbox_entry(key),
516            want_len: buf.len(),
517        }
518    }
519
520    fn rank(&self) -> usize {
521        self.rank
522    }
523
524    fn size(&self) -> usize {
525        self.size
526    }
527
528    fn barrier(&self) {
529        #[cfg(test)]
530        {
531            test_barrier::set_size(self.size);
532            test_barrier::wait();
533        }
534    }
535}
536
537// Optional test barrier for deterministic multi-thread tests.
538#[cfg(test)]
539mod test_barrier {
540    use once_cell::sync::Lazy;
541    use std::sync::{Condvar, Mutex};
542
543    pub struct EpochBarrier {
544        size: usize,
545        arrived: usize,
546        epoch: usize,
547    }
548
549    static BARRIER: Lazy<(Mutex<EpochBarrier>, Condvar)> = Lazy::new(|| {
550        (
551            Mutex::new(EpochBarrier {
552                size: 1,
553                arrived: 0,
554                epoch: 0,
555            }),
556            Condvar::new(),
557        )
558    });
559
560    pub fn set_size(size: usize) {
561        let (lock, _) = &*BARRIER;
562        let mut b = lock.lock().unwrap();
563        b.size = size;
564    }
565
566    pub fn wait() {
567        let (lock, cv) = &*BARRIER;
568        let mut b = lock.lock().unwrap();
569        let e = b.epoch;
570        b.arrived += 1;
571        if b.arrived == b.size {
572            b.arrived = 0;
573            b.epoch += 1;
574            cv.notify_all();
575        } else {
576            while e == b.epoch {
577                b = cv.wait(b).unwrap();
578            }
579        }
580    }
581}
582
583// --- MPI backend ---
584#[cfg(feature = "mpi-support")]
585mod mpi_backend {
586    use super::*;
587    use crate::mesh_error::{CommError, MeshSieveError};
588    use core::ptr::NonNull;
589    use mpi::collective::{CommunicatorCollectives, Root, SystemOperation};
590    use mpi::environment::Universe;
591    use mpi::point_to_point::{Destination, Source};
592    use mpi::topology::{Communicator as _, SimpleCommunicator};
593
594    pub struct MpiComm {
595        _universe: Universe,
596        pub world: SimpleCommunicator,
597        rank: usize,
598        size: usize,
599    }
600
601    unsafe impl Send for MpiComm {}
602    unsafe impl Sync for MpiComm {}
603
604    impl MpiComm {
605        pub fn new() -> Result<Self, MeshSieveError> {
606            let uni = mpi::initialize().ok_or_else(|| {
607                MeshSieveError::Communication(CommError("MPI initialization failed".to_string()))
608            })?;
609            let world = uni.world();
610            let rank = world.rank() as usize;
611            let size = world.size() as usize;
612            Ok(Self {
613                _universe: uni,
614                world,
615                rank,
616                size,
617            })
618        }
619    }
620
621    impl Default for MpiComm {
622        fn default() -> Self {
623            Self::new().expect("MPI initialization failed")
624        }
625    }
626
627    impl Communicator for MpiComm {
628        type SendHandle = MpiSendHandle;
629        type RecvHandle = MpiRecvHandle;
630
631        fn isend(&self, peer: usize, tag: u16, buf: &[u8]) -> Self::SendHandle {
632            use mpi::request::StaticScope;
633            let boxed = buf.to_vec().into_boxed_slice();
634            let raw: *mut [u8] = Box::into_raw(boxed);
635            let slice: &[u8] = unsafe { &*raw };
636            let req = self
637                .world
638                .process_at_rank(peer as i32)
639                .immediate_send_with_tag(StaticScope, slice, tag as i32);
640            MpiSendHandle {
641                req: Some(req),
642                buf: Some(unsafe { NonNull::new_unchecked(raw) }),
643            }
644        }
645
646        fn irecv(&self, peer: usize, tag: u16, template: &mut [u8]) -> Self::RecvHandle {
647            use mpi::request::StaticScope;
648            let len = template.len();
649            let boxed = vec![0u8; len].into_boxed_slice();
650            let raw: *mut [u8] = Box::into_raw(boxed);
651            let slice_mut: &mut [u8] = unsafe { &mut *raw };
652            let req = self
653                .world
654                .process_at_rank(peer as i32)
655                .immediate_receive_into_with_tag(StaticScope, slice_mut, tag as i32);
656            MpiRecvHandle {
657                req: Some(req),
658                buf: Some(unsafe { NonNull::new_unchecked(raw) }),
659                len,
660            }
661        }
662
663        fn rank(&self) -> usize {
664            self.rank
665        }
666        fn size(&self) -> usize {
667            self.size
668        }
669        fn barrier(&self) {
670            self.world.barrier();
671        }
672
673        fn broadcast(&self, root: usize, buf: &mut [u8]) {
674            self.world.process_at_rank(root as i32).broadcast_into(buf);
675        }
676
677        fn allreduce_sum(&self, values: &mut [u64]) {
678            let mut out = vec![0u64; values.len()];
679            self.world
680                .all_reduce_into(values, &mut out, SystemOperation::sum());
681            values.copy_from_slice(&out);
682        }
683
684        fn allgather(&self, sendbuf: &[u8], recvbuf: &mut [u8]) {
685            self.world.all_gather_into(sendbuf, recvbuf);
686        }
687    }
688
689    pub struct MpiSendHandle {
690        req: Option<mpi::request::Request<'static, [u8], mpi::request::StaticScope>>,
691        buf: Option<NonNull<[u8]>>,
692    }
693    impl Wait for MpiSendHandle {
694        fn wait(mut self) -> Option<Vec<u8>> {
695            if let Some(r) = self.req.take() {
696                let _ = r.wait();
697            }
698            if let Some(ptr) = self.buf.take() {
699                unsafe {
700                    drop(Box::from_raw(ptr.as_ptr()));
701                }
702            }
703            None
704        }
705    }
706    impl Drop for MpiSendHandle {
707        fn drop(&mut self) {
708            if let Some(r) = self.req.take() {
709                let _ = r.wait();
710            }
711            if let Some(ptr) = self.buf.take() {
712                unsafe {
713                    drop(Box::from_raw(ptr.as_ptr()));
714                }
715            }
716        }
717    }
718
719    pub struct MpiRecvHandle {
720        req: Option<mpi::request::Request<'static, [u8], mpi::request::StaticScope>>,
721        buf: Option<NonNull<[u8]>>,
722        len: usize,
723    }
724    impl Wait for MpiRecvHandle {
725        fn wait(mut self) -> Option<Vec<u8>> {
726            if let Some(r) = self.req.take() {
727                let _ = r.wait();
728            }
729            let ptr = self.buf.take().expect("buffer missing");
730            let boxed: Box<[u8]> = unsafe { Box::from_raw(ptr.as_ptr()) };
731            let mut v = Vec::from(boxed);
732            v.truncate(self.len);
733            Some(v)
734        }
735    }
736    impl Drop for MpiRecvHandle {
737        fn drop(&mut self) {
738            if let Some(r) = self.req.take() {
739                let _ = r.wait();
740            }
741            if let Some(ptr) = self.buf.take() {
742                unsafe {
743                    drop(Box::from_raw(ptr.as_ptr()));
744                }
745            }
746        }
747    }
748}
749
750#[cfg(feature = "mpi-support")]
751pub use mpi_backend::MpiComm;