Skip to main content

mpi/
transport.rs

1//! Internal networking runtime for pure-Rust MPI.
2//!
3//! This module is the equivalent of the "byte transfer layer" in a C MPI
4//! implementation such as Open MPI or MPICH. It is responsible for:
5//!
6//! * bootstrapping the job (discovering our rank, the world size and the
7//!   network address of every peer) via a tiny PMI-style protocol spoken to
8//!   the `mpiexec` launcher, and
9//! * moving tagged, typed byte buffers between processes over TCP with the
10//!   ordering guarantees MPI requires (non-overtaking messages between a given
11//!   sender/receiver pair on a communicator).
12//!
13//! Everything above this module (datatypes, point-to-point, collectives, …) is
14//! implemented in pure Rust on top of the primitives exposed here.
15
16use std::collections::{HashMap, VecDeque};
17use std::io::{self, Read, Write};
18use std::net::{SocketAddr, TcpListener, TcpStream};
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::{Arc, Condvar, Mutex, OnceLock};
21use std::thread;
22use std::time::Duration;
23
24/// Wildcard matching any source rank (`MPI_ANY_SOURCE`).
25pub const ANY_SOURCE: i32 = -1;
26/// Wildcard matching any tag (`MPI_ANY_TAG`).
27pub const ANY_TAG: i32 = -1;
28
29const MAGIC: u32 = 0x4D50_4921; // "MPI!"
30const HEADER_LEN: usize = 4 + 4 + 4 + 4 + 4 + 8 + 4 + 8; // 40 bytes
31
32/// This host's byte order, reported during bootstrap. The wire format is fixed
33/// little-endian for headers/control data; typed payloads are native bytes, so
34/// a job must be single-endian. Mixed-endian jobs are rejected at startup.
35#[cfg(target_endian = "big")]
36const MY_ENDIAN: &str = "be";
37#[cfg(target_endian = "little")]
38const MY_ENDIAN: &str = "le";
39
40/// Parent inter-communicator info handed to a spawned child:
41/// `(inter-comm context, parent-group addresses)`.
42pub type ParentBlock = (u32, Vec<SocketAddr>);
43
44/// Reserved context for abort notifications. A message on this context makes
45/// the receiving process exit, so one rank's failure tears the whole job down
46/// instead of leaving peers blocked forever.
47const ABORT_CONTEXT: u32 = 0x7AB0_0117;
48
49/// Reserved context on which a receiver requests a large message (clear-to-send).
50const RNDV_CTS_CTEXT: u32 = 0x7AB0_0C75;
51/// Reserved context on which the actual large-message data is delivered.
52const RNDV_DATA_CTEXT: u32 = 0x7AB0_0DA7;
53/// Datatype-field bit marking a rendezvous ready-to-send announcement.
54const RTS_BIT: u32 = 0x8000_0000;
55/// Messages larger than this use the rendezvous protocol (the receiver pulls
56/// the data when ready), bounding memory instead of eagerly buffering.
57const RNDV_THRESHOLD: usize = 64 * 1024;
58
59/// Set once an abort/panic is in progress, to prevent re-entrant propagation.
60static ABORTING: AtomicBool = AtomicBool::new(false);
61
62/// Pending outgoing large messages, keyed by transfer id, awaiting a
63/// clear-to-send from the receiver.
64static RNDV_OUT: Mutex<Option<HashMap<u64, Vec<u8>>>> = Mutex::new(None);
65/// Monotonic transfer-id counter (mixed with rank for global uniqueness).
66static RNDV_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
67
68/// A message that has arrived and is waiting to be matched by a receive.
69#[derive(Debug)]
70struct Incoming {
71    comm: u32,
72    source: i32,
73    tag: i32,
74    count: u64,
75    datatype: u32,
76    payload: Vec<u8>,
77}
78
79/// The header of the wire envelope for a single message.
80#[derive(Debug, Clone, Copy)]
81struct Header {
82    comm: u32,
83    source: i32,
84    dest: i32,
85    tag: i32,
86    count: u64,
87    datatype: u32,
88    len: u64,
89}
90
91impl Header {
92    fn to_bytes(self) -> [u8; HEADER_LEN] {
93        let mut b = [0u8; HEADER_LEN];
94        b[0..4].copy_from_slice(&MAGIC.to_le_bytes());
95        b[4..8].copy_from_slice(&self.comm.to_le_bytes());
96        b[8..12].copy_from_slice(&self.source.to_le_bytes());
97        b[12..16].copy_from_slice(&self.dest.to_le_bytes());
98        b[16..20].copy_from_slice(&self.tag.to_le_bytes());
99        b[20..28].copy_from_slice(&self.count.to_le_bytes());
100        b[28..32].copy_from_slice(&self.datatype.to_le_bytes());
101        b[32..40].copy_from_slice(&self.len.to_le_bytes());
102        b
103    }
104
105    fn from_bytes(b: &[u8; HEADER_LEN]) -> io::Result<Header> {
106        let magic = u32::from_le_bytes(b[0..4].try_into().unwrap());
107        if magic != MAGIC {
108            return Err(io::Error::new(
109                io::ErrorKind::InvalidData,
110                "bad MPI wire magic",
111            ));
112        }
113        Ok(Header {
114            comm: u32::from_le_bytes(b[4..8].try_into().unwrap()),
115            source: i32::from_le_bytes(b[8..12].try_into().unwrap()),
116            dest: i32::from_le_bytes(b[12..16].try_into().unwrap()),
117            tag: i32::from_le_bytes(b[16..20].try_into().unwrap()),
118            count: u64::from_le_bytes(b[20..28].try_into().unwrap()),
119            datatype: u32::from_le_bytes(b[28..32].try_into().unwrap()),
120            len: u64::from_le_bytes(b[32..40].try_into().unwrap()),
121        })
122    }
123}
124
125/// An active-message handler: invoked for messages arriving on a registered
126/// context (used by RMA windows to service `put`/`get`/`accumulate`). Called
127/// with `(source, tag, count, datatype, payload)`.
128pub type Handler = Arc<dyn Fn(i32, i32, u64, u32, Vec<u8>) + Send + Sync>;
129
130/// Shared mailbox of received-but-not-yet-matched messages, plus a table of
131/// active-message handlers for contexts that should be serviced immediately
132/// rather than queued.
133struct Inbox {
134    queue: Mutex<VecDeque<Incoming>>,
135    cvar: Condvar,
136    handlers: Mutex<HashMap<u32, Handler>>,
137}
138
139impl Inbox {
140    fn new() -> Inbox {
141        Inbox {
142            queue: Mutex::new(VecDeque::new()),
143            cvar: Condvar::new(),
144            handlers: Mutex::new(HashMap::new()),
145        }
146    }
147
148    fn push(&self, msg: Incoming) {
149        // Contexts with a registered handler are serviced immediately (e.g. RMA
150        // requests) instead of being queued for a matching receive.
151        let handler = self.handlers.lock().unwrap().get(&msg.comm).cloned();
152        if let Some(h) = handler {
153            h(msg.source, msg.tag, msg.count, msg.datatype, msg.payload);
154            return;
155        }
156        let mut q = self.queue.lock().unwrap();
157        q.push_back(msg);
158        self.cvar.notify_all();
159    }
160
161    /// Test whether a message matches a receive request.
162    fn matches(msg: &Incoming, comm: u32, source: i32, tag: i32) -> bool {
163        msg.comm == comm
164            && (source == ANY_SOURCE || msg.source == source)
165            && (tag == ANY_TAG || msg.tag == tag)
166    }
167
168    /// Block until a matching message is available, then remove and return it.
169    ///
170    /// If the wait lasts unusually long, emit a one-time diagnostic naming the
171    /// receive that is stuck — a likely deadlock — instead of hanging silently.
172    fn take_matching(&self, comm: u32, source: i32, tag: i32) -> Incoming {
173        let mut q = self.queue.lock().unwrap();
174        let mut waited = Duration::ZERO;
175        let mut warned = false;
176        loop {
177            if let Some(pos) = q.iter().position(|m| Inbox::matches(m, comm, source, tag)) {
178                return q.remove(pos).unwrap();
179            }
180            let (guard, res) = self.cvar.wait_timeout(q, Duration::from_secs(5)).unwrap();
181            q = guard;
182            if res.timed_out() {
183                waited += Duration::from_secs(5);
184                if waited >= Duration::from_secs(30) && !warned {
185                    warned = true;
186                    let rank = RUNTIME.get().map(|r| r.rank).unwrap_or(-1);
187                    eprintln!(
188                        "[mpi] rank {rank} has been blocked for {}s in receive \
189                         (comm ctx {comm}, source {source}, tag {tag}) — possible deadlock",
190                        waited.as_secs()
191                    );
192                }
193            }
194        }
195    }
196
197    /// Non-blocking check for a matching message; returns a lightweight
198    /// description of the earliest match without consuming it.
199    fn peek_matching(
200        &self,
201        comm: u32,
202        source: i32,
203        tag: i32,
204    ) -> Option<(i32, i32, u64, u32, usize)> {
205        let q = self.queue.lock().unwrap();
206        q.iter()
207            .find(|m| Inbox::matches(m, comm, source, tag))
208            .map(|m| (m.source, m.tag, m.count, m.datatype, m.payload.len()))
209    }
210}
211
212/// The process-global MPI runtime state.
213pub struct Runtime {
214    pub rank: i32,
215    pub size: i32,
216    pub threading: crate::environment::Threading,
217    /// This process's own advertised address (`None` for a singleton job).
218    my_addr: Option<SocketAddr>,
219    /// World address table, indexed by world rank.
220    addresses: Vec<SocketAddr>,
221    inbox: Arc<Inbox>,
222    /// Outbound connection cache, keyed by peer address (so the same rank id on
223    /// different communication contexts — e.g. inter-communicators reaching a
224    /// separately-bootstrapped world — maps to distinct connections).
225    outgoing: Mutex<HashMap<SocketAddr, Arc<Mutex<TcpStream>>>>,
226    /// Per-context peer address overrides. A context present here routes by
227    /// looking the destination rank up in this table instead of the world
228    /// address table — used by spawned inter-communicators to reach processes
229    /// in another world.
230    context_peers: Mutex<HashMap<u32, Vec<SocketAddr>>>,
231    /// Bytes attached by the user for buffered sends (`MPI_Buffer_attach`).
232    buffer_size: Mutex<usize>,
233    /// Shared-memory fast-path for same-host peers (opt-in `shm` feature).
234    #[cfg(feature = "shm")]
235    shm: Option<crate::shm::ShmTransport>,
236}
237
238static RUNTIME: OnceLock<Runtime> = OnceLock::new();
239
240/// Parent inter-communicator info captured during a spawned child's bootstrap:
241/// `(inter-comm context, parent-group addresses)`. Consumed by
242/// `Communicator::parent()`.
243static SPAWN_PARENT: Mutex<Option<ParentBlock>> = Mutex::new(None);
244
245/// The parent info for a spawned process, if any (see [`SPAWN_PARENT`]).
246pub fn spawn_parent() -> Option<ParentBlock> {
247    SPAWN_PARENT.lock().unwrap().clone()
248}
249
250/// Best-effort notify every peer that the job is aborting, then exit this
251/// process with `code`. Peers receive the notice on [`ABORT_CONTEXT`] and exit
252/// too, so no rank is left blocked in a receive.
253pub fn abort_job(code: i32) -> ! {
254    // Only the first aborter broadcasts; others just exit.
255    if !ABORTING.swap(true, Ordering::SeqCst) {
256        if let Some(rt) = RUNTIME.get() {
257            if std::env::var("MPI_DEBUG").is_ok() {
258                eprintln!(
259                    "[abort] rank {} broadcasting abort {code} to {} peers",
260                    rt.rank,
261                    rt.size - 1
262                );
263            }
264            let header = Header {
265                comm: ABORT_CONTEXT,
266                source: rt.rank,
267                dest: 0,
268                tag: 0,
269                count: 1,
270                datatype: crate::datatype::ids::I32,
271                len: 4,
272            }
273            .to_bytes();
274            let code_bytes = code.to_le_bytes();
275            for w in 0..rt.size {
276                if w == rt.rank {
277                    continue;
278                }
279                if let Some(addr) = rt.peer_addr(w) {
280                    // Short timeout so a dead/blocked peer doesn't stall abort.
281                    match TcpStream::connect_timeout(&addr, Duration::from_millis(300)) {
282                        Ok(mut s) => {
283                            let _ = s.write_all(&header);
284                            let _ = s.write_all(&code_bytes);
285                            let _ = s.flush();
286                            if std::env::var("MPI_DEBUG").is_ok() {
287                                eprintln!("[abort] sent to rank {w} at {addr}");
288                            }
289                        }
290                        Err(e) => {
291                            if std::env::var("MPI_DEBUG").is_ok() {
292                                eprintln!("[abort] connect to rank {w} at {addr} failed: {e}");
293                            }
294                        }
295                    }
296                }
297            }
298        }
299    }
300    std::process::exit(code);
301}
302
303/// Install the abort handler and a panic hook so a panic in one rank propagates
304/// to the rest of the job instead of hanging them.
305fn install_fault_handling() {
306    // A message on ABORT_CONTEXT makes us exit with the sender's code.
307    runtime().register_handler(
308        ABORT_CONTEXT,
309        Arc::new(|_src, _tag, _count, _dt, payload: Vec<u8>| {
310            let code = if payload.len() >= 4 {
311                i32::from_le_bytes(payload[..4].try_into().unwrap())
312            } else {
313                1
314            };
315            if !ABORTING.swap(true, Ordering::SeqCst) {
316                eprintln!("MPI job aborting (peer signalled exit {code})");
317            }
318            std::process::exit(code);
319        }),
320    );
321
322    // Rendezvous clear-to-send: a receiver has asked for a large message; send
323    // it the data now.
324    runtime().register_handler(
325        RNDV_CTS_CTEXT,
326        Arc::new(|_src, _tag, _count, _dt, payload: Vec<u8>| {
327            if payload.len() < 12 {
328                return;
329            }
330            let id = u64::from_le_bytes(payload[0..8].try_into().unwrap());
331            let requester = i32::from_le_bytes(payload[8..12].try_into().unwrap());
332            let data = RNDV_OUT
333                .lock()
334                .unwrap()
335                .as_mut()
336                .and_then(|m| m.remove(&id));
337            if let Some(data) = data {
338                let rt = runtime();
339                let id_tag = (id & 0x7FFF_FFFF) as i32;
340                let _ = rt.send_eager(
341                    RNDV_DATA_CTEXT,
342                    rt.rank,
343                    requester,
344                    id_tag,
345                    data.len() as u64,
346                    crate::datatype::ids::U8,
347                    &data,
348                );
349            }
350        }),
351    );
352
353    // On panic, print as usual then tear the job down.
354    let default_hook = std::panic::take_hook();
355    std::panic::set_hook(Box::new(move |info| {
356        default_hook(info);
357        abort_job(101);
358    }));
359}
360
361/// Whether [`init`] has been called.
362#[allow(dead_code)]
363pub fn is_initialized() -> bool {
364    RUNTIME.get().is_some()
365}
366
367/// Access the initialized runtime, panicking with an MPI-style message if the
368/// library has not been initialized.
369pub fn runtime() -> &'static Runtime {
370    RUNTIME
371        .get()
372        .expect("MPI used before mpi::initialize() / after finalize")
373}
374
375/// Reverse the bytes of each `elem`-sized element in `buf` (endianness swap).
376/// Compiled to a no-op on little-endian hosts, so the LE build and all its
377/// behaviour are unchanged; big-endian hosts use it to transcode typed payloads
378/// to/from the canonical little-endian wire format.
379#[cfg(target_endian = "big")]
380fn swap_elems(buf: &mut [u8], elem: usize) {
381    if elem > 1 {
382        for chunk in buf.chunks_exact_mut(elem) {
383            chunk.reverse();
384        }
385    }
386}
387
388/// Whether a handler is registered for `comm` in this process. Handler contexts
389/// (RMA windows, rendezvous control, abort) carry raw/native bytes and are not
390/// endian-swapped.
391#[cfg(target_endian = "big")]
392impl Runtime {
393    fn handler_registered(&self, comm: u32) -> bool {
394        self.inbox.handlers.lock().unwrap().contains_key(&comm)
395    }
396}
397
398/// Best-effort detection of the local source IP the OS would use to reach
399/// `peer` (an `ip:port` string). Connecting a UDP socket sends no packets; it
400/// just selects the source address.
401fn detect_local_ip_toward(peer: &str) -> Option<String> {
402    let sock = std::net::UdpSocket::bind("0.0.0.0:0").ok()?;
403    sock.connect(peer).ok()?;
404    Some(sock.local_addr().ok()?.ip().to_string())
405}
406
407/// Bootstrap the runtime. Returns an error if called twice.
408pub fn init(threading: crate::environment::Threading) -> Result<(), crate::MpiError> {
409    if RUNTIME.get().is_some() {
410        return Err(crate::MpiError::AlreadyInitialized);
411    }
412
413    let inbox = Arc::new(Inbox::new());
414
415    // Are we launched under `mpiexec`? If not, run as a singleton (rank 0 of 1).
416    let pmi = std::env::var("MPI_PMI_ROOT").ok();
417    let (rank, size, addresses, my_addr) = match pmi {
418        Some(root_addr) => {
419            let rank: i32 = std::env::var("MPI_PMI_RANK")
420                .map_err(|_| crate::MpiError::Bootstrap("MPI_PMI_RANK missing".into()))?
421                .parse()
422                .map_err(|_| crate::MpiError::Bootstrap("MPI_PMI_RANK invalid".into()))?;
423            let size: i32 = std::env::var("MPI_PMI_SIZE")
424                .map_err(|_| crate::MpiError::Bootstrap("MPI_PMI_SIZE missing".into()))?
425                .parse()
426                .map_err(|_| crate::MpiError::Bootstrap("MPI_PMI_SIZE invalid".into()))?;
427
428            // Bind our own data listener and start accepting peer connections.
429            // For multi-host jobs the launcher sets `MPI_MULTIHOST=1`; we then
430            // advertise this node's routable IP (explicit `MPI_HOST_IP`, or
431            // auto-detected as the source address toward the launcher) and bind
432            // all interfaces so peers on other machines can reach us. Without it
433            // we stay on loopback (single-host, unchanged behaviour).
434            let advertise_ip = std::env::var("MPI_HOST_IP").ok().or_else(|| {
435                if std::env::var("MPI_MULTIHOST").is_ok() {
436                    detect_local_ip_toward(&root_addr)
437                } else {
438                    None
439                }
440            });
441            let bind_host: &str = if advertise_ip.is_some() {
442                "0.0.0.0"
443            } else {
444                "127.0.0.1"
445            };
446            let listener = TcpListener::bind((bind_host, 0))
447                .map_err(|e| crate::MpiError::Bootstrap(format!("bind failed: {e}")))?;
448            let port = listener
449                .local_addr()
450                .map_err(|e| crate::MpiError::Bootstrap(format!("local_addr failed: {e}")))?
451                .port();
452            let ip = advertise_ip.unwrap_or_else(|| "127.0.0.1".to_string());
453            let my_addr: SocketAddr = format!("{ip}:{port}")
454                .parse()
455                .map_err(|e| crate::MpiError::Bootstrap(format!("bad advertise addr: {e}")))?;
456
457            if std::env::var("MPI_DEBUG").is_ok() {
458                eprintln!(
459                    "[mpi rank {rank}] multihost={} advertise={my_addr} root={root_addr}",
460                    std::env::var("MPI_MULTIHOST").is_ok()
461                );
462            }
463            let (addresses, parent) = pmi_exchange(&root_addr, rank, size, my_addr)?;
464            if let Some(p) = parent {
465                *SPAWN_PARENT.lock().unwrap() = Some(p);
466            }
467
468            spawn_acceptor(listener, Arc::clone(&inbox));
469
470            (rank, size, addresses, Some(my_addr))
471        }
472        None => {
473            // Singleton job (rank 0 of 1). Still bring up a loopback listener so
474            // the process has an address and can act as a spawn parent.
475            let listener = TcpListener::bind(("127.0.0.1", 0))
476                .map_err(|e| crate::MpiError::Bootstrap(format!("bind failed: {e}")))?;
477            let port = listener.local_addr().map(|a| a.port()).unwrap_or(0);
478            let my_addr: SocketAddr = format!("127.0.0.1:{port}")
479                .parse()
480                .map_err(|e| crate::MpiError::Bootstrap(format!("bad addr: {e}")))?;
481            spawn_acceptor(listener, Arc::clone(&inbox));
482            (0, 1, vec![my_addr], Some(my_addr))
483        }
484    };
485
486    // Optional shared-memory fast-path for same-host peers.
487    #[cfg(feature = "shm")]
488    let shm = {
489        let jobid: u64 = std::env::var("MPI_JOBID")
490            .ok()
491            .and_then(|s| s.parse().ok())
492            .unwrap_or_else(|| std::process::id() as u64);
493        let same_host: Vec<i32> = match my_addr {
494            Some(me) => (0..size)
495                .filter(|&w| {
496                    w != rank
497                        && addresses
498                            .get(w as usize)
499                            .map(|a| a.ip() == me.ip())
500                            .unwrap_or(false)
501                })
502                .collect(),
503            None => Vec::new(),
504        };
505        let inbox_shm = Arc::clone(&inbox);
506        let on_recv: crate::shm::OnRecv = Arc::new(move |framed: Vec<u8>| {
507            if framed.len() >= HEADER_LEN {
508                let arr: [u8; HEADER_LEN] = framed[..HEADER_LEN].try_into().unwrap();
509                if let Ok(h) = Header::from_bytes(&arr) {
510                    inbox_shm.push(Incoming {
511                        comm: h.comm,
512                        source: h.source,
513                        tag: h.tag,
514                        count: h.count,
515                        datatype: h.datatype,
516                        payload: framed[HEADER_LEN..].to_vec(),
517                    });
518                }
519            }
520        });
521        crate::shm::ShmTransport::init(jobid, rank, &same_host, on_recv)
522    };
523
524    let rt = Runtime {
525        rank,
526        size,
527        threading,
528        my_addr,
529        addresses,
530        inbox,
531        outgoing: Mutex::new(HashMap::new()),
532        context_peers: Mutex::new(HashMap::new()),
533        buffer_size: Mutex::new(0),
534        #[cfg(feature = "shm")]
535        shm,
536    };
537
538    RUNTIME
539        .set(rt)
540        .map_err(|_| crate::MpiError::AlreadyInitialized)?;
541    install_fault_handling();
542    Ok(())
543}
544
545/// Speak the PMI rendezvous protocol with the launcher: report our data-plane
546/// address and receive the full address table for the job.
547fn pmi_exchange(
548    root_addr: &str,
549    rank: i32,
550    size: i32,
551    my_addr: SocketAddr,
552) -> Result<(Vec<SocketAddr>, Option<ParentBlock>), crate::MpiError> {
553    let mut stream = TcpStream::connect(root_addr)
554        .map_err(|e| crate::MpiError::Bootstrap(format!("connect to launcher failed: {e}")))?;
555    let line = format!("{} {} {}\n", rank, my_addr, MY_ENDIAN);
556    stream
557        .write_all(line.as_bytes())
558        .map_err(|e| crate::MpiError::Bootstrap(format!("PMI write failed: {e}")))?;
559
560    // Read the address table: `size` newline-terminated "rank addr [endian]" lines.
561    let mut buf = String::new();
562    let mut reader = io::BufReader::new(stream);
563    use std::io::BufRead;
564    let mut table: Vec<Option<SocketAddr>> = vec![None; size as usize];
565    let mut endians: Vec<String> = vec![MY_ENDIAN.to_string(); size as usize];
566    for _ in 0..size {
567        buf.clear();
568        let n = reader
569            .read_line(&mut buf)
570            .map_err(|e| crate::MpiError::Bootstrap(format!("PMI read failed: {e}")))?;
571        if n == 0 {
572            return Err(crate::MpiError::Bootstrap(
573                "launcher closed connection early".into(),
574            ));
575        }
576        let mut parts = buf.split_whitespace();
577        let r: usize = parts
578            .next()
579            .and_then(|s| s.parse().ok())
580            .ok_or_else(|| crate::MpiError::Bootstrap("bad PMI table entry".into()))?;
581        let a: SocketAddr = parts
582            .next()
583            .and_then(|s| s.parse().ok())
584            .ok_or_else(|| crate::MpiError::Bootstrap("bad PMI table addr".into()))?;
585        table[r] = Some(a);
586        if let Some(e) = parts.next() {
587            endians[r] = e.to_string();
588        }
589    }
590
591    // The wire format is canonical little-endian and typed payloads are
592    // transcoded per host, so point-to-point, collectives and rendezvous work
593    // across mixed-endian hosts. RMA windows and derived (`#[derive(Equivalence)]`)
594    // structs are byte-transparent, so warn once if the job is mixed-endian.
595    if rank == 0 {
596        if let Some(bad) = endians.iter().position(|e| e != MY_ENDIAN) {
597            eprintln!(
598                "[mpi] warning: mixed-endian job (rank 0 is {MY_ENDIAN}, rank {bad} is {}); \
599                 point-to-point/collectives are transcoded, but RMA windows and \
600                 #[derive(Equivalence)] structs require same-endian ranks",
601                endians[bad]
602            );
603        }
604    }
605
606    let mut addresses = Vec::with_capacity(size as usize);
607    for (r, a) in table.into_iter().enumerate() {
608        addresses.push(
609            a.ok_or_else(|| crate::MpiError::Bootstrap(format!("missing address for rank {r}")))?,
610        );
611    }
612
613    // Spawned children receive an extra parent block: "PARENT <ictx> <count>"
614    // followed by `count` parent-address lines. Absent for normal jobs.
615    let mut parent = None;
616    if std::env::var("MPI_SPAWN").is_ok() {
617        buf.clear();
618        if reader.read_line(&mut buf).unwrap_or(0) > 0 {
619            let mut it = buf.split_whitespace();
620            if it.next() == Some("PARENT") {
621                let ictx: u32 = it.next().and_then(|s| s.parse().ok()).unwrap_or(0);
622                let count: usize = it.next().and_then(|s| s.parse().ok()).unwrap_or(0);
623                let mut paddrs = Vec::with_capacity(count);
624                for _ in 0..count {
625                    buf.clear();
626                    reader.read_line(&mut buf).ok();
627                    if let Ok(a) = buf.trim().parse::<SocketAddr>() {
628                        paddrs.push(a);
629                    }
630                }
631                parent = Some((ictx, paddrs));
632            }
633        }
634    }
635
636    Ok((addresses, parent))
637}
638
639/// Accept incoming peer connections; each gets a dedicated reader thread.
640fn spawn_acceptor(listener: TcpListener, inbox: Arc<Inbox>) {
641    thread::spawn(move || {
642        for stream in listener.incoming() {
643            match stream {
644                Ok(s) => {
645                    let inbox = Arc::clone(&inbox);
646                    thread::spawn(move || reader_loop(s, inbox));
647                }
648                Err(_) => break,
649            }
650        }
651    });
652}
653
654/// Read framed messages off a single peer connection until it closes.
655fn reader_loop(mut stream: TcpStream, inbox: Arc<Inbox>) {
656    let _ = stream.set_nodelay(true);
657    loop {
658        let mut hdr = [0u8; HEADER_LEN];
659        if stream.read_exact(&mut hdr).is_err() {
660            return;
661        }
662        let header = match Header::from_bytes(&hdr) {
663            Ok(h) => h,
664            Err(_) => return,
665        };
666        let mut payload = vec![0u8; header.len as usize];
667        if stream.read_exact(&mut payload).is_err() {
668            return;
669        }
670        inbox.push(Incoming {
671            comm: header.comm,
672            source: header.source,
673            tag: header.tag,
674            count: header.count,
675            datatype: header.datatype,
676            payload,
677        });
678    }
679}
680
681impl Runtime {
682    /// Resolve `(context, dest)` to a peer address: a context registered in
683    /// `context_peers` routes by that override table (cross-world inter-comms);
684    /// otherwise the world address table is used.
685    fn resolve(&self, comm: u32, dest: i32) -> Option<SocketAddr> {
686        if let Some(peers) = self.context_peers.lock().unwrap().get(&comm) {
687            return peers.get(dest as usize).copied();
688        }
689        self.addresses.get(dest as usize).copied()
690    }
691
692    /// Whether `comm` has a context-peer override (i.e. routes off-world).
693    fn has_override(&self, comm: u32) -> bool {
694        self.context_peers.lock().unwrap().contains_key(&comm)
695    }
696
697    /// Register per-context peer addresses (used by spawned inter-comms).
698    pub fn register_context_peers(&self, ctx: u32, peers: Vec<SocketAddr>) {
699        self.context_peers.lock().unwrap().insert(ctx, peers);
700    }
701
702    /// Remove a context-peer override.
703    #[allow(dead_code)]
704    pub fn unregister_context_peers(&self, ctx: u32) {
705        self.context_peers.lock().unwrap().remove(&ctx);
706    }
707
708    /// The advertised address of a world rank.
709    pub fn peer_addr(&self, world_rank: i32) -> Option<SocketAddr> {
710        self.addresses.get(world_rank as usize).copied()
711    }
712
713    /// This process's own advertised address.
714    pub fn my_addr(&self) -> Option<SocketAddr> {
715        self.my_addr
716    }
717
718    /// Obtain (creating if necessary) the outbound connection to `addr`.
719    fn connection(&self, addr: SocketAddr) -> io::Result<Arc<Mutex<TcpStream>>> {
720        {
721            let map = self.outgoing.lock().unwrap();
722            if let Some(c) = map.get(&addr) {
723                return Ok(Arc::clone(c));
724            }
725        }
726        // Retry briefly: a peer launched on another host (e.g. a second ssh
727        // process) may not have its listener ready the instant we first try.
728        let stream = {
729            let mut attempt = 0u32;
730            loop {
731                match TcpStream::connect(addr) {
732                    Ok(s) => break s,
733                    Err(_) if attempt < 250 => {
734                        attempt += 1;
735                        std::thread::sleep(std::time::Duration::from_millis(20));
736                    }
737                    Err(e) => return Err(e),
738                }
739            }
740        };
741        stream.set_nodelay(true).ok();
742        let conn = Arc::new(Mutex::new(stream));
743        let mut map = self.outgoing.lock().unwrap();
744        // Another thread may have raced us; keep the first.
745        let entry = map.entry(addr).or_insert_with(|| Arc::clone(&conn));
746        Ok(Arc::clone(entry))
747    }
748
749    /// Send a typed byte buffer, choosing eager or rendezvous delivery.
750    ///
751    /// * `comm` is the communicator context (for isolation / matching).
752    /// * `src` is the value stamped as the message source — the sender's rank
753    ///   *within `comm`*, so receivers see communicator-local ranks.
754    /// * `dest_world` is the destination's **world** rank, used for routing.
755    ///
756    /// Messages above [`RNDV_THRESHOLD`] on a normal same-world context use the
757    /// rendezvous protocol so the receiver pulls the data when ready (bounding
758    /// buffered memory). Everything else is delivered eagerly.
759    #[allow(clippy::too_many_arguments)]
760    pub fn send(
761        &self,
762        comm: u32,
763        src: i32,
764        dest_world: i32,
765        tag: i32,
766        count: u64,
767        datatype: u32,
768        payload: &[u8],
769    ) -> io::Result<()> {
770        let normal_ctx = comm != ABORT_CONTEXT && comm != RNDV_CTS_CTEXT && comm != RNDV_DATA_CTEXT;
771        if payload.len() > RNDV_THRESHOLD
772            && normal_ctx
773            && !self.has_override(comm)
774            && dest_world != self.rank
775        {
776            return self.send_rendezvous(comm, src, dest_world, tag, count, datatype, payload);
777        }
778        self.send_eager(comm, src, dest_world, tag, count, datatype, payload)
779    }
780
781    /// Announce a large message and stash it until the receiver asks for it.
782    #[allow(clippy::too_many_arguments)]
783    fn send_rendezvous(
784        &self,
785        comm: u32,
786        src: i32,
787        dest_world: i32,
788        tag: i32,
789        count: u64,
790        datatype: u32,
791        payload: &[u8],
792    ) -> io::Result<()> {
793        let id = ((self.rank as u64) << 40)
794            | (RNDV_SEQ.fetch_add(1, Ordering::Relaxed) & 0xFF_FFFF_FFFF);
795        // Store the payload transcoded to the wire byte order (no-op on LE).
796        let stored = payload.to_vec();
797        #[cfg(target_endian = "big")]
798        let stored = {
799            let mut v = stored;
800            swap_elems(&mut v, crate::datatype::wire_elem_size(datatype));
801            v
802        };
803        RNDV_OUT
804            .lock()
805            .unwrap()
806            .get_or_insert_with(HashMap::new)
807            .insert(id, stored);
808        // RTS record: [id][sender_world][len]; datatype carries the RTS bit.
809        let mut rts = Vec::with_capacity(20);
810        rts.extend_from_slice(&id.to_le_bytes());
811        rts.extend_from_slice(&self.rank.to_le_bytes());
812        rts.extend_from_slice(&(payload.len() as u64).to_le_bytes());
813        self.send_eager(comm, src, dest_world, tag, count, datatype | RTS_BIT, &rts)
814    }
815
816    #[allow(clippy::too_many_arguments)]
817    fn send_eager(
818        &self,
819        comm: u32,
820        src: i32,
821        dest_world: i32,
822        tag: i32,
823        count: u64,
824        datatype: u32,
825        payload: &[u8],
826    ) -> io::Result<()> {
827        // Same-world send to self short-circuits into the local inbox. Contexts
828        // with a peer override (cross-world inter-comms) never target self.
829        if !self.has_override(comm) && dest_world == self.rank {
830            self.inbox.push(Incoming {
831                comm,
832                source: src,
833                tag,
834                count,
835                datatype,
836                payload: payload.to_vec(),
837            });
838            return Ok(());
839        }
840        let addr = self.resolve(comm, dest_world).ok_or_else(|| {
841            io::Error::new(io::ErrorKind::NotFound, "no address for destination rank")
842        })?;
843        // A cross-world send whose destination is our own address is delivered
844        // locally (a process spawning ranks on itself, etc.).
845        if Some(addr) == self.my_addr {
846            self.inbox.push(Incoming {
847                comm,
848                source: src,
849                tag,
850                count,
851                datatype,
852                payload: payload.to_vec(),
853            });
854            return Ok(());
855        }
856        // Big-endian hosts transcode typed payloads to the canonical
857        // little-endian wire (no-op / compiled out on little-endian). Skipped
858        // for rendezvous announcements and handler contexts (raw/native bytes).
859        #[cfg(target_endian = "big")]
860        let swapped: Option<Vec<u8>> = {
861            let esz = crate::datatype::wire_elem_size(datatype);
862            if datatype & RTS_BIT == 0 && esz > 1 && !self.handler_registered(comm) {
863                let mut v = payload.to_vec();
864                swap_elems(&mut v, esz);
865                Some(v)
866            } else {
867                None
868            }
869        };
870        #[cfg(target_endian = "big")]
871        let payload: &[u8] = swapped.as_deref().unwrap_or(payload);
872
873        let header = Header {
874            comm,
875            source: src,
876            dest: dest_world,
877            tag,
878            count,
879            datatype,
880            len: payload.len() as u64,
881        };
882        // Same-host peers on a world context use the shared-memory fast-path.
883        #[cfg(feature = "shm")]
884        {
885            if !self.has_override(comm) {
886                if let Some(shm) = &self.shm {
887                    let mut framed = Vec::with_capacity(HEADER_LEN + payload.len());
888                    framed.extend_from_slice(&header.to_bytes());
889                    framed.extend_from_slice(payload);
890                    if shm.try_send(dest_world, &framed) {
891                        return Ok(());
892                    }
893                }
894            }
895        }
896        let conn = self.connection(addr)?;
897        let mut s = conn.lock().unwrap();
898        s.write_all(&header.to_bytes())?;
899        s.write_all(payload)?;
900        s.flush()?;
901        Ok(())
902    }
903
904    /// Block until a message matching `(comm, source, tag)` arrives; return
905    /// `(actual_source, actual_tag, count, datatype, payload)`. Transparently
906    /// completes the rendezvous handshake for large messages.
907    pub fn recv(&self, comm: u32, source: i32, tag: i32) -> (i32, i32, u64, u32, Vec<u8>) {
908        let m = self.inbox.take_matching(comm, source, tag);
909        if m.datatype & RTS_BIT != 0 {
910            // Rendezvous announcement: ask the sender for the data, then pull it.
911            let id = u64::from_le_bytes(m.payload[0..8].try_into().unwrap());
912            let sender_world = i32::from_le_bytes(m.payload[8..12].try_into().unwrap());
913            let id_tag = (id & 0x7FFF_FFFF) as i32;
914            let mut cts = Vec::with_capacity(12);
915            cts.extend_from_slice(&id.to_le_bytes());
916            cts.extend_from_slice(&self.rank.to_le_bytes());
917            let _ = self.send_eager(
918                RNDV_CTS_CTEXT,
919                self.rank,
920                sender_world,
921                0,
922                1,
923                crate::datatype::ids::U8,
924                &cts,
925            );
926            let data = self
927                .inbox
928                .take_matching(RNDV_DATA_CTEXT, sender_world, id_tag);
929            let real_dt = m.datatype & !RTS_BIT;
930            let payload = data.payload;
931            // Transcode the pulled bulk data from wire order (no-op on LE).
932            #[cfg(target_endian = "big")]
933            let payload = {
934                let mut v = payload;
935                swap_elems(&mut v, crate::datatype::wire_elem_size(real_dt));
936                v
937            };
938            return (m.source, m.tag, m.count, real_dt, payload);
939        }
940        // Transcode typed eager payloads from wire order (no-op on LE).
941        let payload = m.payload;
942        #[cfg(target_endian = "big")]
943        let payload = {
944            let mut v = payload;
945            swap_elems(&mut v, crate::datatype::wire_elem_size(m.datatype));
946            v
947        };
948        (m.source, m.tag, m.count, m.datatype, payload)
949    }
950
951    /// Non-blocking probe: describe the earliest matching message, if any.
952    pub fn probe(&self, comm: u32, source: i32, tag: i32) -> Option<(i32, i32, u64, u32, usize)> {
953        self.inbox.peek_matching(comm, source, tag)
954    }
955
956    /// Blocking probe: wait until a matching message exists, then describe it
957    /// without consuming it.
958    pub fn probe_blocking(&self, comm: u32, source: i32, tag: i32) -> (i32, i32, u64, u32, usize) {
959        loop {
960            if let Some(info) = self.probe(comm, source, tag) {
961                return info;
962            }
963            // Wait for any new arrival, then re-check.
964            let q = self.inbox.queue.lock().unwrap();
965            let _unused = self.inbox.cvar.wait(q).unwrap();
966        }
967    }
968
969    /// Total buffer size attached for buffered sends.
970    pub fn buffer_attach(&self, size: usize) {
971        *self.buffer_size.lock().unwrap() += size;
972    }
973
974    /// Detach and return the current attached buffer size.
975    pub fn buffer_detach(&self) -> usize {
976        let mut b = self.buffer_size.lock().unwrap();
977        std::mem::replace(&mut *b, 0)
978    }
979
980    /// Register an active-message handler for a context (used by RMA windows).
981    pub fn register_handler(&self, ctx: u32, handler: Handler) {
982        self.inbox.handlers.lock().unwrap().insert(ctx, handler);
983    }
984
985    /// Remove the active-message handler for a context.
986    pub fn unregister_handler(&self, ctx: u32) {
987        self.inbox.handlers.lock().unwrap().remove(&ctx);
988    }
989
990    /// The currently attached buffered-send buffer size.
991    pub fn buffer_size(&self) -> usize {
992        *self.buffer_size.lock().unwrap()
993    }
994
995    /// Replace the attached buffered-send buffer size.
996    pub fn set_buffer_size(&self, size: usize) {
997        *self.buffer_size.lock().unwrap() = size;
998    }
999}