Skip to main content

hataori/
pmap.rs

1use crate::domain::{Domain, LocalMode};
2#[cfg(feature = "rayon")]
3use crate::lanes::batch::{
4    idle_backoff, run_lane, LocalBatch, LocalBatchOutcome, LocalBatchResult, LocalQueue,
5};
6#[cfg(feature = "rayon")]
7use crate::lanes::{LaneControl, Lanes, LanesError};
8#[cfg(feature = "rayon")]
9use crate::local::{in_rayon_worker_context, RayonLanes};
10use crate::map::truncate_message;
11#[cfg(feature = "rayon")]
12use crate::mpi_backend;
13use crate::mpi_backend::collective::SystemOperation;
14use crate::mpi_backend::traits::*;
15use crate::mpi_check::mpi_call;
16use crate::scheduler::{BatchId, Coordinator, Dispatch, ItemResult, SchedulerError};
17use crate::wire::{self, ErrorClass, ErrorKey, Header, MessageKind, MessageStatus};
18use serde::{de::DeserializeOwned, Deserialize, Serialize};
19use std::cell::Cell;
20#[cfg(feature = "rayon")]
21use std::convert::Infallible;
22use std::fmt::{self, Display, Formatter};
23use std::num::NonZeroUsize;
24#[cfg(all(test, not(feature = "rayon")))]
25use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
26
27const HEADER_TAG: i32 = 0;
28const PAYLOAD_TAG: i32 = 1;
29
30#[cfg(all(test, not(feature = "rayon")))]
31static CORRUPT_NEXT_RESULT: AtomicBool = AtomicBool::new(false);
32#[cfg(all(test, not(feature = "rayon")))]
33static FAULT_PHASE: AtomicBool = AtomicBool::new(false);
34#[cfg(all(test, not(feature = "rayon")))]
35static TEST_ASSIGNED_REMOTES: AtomicU64 = AtomicU64::new(0);
36#[cfg(all(test, not(feature = "rayon")))]
37static TEST_USER_RESULTS: AtomicUsize = AtomicUsize::new(0);
38#[cfg(all(test, not(feature = "rayon")))]
39static TEST_DECODE_FAILURES: AtomicUsize = AtomicUsize::new(0);
40
41#[cfg(all(test, not(feature = "rayon")))]
42fn reset_fault_trace() {
43    CORRUPT_NEXT_RESULT.store(false, Ordering::SeqCst);
44    FAULT_PHASE.store(false, Ordering::SeqCst);
45    TEST_ASSIGNED_REMOTES.store(0, Ordering::SeqCst);
46    TEST_USER_RESULTS.store(0, Ordering::SeqCst);
47    TEST_DECODE_FAILURES.store(0, Ordering::SeqCst);
48}
49
50#[cfg(all(test, not(feature = "rayon")))]
51fn wait_for_marker(path: &std::path::Path) {
52    let observed = (0..500).any(|_| {
53        if path.exists() {
54            true
55        } else {
56            std::thread::sleep(std::time::Duration::from_millis(10));
57            false
58        }
59    });
60    assert!(observed, "timed out waiting for fault-test marker");
61}
62
63type LocalError = (PmapErrorKind, String);
64type RootOutcome<U> = (Vec<U>, i64, Option<LocalError>);
65
66#[cfg(feature = "rayon")]
67#[derive(Debug, Copy, Clone, Eq, PartialEq)]
68enum RootEvent {
69    Local,
70    Remote,
71    Neither,
72}
73
74#[cfg(feature = "rayon")]
75fn choose_root_event(
76    local_ready: bool,
77    remote_ready: bool,
78    prefer_local: bool,
79) -> (RootEvent, bool) {
80    match (local_ready, remote_ready) {
81        (true, true) if prefer_local => (RootEvent::Local, false),
82        (true, true) => (RootEvent::Remote, true),
83        (true, false) => (RootEvent::Local, prefer_local),
84        (false, true) => (RootEvent::Remote, prefer_local),
85        (false, false) => (RootEvent::Neither, prefer_local),
86    }
87}
88
89/// Options shared by every rank in one collective [`pmap`] call.
90#[derive(Debug, Copy, Clone, Eq, PartialEq)]
91pub struct PmapOptions {
92    /// Rank that owns the input and receives the ordered output.
93    pub root: i32,
94    /// Fixed positive number of items assigned in one batch.
95    pub batch_size: NonZeroUsize,
96    /// Rank-local execution mode; MPI-only calls require [`LocalMode::Sequential`].
97    pub local_mode: LocalMode,
98    /// Allows one prefetched batch per remote hybrid domain.
99    ///
100    /// The default is `false`. MPI-only calls reject `true` during collective
101    /// preflight; hybrid calls keep the root domain at capacity one.
102    pub prefetch: bool,
103}
104
105impl Default for PmapOptions {
106    fn default() -> Self {
107        Self {
108            root: 0,
109            batch_size: NonZeroUsize::new(1).unwrap(),
110            local_mode: LocalMode::Sequential,
111            prefetch: false,
112        }
113    }
114}
115
116/// Stable category of a collective [`pmap`] failure.
117#[derive(Debug, Copy, Clone, Eq, PartialEq)]
118pub enum PmapErrorKind {
119    Reentrant,
120    Preflight,
121    User,
122    Wire,
123    Protocol,
124}
125
126/// Error converged across every rank in a collective [`pmap`] call.
127#[derive(Debug, Clone, Eq, PartialEq)]
128pub struct PmapError {
129    kind: PmapErrorKind,
130    key: Option<i64>,
131    message: String,
132}
133
134impl PmapError {
135    fn new(kind: PmapErrorKind, message: impl Display) -> Self {
136        Self {
137            kind,
138            key: None,
139            message: truncate_message(message.to_string()),
140        }
141    }
142
143    fn keyed(kind: PmapErrorKind, key: i64, message: impl Display) -> Self {
144        Self {
145            kind,
146            key: Some(key),
147            message: truncate_message(message.to_string()),
148        }
149    }
150
151    pub fn kind(&self) -> PmapErrorKind {
152        self.kind
153    }
154
155    pub fn key(&self) -> Option<i64> {
156        self.key
157    }
158
159    pub fn message(&self) -> &str {
160        &self.message
161    }
162}
163
164impl Display for PmapError {
165    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
166        formatter.write_str(self.message())
167    }
168}
169
170impl std::error::Error for PmapError {}
171
172thread_local! {
173    static ACTIVE: Cell<bool> = const { Cell::new(false) };
174}
175
176pub struct Active;
177
178impl Active {
179    fn acquire() -> Result<Self, PmapError> {
180        ACTIVE.with(|active| {
181            if active.replace(true) {
182                Err(PmapError::new(
183                    PmapErrorKind::Reentrant,
184                    "pmap is already active on this thread",
185                ))
186            } else {
187                Ok(Self)
188            }
189        })
190    }
191}
192
193impl Drop for Active {
194    fn drop(&mut self) {
195        ACTIVE.with(|active| active.set(false));
196    }
197}
198
199fn wire_error(error: impl Display) -> PmapError {
200    PmapError::new(PmapErrorKind::Wire, error)
201}
202
203fn protocol_error(error: impl Display) -> PmapError {
204    PmapError::new(PmapErrorKind::Protocol, error)
205}
206
207fn scheduler_error(error: SchedulerError) -> PmapError {
208    protocol_error(error)
209}
210
211fn header(
212    kind: MessageKind,
213    status: MessageStatus,
214    batch_id: u64,
215    item_count: u64,
216    payload_len: usize,
217) -> Result<Header, PmapError> {
218    Header::new(kind, status, batch_id, item_count, payload_len as u64).map_err(wire_error)
219}
220
221fn send_header<C: Communicator>(comm: &C, rank: i32, value: Header) {
222    mpi_call!(comm
223        .process_at_rank(rank)
224        .send_with_tag(&value.encode(), HEADER_TAG));
225}
226
227fn receive_header<C: Communicator>(comm: &C, rank: i32) -> Result<Header, PmapError> {
228    let (bytes, _) = mpi_call!(comm
229        .process_at_rank(rank)
230        .receive_vec_with_tag::<u8>(HEADER_TAG));
231    Header::decode(&bytes).map_err(wire_error)
232}
233
234#[cfg(not(feature = "rayon"))]
235fn receive_any_header<C: Communicator>(comm: &C) -> Result<(i32, Header), PmapError> {
236    let (bytes, status) = mpi_call!(comm.any_process().receive_vec_with_tag::<u8>(HEADER_TAG));
237    Header::decode(&bytes)
238        .map(|value| (status.source_rank(), value))
239        .map_err(wire_error)
240}
241
242fn receive_payload<C: Communicator, T: DeserializeOwned>(
243    comm: &C,
244    rank: i32,
245    value: Header,
246) -> Result<T, PmapError> {
247    let count = wire::checked_mpi_count(value.payload_len()).map_err(wire_error)?;
248    let _length = wire::checked_usize_length(value.payload_len()).map_err(wire_error)?;
249    let (bytes, _) = mpi_call!(comm
250        .process_at_rank(rank)
251        .receive_vec_with_tag::<u8>(PAYLOAD_TAG));
252    if bytes.len() != count as usize {
253        return Err(protocol_error("payload length does not match its header"));
254    }
255    wire::decode_payload(&bytes).map_err(wire_error)
256}
257
258fn send_frame<C: Communicator, T: Serialize>(
259    comm: &C,
260    rank: i32,
261    kind: MessageKind,
262    status: MessageStatus,
263    batch_id: u64,
264    item_count: usize,
265    payload: &T,
266) -> Result<(), PmapError> {
267    let bytes = wire::encode_payload(payload).map_err(wire_error)?;
268    #[cfg(all(test, not(feature = "rayon")))]
269    let mut bytes = bytes;
270    #[cfg(all(test, not(feature = "rayon")))]
271    if kind == MessageKind::Result
272        && mpi_call!(comm.rank()) == 1
273        && CORRUPT_NEXT_RESULT.swap(false, Ordering::SeqCst)
274    {
275        bytes = vec![0xff];
276    }
277    let value = header(kind, status, batch_id, item_count as u64, bytes.len())?;
278    send_header(comm, rank, value);
279    mpi_call!(comm
280        .process_at_rank(rank)
281        .send_with_tag(bytes.as_slice(), PAYLOAD_TAG));
282    Ok(())
283}
284
285fn send_empty<C: Communicator>(
286    comm: &C,
287    rank: i32,
288    kind: MessageKind,
289    batch_id: u64,
290) -> Result<(), PmapError> {
291    let value = header(kind, MessageStatus::None, batch_id, 0, 0)?;
292    send_header(comm, rank, value);
293    Ok(())
294}
295
296#[derive(Serialize, Deserialize)]
297struct CallbackFailure {
298    index: usize,
299    kind: i32,
300    message: String,
301}
302
303#[derive(Serialize, Deserialize)]
304enum ResultPayload<U> {
305    Values(Vec<(usize, U)>),
306    Error(CallbackFailure),
307}
308
309fn error_key(
310    task_key: usize,
311    class: ErrorClass,
312    rank: i32,
313    world_size: i32,
314) -> Result<i64, PmapError> {
315    ErrorKey::new(task_key as u64, class, rank, world_size)
316        .map(|key| key.get())
317        .map_err(wire_error)
318}
319
320fn convergence<C: Communicator>(comm: &C, local_key: i64) -> i64 {
321    let mut selected = local_key;
322    mpi_call!(comm.all_reduce_into(&local_key, &mut selected, SystemOperation::min()));
323    selected
324}
325
326fn broadcast_error<C: Communicator>(
327    comm: &C,
328    selected: i64,
329    local_key: i64,
330    local: Option<(PmapErrorKind, String)>,
331) -> Result<Option<(PmapErrorKind, String)>, PmapError> {
332    if selected == ErrorKey::NO_ERROR_KEY {
333        return Ok(None);
334    }
335    if selected < 0 || mpi_call!(comm.size()) <= 0 {
336        return Err(protocol_error("invalid selected error key"));
337    }
338    let winner = (selected % i64::from(mpi_call!(comm.size()))) as i32;
339    let is_winner = mpi_call!(comm.rank()) == winner;
340    let winner_local = if is_winner && local_key == selected {
341        local
342    } else if is_winner {
343        return Err(protocol_error("winner does not own selected error"));
344    } else {
345        None
346    };
347
348    let mut kind = winner_local
349        .as_ref()
350        .map_or(PmapErrorKind::Protocol.code(), |(kind, _)| kind.code());
351    let mut key = selected;
352    mpi_call!(comm.process_at_rank(winner).broadcast_into(&mut kind));
353    mpi_call!(comm.process_at_rank(winner).broadcast_into(&mut key));
354    if key != selected {
355        return Err(protocol_error("broadcast error key mismatch"));
356    }
357
358    let mut message = winner_local
359        .map(|(_, message)| message.into_bytes())
360        .unwrap_or_default();
361    let mut length = i32::try_from(message.len())
362        .map_err(|_| protocol_error("error message length overflow"))?;
363    mpi_call!(comm.process_at_rank(winner).broadcast_into(&mut length));
364    if length < 0 || length as usize > crate::map::MAX_ERROR_MESSAGE_BYTES {
365        return Err(protocol_error("invalid broadcast error message length"));
366    }
367    if !is_winner {
368        message.resize(length as usize, 0);
369    }
370    mpi_call!(comm
371        .process_at_rank(winner)
372        .broadcast_into(message.as_mut_slice()));
373
374    let kind =
375        PmapErrorKind::from_code(kind).ok_or_else(|| protocol_error("invalid error kind"))?;
376    let message = String::from_utf8(message).map_err(protocol_error)?;
377    Ok(Some((kind, message)))
378}
379
380fn preflight<C, T>(
381    comm: &C,
382    options: &PmapOptions,
383    root_items: Option<&Vec<T>>,
384    execution_valid: bool,
385) -> Result<(), PmapError>
386where
387    C: Communicator,
388{
389    let rank = mpi_call!(comm.rank());
390    let size = mpi_call!(comm.size());
391    let local_batch = options.batch_size.get() as u64;
392    let local_mode = match options.local_mode {
393        LocalMode::Sequential => 0_i32,
394        LocalMode::Outer => 1,
395        LocalMode::Inner => 2,
396    };
397    let local_prefetch = i32::from(options.prefetch);
398
399    let mut min_root = options.root;
400    let mut max_root = options.root;
401    let mut min_batch = local_batch;
402    let mut max_batch = local_batch;
403    let mut min_mode = local_mode;
404    let mut max_mode = local_mode;
405    let mut min_prefetch = local_prefetch;
406    let mut max_prefetch = local_prefetch;
407    mpi_call!(comm.all_reduce_into(&options.root, &mut min_root, SystemOperation::min()));
408    mpi_call!(comm.all_reduce_into(&options.root, &mut max_root, SystemOperation::max()));
409    mpi_call!(comm.all_reduce_into(&local_batch, &mut min_batch, SystemOperation::min()));
410    mpi_call!(comm.all_reduce_into(&local_batch, &mut max_batch, SystemOperation::max()));
411    mpi_call!(comm.all_reduce_into(&local_mode, &mut min_mode, SystemOperation::min()));
412    mpi_call!(comm.all_reduce_into(&local_mode, &mut max_mode, SystemOperation::max()));
413    mpi_call!(comm.all_reduce_into(&local_prefetch, &mut min_prefetch, SystemOperation::min()));
414    mpi_call!(comm.all_reduce_into(&local_prefetch, &mut max_prefetch, SystemOperation::max()));
415
416    let local_valid = mpi_call!(crate::mpi_check::is_thread_main())
417        && options.root >= 0
418        && options.root < size
419        && ((rank == options.root) == root_items.is_some())
420        && execution_valid
421        && min_root == max_root
422        && min_batch == max_batch
423        && min_mode == max_mode
424        && min_prefetch == max_prefetch;
425    let candidate = wire::preflight_candidate(!local_valid, rank, size).map_err(wire_error)?;
426    let mut failing_rank = candidate;
427    mpi_call!(comm.all_reduce_into(&candidate, &mut failing_rank, SystemOperation::min()));
428    if failing_rank != size {
429        return Err(PmapError::new(
430            PmapErrorKind::Preflight,
431            format!("pmap preflight failed on rank {failing_rank}"),
432        ));
433    }
434    Ok(())
435}
436
437#[cfg(not(feature = "rayon"))]
438fn worker_loop<C, T, U, E, F>(
439    comm: &C,
440    root: i32,
441    mut f: F,
442) -> Result<(i64, Option<LocalError>), PmapError>
443where
444    C: Communicator,
445    T: Serialize + DeserializeOwned,
446    U: Serialize + DeserializeOwned,
447    E: Display,
448    F: FnMut(T) -> Result<U, E>,
449{
450    let rank = mpi_call!(comm.rank());
451    let size = mpi_call!(comm.size());
452    let mut local_key = ErrorKey::NO_ERROR_KEY;
453    let mut local_error = None;
454
455    loop {
456        #[cfg(all(test, not(feature = "rayon")))]
457        if FAULT_PHASE.load(Ordering::SeqCst) && rank >= 2 {
458            if let Some(base) = std::env::var_os("HATAORI_FAULT_MARKER") {
459                let assigned = std::path::PathBuf::from(base).with_extension("assigned");
460                wait_for_marker(&assigned);
461            }
462        }
463        send_empty(comm, root, MessageKind::Ready, 0)?;
464        let value = receive_header(comm, root)?;
465        match value.kind() {
466            MessageKind::Task => {
467                let items: Vec<(usize, T)> = match receive_payload(comm, root, value) {
468                    Ok(items) => items,
469                    Err(error) => {
470                        let index = usize::try_from(value.item_count()).unwrap_or(usize::MAX);
471                        let key = error_key(index, ErrorClass::WireProtocol, rank, size)?;
472                        local_key = local_key.min(key);
473                        local_error = Some((PmapErrorKind::Wire, error.message().to_owned()));
474                        let failure = CallbackFailure {
475                            index,
476                            kind: PmapErrorKind::Wire.code(),
477                            message: error.message().to_owned(),
478                        };
479                        send_frame(
480                            comm,
481                            root,
482                            MessageKind::Result,
483                            MessageStatus::Error,
484                            value.batch_id(),
485                            1,
486                            &ResultPayload::<U>::Error(failure),
487                        )?;
488                        continue;
489                    }
490                };
491                let mut results = Vec::with_capacity(items.len());
492                let mut failure = None;
493                for (index, item) in items {
494                    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(item))) {
495                        Ok(Ok(result)) => results.push((index, result)),
496                        Ok(Err(error)) => {
497                            failure = Some(CallbackFailure {
498                                index,
499                                kind: PmapErrorKind::User.code(),
500                                message: truncate_message(error.to_string()),
501                            });
502                            break;
503                        }
504                        Err(_) => mpi_call!(comm.abort(71)),
505                    }
506                }
507                match failure {
508                    Some(failure) => {
509                        let key = error_key(failure.index, ErrorClass::Callback, rank, size)?;
510                        local_key = local_key.min(key);
511                        local_error = Some((PmapErrorKind::User, failure.message.clone()));
512                        send_frame(
513                            comm,
514                            root,
515                            MessageKind::Result,
516                            MessageStatus::Error,
517                            value.batch_id(),
518                            value.item_count() as usize,
519                            &ResultPayload::<U>::Error(failure),
520                        )?;
521                    }
522                    None => send_frame(
523                        comm,
524                        root,
525                        MessageKind::Result,
526                        MessageStatus::Ok,
527                        value.batch_id(),
528                        results.len(),
529                        &ResultPayload::Values(results),
530                    )?,
531                }
532            }
533            MessageKind::Stop => {
534                send_empty(comm, root, MessageKind::Drain, 0)?;
535                return Ok((local_key, local_error));
536            }
537            _ => {
538                return Err(protocol_error("worker received an unexpected message"));
539            }
540        }
541    }
542}
543
544#[cfg(feature = "rayon")]
545enum PrefetchedBatch<T, U> {
546    Work {
547        batch_id: u64,
548        items: Vec<(usize, T)>,
549    },
550    Ready(LocalBatchOutcome<U>),
551    Stop,
552}
553
554#[cfg(feature = "rayon")]
555fn receive_prefetched_batch<C, T, U>(
556    comm: &C,
557    root: i32,
558) -> Result<PrefetchedBatch<T, U>, PmapError>
559where
560    C: Communicator,
561    T: DeserializeOwned,
562{
563    let value = receive_header(comm, root)?;
564    match value.kind() {
565        MessageKind::Task => {
566            let batch_id = value.batch_id();
567            match receive_payload::<_, Vec<(usize, T)>>(comm, root, value) {
568                Ok(items) => Ok(PrefetchedBatch::Work { batch_id, items }),
569                // The task transfer failed after its header: report one wire
570                // failure for this batch instead of handing it to a lane.
571                Err(error) => Ok(PrefetchedBatch::Ready(LocalBatchOutcome::wire_error(
572                    batch_id,
573                    1,
574                    usize::try_from(value.item_count()).unwrap_or(usize::MAX),
575                    error.message().to_owned(),
576                ))),
577            }
578        }
579        MessageKind::Stop => Ok(PrefetchedBatch::Stop),
580        _ => Err(protocol_error("worker received an unexpected message")),
581    }
582}
583
584#[cfg(feature = "rayon")]
585fn send_worker_outcome<C, U>(
586    comm: &C,
587    root: i32,
588    rank: i32,
589    size: i32,
590    outcome: LocalBatchOutcome<U>,
591    local_key: &mut i64,
592    local_error: &mut Option<LocalError>,
593) -> Result<(), PmapError>
594where
595    C: Communicator,
596    U: Serialize,
597{
598    let failure = match outcome.result {
599        LocalBatchResult::Values(values) => {
600            return send_frame(
601                comm,
602                root,
603                MessageKind::Result,
604                MessageStatus::Ok,
605                outcome.batch_id,
606                values.len(),
607                &ResultPayload::Values(values),
608            )
609        }
610        LocalBatchResult::CallbackError { index, message } => BatchFailure {
611            index,
612            message,
613            class: ErrorClass::Callback,
614            kind: PmapErrorKind::User,
615        },
616        LocalBatchResult::WireError { index, message } => BatchFailure {
617            index,
618            message,
619            class: ErrorClass::WireProtocol,
620            kind: PmapErrorKind::Wire,
621        },
622        LocalBatchResult::Panic => mpi_call!(comm.abort(71)),
623    };
624    let key = error_key(failure.index, failure.class, rank, size)?;
625    if key < *local_key {
626        *local_key = key;
627        *local_error = Some((failure.kind, failure.message.clone()));
628    }
629    let payload = CallbackFailure {
630        index: failure.index,
631        kind: failure.kind.code(),
632        message: failure.message,
633    };
634    send_frame(
635        comm,
636        root,
637        MessageKind::Result,
638        MessageStatus::Error,
639        outcome.batch_id,
640        outcome.item_count,
641        &ResultPayload::<U>::Error(payload),
642    )
643}
644
645#[cfg(feature = "rayon")]
646struct BatchFailure {
647    index: usize,
648    message: String,
649    class: ErrorClass,
650    kind: PmapErrorKind,
651}
652
653/// How the coordinator hands one local batch to the execution resource.
654#[cfg(feature = "rayon")]
655trait BatchSink<T, U> {
656    /// Submits `batch`. A concurrent sink returns immediately; an inline sink
657    /// has already executed the batch when this returns.
658    fn submit(&mut self, batch: LocalBatch<T>);
659
660    /// Returns the outcome of `batch_id` if the lanes already produced it.
661    fn try_wait(&mut self, batch_id: u64) -> Option<LocalBatchOutcome<U>>;
662
663    /// Waits for the outcome of `batch_id`, bounded by executor cancellation.
664    fn wait(&mut self, batch_id: u64) -> Option<LocalBatchOutcome<U>> {
665        loop {
666            if let Some(outcome) = self.try_wait(batch_id) {
667                return Some(outcome);
668            }
669            if self.cancelled() {
670                return None;
671            }
672            idle_backoff();
673        }
674    }
675
676    /// Whether the executor cancelled the run (lane infrastructure failure).
677    fn cancelled(&self) -> bool;
678}
679
680#[cfg(feature = "rayon")]
681struct ConcurrentSink<'a, T, U> {
682    queue: &'a LocalQueue<T, U>,
683    control: &'a LaneControl<'a>,
684}
685
686#[cfg(feature = "rayon")]
687impl<T, U> BatchSink<T, U> for ConcurrentSink<'_, T, U> {
688    fn submit(&mut self, batch: LocalBatch<T>) {
689        self.queue.submit(batch);
690    }
691
692    fn try_wait(&mut self, batch_id: u64) -> Option<LocalBatchOutcome<U>> {
693        self.queue.take_outcome(batch_id)
694    }
695
696    fn cancelled(&self) -> bool {
697        self.control.cancelled()
698    }
699}
700
701/// A sink that executes each batch on the coordinator thread through `lanes.run`.
702#[cfg(feature = "rayon")]
703struct InlineSink<S, U> {
704    run_batch: S,
705    outcome: Option<LocalBatchOutcome<U>>,
706}
707
708#[cfg(feature = "rayon")]
709impl<S, T, U> BatchSink<T, U> for InlineSink<S, U>
710where
711    S: FnMut(LocalBatch<T>) -> LocalBatchOutcome<U>,
712{
713    fn submit(&mut self, batch: LocalBatch<T>) {
714        let outcome = (self.run_batch)(batch);
715        self.outcome = Some(outcome);
716    }
717
718    fn try_wait(&mut self, batch_id: u64) -> Option<LocalBatchOutcome<U>> {
719        self.outcome
720            .take()
721            .filter(|outcome| outcome.batch_id == batch_id)
722    }
723
724    fn cancelled(&self) -> bool {
725        false
726    }
727}
728
729/// Runs one batch on the calling thread through the lanes executor.
730#[cfg(feature = "rayon")]
731fn run_batch_inline<L, T, U, E, F>(
732    lanes: &mut L,
733    f: &F,
734    batch: LocalBatch<T>,
735) -> LocalBatchOutcome<U>
736where
737    L: Lanes,
738    T: Send,
739    U: Send,
740    E: Display + Send,
741    F: Fn(T, &mut L::Lane<'_>) -> Result<U, E> + Sync,
742{
743    let batch_id = batch.batch_id;
744    let item_count = batch.item_count;
745    let queue = LocalQueue::<T, U>::new();
746    queue.submit(batch);
747    queue.close();
748    let joined = lanes.run(|_index, lane, control| {
749        run_lane(&queue, f, control, lane);
750        Ok::<(), Infallible>(())
751    });
752    let joined = match joined {
753        Ok(()) => true,
754        Err(LanesError::Lane(never)) => match never {},
755        Err(LanesError::Coordinator(never)) => match never {},
756        // The coordinator turns an executor failure into the ordinary lane abort.
757        Err(LanesError::SingleLane) | Err(LanesError::Reentrant) | Err(LanesError::Executor(_)) => {
758            false
759        }
760    };
761    if joined {
762        if let Some(outcome) = queue.take_outcome(batch_id) {
763            return outcome;
764        }
765    }
766    LocalBatchOutcome {
767        batch_id,
768        item_count,
769        result: LocalBatchResult::Panic,
770    }
771}
772
773#[cfg(feature = "rayon")]
774fn lane_executor_failure(error: impl Display) -> PmapError {
775    PmapError::new(
776        PmapErrorKind::Protocol,
777        format!("lane executor failed: {error}"),
778    )
779}
780
781/// The worker-side MPI protocol, generic over how local batches execute.
782#[cfg(feature = "rayon")]
783fn hybrid_worker_protocol<C, S, T, U>(
784    comm: &C,
785    root: i32,
786    mode: LocalMode,
787    prefetch: bool,
788    sink: &mut S,
789) -> Result<(i64, Option<LocalError>), PmapError>
790where
791    C: Communicator,
792    S: BatchSink<T, U>,
793    T: Serialize + DeserializeOwned + Send,
794    U: Serialize + DeserializeOwned + Send,
795{
796    if prefetch {
797        hybrid_worker_protocol_prefetch(comm, root, mode, sink)
798    } else {
799        hybrid_worker_protocol_plain(comm, root, mode, sink)
800    }
801}
802
803#[cfg(feature = "rayon")]
804fn hybrid_worker_protocol_plain<C, S, T, U>(
805    comm: &C,
806    root: i32,
807    mode: LocalMode,
808    sink: &mut S,
809) -> Result<(i64, Option<LocalError>), PmapError>
810where
811    C: Communicator,
812    S: BatchSink<T, U>,
813    T: Serialize + DeserializeOwned + Send,
814    U: Serialize + DeserializeOwned + Send,
815{
816    let rank = mpi_call!(comm.rank());
817    let size = mpi_call!(comm.size());
818    let mut local_key = ErrorKey::NO_ERROR_KEY;
819    let mut local_error = None;
820
821    loop {
822        send_empty(comm, root, MessageKind::Ready, 0)?;
823        let value = receive_header(comm, root)?;
824        match value.kind() {
825            MessageKind::Task => {
826                let items: Vec<(usize, T)> = match receive_payload(comm, root, value) {
827                    Ok(items) => items,
828                    Err(error) => {
829                        let index = usize::try_from(value.item_count()).unwrap_or(usize::MAX);
830                        let key = error_key(index, ErrorClass::WireProtocol, rank, size)?;
831                        local_key = local_key.min(key);
832                        local_error = Some((PmapErrorKind::Wire, error.message().to_owned()));
833                        let failure = CallbackFailure {
834                            index,
835                            kind: PmapErrorKind::Wire.code(),
836                            message: error.message().to_owned(),
837                        };
838                        send_frame(
839                            comm,
840                            root,
841                            MessageKind::Result,
842                            MessageStatus::Error,
843                            value.batch_id(),
844                            1,
845                            &ResultPayload::<U>::Error(failure),
846                        )?;
847                        continue;
848                    }
849                };
850                let batch_id = value.batch_id();
851                sink.submit(LocalBatch::new(batch_id, mode, items));
852                let outcome = match sink.wait(batch_id) {
853                    Some(outcome) => outcome,
854                    None => mpi_call!(comm.abort(71)),
855                };
856                send_worker_outcome(
857                    comm,
858                    root,
859                    rank,
860                    size,
861                    outcome,
862                    &mut local_key,
863                    &mut local_error,
864                )?;
865            }
866            MessageKind::Stop => {
867                send_empty(comm, root, MessageKind::Drain, 0)?;
868                return Ok((local_key, local_error));
869            }
870            _ => return Err(protocol_error("worker received an unexpected message")),
871        }
872    }
873}
874
875#[cfg(feature = "rayon")]
876fn hybrid_worker_protocol_prefetch<C, S, T, U>(
877    comm: &C,
878    root: i32,
879    mode: LocalMode,
880    sink: &mut S,
881) -> Result<(i64, Option<LocalError>), PmapError>
882where
883    C: Communicator,
884    S: BatchSink<T, U>,
885    T: Serialize + DeserializeOwned + Send,
886    U: Serialize + DeserializeOwned + Send,
887{
888    let rank = mpi_call!(comm.rank());
889    let size = mpi_call!(comm.size());
890    let mut local_key = ErrorKey::NO_ERROR_KEY;
891    let mut local_error = None;
892
893    'idle: loop {
894        send_empty(comm, root, MessageKind::Ready, 0)?;
895        let mut current = match receive_prefetched_batch::<_, T, U>(comm, root)? {
896            PrefetchedBatch::Work { batch_id, items } => {
897                sink.submit(LocalBatch::new(batch_id, mode, items));
898                batch_id
899            }
900            PrefetchedBatch::Ready(outcome) => {
901                send_worker_outcome(
902                    comm,
903                    root,
904                    rank,
905                    size,
906                    outcome,
907                    &mut local_key,
908                    &mut local_error,
909                )?;
910                continue 'idle;
911            }
912            PrefetchedBatch::Stop => {
913                send_empty(comm, root, MessageKind::Drain, 0)?;
914                return Ok((local_key, local_error));
915            }
916        };
917
918        loop {
919            // Overlap the next blocking transfer with the resident batch.
920            if send_empty(comm, root, MessageKind::Ready, 0).is_err() {
921                mpi_call!(comm.abort(75));
922            }
923            let next = match receive_prefetched_batch::<_, T, U>(comm, root) {
924                Ok(next) => next,
925                Err(_) => mpi_call!(comm.abort(75)),
926            };
927            let outcome = match sink.wait(current) {
928                Some(outcome) => outcome,
929                None => mpi_call!(comm.abort(71)),
930            };
931            // A panicking callback aborts before the prefetched batch is handed
932            // to the lanes or executed inline, so a hung peer callback can never
933            // delay the abort.
934            if matches!(outcome.result, LocalBatchResult::Panic) {
935                mpi_call!(comm.abort(71));
936            }
937
938            match next {
939                PrefetchedBatch::Work { batch_id, items } => {
940                    // The resident batch has completed, so the lanes can start
941                    // the prefetched batch while this result is transferred.
942                    sink.submit(LocalBatch::new(batch_id, mode, items));
943                    current = batch_id;
944                    if send_worker_outcome(
945                        comm,
946                        root,
947                        rank,
948                        size,
949                        outcome,
950                        &mut local_key,
951                        &mut local_error,
952                    )
953                    .is_err()
954                    {
955                        mpi_call!(comm.abort(75));
956                    }
957                }
958                PrefetchedBatch::Ready(next_outcome) => {
959                    if send_worker_outcome(
960                        comm,
961                        root,
962                        rank,
963                        size,
964                        outcome,
965                        &mut local_key,
966                        &mut local_error,
967                    )
968                    .is_err()
969                    {
970                        mpi_call!(comm.abort(75));
971                    }
972                    send_worker_outcome(
973                        comm,
974                        root,
975                        rank,
976                        size,
977                        next_outcome,
978                        &mut local_key,
979                        &mut local_error,
980                    )?;
981                    continue 'idle;
982                }
983                PrefetchedBatch::Stop => {
984                    if send_worker_outcome(
985                        comm,
986                        root,
987                        rank,
988                        size,
989                        outcome,
990                        &mut local_key,
991                        &mut local_error,
992                    )
993                    .is_err()
994                    {
995                        mpi_call!(comm.abort(75));
996                    }
997                    send_empty(comm, root, MessageKind::Drain, 0)?;
998                    return Ok((local_key, local_error));
999                }
1000            }
1001        }
1002    }
1003}
1004
1005/// Runs the hybrid worker protocol with a caller-thread coordinator.
1006///
1007/// The lanes execute local batches while the calling thread keeps MPI progress.
1008/// An executor that rejects a concurrent coordinator run (a one-lane backend)
1009/// falls back explicitly to executing each batch on the coordinator thread
1010/// through [`Lanes::run`] between MPI progress steps.
1011#[cfg(feature = "rayon")]
1012fn hybrid_worker_loop<L, C, T, U, E, F>(
1013    comm: &C,
1014    root: i32,
1015    mode: LocalMode,
1016    prefetch: bool,
1017    lanes: &mut L,
1018    f: &F,
1019) -> Result<(i64, Option<LocalError>), PmapError>
1020where
1021    L: Lanes,
1022    C: Communicator,
1023    T: Serialize + DeserializeOwned + Send,
1024    U: Serialize + DeserializeOwned + Send,
1025    E: Display + Send,
1026    F: Fn(T, &mut L::Lane<'_>) -> Result<U, E> + Sync,
1027{
1028    let queue = LocalQueue::<T, U>::new();
1029    let concurrent = lanes.run_with_coordinator(
1030        |control| {
1031            let mut sink = ConcurrentSink {
1032                queue: &queue,
1033                control,
1034            };
1035            hybrid_worker_protocol(comm, root, mode, prefetch, &mut sink)
1036        },
1037        // A lane callback returns only infrastructure failures; item and batch
1038        // failures travel through the queue as data.
1039        |_index, lane, control| {
1040            run_lane(&queue, f, control, lane);
1041            Ok::<(), PmapError>(())
1042        },
1043    );
1044    match concurrent {
1045        Ok(outcome) => Ok(outcome),
1046        Err(LanesError::SingleLane) => {
1047            let mut sink = InlineSink {
1048                run_batch: |batch| run_batch_inline(lanes, f, batch),
1049                outcome: None,
1050            };
1051            hybrid_worker_protocol(comm, root, mode, prefetch, &mut sink)
1052        }
1053        Err(LanesError::Coordinator(error)) => Err(error),
1054        Err(LanesError::Executor(error)) => Err(lane_executor_failure(error)),
1055        Err(LanesError::Reentrant) => Err(lane_executor_failure(
1056            "executor cannot run a coordinator from one of its own workers",
1057        )),
1058        Err(LanesError::Lane(error)) => Err(error),
1059    }
1060}
1061
1062#[cfg(not(feature = "rayon"))]
1063fn execute_root_batch<C, T, U, E, F>(
1064    comm: &C,
1065    scheduler: &mut Coordinator<T, U>,
1066    f: &mut F,
1067    rank: i32,
1068    size: i32,
1069    local_key: &mut i64,
1070    local_error: &mut Option<LocalError>,
1071) -> Result<bool, PmapError>
1072where
1073    C: Communicator,
1074    E: Display,
1075    F: FnMut(T) -> Result<U, E>,
1076{
1077    let Some(batch) = scheduler.next_root_batch().map_err(scheduler_error)? else {
1078        return Ok(false);
1079    };
1080    let batch_id = batch.id();
1081    let mut results = Vec::new();
1082    let mut failure = None;
1083    for item in batch.into_items() {
1084        let index = item.original_index();
1085        let callback =
1086            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(item.into_value())));
1087        match callback {
1088            Ok(Ok(result)) => results.push(ItemResult::new(index, result)),
1089            Ok(Err(error)) => {
1090                failure = Some((index, truncate_message(error.to_string())));
1091                break;
1092            }
1093            Err(_) => mpi_call!(comm.abort(70)),
1094        }
1095    }
1096    match failure {
1097        Some((index, message)) => {
1098            let key = error_key(index, ErrorClass::Callback, rank, size)?;
1099            *local_key = (*local_key).min(key);
1100            *local_error = Some((PmapErrorKind::User, message));
1101            scheduler.on_root_error(batch_id).map_err(scheduler_error)?;
1102        }
1103        None => {
1104            scheduler
1105                .on_root_success(batch_id, results)
1106                .map_err(scheduler_error)?;
1107        }
1108    }
1109    Ok(true)
1110}
1111
1112struct RootErrorState<'a> {
1113    call_task_key: usize,
1114    rank: i32,
1115    size: i32,
1116    local_key: &'a mut i64,
1117    local_error: &'a mut Option<LocalError>,
1118}
1119
1120fn process_remote_header<C, T, U>(
1121    comm: &C,
1122    scheduler: &mut Coordinator<T, U>,
1123    source: i32,
1124    value: Header,
1125    errors: &mut RootErrorState<'_>,
1126) -> Result<(), PmapError>
1127where
1128    C: Communicator,
1129    T: Serialize + DeserializeOwned,
1130    U: Serialize + DeserializeOwned,
1131{
1132    let call_task_key = errors.call_task_key;
1133    let rank = errors.rank;
1134    let size = errors.size;
1135    let local_key = &mut *errors.local_key;
1136    let local_error = &mut *errors.local_error;
1137    match value.kind() {
1138        MessageKind::Ready => match scheduler.on_remote_ready(source).map_err(scheduler_error)? {
1139            Dispatch::Task(batch) => {
1140                #[cfg(all(test, not(feature = "rayon")))]
1141                if (0..64).contains(&source) {
1142                    TEST_ASSIGNED_REMOTES.fetch_or(1_u64 << source, Ordering::SeqCst);
1143                }
1144                let batch_id = batch.id();
1145                let items: Vec<(usize, T)> = batch
1146                    .into_items()
1147                    .into_iter()
1148                    .map(|item| (item.original_index(), item.into_value()))
1149                    .collect();
1150                send_frame(
1151                    comm,
1152                    source,
1153                    MessageKind::Task,
1154                    MessageStatus::None,
1155                    batch_id.get(),
1156                    items.len(),
1157                    &items,
1158                )?;
1159            }
1160            Dispatch::Stop => send_empty(comm, source, MessageKind::Stop, 0)?,
1161        },
1162        MessageKind::Result => {
1163            let payload: ResultPayload<U> = match receive_payload(comm, source, value) {
1164                Ok(payload) => payload,
1165                Err(error) => {
1166                    // receive_payload consumed the actual payload before decode failed.
1167                    #[cfg(all(test, not(feature = "rayon")))]
1168                    TEST_DECODE_FAILURES.fetch_add(1, Ordering::SeqCst);
1169                    let key = error_key(call_task_key, ErrorClass::WireProtocol, rank, size)?;
1170                    *local_key = (*local_key).min(key);
1171                    *local_error = Some((error.kind(), error.message().to_owned()));
1172                    scheduler
1173                        .on_remote_protocol_error(source)
1174                        .map_err(scheduler_error)?;
1175                    return Ok(());
1176                }
1177            };
1178            match (value.status(), payload) {
1179                (MessageStatus::Ok, ResultPayload::Values(values)) => {
1180                    let results = values
1181                        .into_iter()
1182                        .map(|(index, result)| ItemResult::new(index, result))
1183                        .collect();
1184                    scheduler
1185                        .on_remote_success(source, BatchId::from_raw(value.batch_id()), results)
1186                        .map_err(scheduler_error)?;
1187                }
1188                (MessageStatus::Error, ResultPayload::Error(_failure)) => {
1189                    #[cfg(all(test, not(feature = "rayon")))]
1190                    TEST_USER_RESULTS.fetch_add(1, Ordering::SeqCst);
1191                    scheduler
1192                        .on_remote_error(source, BatchId::from_raw(value.batch_id()))
1193                        .map_err(scheduler_error)?;
1194                }
1195                _ => {
1196                    let key = error_key(call_task_key, ErrorClass::WireProtocol, rank, size)?;
1197                    *local_key = (*local_key).min(key);
1198                    *local_error =
1199                        Some((PmapErrorKind::Protocol, "invalid result status".to_owned()));
1200                    scheduler
1201                        .on_remote_protocol_error(source)
1202                        .map_err(scheduler_error)?;
1203                }
1204            }
1205        }
1206        MessageKind::Drain => scheduler.on_remote_drain(source).map_err(scheduler_error)?,
1207        _ => {
1208            let key = error_key(call_task_key, ErrorClass::WireProtocol, rank, size)?;
1209            *local_key = (*local_key).min(key);
1210            *local_error = Some((
1211                PmapErrorKind::Protocol,
1212                "unexpected message from worker".to_owned(),
1213            ));
1214            scheduler
1215                .on_remote_protocol_error(source)
1216                .map_err(scheduler_error)?;
1217        }
1218    }
1219    Ok(())
1220}
1221
1222#[cfg(not(feature = "rayon"))]
1223fn root_loop<C, T, U, E, F>(
1224    comm: &C,
1225    options: &PmapOptions,
1226    root_items: Vec<T>,
1227    mut f: F,
1228) -> Result<RootOutcome<U>, PmapError>
1229where
1230    C: Communicator,
1231    T: Serialize + DeserializeOwned,
1232    U: Serialize + DeserializeOwned,
1233    E: Display,
1234    F: FnMut(T) -> Result<U, E>,
1235{
1236    let root = options.root;
1237    let rank = mpi_call!(comm.rank());
1238    let size = mpi_call!(comm.size());
1239    let remotes: Vec<i32> = (0..size).filter(|candidate| *candidate != root).collect();
1240    let call_task_key = root_items.len();
1241    let mut scheduler = Coordinator::<T, U>::new_with_prefetch(
1242        root,
1243        remotes,
1244        root_items,
1245        options.batch_size,
1246        options.prefetch,
1247    )
1248    .map_err(scheduler_error)?;
1249    let mut local_key = ErrorKey::NO_ERROR_KEY;
1250    let mut local_error = None;
1251
1252    loop {
1253        if size == 1 {
1254            if !execute_root_batch(
1255                comm,
1256                &mut scheduler,
1257                &mut f,
1258                rank,
1259                size,
1260                &mut local_key,
1261                &mut local_error,
1262            )? {
1263                break;
1264            }
1265            continue;
1266        }
1267
1268        if scheduler.is_finished() && scheduler.is_quiescent() {
1269            break;
1270        }
1271
1272        // Serve every worker whose message is already pending. The root must
1273        // not race the workers for batches: taking one batch per received
1274        // message makes it execute the majority of the work itself, and rank 0
1275        // becomes the serialized bottleneck that prevents strong scaling with
1276        // `-n`. The root therefore serves first and loops back while messages
1277        // keep arriving; it computes a local batch only when the pending
1278        // queue is empty (every worker is busy computing), and then blocks
1279        // for the next message to pace itself instead of taking another batch
1280        // back-to-back.
1281        let mut served = 0usize;
1282        while mpi_call!(comm.any_process().immediate_probe_with_tag(HEADER_TAG)).is_some() {
1283            let (source, value) = receive_any_header(comm)?;
1284            process_remote_header(
1285                comm,
1286                &mut scheduler,
1287                source,
1288                value,
1289                &mut RootErrorState {
1290                    call_task_key,
1291                    rank,
1292                    size,
1293                    local_key: &mut local_key,
1294                    local_error: &mut local_error,
1295                },
1296            )?;
1297            served += 1;
1298        }
1299        // `is_finished` only turns true once every worker has drained; the
1300        // top-of-loop break then terminates the loop, so never block on a
1301        // message after completion.
1302        if scheduler.is_finished() {
1303            continue;
1304        }
1305        if served == 0 {
1306            let _ = execute_root_batch(
1307                comm,
1308                &mut scheduler,
1309                &mut f,
1310                rank,
1311                size,
1312                &mut local_key,
1313                &mut local_error,
1314            )?;
1315            // `execute_root_batch` may have just stopped the root lane (no
1316            // pending work left, or the run failed); re-check completion
1317            // before blocking, or the root waits forever for a Drain that
1318            // already arrived.
1319            if scheduler.is_finished() {
1320                continue;
1321            }
1322            let (source, value) = receive_any_header(comm)?;
1323            process_remote_header(
1324                comm,
1325                &mut scheduler,
1326                source,
1327                value,
1328                &mut RootErrorState {
1329                    call_task_key,
1330                    rank,
1331                    size,
1332                    local_key: &mut local_key,
1333                    local_error: &mut local_error,
1334                },
1335            )?;
1336            continue;
1337        }
1338    }
1339
1340    let results = if scheduler.failed() {
1341        Vec::new()
1342    } else {
1343        scheduler.into_results().map_err(scheduler_error)?
1344    };
1345    Ok((results, local_key, local_error))
1346}
1347
1348/// The root-side MPI protocol, generic over how local batches execute.
1349///
1350/// This is the shared root loop: the coordinator on the calling thread keeps
1351/// scheduler and MPI progress while root-local batches execute on the lanes.
1352#[cfg(feature = "rayon")]
1353fn hybrid_root_protocol<C, S, T, U>(
1354    comm: &C,
1355    options: &PmapOptions,
1356    root_items: Vec<T>,
1357    sink: &mut S,
1358) -> Result<RootOutcome<U>, PmapError>
1359where
1360    C: Communicator,
1361    S: BatchSink<T, U>,
1362    T: Serialize + DeserializeOwned + Send,
1363    U: Serialize + DeserializeOwned + Send,
1364{
1365    let root = options.root;
1366    let rank = mpi_call!(comm.rank());
1367    let size = mpi_call!(comm.size());
1368    let remotes: Vec<i32> = (0..size).filter(|candidate| *candidate != root).collect();
1369    let call_task_key = root_items.len();
1370    let mut scheduler = Coordinator::<T, U>::new_with_prefetch(
1371        root,
1372        remotes,
1373        root_items,
1374        options.batch_size,
1375        options.prefetch,
1376    )
1377    .map_err(scheduler_error)?;
1378    let mut local_key = ErrorKey::NO_ERROR_KEY;
1379    let mut local_error = None;
1380    let mut running: Option<u64> = None;
1381    let mut pending_local: Option<LocalBatchOutcome<U>> = None;
1382    let mut prefer_local = true;
1383
1384    loop {
1385        if sink.cancelled() {
1386            // A lane infrastructure failure is unrecoverable on this rank.
1387            mpi_call!(comm.abort(70));
1388        }
1389        if running.is_none() {
1390            if let Some(batch) = scheduler.next_root_batch().map_err(scheduler_error)? {
1391                let batch_id = batch.id().get();
1392                let items = batch
1393                    .into_items()
1394                    .into_iter()
1395                    .map(|item| (item.original_index(), item.into_value()))
1396                    .collect();
1397                sink.submit(LocalBatch::new(batch_id, options.local_mode, items));
1398                running = Some(batch_id);
1399            }
1400        }
1401        if running.is_none() && scheduler.is_finished() && scheduler.is_quiescent() {
1402            break;
1403        }
1404        if pending_local.is_none() {
1405            if let Some(batch_id) = running {
1406                pending_local = sink.try_wait(batch_id);
1407            }
1408        }
1409        let remote_source = if size > 1 {
1410            mpi_call!(comm.any_process().immediate_probe_with_tag(HEADER_TAG))
1411                .map(|status| status.source_rank())
1412        } else {
1413            None
1414        };
1415        let (event, next_preference) = choose_root_event(
1416            pending_local.is_some(),
1417            remote_source.is_some(),
1418            prefer_local,
1419        );
1420        prefer_local = next_preference;
1421
1422        match event {
1423            RootEvent::Local => {
1424                let outcome = pending_local
1425                    .take()
1426                    .ok_or_else(|| protocol_error("missing root-local outcome"))?;
1427                running = None;
1428                let batch_id = BatchId::from_raw(outcome.batch_id);
1429                match outcome.result {
1430                    LocalBatchResult::Values(values) => {
1431                        let results = values
1432                            .into_iter()
1433                            .map(|(index, value)| ItemResult::new(index, value))
1434                            .collect();
1435                        scheduler
1436                            .on_root_success(batch_id, results)
1437                            .map_err(scheduler_error)?;
1438                    }
1439                    LocalBatchResult::CallbackError { index, message } => {
1440                        let key = error_key(index, ErrorClass::Callback, rank, size)?;
1441                        local_key = local_key.min(key);
1442                        local_error = Some((PmapErrorKind::User, message));
1443                        scheduler.on_root_error(batch_id).map_err(scheduler_error)?;
1444                    }
1445                    LocalBatchResult::WireError { .. } => {
1446                        return Err(protocol_error("root lane produced a wire error"))
1447                    }
1448                    LocalBatchResult::Panic => mpi_call!(comm.abort(70)),
1449                }
1450            }
1451            RootEvent::Remote => {
1452                let source =
1453                    remote_source.ok_or_else(|| protocol_error("missing probed remote source"))?;
1454                let value = receive_header(comm, source)?;
1455                process_remote_header(
1456                    comm,
1457                    &mut scheduler,
1458                    source,
1459                    value,
1460                    &mut RootErrorState {
1461                        call_task_key,
1462                        rank,
1463                        size,
1464                        local_key: &mut local_key,
1465                        local_error: &mut local_error,
1466                    },
1467                )?;
1468            }
1469            RootEvent::Neither => std::thread::yield_now(),
1470        }
1471    }
1472
1473    let results = if scheduler.failed() {
1474        Vec::new()
1475    } else {
1476        scheduler.into_results().map_err(scheduler_error)?
1477    };
1478    Ok((results, local_key, local_error))
1479}
1480
1481/// Runs the hybrid root protocol with a caller-thread coordinator.
1482///
1483/// See [`hybrid_worker_loop`] for the one-lane fallback rule.
1484#[cfg(feature = "rayon")]
1485fn hybrid_root_loop<L, C, T, U, E, F>(
1486    comm: &C,
1487    options: &PmapOptions,
1488    root_items: Vec<T>,
1489    lanes: &mut L,
1490    f: &F,
1491) -> Result<RootOutcome<U>, PmapError>
1492where
1493    L: Lanes,
1494    C: Communicator,
1495    T: Serialize + DeserializeOwned + Send,
1496    U: Serialize + DeserializeOwned + Send,
1497    E: Display + Send,
1498    F: Fn(T, &mut L::Lane<'_>) -> Result<U, E> + Sync,
1499{
1500    let queue = LocalQueue::<T, U>::new();
1501    let mut root_input = Some(root_items);
1502    let concurrent = lanes.run_with_coordinator(
1503        |control| {
1504            let mut sink = ConcurrentSink {
1505                queue: &queue,
1506                control,
1507            };
1508            hybrid_root_protocol(
1509                comm,
1510                options,
1511                root_input.take().expect("root input is passed once"),
1512                &mut sink,
1513            )
1514        },
1515        // A lane callback returns only infrastructure failures; item and batch
1516        // failures travel through the queue as data.
1517        |_index, lane, control| {
1518            run_lane(&queue, f, control, lane);
1519            Ok::<(), PmapError>(())
1520        },
1521    );
1522    match concurrent {
1523        Ok(outcome) => Ok(outcome),
1524        Err(LanesError::SingleLane) => {
1525            let mut sink = InlineSink {
1526                run_batch: |batch| run_batch_inline(lanes, f, batch),
1527                outcome: None,
1528            };
1529            hybrid_root_protocol(
1530                comm,
1531                options,
1532                root_input.take().expect("root input is passed once"),
1533                &mut sink,
1534            )
1535        }
1536        Err(LanesError::Coordinator(error)) => Err(error),
1537        Err(LanesError::Executor(error)) => Err(lane_executor_failure(error)),
1538        Err(LanesError::Reentrant) => Err(lane_executor_failure(
1539            "executor cannot run a coordinator from one of its own workers",
1540        )),
1541        Err(LanesError::Lane(error)) => Err(error),
1542    }
1543}
1544
1545/// Runs one hybrid `pmap` call on an already duplicated private communicator.
1546#[cfg(feature = "rayon")]
1547fn run_collective<L, C, T, U, E, F>(
1548    comm: &C,
1549    lanes: &mut L,
1550    options: &PmapOptions,
1551    root_items: Option<Vec<T>>,
1552    f: &F,
1553) -> Result<Option<Vec<U>>, PmapError>
1554where
1555    L: Lanes,
1556    C: Communicator,
1557    T: Serialize + DeserializeOwned + Send,
1558    U: Serialize + DeserializeOwned + Send,
1559    E: Display + Send,
1560    F: Fn(T, &mut L::Lane<'_>) -> Result<U, E> + Sync,
1561{
1562    let rank = mpi_call!(comm.rank());
1563    let outcome = if rank == options.root {
1564        hybrid_root_loop(
1565            comm,
1566            options,
1567            root_items.expect("successful preflight guarantees root input"),
1568            lanes,
1569            f,
1570        )
1571    } else {
1572        hybrid_worker_loop(
1573            comm,
1574            options.root,
1575            options.local_mode,
1576            options.prefetch,
1577            lanes,
1578            f,
1579        )
1580        .map(|(key, error)| (Vec::new(), key, error))
1581    };
1582    let (results, local_key, local_error) = match outcome {
1583        Ok(outcome) => outcome,
1584        Err(_) => mpi_call!(comm.abort(72)),
1585    };
1586
1587    let selected = convergence(comm, local_key);
1588    let error = match broadcast_error(comm, selected, local_key, local_error) {
1589        Ok(error) => error,
1590        Err(_) => mpi_call!(comm.abort(73)),
1591    };
1592    match error {
1593        Some((kind, message)) => Err(PmapError::keyed(kind, selected, message)),
1594        None if rank == options.root => Ok(Some(results)),
1595        None => Ok(None),
1596    }
1597}
1598
1599/// Dynamically maps root-owned items across MPI ranks and caller-supplied lanes
1600/// for each rank's local batches.
1601///
1602/// Every rank must call this collective in the same order with identical
1603/// options. Only `options.root` supplies items and receives ordered results.
1604/// The call must originate on the MPI initialization/main thread, and MPI must
1605/// provide `MPI_THREAD_FUNNELED` or stronger, because the coordinator keeps MPI
1606/// progress on that thread while the lanes compute. Do not enter it from a
1607/// worker of the executor's own pool.
1608///
1609/// The coordinator is the protocol loop on the calling thread: it dispatches
1610/// scheduler batches, keeps MPI progress, and applies the root's immediate abort
1611/// rules before anything is joined. Lanes execute the local batches with the
1612/// requested [`LocalMode`]: `Outer` spreads a batch over the lanes, while
1613/// `Sequential` and `Inner` hand the batch to one lane. When
1614/// [`PmapOptions::prefetch`] is true, each remote domain may hold one additional
1615/// batch so blocking task/result transfer overlaps adjacent batch execution; the
1616/// root domain remains capacity one.
1617///
1618/// The per-item callback receives the lane resource of the lane that runs the
1619/// item (`&mut L::Lane<'_>`), so a backend adapter can hand its own per-worker
1620/// context, such as a tenferro child session, to the callback. [`pmap`] adapts
1621/// the ordinary `Fn(T)` shape to `RayonLanes`' unit lane.
1622///
1623/// A lanes executor that reports [`LanesError::SingleLane`] falls back
1624/// explicitly to executing each batch on the coordinator thread through
1625/// [`Lanes::run`] between MPI progress steps. This is the documented one-lane
1626/// path; it never happens silently and never runs work twice.
1627///
1628/// # Errors
1629///
1630/// Returns [`PmapErrorKind::Preflight`] when the collective preconditions fail
1631/// (including entry from a Rayon worker), [`PmapErrorKind::User`] with a
1632/// converged error key when a callback fails, [`PmapErrorKind::Wire`] or
1633/// [`PmapErrorKind::Protocol`] for transport, protocol, or lane-executor
1634/// failures, and [`PmapErrorKind::Reentrant`] when `pmap` is already active on
1635/// this thread.
1636#[cfg(feature = "rayon")]
1637pub fn pmap_lanes<L, C, T, U, E, F>(
1638    world: &C,
1639    lanes: &mut L,
1640    options: PmapOptions,
1641    root_items: Option<Vec<T>>,
1642    f: F,
1643) -> Result<Option<Vec<U>>, PmapError>
1644where
1645    L: Lanes,
1646    C: Communicator,
1647    T: Serialize + DeserializeOwned + Send,
1648    U: Serialize + DeserializeOwned + Send,
1649    E: Display + Send,
1650    F: Fn(T, &mut L::Lane<'_>) -> Result<U, E> + Sync,
1651{
1652    if in_rayon_worker_context() {
1653        return Err(PmapError::new(
1654            PmapErrorKind::Preflight,
1655            "hybrid pmap cannot enter from a Rayon worker",
1656        ));
1657    }
1658    execute(world, Some(lanes), options, root_items, &f)
1659}
1660
1661/// Runs one hybrid collective call on an already built lane executor.
1662///
1663/// A missing or lane-less executor is folded into collective preflight instead
1664/// of returning locally, so peers are never stranded.
1665#[cfg(feature = "rayon")]
1666fn execute<L, C, T, U, E, F>(
1667    world: &C,
1668    lanes: Option<&mut L>,
1669    options: PmapOptions,
1670    root_items: Option<Vec<T>>,
1671    f: &F,
1672) -> Result<Option<Vec<U>>, PmapError>
1673where
1674    L: Lanes,
1675    C: Communicator,
1676    T: Serialize + DeserializeOwned + Send,
1677    U: Serialize + DeserializeOwned + Send,
1678    E: Display + Send,
1679    F: Fn(T, &mut L::Lane<'_>) -> Result<U, E> + Sync,
1680{
1681    let _active = Active::acquire()?;
1682    let executor_valid = lanes.as_ref().is_some_and(|lanes| lanes.lane_count() > 0);
1683    let execution_valid = executor_valid
1684        && mpi_call!(mpi_backend::environment::threading_support())
1685            >= mpi_backend::environment::Threading::Funneled;
1686    let comm = mpi_call!(world.duplicate());
1687    preflight(&comm, &options, root_items.as_ref(), execution_valid)?;
1688    let lanes = lanes.expect("successful preflight guarantees a lane executor");
1689    run_collective(&comm, lanes, &options, root_items, f)
1690}
1691impl PmapErrorKind {
1692    fn code(self) -> i32 {
1693        match self {
1694            Self::Reentrant => 0,
1695            Self::Preflight => 1,
1696            Self::User => 2,
1697            Self::Wire => 3,
1698            Self::Protocol => 4,
1699        }
1700    }
1701
1702    fn from_code(code: i32) -> Option<Self> {
1703        Some(match code {
1704            0 => Self::Reentrant,
1705            1 => Self::Preflight,
1706            2 => Self::User,
1707            3 => Self::Wire,
1708            4 => Self::Protocol,
1709            _ => return None,
1710        })
1711    }
1712}
1713
1714/// Dynamically maps root-owned items across all ranks of `world`.
1715///
1716/// Every rank must call this function in the same collective order with the
1717/// same communicator and options. Only `options.root` supplies `Some(items)`
1718/// and receives `Ok(Some(results))`; every other rank supplies `None` and
1719/// receives `Ok(None)`. Results preserve input order. The root rank must lie
1720/// in `[0, world.size())`.
1721///
1722/// This MPI-only entry is synchronous and adds no `Send`, `Sync`, or `'static`
1723/// bounds to the callback or values. It rejects [`PmapOptions::prefetch`] during
1724/// collective preflight.
1725#[cfg(not(feature = "rayon"))]
1726pub fn pmap<C, T, U, E, F>(
1727    world: &C,
1728    domain: &Domain,
1729    options: PmapOptions,
1730    root_items: Option<Vec<T>>,
1731    f: F,
1732) -> Result<Option<Vec<U>>, PmapError>
1733where
1734    C: Communicator,
1735    T: Serialize + DeserializeOwned,
1736    U: Serialize + DeserializeOwned,
1737    E: Display,
1738    F: FnMut(T) -> Result<U, E>,
1739{
1740    let _active = Active::acquire()?;
1741    let admission = domain.try_admit();
1742    let comm = mpi_call!(world.duplicate());
1743    preflight(
1744        &comm,
1745        &options,
1746        root_items.as_ref(),
1747        admission.is_ok()
1748            && domain.id().get() == 0
1749            && domain.worker_count() == 1
1750            && options.local_mode == LocalMode::Sequential
1751            && !options.prefetch,
1752    )?;
1753    let _admission = admission.expect("successful preflight guarantees domain admission");
1754
1755    let rank = mpi_call!(comm.rank());
1756    let outcome = if rank == options.root {
1757        root_loop(&comm, &options, root_items.unwrap(), f)
1758    } else {
1759        worker_loop(&comm, options.root, f).map(|(key, error)| (Vec::new(), key, error))
1760    };
1761    let (results, local_key, local_error) = match outcome {
1762        Ok(outcome) => outcome,
1763        Err(_) => mpi_call!(comm.abort(72)),
1764    };
1765
1766    let selected = convergence(&comm, local_key);
1767    let error = match broadcast_error(&comm, selected, local_key, local_error) {
1768        Ok(error) => error,
1769        Err(_) => mpi_call!(comm.abort(73)),
1770    };
1771    match error {
1772        Some((kind, message)) => Err(PmapError::keyed(kind, selected, message)),
1773        None if rank == options.root => Ok(Some(results)),
1774        None => Ok(None),
1775    }
1776}
1777
1778/// Dynamically maps root-owned items across MPI ranks and each rank's explicit
1779/// Rayon domain.
1780///
1781/// Every rank must call this collective in the same order with identical
1782/// options. Only `options.root` supplies items and receives ordered results.
1783/// The call must originate on the MPI initialization/main thread outside every
1784/// Rayon worker, and MPI must provide `MPI_THREAD_FUNNELED` or stronger.
1785/// Callback state may borrow non-`'static` data while satisfying the stated
1786/// Rayon `Send`/`Sync` bounds; values retain the serialization bounds required
1787/// by remote execution.
1788///
1789/// Local execution is [`pmap_lanes`] with [`RayonLanes`]: the coordinator keeps MPI progress on the calling thread
1790/// while lanes execute local batches, so when [`PmapOptions::prefetch`] is true
1791/// each remote domain may hold one additional batch and blocking task/result
1792/// transfer overlaps adjacent batch execution. The root domain remains capacity
1793/// one, and all MPI calls remain on the calling thread.
1794///
1795/// # Errors
1796///
1797/// Returns [`PmapErrorKind::Preflight`] when a rank lacks a pooled, admitted
1798/// domain, enters from a Rayon worker, or disagrees about options;
1799/// [`PmapErrorKind::User`] with a converged error key when a callback fails;
1800/// [`PmapErrorKind::Wire`] or [`PmapErrorKind::Protocol`] for transport or
1801/// protocol failures; and [`PmapErrorKind::Reentrant`] when `pmap` is already
1802/// active on this thread.
1803#[cfg(feature = "rayon")]
1804pub fn pmap<C, T, U, E, F>(
1805    world: &C,
1806    domain: &Domain,
1807    options: PmapOptions,
1808    root_items: Option<Vec<T>>,
1809    f: F,
1810) -> Result<Option<Vec<U>>, PmapError>
1811where
1812    C: Communicator,
1813    T: Serialize + DeserializeOwned + Send,
1814    U: Serialize + DeserializeOwned + Send,
1815    E: Display + Send,
1816    F: Fn(T) -> Result<U, E> + Send + Sync,
1817{
1818    if in_rayon_worker_context() {
1819        return Err(PmapError::new(
1820            PmapErrorKind::Preflight,
1821            "hybrid pmap cannot enter from a Rayon worker",
1822        ));
1823    }
1824    // Building the executor admits the domain. A construction failure is a
1825    // collective preflight failure, never a local early return that would
1826    // strand peers.
1827    let mut lanes = RayonLanes::new(domain).ok();
1828    execute(
1829        world,
1830        lanes.as_mut(),
1831        options,
1832        root_items,
1833        &|item: T, _lane: &mut ()| f(item),
1834    )
1835}
1836
1837#[cfg(all(test, not(feature = "rayon")))]
1838mod mpi_fault_tests;
1839
1840#[cfg(all(test, feature = "rayon"))]
1841mod tests;