Skip to main content

hataori/
placement.rs

1use crate::map::{truncate_message, MAX_ERROR_MESSAGE_BYTES};
2use crate::mpi_backend::collective::SystemOperation;
3use crate::mpi_backend::traits::*;
4use crate::mpi_check::{is_thread_main, mpi_call};
5use crate::wire::{self, Header, MessageKind, MessageStatus};
6use serde::{de::DeserializeOwned, Serialize};
7use std::fmt::{self, Display, Formatter};
8
9const HEADER_TAG: i32 = 2;
10const PAYLOAD_TAG: i32 = 3;
11const FRAME_ID: u64 = 0;
12const FRAME_ITEMS: u64 = 1;
13
14#[derive(Debug, Copy, Clone, Eq, PartialEq)]
15enum Operation {
16    Broadcast,
17    Scatter,
18    Gather,
19}
20
21impl Operation {
22    const fn code(self) -> i32 {
23        match self {
24            Self::Broadcast => 0,
25            Self::Scatter => 1,
26            Self::Gather => 2,
27        }
28    }
29
30    const fn message_kind(self) -> MessageKind {
31        match self {
32            Self::Broadcast => MessageKind::Broadcast,
33            Self::Scatter => MessageKind::Scatter,
34            Self::Gather => MessageKind::Gather,
35        }
36    }
37}
38
39/// Stable category of a collective placement failure.
40#[derive(Debug, Copy, Clone, Eq, PartialEq)]
41pub enum PlacementErrorKind {
42    /// Collective arguments or calling-thread requirements disagreed or were invalid.
43    Preflight,
44    /// Serialization, deserialization, or wire framing failed.
45    Wire,
46    /// A received placement frame violated its operation schema.
47    Protocol,
48}
49
50/// Error converged across every rank in a placement collective.
51#[derive(Debug, Clone, Eq, PartialEq)]
52pub struct PlacementError {
53    kind: PlacementErrorKind,
54    rank: Option<i32>,
55    message: String,
56}
57
58impl PlacementError {
59    fn new(kind: PlacementErrorKind, rank: Option<i32>, message: impl Display) -> Self {
60        Self {
61            kind,
62            rank,
63            message: truncate_message(message.to_string()),
64        }
65    }
66
67    /// Returns the stable failure category.
68    pub const fn kind(&self) -> PlacementErrorKind {
69        self.kind
70    }
71
72    /// Returns the lowest rank that reported the converged failure.
73    pub const fn rank(&self) -> Option<i32> {
74        self.rank
75    }
76
77    /// Returns the bounded diagnostic message.
78    pub fn message(&self) -> &str {
79        &self.message
80    }
81}
82
83impl Display for PlacementError {
84    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
85        formatter.write_str(self.message())
86    }
87}
88
89impl std::error::Error for PlacementError {}
90
91struct Failure {
92    kind: PlacementErrorKind,
93    message: String,
94}
95
96impl Failure {
97    fn new(kind: PlacementErrorKind, message: impl Display) -> Self {
98        Self {
99            kind,
100            message: truncate_message(message.to_string()),
101        }
102    }
103}
104
105fn record_failure(slot: &mut Option<Failure>, failure: Failure) {
106    if slot.is_none() {
107        *slot = Some(failure);
108    }
109}
110
111impl PlacementErrorKind {
112    const fn code(self) -> i32 {
113        match self {
114            Self::Preflight => 0,
115            Self::Wire => 1,
116            Self::Protocol => 2,
117        }
118    }
119
120    const fn from_code(code: i32) -> Option<Self> {
121        match code {
122            0 => Some(Self::Preflight),
123            1 => Some(Self::Wire),
124            2 => Some(Self::Protocol),
125            _ => None,
126        }
127    }
128}
129
130fn converge<C: Communicator>(comm: &C, local: Option<Failure>) -> Option<PlacementError> {
131    let rank = mpi_call!(comm.rank());
132    let size = mpi_call!(comm.size());
133    let candidate = if local.is_some() { rank } else { size };
134    let mut winner = candidate;
135    mpi_call!(comm.all_reduce_into(&candidate, &mut winner, SystemOperation::min()));
136    if winner == size {
137        return None;
138    }
139    if winner < 0 || winner >= size {
140        mpi_call!(comm.abort(74));
141    }
142
143    let is_winner = rank == winner;
144    if is_winner && local.is_none() {
145        mpi_call!(comm.abort(74));
146    }
147    let mut kind = local
148        .as_ref()
149        .filter(|_| is_winner)
150        .map_or(PlacementErrorKind::Protocol.code(), |failure| {
151            failure.kind.code()
152        });
153    let mut bytes = local
154        .filter(|_| is_winner)
155        .map_or_else(Vec::new, |failure| failure.message.into_bytes());
156    let mut length = match i32::try_from(bytes.len()) {
157        Ok(length) => length,
158        Err(_) => mpi_call!(comm.abort(74)),
159    };
160
161    mpi_call!(comm.process_at_rank(winner).broadcast_into(&mut kind));
162    mpi_call!(comm.process_at_rank(winner).broadcast_into(&mut length));
163    if length < 0 || length as usize > MAX_ERROR_MESSAGE_BYTES {
164        mpi_call!(comm.abort(74));
165    }
166    if !is_winner {
167        bytes.resize(length as usize, 0);
168    }
169    mpi_call!(comm
170        .process_at_rank(winner)
171        .broadcast_into(bytes.as_mut_slice()));
172
173    let Some(kind) = PlacementErrorKind::from_code(kind) else {
174        mpi_call!(comm.abort(74));
175    };
176    let message = match String::from_utf8(bytes) {
177        Ok(message) => message,
178        Err(_) => mpi_call!(comm.abort(74)),
179    };
180    Some(PlacementError::new(kind, Some(winner), message))
181}
182
183fn preflight<C: Communicator>(
184    comm: &C,
185    operation: Operation,
186    root: i32,
187    local_shape: bool,
188) -> Result<(), PlacementError> {
189    let local_operation = operation.code();
190    let mut min_operation = local_operation;
191    let mut max_operation = local_operation;
192    mpi_call!(comm.all_reduce_into(&local_operation, &mut min_operation, SystemOperation::min()));
193    mpi_call!(comm.all_reduce_into(&local_operation, &mut max_operation, SystemOperation::max()));
194
195    let mut min_root = root;
196    let mut max_root = root;
197    mpi_call!(comm.all_reduce_into(&root, &mut min_root, SystemOperation::min()));
198    mpi_call!(comm.all_reduce_into(&root, &mut max_root, SystemOperation::max()));
199
200    let size = mpi_call!(comm.size());
201    let local = if min_operation != max_operation {
202        Some(Failure::new(
203            PlacementErrorKind::Preflight,
204            "placement operation mismatch",
205        ))
206    } else if min_root != max_root {
207        Some(Failure::new(
208            PlacementErrorKind::Preflight,
209            "placement root mismatch",
210        ))
211    } else if root < 0 || root >= size {
212        Some(Failure::new(
213            PlacementErrorKind::Preflight,
214            "placement root is out of range",
215        ))
216    } else if !local_shape {
217        Some(Failure::new(
218            PlacementErrorKind::Preflight,
219            "placement input shape is invalid",
220        ))
221    } else if !is_thread_main() {
222        Some(Failure::new(
223            PlacementErrorKind::Preflight,
224            "placement helper must run on the MPI main thread",
225        ))
226    } else {
227        None
228    };
229    match converge(comm, local) {
230        Some(error) => Err(error),
231        None => Ok(()),
232    }
233}
234
235fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, Failure> {
236    wire::encode_payload(value).map_err(|error| Failure::new(PlacementErrorKind::Wire, error))
237}
238
239fn send_frame<C: Communicator>(comm: &C, destination: i32, operation: Operation, payload: &[u8]) {
240    let header = match Header::new(
241        operation.message_kind(),
242        MessageStatus::None,
243        FRAME_ID,
244        FRAME_ITEMS,
245        payload.len() as u64,
246    ) {
247        Ok(header) => header,
248        Err(_) => mpi_call!(comm.abort(74)),
249    };
250    mpi_call!(comm
251        .process_at_rank(destination)
252        .send_with_tag(&header.encode(), HEADER_TAG));
253    mpi_call!(comm
254        .process_at_rank(destination)
255        .send_with_tag(payload, PAYLOAD_TAG));
256}
257
258fn receive_frame<C: Communicator>(comm: &C, source: i32) -> (Vec<u8>, Vec<u8>) {
259    // Drain both actual MPI messages before inspecting an untrusted header.
260    let (header, _) = mpi_call!(comm
261        .process_at_rank(source)
262        .receive_vec_with_tag::<u8>(HEADER_TAG));
263    let (payload, _) = mpi_call!(comm
264        .process_at_rank(source)
265        .receive_vec_with_tag::<u8>(PAYLOAD_TAG));
266    (header, payload)
267}
268
269fn decode_received<T: DeserializeOwned>(
270    header_bytes: &[u8],
271    payload: &[u8],
272    operation: Operation,
273) -> Result<T, Failure> {
274    let header = Header::decode(header_bytes)
275        .map_err(|error| Failure::new(PlacementErrorKind::Wire, error))?;
276    if header.kind() != operation.message_kind()
277        || header.status() != MessageStatus::None
278        || header.batch_id() != FRAME_ID
279        || header.item_count() != FRAME_ITEMS
280    {
281        return Err(Failure::new(
282            PlacementErrorKind::Protocol,
283            "invalid placement frame schema",
284        ));
285    }
286    wire::checked_mpi_count(header.payload_len())
287        .map_err(|error| Failure::new(PlacementErrorKind::Wire, error))?;
288    wire::checked_usize_length(header.payload_len())
289        .map_err(|error| Failure::new(PlacementErrorKind::Wire, error))?;
290    if payload.len() as u64 != header.payload_len() {
291        return Err(Failure::new(
292            PlacementErrorKind::Protocol,
293            "placement payload length does not match its header",
294        ));
295    }
296    wire::checked_mpi_count(payload.len() as u64)
297        .map_err(|error| Failure::new(PlacementErrorKind::Wire, error))?;
298    wire::decode_payload(payload).map_err(|error| Failure::new(PlacementErrorKind::Wire, error))
299}
300
301fn preencode_scatter<T: Serialize>(
302    shards: &mut [Option<T>],
303    root: i32,
304    size: i32,
305) -> (Option<T>, Vec<Option<Vec<u8>>>, Option<Failure>) {
306    let local = shards[root as usize].take();
307    let mut encoded = (0..size).map(|_| None).collect::<Vec<_>>();
308    let mut failure = None;
309    for destination in 0..size {
310        if destination == root {
311            continue;
312        }
313        match shards[destination as usize].as_ref() {
314            Some(value) => match encode(value) {
315                Ok(bytes) => encoded[destination as usize] = Some(bytes),
316                Err(error) => record_failure(&mut failure, error),
317            },
318            None => record_failure(
319                &mut failure,
320                Failure::new(PlacementErrorKind::Protocol, "missing scatter shard"),
321            ),
322        }
323    }
324    (local, encoded, failure)
325}
326
327/// Copies one root-owned value to every rank.
328///
329/// Every rank calls this collective with the same communicator and root. Only
330/// the root supplies `Some`; every rank receives an owned value.
331pub fn broadcast<C, T>(world: &C, root: i32, root_value: Option<T>) -> Result<T, PlacementError>
332where
333    C: Communicator,
334    T: Serialize + DeserializeOwned,
335{
336    let comm = mpi_call!(world.duplicate());
337    let rank = mpi_call!(comm.rank());
338    let size = mpi_call!(comm.size());
339    preflight(
340        &comm,
341        Operation::Broadcast,
342        root,
343        (rank == root) == root_value.is_some(),
344    )?;
345
346    if size == 1 {
347        return Ok(root_value.unwrap_or_else(|| mpi_call!(comm.abort(74))));
348    }
349
350    let (encoded, encode_failure) = if rank == root {
351        match root_value.as_ref() {
352            Some(value) => match encode(value) {
353                Ok(bytes) => (Some(bytes), None),
354                Err(error) => (None, Some(error)),
355            },
356            None => (
357                None,
358                Some(Failure::new(
359                    PlacementErrorKind::Protocol,
360                    "missing root broadcast value",
361                )),
362            ),
363        }
364    } else {
365        (None, None)
366    };
367    if let Some(error) = converge(&comm, encode_failure) {
368        return Err(error);
369    }
370
371    let mut received = None;
372    let mut local_failure = None;
373    if rank == root {
374        let payload = encoded
375            .as_deref()
376            .unwrap_or_else(|| mpi_call!(comm.abort(74)));
377        for destination in 0..size {
378            if destination != root {
379                send_frame(&comm, destination, Operation::Broadcast, payload);
380            }
381        }
382    } else {
383        let (header, payload) = receive_frame(&comm, root);
384        match decode_received(&header, &payload, Operation::Broadcast) {
385            Ok(value) => received = Some(value),
386            Err(error) => local_failure = Some(error),
387        }
388    }
389    if let Some(error) = converge(&comm, local_failure) {
390        return Err(error);
391    }
392
393    if rank == root {
394        Ok(root_value.unwrap_or_else(|| mpi_call!(comm.abort(74))))
395    } else {
396        Ok(received.unwrap_or_else(|| mpi_call!(comm.abort(74))))
397    }
398}
399
400/// Distributes one owned shard from the root to each rank.
401///
402/// Only the root supplies `Some`, containing exactly one shard per rank. Every
403/// rank receives its rank-indexed owned shard.
404pub fn scatter<C, T>(world: &C, root: i32, root_shards: Option<Vec<T>>) -> Result<T, PlacementError>
405where
406    C: Communicator,
407    T: Serialize + DeserializeOwned,
408{
409    let comm = mpi_call!(world.duplicate());
410    let rank = mpi_call!(comm.rank());
411    let size = mpi_call!(comm.size());
412    let local_shape = (rank == root) == root_shards.is_some()
413        && (rank != root
414            || root_shards
415                .as_ref()
416                .is_some_and(|shards| shards.len() == size as usize));
417    preflight(&comm, Operation::Scatter, root, local_shape)?;
418
419    let mut shards = root_shards.map(|shards| shards.into_iter().map(Some).collect::<Vec<_>>());
420    if size == 1 {
421        return Ok(shards
422            .as_mut()
423            .and_then(|shards| shards[0].take())
424            .unwrap_or_else(|| mpi_call!(comm.abort(74))));
425    }
426
427    let (local_value, encoded, encode_failure) = if rank == root {
428        preencode_scatter(
429            shards.as_mut().unwrap_or_else(|| mpi_call!(comm.abort(74))),
430            root,
431            size,
432        )
433    } else {
434        (None, Vec::new(), None)
435    };
436    if let Some(error) = converge(&comm, encode_failure) {
437        return Err(error);
438    }
439
440    let mut received = None;
441    let mut local_failure = None;
442    if rank == root {
443        for destination in 0..size {
444            if destination != root {
445                let payload = encoded[destination as usize]
446                    .as_deref()
447                    .unwrap_or_else(|| mpi_call!(comm.abort(74)));
448                send_frame(&comm, destination, Operation::Scatter, payload);
449            }
450        }
451    } else {
452        let (header, payload) = receive_frame(&comm, root);
453        match decode_received(&header, &payload, Operation::Scatter) {
454            Ok(value) => received = Some(value),
455            Err(error) => local_failure = Some(error),
456        }
457    }
458    if let Some(error) = converge(&comm, local_failure) {
459        return Err(error);
460    }
461
462    if rank == root {
463        Ok(local_value.unwrap_or_else(|| mpi_call!(comm.abort(74))))
464    } else {
465        Ok(received.unwrap_or_else(|| mpi_call!(comm.abort(74))))
466    }
467}
468
469/// Collects one value from every rank in rank order on the root.
470///
471/// Every rank supplies one owned value. Only the root receives `Some(values)`;
472/// all other ranks receive `None`.
473pub fn gather<C, T>(world: &C, root: i32, value: T) -> Result<Option<Vec<T>>, PlacementError>
474where
475    C: Communicator,
476    T: Serialize + DeserializeOwned,
477{
478    let comm = mpi_call!(world.duplicate());
479    let rank = mpi_call!(comm.rank());
480    let size = mpi_call!(comm.size());
481    preflight(&comm, Operation::Gather, root, true)?;
482
483    if size == 1 {
484        return Ok(Some(vec![value]));
485    }
486
487    let (encoded, encode_failure) = if rank == root {
488        (None, None)
489    } else {
490        match encode(&value) {
491            Ok(bytes) => (Some(bytes), None),
492            Err(error) => (None, Some(error)),
493        }
494    };
495    if let Some(error) = converge(&comm, encode_failure) {
496        return Err(error);
497    }
498
499    let mut gathered = if rank == root {
500        let mut values = (0..size).map(|_| None).collect::<Vec<Option<T>>>();
501        values[root as usize] = Some(value);
502        Some(values)
503    } else {
504        None
505    };
506    let mut local_failure = None;
507    if rank == root {
508        for source in 0..size {
509            if source == root {
510                continue;
511            }
512            let (header, payload) = receive_frame(&comm, source);
513            match decode_received(&header, &payload, Operation::Gather) {
514                Ok(value) => {
515                    gathered
516                        .as_mut()
517                        .unwrap_or_else(|| mpi_call!(comm.abort(74)))[source as usize] =
518                        Some(value);
519                }
520                Err(error) => record_failure(&mut local_failure, error),
521            }
522        }
523    } else {
524        send_frame(
525            &comm,
526            root,
527            Operation::Gather,
528            encoded
529                .as_deref()
530                .unwrap_or_else(|| mpi_call!(comm.abort(74))),
531        );
532    }
533    if let Some(error) = converge(&comm, local_failure) {
534        return Err(error);
535    }
536
537    if rank == root {
538        let values = gathered.unwrap_or_else(|| mpi_call!(comm.abort(74)));
539        let mut output = Vec::with_capacity(size as usize);
540        for value in values {
541            output.push(value.unwrap_or_else(|| mpi_call!(comm.abort(74))));
542        }
543        Ok(Some(output))
544    } else {
545        Ok(None)
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn error_accessors_and_bound_are_stable() {
555        let error = PlacementError::new(
556            PlacementErrorKind::Wire,
557            Some(3),
558            "é".repeat(MAX_ERROR_MESSAGE_BYTES),
559        );
560        assert_eq!(error.kind(), PlacementErrorKind::Wire);
561        assert_eq!(error.rank(), Some(3));
562        assert!(error.message().len() <= MAX_ERROR_MESSAGE_BYTES);
563        assert!(error.message().is_char_boundary(error.message().len()));
564        assert_eq!(error.to_string(), error.message());
565    }
566
567    #[test]
568    fn operation_kinds_have_checked_none_status_headers() {
569        for operation in [Operation::Broadcast, Operation::Scatter, Operation::Gather] {
570            let header = Header::new(
571                operation.message_kind(),
572                MessageStatus::None,
573                FRAME_ID,
574                FRAME_ITEMS,
575                0,
576            )
577            .unwrap();
578            assert_eq!(header.kind(), operation.message_kind());
579            assert_eq!(Header::decode(&header.encode()).unwrap(), header);
580        }
581    }
582
583    #[test]
584    fn malformed_announced_length_is_rejected_after_payload_is_available() {
585        let header = Header::new(
586            MessageKind::Broadcast,
587            MessageStatus::None,
588            FRAME_ID,
589            FRAME_ITEMS,
590            8,
591        )
592        .unwrap();
593        let error =
594            decode_received::<i32>(&header.encode(), &[0_u8; 4], Operation::Broadcast).unwrap_err();
595        assert_eq!(error.kind, PlacementErrorKind::Protocol);
596        assert_eq!(
597            error.message,
598            "placement payload length does not match its header"
599        );
600    }
601
602    #[test]
603    fn scatter_preencode_takes_root_shard_without_cloning() {
604        let mut shards = vec![Some(String::from("root")), Some(String::from("remote"))];
605        let (local, encoded, failure) = preencode_scatter(&mut shards, 0, 2);
606        assert_eq!(local.as_deref(), Some("root"));
607        assert!(shards[0].is_none());
608        assert!(shards[1].is_some());
609        assert!(encoded[1].is_some());
610        assert!(failure.is_none());
611    }
612
613    #[test]
614    fn operation_discriminators_are_distinct() {
615        assert_ne!(Operation::Broadcast.code(), Operation::Scatter.code());
616        assert_ne!(Operation::Scatter.code(), Operation::Gather.code());
617        assert_eq!(Operation::Gather.message_kind().code(), 7);
618    }
619}