Skip to main content

hyperactor_mesh/
casting.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Casting utilities for actor meshes.
10
11use std::collections::BTreeSet;
12
13use hyperactor::ActorRef;
14use hyperactor::RemoteEndpoint as _;
15use hyperactor::RemoteHandles;
16use hyperactor::RemoteMessage;
17use hyperactor::actor::Referable;
18use hyperactor::config::ENABLE_DEST_ACTOR_REORDERING_BUFFER;
19use hyperactor::context;
20use hyperactor::mailbox;
21use hyperactor::mailbox::MailboxSenderError;
22use hyperactor::mailbox::MessageEnvelope;
23use hyperactor::mailbox::Undeliverable;
24use hyperactor_config::Flattrs;
25use hyperactor_config::attrs::declare_attrs;
26use ndslice::Selection;
27use ndslice::Shape;
28use ndslice::ShapeError;
29use ndslice::SliceError;
30use ndslice::reshape::Limit;
31use ndslice::reshape::ReshapeError;
32use ndslice::reshape::ReshapeSliceExt;
33use ndslice::reshape::reshape_selection;
34use ndslice::selection;
35use ndslice::selection::EvalOpts;
36use ndslice::selection::ReifySlice;
37use ndslice::selection::normal;
38
39use crate::CommActor;
40use crate::comm::ENABLE_NATIVE_V1_CASTING;
41use crate::comm::multicast::CAST_ORIGINATING_SENDER;
42use crate::comm::multicast::CastMessage;
43use crate::comm::multicast::CastMessageEnvelope;
44use crate::comm::multicast::Uslice;
45use crate::config::MAX_CAST_DIMENSION_SIZE;
46use crate::mesh_id::ActorMeshId;
47use crate::metrics;
48
49/// Returns true if native V1 casting is enabled. Panics if V1 casting
50/// is on but the required dest actor reordering buffer is not.
51pub(crate) fn v1_casting_enabled() -> bool {
52    let enabled = hyperactor_config::global::get(ENABLE_NATIVE_V1_CASTING);
53    if enabled {
54        assert!(
55            hyperactor_config::global::get(ENABLE_DEST_ACTOR_REORDERING_BUFFER),
56            "native V1 casting requires ENABLE_DEST_ACTOR_REORDERING_BUFFER to be enabled",
57        );
58    }
59    enabled
60}
61
62declare_attrs! {
63    /// Which mesh this message was cast to. Used for undeliverable message
64    /// handling, where the CastMessageEnvelope is serialized, and its content
65    /// cannot be inspected.
66    pub attr CAST_ACTOR_MESH_ID: ActorMeshId;
67}
68
69/// An undeliverable might have its sender address set as the comm actor instead
70/// of the original sender. Update it based on the headers present in the message
71/// so it matches the sender.
72pub fn update_undeliverable_envelope_for_casting(
73    mut envelope: Undeliverable<MessageEnvelope>,
74) -> Undeliverable<MessageEnvelope> {
75    let Some(message) = envelope.as_message_mut() else {
76        return envelope;
77    };
78    let old_actor = message.sender().clone();
79    if let Some(actor_id) = message.headers().get(CAST_ORIGINATING_SENDER) {
80        tracing::debug!(
81            actor_id = %old_actor,
82            "remapped comm-actor id to id from CAST_ORIGINATING_SENDER {}", actor_id
83        );
84        message.update_sender(actor_id);
85    }
86    // Else do nothing, it wasn't from a comm actor.
87    envelope
88}
89
90/// Common implementation for `ActorMesh`s and `ActorMeshRef`s to cast
91/// an `M`-typed message.
92///
93/// `caller_headers` are caller-supplied envelope headers (e.g.
94/// operation-context keys) merged into the inner envelope headers so
95/// receivers see them on `cx.headers()`.
96#[allow(clippy::result_large_err)] // TODO: Consider reducing the size of `CastError`.
97#[tracing::instrument(level = "debug", skip_all)]
98pub(crate) fn actor_mesh_cast<A, M>(
99    cx: &impl context::Actor,
100    actor_mesh_id: ActorMeshId,
101    comm_actor_ref: &ActorRef<CommActor>,
102    selection_of_root: Selection,
103    root_mesh_shape: &Shape,
104    cast_mesh_shape: &Shape,
105    message: M,
106    caller_headers: &Flattrs,
107) -> Result<(), CastError>
108where
109    A: Referable + RemoteHandles<M>,
110    M: RemoteMessage,
111{
112    let _ = metrics::ACTOR_MESH_CAST_DURATION.start(hyperactor::kv_pairs!(
113        "message_type" => M::typename(),
114        "message_variant" => message.arm().unwrap_or_default(),
115    ));
116
117    // Caller-known headers ride first; cast-info (timestamp,
118    // message type, mesh id) is stamped afterward and wins on
119    // collision because those keys are owned by this layer.
120    let mut headers = caller_headers.clone();
121    mailbox::headers::set_send_timestamp(&mut headers);
122    mailbox::headers::set_rust_message_type::<M>(&mut headers);
123    headers.set(CAST_ACTOR_MESH_ID, actor_mesh_id.clone());
124    let message = CastMessageEnvelope::new::<A, M>(
125        actor_mesh_id.clone(),
126        cx.mailbox().actor_addr().clone(),
127        cast_mesh_shape.clone(),
128        headers,
129        message,
130    )?;
131
132    // Mesh's shape might have large extents on some dimensions. Those
133    // dimensions would cause large fanout in our comm actor
134    // implementation. To avoid that, we reshape it by increasing
135    // dimensionality and limiting the extent of each dimension. Note
136    // the reshape is only visible to the internal algorithm. The
137    // shape that user sees maintains intact.
138    //
139    // For example, a typical shape is [hosts=1024, gpus=8]. By using
140    // limit 8, it becomes [8, 8, 8, 2, 8] during casting. In other
141    // words, it adds 3 extra layers to the comm actor tree, while
142    // keeping the fanout in each layer per dimension at 8 or smaller.
143    //
144    // An important note here is that max dimension size != max fanout.
145    // Rank 0 must send a message to all ranks at index 0 for every dimension.
146    // If our reshaped shape is [8, 8, 8, 2, 8], rank 0 must send
147    // 7 + 7 + 7 + 1 + 7 = 21 messages.
148
149    let slice_of_root = root_mesh_shape.slice();
150
151    let max_cast_dimension_size = hyperactor_config::global::get(MAX_CAST_DIMENSION_SIZE);
152
153    let slice_of_cast = slice_of_root.reshape_with_limit(Limit::from(max_cast_dimension_size));
154
155    let selection_of_cast =
156        reshape_selection(selection_of_root, root_mesh_shape.slice(), &slice_of_cast)?;
157
158    let cast_message = CastMessage {
159        dest: Uslice {
160            slice: slice_of_cast,
161            selection: selection_of_cast,
162        },
163        message,
164    };
165
166    // TEMPORARY: remove with v0 support. Same ownership rule as
167    // the inner envelope: caller-known headers ride first, cast-info
168    // wins on collision.
169    let mut headers = caller_headers.clone();
170    headers.set(CAST_ACTOR_MESH_ID, actor_mesh_id);
171
172    comm_actor_ref
173        .port()
174        .post_with_headers(cx, headers, cast_message);
175
176    Ok(())
177}
178
179#[allow(clippy::result_large_err)] // TODO: Consider reducing the size of `CastError`.
180pub(crate) fn cast_to_sliced_mesh<A, M>(
181    cx: &impl context::Actor,
182    actor_mesh_id: ActorMeshId,
183    comm_actor_ref: &ActorRef<CommActor>,
184    sel_of_sliced: &Selection,
185    message: M,
186    sliced_shape: &Shape,
187    root_mesh_shape: &Shape,
188    caller_headers: &Flattrs,
189) -> Result<(), CastError>
190where
191    A: Referable + RemoteHandles<M>,
192    M: RemoteMessage,
193{
194    let root_slice = root_mesh_shape.slice();
195
196    // Casting to `*`?
197    let sel_of_root = if selection::normalize(sel_of_sliced) == normal::NormalizedSelection::True {
198        // Reify this view into base.
199        root_slice.reify_slice(sliced_shape.slice())?
200    } else {
201        // No, fall back on `of_ranks`.
202        let ranks = sel_of_sliced
203            .eval(&EvalOpts::strict(), sliced_shape.slice())?
204            .collect::<BTreeSet<_>>();
205        Selection::of_ranks(root_slice, &ranks)?
206    };
207
208    // Cast.
209    actor_mesh_cast::<A, M>(
210        cx,
211        actor_mesh_id,
212        comm_actor_ref,
213        sel_of_root,
214        root_mesh_shape,
215        sliced_shape,
216        message,
217        caller_headers,
218    )
219}
220
221/// The type of error of casting operations.
222#[derive(Debug, thiserror::Error)]
223pub enum CastError {
224    #[error("invalid selection {0}: {1}")]
225    InvalidSelection(Selection, ShapeError),
226
227    #[error("send on rank {0}: {1}")]
228    MailboxSenderError(usize, MailboxSenderError),
229
230    #[error("unsupported selection: {0}")]
231    SelectionNotSupported(String),
232
233    #[error(transparent)]
234    RootMailboxSenderError(#[from] MailboxSenderError),
235
236    #[error(transparent)]
237    ShapeError(#[from] ShapeError),
238
239    #[error(transparent)]
240    SliceError(#[from] SliceError),
241
242    #[error(transparent)]
243    SerializationEncodeError(#[from] bincode::error::EncodeError),
244
245    #[error(transparent)]
246    SerializationDecodeError(#[from] bincode::error::DecodeError),
247
248    #[error(transparent)]
249    Other(#[from] anyhow::Error),
250
251    #[error(transparent)]
252    ReshapeError(#[from] ReshapeError),
253}