Skip to main content

hataori/
pmap.rs

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