Skip to main content

monarch_tensor_worker/
stream.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
9use std::cell::OnceCell;
10use std::collections::HashMap;
11use std::collections::HashSet;
12use std::collections::hash_map::Entry;
13use std::future::Future;
14use std::sync::Arc;
15use std::sync::OnceLock;
16use std::time::Duration;
17
18use anyhow::Context as _;
19use anyhow::Result;
20use anyhow::anyhow;
21use anyhow::bail;
22use anyhow::ensure;
23use async_trait::async_trait;
24use hyperactor as reference;
25use hyperactor::Actor;
26use hyperactor::Context;
27use hyperactor::Endpoint as _;
28use hyperactor::HandleClient;
29use hyperactor::Handler;
30use hyperactor::Instance;
31use hyperactor::PortHandle;
32use hyperactor::actor::ActorHandle;
33use hyperactor::handle;
34use hyperactor::id::Label;
35use hyperactor::mailbox::OncePortHandle;
36use hyperactor::mailbox::PortReceiver;
37use hyperactor::proc::Proc;
38use hyperactor::runtime_identity::RuntimeKind;
39use hyperactor::runtime_identity::tag_current_thread;
40use monarch_gil::GilSite;
41use monarch_gil::monarch_with_gil_blocking;
42use monarch_hyperactor::actor::PythonMessage;
43use monarch_hyperactor::actor::PythonMessageKind;
44use monarch_hyperactor::local_state_broker::BrokerId;
45use monarch_hyperactor::local_state_broker::LocalState;
46use monarch_hyperactor::local_state_broker::LocalStateBrokerMessage;
47use monarch_hyperactor::pickle::pickle;
48use monarch_messages::controller::ControllerMessageClient;
49use monarch_messages::controller::Seq;
50use monarch_messages::controller::WorkerError;
51use monarch_messages::worker::ActorCallParams;
52use monarch_messages::worker::ActorMethodParams;
53use monarch_messages::worker::ArgsKwargs;
54use monarch_messages::worker::CallFunctionError;
55use monarch_messages::worker::CallFunctionParams;
56use monarch_messages::worker::SeqError;
57use monarch_messages::worker::StreamRef;
58use monarch_types::PyTree;
59use monarch_types::SerializablePyErr;
60use monarch_types::TryIntoPyObjectUnsafe;
61use pyo3::prelude::*;
62use tokio::runtime::Handle;
63use tokio::sync::Mutex;
64use tokio::task::JoinHandle;
65use torch_sys_cuda::cuda::Event;
66use torch_sys_cuda::cuda::Stream;
67use torch_sys2::CloneUnsafe;
68use torch_sys2::CudaDevice;
69use torch_sys2::TensorCell;
70use torch_sys2::deep_clone;
71use torch_sys2::factory_empty;
72use torch_sys2::factory_zeros;
73use tracing_subscriber::fmt::Subscriber;
74use typeuri::Named;
75
76use crate::ControllerActor;
77use crate::DeviceMesh;
78use crate::Factory;
79use crate::Reduction;
80use crate::Ref;
81use crate::ResolvableFunction;
82use crate::StreamCreationMode;
83use crate::WireValue;
84use crate::comm::CommMessage;
85use crate::comm::CommMessageClient;
86use crate::comm::NcclCommActor;
87
88pub type TensorCellResult = Result<TensorCell, Arc<SeqError>>;
89
90// These thread locals are accessed by the python runtime for debugging sessions.
91thread_local! {
92    pub static CONTROLLER_ACTOR_REF: OnceCell<reference::ActorRef<ControllerActor>> = const { OnceCell::new() };
93    pub static PROC: OnceCell<Proc> = const { OnceCell::new() };
94    pub static ROOT_ACTOR_ID: OnceCell<reference::ActorId> = const { OnceCell::new() };
95}
96
97fn pickle_python_result(
98    py: Python<'_>,
99    result: Bound<'_, PyAny>,
100    worker_rank: usize,
101) -> Result<PythonMessage, anyhow::Error> {
102    let mut state = pickle(py, result.unbind(), false, false)
103        .map_err(|pyerr| anyhow::Error::from(SerializablePyErr::from(py, &pyerr)))?;
104    let inner = state
105        .take_inner()
106        .map_err(|pyerr| anyhow::Error::from(SerializablePyErr::from(py, &pyerr)))?;
107    Ok(PythonMessage::new_from_buf(
108        PythonMessageKind::Result {
109            rank: Some(worker_rank),
110        },
111        inner.take_buffer(),
112    ))
113}
114
115#[derive(Debug)]
116struct Recording {
117    messages: Vec<StreamMessage>,
118}
119
120impl Recording {
121    fn new() -> Self {
122        Self {
123            messages: Vec::new(),
124        }
125    }
126}
127
128#[derive(Debug, PartialEq)]
129enum RecordingState {
130    Defining {
131        recording: Ref,
132        // Set of borrow ids used to track proper borrow usage inside
133        // a recording.
134        defined_borrows: HashSet<u64>,
135    },
136    Running,
137}
138
139/// Messages handled by the stream. Generally these are stream-local versions of
140/// [`crate::WorkerMessage`].
141#[derive(Handler, HandleClient, Debug, Named)]
142pub enum StreamMessage {
143    CallFunction(
144        CallFunctionParams,
145        HashMap<Ref, DeviceMesh>,
146        HashMap<Ref, (DeviceMesh, Vec<String>, Arc<ActorHandle<NcclCommActor>>)>,
147    ),
148
149    BorrowCreate {
150        /// Id for the borrow.
151        borrow: u64,
152        /// Tensor to borrow.
153        tensor: Ref,
154        /// Port for sending the first use CUDA event + borrowed tensor to
155        /// the borrower.
156        first_use_sender: PortHandle<(Option<Event>, TensorCellResult)>,
157    },
158
159    BorrowFirstUse {
160        /// Id for the borrow.
161        borrow: u64,
162        /// Ref for storing the borrowed tensor.
163        result: Ref,
164        /// Port for receiving the first use CUDA event + borrowed tensor from
165        /// the provider stream.
166        first_use_receiver: Arc<Mutex<PortReceiver<(Option<Event>, TensorCellResult)>>>,
167    },
168
169    BorrowLastUse {
170        /// Id for the borrow.
171        borrow: u64,
172        /// Ref for the borrowed tensor.
173        result: Ref,
174        /// Port for sending the last use CUDA event and borrowed tensor.
175        last_use_sender: PortHandle<(Option<Event>, TensorCellResult)>,
176    },
177
178    BorrowDrop {
179        borrow: u64,
180        /// Port for receiving the last use CUDA event and borrowed tensor.
181        last_use_receiver: Arc<Mutex<PortReceiver<(Option<Event>, TensorCellResult)>>>,
182    },
183
184    DeleteRefs(Vec<Ref>),
185
186    RequestStatus(#[reply] OncePortHandle<()>),
187
188    InitComm(ActorHandle<NcclCommActor>),
189
190    Reduce {
191        comm: Arc<ActorHandle<NcclCommActor>>,
192        dim_size: i64,
193        result: Ref,
194        local_tensor: Ref,
195        factory: Factory,
196        reduction: Reduction,
197        scatter: bool,
198        in_place: bool,
199        out: Option<Ref>,
200    },
201
202    SendTensor {
203        result: Ref,
204        from_rank: Option<usize>,
205        to_rank: Option<usize>,
206        tensor: Ref,
207        factory: Factory,
208        comm: Option<Arc<ActorHandle<NcclCommActor>>>,
209    },
210
211    SendValue {
212        seq: Seq,
213        worker_actor_id: reference::ActorAddr,
214        mutates: Vec<Ref>,
215        function: Option<ResolvableFunction>,
216        args_kwargs: ArgsKwargs,
217        device_meshes: HashMap<Ref, DeviceMesh>,
218    },
219
220    DefineRecording {
221        recording: Ref,
222    },
223
224    FinalizeRecording {
225        recording: Ref,
226    },
227
228    CallRecording {
229        seq: Seq,
230        recording: Ref,
231        results: Vec<Ref>,
232        actuals: Vec<Ref>,
233    },
234
235    RecordingFormal {
236        result: Ref,
237        argument_index: usize,
238    },
239
240    RecordingResult {
241        result: Ref,
242        output_index: usize,
243    },
244
245    SetRefUnitTestsOnly(Ref, WireValue),
246
247    SetTensorRefUnitTestsOnly(Ref, TensorCellResult),
248
249    GetRefUnitTestsOnly(
250        Ref, // value
251        #[reply] OncePortHandle<Option<Result<WireValue, String>>>,
252    ),
253
254    GetTensorRefUnitTestsOnly(Ref, #[reply] OncePortHandle<Option<TensorCellResult>>),
255
256    SendResultOfActorCall(ActorCallParams),
257    CallActorMethod(ActorMethodParams),
258}
259
260impl StreamMessage {
261    fn clone_for_recording(&self) -> Self {
262        match self {
263            StreamMessage::RecordingFormal {
264                result,
265                argument_index,
266            } => StreamMessage::RecordingFormal {
267                result: *result,
268                argument_index: *argument_index,
269            },
270            StreamMessage::RecordingResult {
271                result,
272                output_index,
273            } => StreamMessage::RecordingResult {
274                result: *result,
275                output_index: *output_index,
276            },
277            StreamMessage::DeleteRefs(refs) => StreamMessage::DeleteRefs(refs.clone()),
278            StreamMessage::CallFunction(params, device_meshes, remote_process_groups) => {
279                StreamMessage::CallFunction(
280                    params.clone(),
281                    device_meshes.clone(),
282                    remote_process_groups.clone(),
283                )
284            }
285            StreamMessage::BorrowCreate {
286                borrow,
287                tensor,
288                first_use_sender,
289            } => StreamMessage::BorrowCreate {
290                borrow: *borrow,
291                tensor: *tensor,
292                first_use_sender: first_use_sender.clone(),
293            },
294            StreamMessage::BorrowFirstUse {
295                borrow,
296                result,
297                first_use_receiver,
298            } => StreamMessage::BorrowFirstUse {
299                borrow: *borrow,
300                result: *result,
301                first_use_receiver: first_use_receiver.clone(),
302            },
303            StreamMessage::BorrowLastUse {
304                borrow,
305                result,
306                last_use_sender,
307            } => StreamMessage::BorrowLastUse {
308                borrow: *borrow,
309                result: *result,
310                last_use_sender: last_use_sender.clone(),
311            },
312            StreamMessage::BorrowDrop {
313                borrow,
314                last_use_receiver,
315            } => StreamMessage::BorrowDrop {
316                borrow: *borrow,
317                last_use_receiver: last_use_receiver.clone(),
318            },
319            StreamMessage::Reduce {
320                comm,
321                dim_size,
322                result,
323                local_tensor,
324                factory,
325                reduction,
326                scatter,
327                in_place,
328                out,
329            } => StreamMessage::Reduce {
330                comm: comm.clone(),
331                dim_size: *dim_size,
332                result: *result,
333                local_tensor: *local_tensor,
334                factory: factory.clone(),
335                reduction: reduction.clone(),
336                scatter: *scatter,
337                in_place: *in_place,
338                out: *out,
339            },
340            StreamMessage::SendTensor {
341                result,
342                from_rank,
343                to_rank,
344                tensor,
345                factory,
346                comm,
347            } => StreamMessage::SendTensor {
348                result: *result,
349                from_rank: *from_rank,
350                to_rank: *to_rank,
351                tensor: *tensor,
352                factory: factory.clone(),
353                comm: comm.clone(),
354            },
355            other => panic!(
356                "StreamMessage variant not supported in recording: {:?}",
357                other
358            ),
359        }
360    }
361
362    // Get the set of refs that this message defines.
363    fn get_defined_refs(&self) -> HashSet<Ref> {
364        match self {
365            StreamMessage::RecordingFormal { result, .. } => HashSet::from([*result]),
366            StreamMessage::CallFunction(params, ..) => {
367                params.results.iter().filter_map(|&ref_| ref_).collect()
368            }
369            StreamMessage::BorrowFirstUse { result, .. } => HashSet::from([*result]),
370            StreamMessage::Reduce { result, .. } => HashSet::from([*result]),
371            StreamMessage::SendTensor {
372                result, from_rank, ..
373            } => {
374                if from_rank.is_some() {
375                    HashSet::from([*result])
376                } else {
377                    HashSet::new()
378                }
379            }
380            // TODO(slurye): Add SendValue eventually.
381            _ => HashSet::new(),
382        }
383    }
384
385    // Get the set of refs that this message mutates.
386    fn get_mutated_refs(&self) -> HashSet<Ref> {
387        match self {
388            StreamMessage::CallFunction(params, ..) => HashSet::from_iter(params.mutates.clone()),
389            StreamMessage::Reduce {
390                out,
391                in_place,
392                local_tensor,
393                ..
394            } => {
395                if *in_place {
396                    HashSet::from([*local_tensor])
397                } else if let Some(out) = out {
398                    HashSet::from([*out])
399                } else {
400                    HashSet::new()
401                }
402            }
403            // TODO(slurye): Add SendValue eventually.
404            _ => HashSet::new(),
405        }
406    }
407}
408
409/// A stream represents a linear sequence of execution. Operations on different
410/// streams can execute concurrently.
411///
412/// For CUDA operators, streams will invoke the corresponding stream management
413/// APIs to perform synchronization.
414///
415/// For CPU operators, streams will just execute synchronously on their own OS
416/// thread.
417#[derive(Debug)]
418pub struct StreamActor {
419    _world_size: usize,
420    rank: usize,
421    /// Mapping of refs in the controller environment to TensorIndex in this
422    /// stream's local environment.
423    // TODO(agallagher): Use `ValueError` as the error type.
424    env: HashMap<Ref, Result<Py<PyAny>, Arc<SeqError>>>,
425    /// How to create the stream.
426    creation_mode: StreamCreationMode,
427    /// CUDA stream that this actor will enqueue operations on. None if "device"
428    /// is not a CUDA device.
429    /// NOTE: We lazily create the stream, so that we do it from the dedicated
430    /// Stream OS thread as, otherwise, we see deadlocks when done from
431    /// unexpected threads.
432    cuda_stream: OnceLock<Option<Stream>>,
433    /// Device this stream should be scheduled on.
434    device: Option<CudaDevice>,
435    /// Communicator for this stream. Optional as we lazily initialize it.
436    comm: Option<ActorHandle<NcclCommActor>>,
437    /// Actor ref of the controller that created this stream.
438    controller_actor: reference::ActorRef<ControllerActor>,
439    remote_process_groups: HashMap<Ref, Py<PyAny>>,
440    recordings: HashMap<Ref, Recording>,
441    active_recording: Option<RecordingState>,
442    respond_with_python_message: bool,
443    last_seq_error: Option<Arc<SeqError>>,
444}
445
446/// Parameters for creating a [`Stream`].
447#[derive(Debug, Clone)]
448pub struct StreamParams {
449    pub world_size: usize,
450    pub rank: usize,
451    /// Controls how the underlying CUDA stream is created.
452    pub creation_mode: StreamCreationMode,
453    /// Id of this stream in the worker actor's stream table.
454    pub id: StreamRef,
455    /// Device this stream should be scheduled on. If none, don't do stream
456    /// synchronization.
457    pub device: Option<CudaDevice>,
458    /// Actor ref of the controller that created this stream.
459    pub controller_actor: reference::ActorRef<ControllerActor>,
460    pub respond_with_python_message: bool,
461}
462
463impl StreamActor {
464    pub fn new(
465        StreamParams {
466            world_size,
467            rank,
468            id: _,
469            device,
470            controller_actor,
471            creation_mode,
472            respond_with_python_message,
473        }: StreamParams,
474    ) -> Self {
475        Self {
476            _world_size: world_size,
477            rank,
478            env: HashMap::new(),
479            creation_mode,
480            cuda_stream: OnceLock::new(),
481            device,
482            comm: None,
483            controller_actor,
484            remote_process_groups: HashMap::new(),
485            recordings: HashMap::new(),
486            active_recording: None,
487            respond_with_python_message,
488            last_seq_error: None,
489        }
490    }
491}
492
493#[async_trait]
494impl Actor for StreamActor {
495    async fn init(&mut self, cx: &Instance<Self>) -> Result<()> {
496        // These thread locals are exposed via python functions, so we need to set them in the
497        // same thread that python will run in. That means we need to initialize them here in
498        // StreamActor::init instead of in StreamActor::new.
499        CONTROLLER_ACTOR_REF.with(
500            |controller_actor_ref: &OnceCell<reference::ActorRef<ControllerActor>>| {
501                controller_actor_ref.set(self.controller_actor.clone()).ok()
502            },
503        );
504        PROC.with(|proc| proc.set(cx.proc().clone()).ok());
505        ROOT_ACTOR_ID.with(|root_actor_id: &OnceCell<reference::ActorId>| {
506            let root_label = cx
507                .self_addr()
508                .label()
509                .cloned()
510                .unwrap_or_else(|| Label::new("stream").unwrap());
511            root_actor_id
512                .set(reference::ActorId::singleton(
513                    root_label,
514                    cx.self_addr().proc_addr().id().clone(),
515                ))
516                .ok()
517        });
518        // Set the current stream for this actor thread.
519        if let Some(stream) = self.cuda_stream()? {
520            Stream::set_current_stream(stream)?;
521        }
522        Ok(())
523    }
524
525    /// Specialize spawn_server_task for StreamActor, because we want to run the stream on a
526    /// dedicated OS thread. This is because:
527    ///   - Streams do expensive blocking CPU operations (like calling CPU kernels).
528    ///   - Torch/CUDA make use of thread-local state, so moving tasks across
529    ///     threads is problematic.
530    fn spawn_server_task<F>(future: F) -> JoinHandle<F::Output>
531    where
532        F: Future + Send + 'static,
533        F::Output: Send + 'static,
534    {
535        let (join_tx, join_rx) = tokio::sync::oneshot::channel();
536        // It is important that we spawn a standalone thread for the work here,
537        // as opposed to using `spawn_blocking` to spawn a tokio-managed thread.
538        // This is because the worker stream may call uninterruptible FFI code
539        // that can deadlock (CUDA, NCCL).
540        // If we use a tokio-managed blocking thread, then runtime teardown will
541        // try to wait for tasks on that thread to reach an await point, and
542        // hang forever.
543        let builder = std::thread::Builder::new().name("worker-stream".to_string());
544        let _thread_handle = builder.spawn(move || {
545            // Data-plane worker. The stream actor loop runs on THIS thread (via
546            // `rt.block_on` below, which drives its future on the calling thread,
547            // not a runtime worker), so tag this thread directly. The
548            // `on_thread_start` below additionally tags the runtime's own
549            // worker/blocking threads. Either way the Torch/CUDA GIL use here
550            // reads `DataPlane("stream")`, not the control plane. See
551            // `hyperactor::runtime_identity` (RI-6).
552            tag_current_thread(RuntimeKind::DataPlane("stream"));
553            // Spawn a new thread with a single-threaded tokio runtime to run the
554            // actor loop.  We avoid the current-threaded runtime, so that we can
555            // use `block_in_place` for nested async-to-sync-to-async flows.
556            let rt = tokio::runtime::Builder::new_multi_thread()
557                .worker_threads(1)
558                .on_thread_start(|| tag_current_thread(RuntimeKind::DataPlane("stream")))
559                .enable_all()
560                .build()
561                .unwrap();
562            // Raw `Python::attach` (not `monarch_with_gil_blocking`): this is the
563            // stream actor-loop body, which attaches only to immediately `py.detach`
564            // and run the loop, so a wrapper's reentrancy guard would span the whole
565            // detached loop. Runs on the `DataPlane("stream")` thread, off the control plane.
566            #[allow(clippy::disallowed_methods)]
567            let result = rt.block_on(async {
568                tokio::task::block_in_place(|| {
569                    // Allow e.g. destructing py objects on this thread, which
570                    // can happen at shutdown when the a stream actors env map
571                    // for rvalues is dropped (e.g. P1673311499).
572                    // https://github.com/PyO3/pyo3/discussions/3499
573                    Python::attach(|py| {
574                        py.detach(|| {
575                            let result = Handle::current().block_on(future);
576                            if join_tx.send(result).is_err() {
577                                panic!("could not send join result")
578                            }
579                        })
580                    })
581                })
582            });
583            rt.shutdown_timeout(Duration::from_weeks(1));
584            result
585        });
586
587        // In order to bridge the synchronous join handle with the async world,
588        // smuggle the result through a channel.
589        tokio::spawn(async move { join_rx.await.unwrap() })
590    }
591}
592
593/// The arguments we accept as inputs to Python function calls.
594#[derive(Debug)]
595enum PyArg {
596    Object(Py<PyAny>),
597}
598
599/// Serialize into a `Py<PyAny>`.
600impl<'py> TryIntoPyObjectUnsafe<'py, PyAny> for &PyArg {
601    unsafe fn try_to_object_unsafe(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
602        match self {
603            PyArg::Object(obj) => Ok(obj.clone_ref(py).into_bound(py)),
604        }
605    }
606}
607
608impl StreamActor {
609    fn tensor_to_pyobject(tensor_cell: TensorCell) -> Py<PyAny> {
610        monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
611            // SAFETY: Cloning a tensor was unsafe because we were tracking their references like
612            // Rust objects (single mutable reference or many immutable references). We are
613            // removing this functionality in upcoming patches, so we use the unsafe version here
614            // until that happens.
615            let tensor = unsafe {
616                // Get the owned tensor by calling clone_unsafe on the reference
617                tensor_cell.get_unchecked().clone_unsafe()
618            };
619            tensor.into_pyobject(py).unwrap().unbind()
620        })
621    }
622
623    /// Extract a TensorCell from a Py<PyAny>.
624    /// SAFETY: Uses new to create the TensorCell. Caller must ensure the Py<PyAny>
625    /// contains a valid tensor.
626    fn pyobject_to_tensor(py: Python<'_>, pyobj: &Py<PyAny>) -> PyResult<TensorCell> {
627        use torch_sys2::Tensor;
628        let tensor = pyobj.bind(py).extract::<Tensor>()?;
629        // Create a new TensorCell from the extracted tensor
630        Ok(TensorCell::new(tensor))
631    }
632
633    fn cuda_stream(&self) -> PyResult<Option<&Stream>> {
634        if self.cuda_stream.get().is_none() {
635            let stream = match self.device {
636                Some(device) => Some(match self.creation_mode {
637                    StreamCreationMode::UseDefaultStream => {
638                        Stream::get_current_stream_on_device(device)?
639                    }
640                    StreamCreationMode::CreateNewStream => Stream::new_with_device(device)?,
641                }),
642                None => None,
643            };
644            // Another thread may have won the initialization race; keep whichever value is stored.
645            let _ = self.cuda_stream.set(stream);
646        }
647        Ok(self
648            .cuda_stream
649            .get()
650            .expect("stream initialized above")
651            .as_ref())
652    }
653
654    fn ref_to_pyobject(&self, ref_: &Ref) -> Result<Py<PyAny>, CallFunctionError> {
655        let pyobject = self
656            .env
657            .get(ref_)
658            .ok_or_else(|| CallFunctionError::RefNotFound(*ref_))?;
659        match pyobject {
660            Ok(val) => Ok(val.clone()),
661            Err(err) => Err(CallFunctionError::DependentError(err.clone())),
662        }
663    }
664
665    async fn report_seq_error(
666        &mut self,
667        cx: &Context<'_, Self>,
668        seq: Seq,
669        error: CallFunctionError,
670    ) -> Result<Arc<SeqError>, anyhow::Error> {
671        match error {
672            CallFunctionError::DependentError(root) => Ok(root),
673            CallFunctionError::Error(e) => {
674                if self.active_recording.is_none() {
675                    let worker_error = WorkerError {
676                        backtrace: format!("{e}"),
677                        worker_actor_id: cx.self_addr().clone(),
678                    };
679                    tracing::info!("Propagating remote function error to client: {worker_error}");
680                    self.controller_actor
681                        .remote_function_failed(cx, seq, worker_error)
682                        .await?
683                }
684                let err = Arc::new(SeqError { seq, error: e });
685                self.last_seq_error = Some(err.clone());
686                Ok(err)
687            }
688        }
689    }
690
691    async fn try_define<F>(
692        &mut self,
693        cx: &Context<'_, Self>,
694        seq: Seq,
695        result_refs: Vec<Option<Ref>>,
696        mutates: &Vec<Ref>,
697        f: F,
698    ) -> Result<()>
699    where
700        F: AsyncFnOnce(&mut Self) -> Result<Vec<Py<PyAny>>, CallFunctionError>,
701    {
702        let actual_results = f(self).await;
703        // Check if the expected number of returns is correct, otherwise convert
704        // into an error.
705        let op_results = actual_results.and_then(|actual_results| {
706            if result_refs.len() == actual_results.len() {
707                Ok(actual_results
708                    .into_iter()
709                    .zip(result_refs.iter())
710                    .filter_map(|(result, ref_)| ref_.map(|ref_| (ref_, result)))
711                    .collect::<Vec<(Ref, Py<PyAny>)>>())
712            } else {
713                Err(CallFunctionError::UnexpectedNumberOfReturns(
714                    result_refs.len(),
715                    actual_results.len(),
716                ))
717            }
718        });
719
720        // Propagate the results (either the actual values or an error) to the
721        // right entries in the global env mapping.
722        match op_results {
723            Ok(op_results) => {
724                for (ref_, pyobject) in op_results.into_iter() {
725                    let prev = self.env.insert(ref_, Ok(pyobject));
726                    assert!(prev.is_none(), "Duplicate write to reference: {:?}", ref_);
727                }
728            }
729            Err(err) => {
730                let err = self.report_seq_error(cx, seq, err).await?;
731                for ref_ in result_refs {
732                    match ref_ {
733                        Some(ref_) => {
734                            let prev = self.env.insert(ref_, Err(err.clone()));
735                            assert!(prev.is_none(), "Duplicate write to reference: {:?}", ref_);
736                        }
737                        None => {}
738                    }
739                }
740                for ref_ in mutates {
741                    self.env.insert(*ref_, Err(err.clone()));
742                }
743            }
744        }
745        Ok(())
746    }
747
748    fn call_python_fn<'py>(
749        &mut self,
750        py: Python<'py>,
751        _cx: &Context<Self>,
752        function: Option<ResolvableFunction>,
753        args_kwargs: ArgsKwargs,
754        _mutates: &[Ref],
755        device_meshes: HashMap<Ref, DeviceMesh>,
756        remote_process_groups: HashMap<
757            Ref,
758            (DeviceMesh, Vec<String>, Arc<ActorHandle<NcclCommActor>>),
759        >,
760    ) -> Result<Bound<'py, PyAny>, CallFunctionError> {
761        let (args_tuple, kwargs_dict) = args_kwargs
762            .to_python(py)
763            .map_err(|e| CallFunctionError::Error(e.into()))?;
764        let function = function
765            .map(|function| {
766                function.resolve(py).map_err(|e| {
767                    CallFunctionError::InvalidRemoteFunction(format!(
768                        "failed to resolve function {}: {}",
769                        function,
770                        SerializablePyErr::from(py, &e)
771                    ))
772                })
773            })
774            .transpose()?;
775
776        let remote_process_groups = remote_process_groups
777            .into_iter()
778            .map(|(gref, (_mesh, _dims, _comm))| {
779                let group = match self.remote_process_groups.entry(gref) {
780                    Entry::Occupied(ent) => ent.get().clone_ref(py),
781                    Entry::Vacant(_ent) => {
782                        panic!("no longer implemented");
783                    }
784                };
785                PyResult::Ok((gref, group))
786            })
787            .collect::<Result<HashMap<_, _>, _>>()
788            .map_err(SerializablePyErr::from_fn(py))?;
789
790        let resolve = |val: Bound<'py, PyAny>| {
791            val.extract::<PyTree<Py<PyAny>>>()
792                .map_err(SerializablePyErr::from_fn(py))?
793                .try_into_map(|obj| {
794                    Ok(if let Ok(ref_) = Ref::from_py_object(obj.bind(py)) {
795                        if let Some(mesh) = device_meshes.get(&ref_) {
796                            PyArg::Object(
797                                Py::new(py, mesh.clone())
798                                    .map_err(SerializablePyErr::from_fn(py))?
799                                    .into(),
800                            )
801                        } else if let Some(pg) = remote_process_groups.get(&ref_) {
802                            PyArg::Object(pg.clone_ref(py))
803                        } else {
804                            let pyobj = self.ref_to_pyobject(&ref_)?;
805                            PyArg::Object(pyobj)
806                        }
807                    } else {
808                        PyArg::Object(obj)
809                    })
810                })
811        };
812
813        // Resolve args and kwargs
814        let py_args: Vec<PyTree<PyArg>> = args_tuple
815            .iter()
816            .map(&resolve)
817            .collect::<Result<_, CallFunctionError>>()?;
818
819        let py_kwargs: HashMap<String, PyTree<PyArg>> = kwargs_dict
820            .iter()
821            .map(|(k, v)| {
822                let key = k
823                    .extract::<String>()
824                    .map_err(SerializablePyErr::from_fn(py))?;
825                let value = resolve(v)?;
826                Ok((key, value))
827            })
828            .collect::<Result<_, CallFunctionError>>()?;
829
830        // Call function.
831        // Use custom subscriber to route Worker messages to stdout.
832        let scoped_subscriber = Subscriber::builder().with_writer(std::io::stdout).finish();
833        let result: Bound<'_, PyAny> =
834            tracing::subscriber::with_default(scoped_subscriber, || {
835                // TODO(agallagher): The args/kwargs conversion traits generate
836                // the appropriate types here, but they get casted to `PyAny`.
837                // It'd be nice to make `TryToPy<PyAny>Unsafe` take a template
838                // arg for the converted py object to avoid this downcast.
839                // SAFETY: Tensor operations were unsafe because we were tracking their references
840                // like Rust objects (single mutable reference or many immutable references). We are
841                // removing this functionality in upcoming patches, so we use the unsafe version here
842                // until that happens.
843                let args = unsafe { py_args.try_to_object_unsafe(py) }
844                    .map_err(SerializablePyErr::from_fn(py))?;
845                // SAFETY: Same as above - reference tracking functionality is being removed.
846                let kwargs = &unsafe { py_kwargs.try_to_object_unsafe(py) }
847                    .map_err(SerializablePyErr::from_fn(py))?;
848
849                if let Some(function) = function {
850                    function
851                        .call(args, Some(kwargs))
852                        .map_err(SerializablePyErr::from_fn(py))
853                } else {
854                    Ok(args.get_item(0).unwrap())
855                }
856            })?;
857        Ok(result)
858    }
859
860    fn call_python_fn_pytree(
861        &mut self,
862        cx: &hyperactor::Context<Self>,
863        function: ResolvableFunction,
864        args_kwargs: ArgsKwargs,
865        mutates: &[Ref],
866        device_meshes: HashMap<Ref, DeviceMesh>,
867        remote_process_groups: HashMap<
868            Ref,
869            (DeviceMesh, Vec<String>, Arc<ActorHandle<NcclCommActor>>),
870        >,
871    ) -> Result<PyTree<Py<PyAny>>, CallFunctionError> {
872        monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
873            let result = self.call_python_fn(
874                py,
875                cx,
876                Some(function),
877                args_kwargs,
878                mutates,
879                device_meshes,
880                remote_process_groups,
881            )?;
882            Ok(result
883                .extract::<PyTree<Py<PyAny>>>()
884                .map_err(SerializablePyErr::from_fn(py))?)
885        })
886    }
887    /// Retrieve `ref_` or create a fake value with the provided factory if it
888    /// is an error. We use this for collective calls, where even if there was
889    /// an upstream failure, we still have participate in the collective to
890    /// avoid deadlocking the other ranks. It's okay to just put a nonsense
891    /// value here of the correct shape; the controller will have been notified
892    /// of the upstream failure and will know to ignore everything dependent on
893    /// it.
894    fn get_or_fake_on_err(&self, ref_: Ref, factory: &Factory) -> Result<TensorCell> {
895        let pyobject = self
896            .env
897            .get(&ref_)
898            .ok_or_else(|| anyhow!("tensor not found in stream: {ref_:#?}"))?;
899
900        match pyobject {
901            Ok(val) => monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
902                Self::pyobject_to_tensor(py, val)
903                    .map_err(|pyerr| anyhow::Error::from(SerializablePyErr::from(py, &pyerr)))
904            }),
905            Err(_) => {
906                let t = factory_zeros(&factory.size, factory.dtype, factory.layout, factory.device);
907                Ok(TensorCell::new(t))
908            }
909        }
910    }
911
912    fn get_defining_recording(&mut self) -> Option<(&mut Recording, &mut HashSet<u64>)> {
913        self.active_recording
914            .as_mut()
915            .and_then(|state| match state {
916                RecordingState::Defining {
917                    recording,
918                    defined_borrows,
919                } => {
920                    match self.recordings.get_mut(recording) {
921                        Some(recording) => Some((recording, defined_borrows)),
922                        // Panic, because this would be a logic error in the program.
923                        None => panic!("recording not found: {:?}", recording),
924                    }
925                }
926                RecordingState::Running => None,
927            })
928    }
929
930    fn get_first_error(&self, refs: &[Ref]) -> Result<Option<Arc<SeqError>>> {
931        for ref_ in refs {
932            let rvalue_or_err = self
933                .env
934                .get(ref_)
935                .ok_or_else(|| anyhow!("tensor not found in stream: {ref_:#?}"))?;
936            if let Err(err) = rvalue_or_err {
937                return Ok(Some(err.clone()));
938            }
939        }
940        Ok(None)
941    }
942    async fn send_value_python_message(
943        &mut self,
944        cx: &hyperactor::Context<'_, Self>,
945        seq: Seq,
946        mutates: Vec<Ref>,
947        function: Option<ResolvableFunction>,
948        args_kwargs: ArgsKwargs,
949        device_meshes: HashMap<Ref, DeviceMesh>,
950    ) -> Result<()> {
951        let rank = self.rank;
952        self.try_define(cx, seq, vec![], &vec![], async |self_| {
953            let python_message = monarch_with_gil_blocking(
954                GilSite::StreamCompute,
955                |py| -> Result<PythonMessage, CallFunctionError> {
956                    let python_result = tokio::task::block_in_place(|| {
957                        self_.call_python_fn(
958                            py,
959                            cx,
960                            function,
961                            args_kwargs,
962                            &mutates,
963                            device_meshes,
964                            HashMap::new(),
965                        )
966                    })?;
967                    pickle_python_result(py, python_result, rank).map_err(CallFunctionError::Error)
968                },
969            )?;
970            let ser = wirevalue::Any::serialize(&python_message).unwrap();
971            self_
972                .controller_actor
973                .fetch_result(cx, seq, Ok(ser))
974                .await?;
975            Ok(vec![])
976        })
977        .await
978    }
979    fn define_ref(&mut self, dest: Ref, src: Ref) -> Result<(), anyhow::Error> {
980        let rvalue = self
981            .env
982            .get(&src)
983            .ok_or_else(|| CallFunctionError::RefNotFound(src))?;
984        self.env.insert(
985            dest,
986            monarch_with_gil_blocking(GilSite::StreamCompute, |_py| rvalue.clone()),
987        );
988        Ok(())
989    }
990    async fn call_actor(
991        &mut self,
992        cx: &Context<'_, Self>,
993        params: ActorCallParams,
994    ) -> Result<Py<PyAny>, CallFunctionError> {
995        let local_state: Result<Vec<Py<PyAny>>> =
996            monarch_with_gil_blocking(GilSite::StreamCompute, |_py| {
997                params
998                    .local_state
999                    .into_iter()
1000                    .map(|elem| {
1001                        let pyobj = self.ref_to_pyobject(&elem)?;
1002                        Ok(pyobj.into_any())
1003                    })
1004                    .collect()
1005            });
1006
1007        let (send, recv) = cx.open_once_port();
1008        let state = LocalState {
1009            response_port: send,
1010            state: local_state?,
1011        };
1012        let x: u64 = params.seq.into();
1013        let message = LocalStateBrokerMessage::Set(x as usize, state);
1014
1015        let broker = BrokerId::new(params.broker_id).resolve(cx).await;
1016        broker.post(cx, message);
1017        let result = recv
1018            .recv()
1019            .await
1020            .map_err(|e| CallFunctionError::Error(e.into()))?;
1021
1022        result.map_err(|pyerr| anyhow::Error::msg(pyerr.to_string()).into())
1023    }
1024}
1025
1026#[async_trait]
1027#[handle(StreamMessage)]
1028impl StreamMessageHandler for StreamActor {
1029    async fn call_function(
1030        &mut self,
1031        cx: &Context<Self>,
1032        params: CallFunctionParams,
1033        device_meshes: HashMap<Ref, DeviceMesh>,
1034        remote_process_groups: HashMap<
1035            Ref,
1036            (DeviceMesh, Vec<String>, Arc<ActorHandle<NcclCommActor>>),
1037        >,
1038    ) -> Result<()> {
1039        if let Some((recording, _)) = self.get_defining_recording() {
1040            recording.messages.push(StreamMessage::CallFunction(
1041                params,
1042                device_meshes,
1043                remote_process_groups,
1044            ));
1045            return Ok(());
1046        }
1047
1048        params.function.panic_if_requested();
1049        self.try_define(
1050            cx,
1051            params.seq,
1052            params.results,
1053            &params.mutates,
1054            async |self| {
1055                tokio::task::block_in_place(|| {
1056                    self.call_python_fn_pytree(
1057                        cx,
1058                        params.function,
1059                        params.args_kwargs,
1060                        &params.mutates,
1061                        device_meshes,
1062                        remote_process_groups,
1063                    )
1064                    .map(|results| results.into_leaves())
1065                })
1066            },
1067        )
1068        .await?;
1069        Ok(())
1070    }
1071
1072    async fn borrow_create(
1073        &mut self,
1074        cx: &Context<Self>,
1075        borrow: u64,
1076        tensor: Ref,
1077        first_use_sender: PortHandle<(Option<Event>, TensorCellResult)>,
1078    ) -> Result<()> {
1079        if let Some((recording, defined_borrows)) = self.get_defining_recording() {
1080            recording.messages.push(StreamMessage::BorrowCreate {
1081                borrow,
1082                tensor,
1083                first_use_sender,
1084            });
1085            ensure!(
1086                defined_borrows.insert(borrow),
1087                "duplicate borrow create in recording"
1088            );
1089            return Ok(());
1090        }
1091
1092        let pyobj_result = self
1093            .env
1094            .get(&tensor)
1095            .ok_or_else(|| anyhow!("invalid reference for borrow_create: {:#?}", tensor))?;
1096
1097        let result = match pyobj_result {
1098            Ok(pyobj) => monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1099                Ok(Self::pyobject_to_tensor(py, pyobj).unwrap())
1100            }),
1101            Err(e) => Err(e.clone()),
1102        };
1103
1104        let event = self
1105            .cuda_stream()?
1106            .map(|stream| stream.record_event(None))
1107            .transpose()?;
1108        first_use_sender.post(cx, (event, result));
1109        Ok(())
1110    }
1111
1112    async fn borrow_first_use(
1113        &mut self,
1114        _cx: &Context<Self>,
1115        borrow: u64,
1116        result: Ref,
1117        first_use_receiver: Arc<Mutex<PortReceiver<(Option<Event>, TensorCellResult)>>>,
1118    ) -> Result<()> {
1119        if let Some((recording, _)) = self.get_defining_recording() {
1120            recording.messages.push(StreamMessage::BorrowFirstUse {
1121                borrow,
1122                result,
1123                first_use_receiver: first_use_receiver.clone(),
1124            });
1125            return Ok(());
1126        }
1127
1128        let (first_use_event, cell) =
1129            first_use_receiver
1130                .lock()
1131                .await
1132                .recv()
1133                .await
1134                .map_err(|err| {
1135                    anyhow!(
1136                        "failed receiving first use event for borrow {:?}: {:?}",
1137                        borrow,
1138                        err
1139                    )
1140                })?;
1141
1142        if let Some(stream) = self.cuda_stream()? {
1143            stream.wait_event(
1144                &mut first_use_event.expect("sent borrow to CUDA stream, expected a CUDA event"),
1145            )?;
1146        }
1147        match cell {
1148            Ok(cell) => {
1149                let pyobj = Self::tensor_to_pyobject(cell);
1150                self.env.insert(result, Ok(pyobj));
1151            }
1152            Err(err) => {
1153                self.env.insert(result, Err(err.clone()));
1154            }
1155        }
1156        Ok(())
1157    }
1158
1159    async fn borrow_last_use(
1160        &mut self,
1161        cx: &Context<Self>,
1162        borrow: u64,
1163        result: Ref,
1164        last_use_sender: PortHandle<(Option<Event>, TensorCellResult)>,
1165    ) -> Result<()> {
1166        if let Some((recording, _)) = self.get_defining_recording() {
1167            recording.messages.push(StreamMessage::BorrowLastUse {
1168                borrow,
1169                result,
1170                last_use_sender,
1171            });
1172            return Ok(());
1173        }
1174
1175        let event = self
1176            .cuda_stream()?
1177            .map(|stream| stream.record_event(None))
1178            .transpose()?;
1179        let pyobj_or_err = self.env.remove(&result).ok_or(anyhow!(
1180            "Invalid reference for borrow_last_use: {result:#?}"
1181        ))?;
1182        let tensor = match pyobj_or_err {
1183            Ok(pyobj) => Ok(monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1184                Self::pyobject_to_tensor(py, &pyobj).unwrap()
1185            })),
1186            Err(e) => Err(e),
1187        };
1188
1189        last_use_sender.post(cx, (event, tensor));
1190        Ok(())
1191    }
1192
1193    async fn borrow_drop(
1194        &mut self,
1195        _cx: &Context<Self>,
1196        borrow: u64,
1197        last_use_receiver: Arc<Mutex<PortReceiver<(Option<Event>, TensorCellResult)>>>,
1198    ) -> Result<()> {
1199        if let Some((recording, defined_borrows)) = self.get_defining_recording() {
1200            recording.messages.push(StreamMessage::BorrowDrop {
1201                borrow,
1202                last_use_receiver: last_use_receiver.clone(),
1203            });
1204            ensure!(
1205                defined_borrows.remove(&borrow),
1206                "borrow drop for borrow not defined in recording"
1207            );
1208            return Ok(());
1209        }
1210
1211        // The borrowed cell isn't used directly, but we still want to receive it here
1212        // so that the underlying tensor isn't dropped until after we synchronize the
1213        // CUDA streams.
1214        let (last_use_event, _cell) =
1215            last_use_receiver.lock().await.recv().await.map_err(|err| {
1216                anyhow!(
1217                    "failed receiving last use event for borrow {:?}: {:?}",
1218                    borrow,
1219                    err
1220                )
1221            })?;
1222
1223        if let Some(stream) = self.cuda_stream()? {
1224            stream.wait_event(
1225                &mut last_use_event.expect("sent borrow to CUDA stream, expected a CUDA event"),
1226            )?;
1227        }
1228        // let the cell drop.
1229        Ok(())
1230    }
1231
1232    async fn delete_refs(&mut self, _cx: &Context<Self>, refs: Vec<Ref>) -> Result<()> {
1233        if let Some((recording, _)) = self.get_defining_recording() {
1234            recording.messages.push(StreamMessage::DeleteRefs(refs));
1235            return Ok(());
1236        }
1237
1238        for ref_ in refs.iter() {
1239            self.env.remove(ref_);
1240        }
1241        Ok(())
1242    }
1243
1244    async fn request_status(&mut self, _cx: &Context<Self>) -> Result<()> {
1245        if self.get_defining_recording().is_some() {
1246            bail!("request_status not allowed in recording");
1247        }
1248
1249        Ok(())
1250    }
1251
1252    async fn init_comm(
1253        &mut self,
1254        _cx: &Context<Self>,
1255        comm: ActorHandle<NcclCommActor>,
1256    ) -> Result<()> {
1257        if self.get_defining_recording().is_some() {
1258            bail!("init_comm not allowed in recording");
1259        }
1260
1261        self.comm = Some(comm);
1262        Ok(())
1263    }
1264
1265    async fn reduce(
1266        &mut self,
1267        cx: &Context<Self>,
1268        comm: Arc<ActorHandle<NcclCommActor>>,
1269        dim_size: i64,
1270        result: Ref,
1271        local_tensor: Ref,
1272        factory: Factory,
1273        reduction: Reduction,
1274        scatter: bool,
1275        in_place: bool,
1276        out: Option<Ref>,
1277    ) -> Result<()> {
1278        if let Some((recording, _)) = self.get_defining_recording() {
1279            recording.messages.push(StreamMessage::Reduce {
1280                comm,
1281                dim_size,
1282                result,
1283                local_tensor,
1284                factory,
1285                reduction,
1286                scatter,
1287                in_place,
1288                out,
1289            });
1290            return Ok(());
1291        }
1292
1293        let stream = self
1294            .cuda_stream()?
1295            .expect("reductions not yet supported for non-CUDA workers")
1296            .clone();
1297        let input_cell = self.get_or_fake_on_err(local_tensor, &factory)?;
1298        let out_cell = out
1299            .map(|out| self.get_or_fake_on_err(out, &factory))
1300            .transpose()?;
1301        let output_cell = match reduction {
1302            Reduction::Stack => {
1303                if scatter {
1304                    let output_cell = if in_place {
1305                        input_cell.clone()
1306                    } else {
1307                        out_cell.unwrap_or({
1308                            let borrow = input_cell.try_borrow().map_err(|e| anyhow!("{e:?}"))?;
1309                            let cloned = deep_clone(&borrow);
1310                            TensorCell::new(cloned)
1311                        })
1312                    };
1313                    comm.all_to_all_single(cx, output_cell.clone(), input_cell, stream)
1314                        .await?;
1315                    output_cell
1316                } else {
1317                    ensure!(
1318                        !in_place,
1319                        "in-place, non-scatter not supported for stack reduce"
1320                    );
1321
1322                    let output_cell = out_cell.unwrap_or({
1323                        // In Python, this would be [dim_size, *factory.sizes]
1324                        let sizes = [&[dim_size][..], &factory.size[..]].concat();
1325                        let output =
1326                            factory_empty(&sizes, factory.dtype, factory.layout, factory.device);
1327                        TensorCell::new(output)
1328                    });
1329
1330                    comm.all_gather_into_tensor(cx, output_cell.clone(), input_cell, stream)
1331                        .await?;
1332                    output_cell
1333                }
1334            }
1335            Reduction::ReduceOp(op) => {
1336                if scatter {
1337                    ensure!(!in_place, "in-place, scatter not supported for reduce");
1338
1339                    let output_cell = out_cell.unwrap_or({
1340                        let output = factory_empty(
1341                            &factory.size[1..],
1342                            factory.dtype,
1343                            factory.layout,
1344                            factory.device,
1345                        );
1346                        TensorCell::new(output)
1347                    });
1348                    comm.reduce_scatter_tensor(cx, output_cell.clone(), input_cell, op, stream)
1349                        .await?;
1350                    output_cell
1351                } else {
1352                    let output_cell = if in_place {
1353                        input_cell.clone()
1354                    } else {
1355                        out_cell.map_or(
1356                            {
1357                                let borrow =
1358                                    input_cell.try_borrow().map_err(|e| anyhow!("{e:?}"))?;
1359                                let cloned = deep_clone(&borrow);
1360                                Ok(TensorCell::new(cloned))
1361                            },
1362                            |out_cell| -> Result<_, anyhow::Error> {
1363                                let mut out_borrow =
1364                                    out_cell.try_borrow_mut().map_err(|e| anyhow!("{e:?}"))?;
1365                                let in_borrow =
1366                                    input_cell.try_borrow().map_err(|e| anyhow!("{e:?}"))?;
1367                                out_borrow.copy_(&in_borrow);
1368                                drop(out_borrow);
1369                                Ok(out_cell)
1370                            },
1371                        )?
1372                    };
1373
1374                    comm.all_reduce(cx, output_cell.clone(), op, stream).await?;
1375                    output_cell
1376                }
1377            }
1378        };
1379
1380        let pyobj = Self::tensor_to_pyobject(output_cell);
1381        self.env.insert(result, Ok(pyobj));
1382        Ok(())
1383    }
1384
1385    async fn send_tensor(
1386        &mut self,
1387        cx: &Context<Self>,
1388        result: Ref,
1389        from_rank: Option<usize>,
1390        to_rank: Option<usize>,
1391        tensor: Ref,
1392        factory: Factory,
1393        comm: Option<Arc<ActorHandle<NcclCommActor>>>,
1394    ) -> Result<()> {
1395        if let Some((recording, _)) = self.get_defining_recording() {
1396            recording.messages.push(StreamMessage::SendTensor {
1397                result,
1398                from_rank,
1399                to_rank,
1400                tensor,
1401                factory,
1402                comm,
1403            });
1404            return Ok(());
1405        }
1406
1407        if to_rank.is_none() && from_rank.is_none() {
1408            bail!("tried to send tensor without a to/from rank");
1409        }
1410
1411        // Value is local, so we do not have to actually send it.
1412        if from_rank == to_rank {
1413            let input_cell: &std::result::Result<Py<PyAny>, Arc<SeqError>> = self
1414                .env
1415                .get(&tensor)
1416                .ok_or_else(|| anyhow!("tensor not found in stream: {tensor:#?}"))?;
1417            let output_cell: Result<Py<PyAny>, Arc<SeqError>> = match input_cell {
1418                Ok(pyobj) => {
1419                    monarch_with_gil_blocking(
1420                        GilSite::StreamCompute,
1421                        |py| -> Result<Py<PyAny>, Arc<SeqError>> {
1422                            let input_tensor = Self::pyobject_to_tensor(py, pyobj).unwrap();
1423                            // We create a defensive copy here to prevent mutations on
1424                            // the input tensor from affecting output tensor.
1425                            // Should we copy if input ref == output ref?
1426                            // Should we support copy-on-write to avoid unnecessary copy?
1427                            let borrow = input_tensor.try_borrow().unwrap();
1428                            let cloned = deep_clone(&borrow);
1429                            let cloned_cell = TensorCell::new(cloned);
1430                            Ok(Self::tensor_to_pyobject(cloned_cell))
1431                        },
1432                    )
1433                }
1434                Err(err) => Err(err.clone()),
1435            };
1436            self.env.insert(result, output_cell);
1437            return Ok(());
1438        }
1439
1440        let comm = comm.context("send_tensor requires backend comm")?;
1441
1442        let mut messages = Vec::new();
1443
1444        if let Some(to_rank) = to_rank {
1445            let input_cell = self.get_or_fake_on_err(tensor, &factory)?;
1446            messages.push(CommMessage::Send(
1447                input_cell,
1448                to_rank.try_into().unwrap(),
1449                self.cuda_stream()?
1450                    .expect("tried to send_tensor on non-cuda stream")
1451                    .clone(),
1452                cx.open_once_port().0,
1453            ));
1454        }
1455
1456        if let Some(from_rank) = from_rank {
1457            let output_cell = TensorCell::new(factory_empty(
1458                &factory.size,
1459                factory.dtype,
1460                factory.layout,
1461                factory.device,
1462            ));
1463            messages.push(CommMessage::Recv(
1464                output_cell.clone(),
1465                from_rank.try_into().unwrap(),
1466                self.cuda_stream()?
1467                    .expect("tried to send_tensor on non-cuda stream")
1468                    .clone(),
1469                cx.open_once_port().0,
1470            ));
1471            let pyobj = Self::tensor_to_pyobject(output_cell);
1472            self.env.insert(result, Ok(pyobj));
1473        }
1474
1475        comm.group(
1476            cx,
1477            messages,
1478            self.cuda_stream()?
1479                .expect("tried to send_tensor on non-cuda stream")
1480                .clone(),
1481        )
1482        .await?;
1483        Ok(())
1484    }
1485
1486    async fn send_value(
1487        &mut self,
1488        cx: &Context<Self>,
1489        seq: Seq,
1490        worker_actor_id: reference::ActorAddr,
1491        mutates: Vec<Ref>,
1492        function: Option<ResolvableFunction>,
1493        args_kwargs: ArgsKwargs,
1494        device_meshes: HashMap<Ref, DeviceMesh>,
1495    ) -> Result<()> {
1496        if self.respond_with_python_message {
1497            return self
1498                .send_value_python_message(cx, seq, mutates, function, args_kwargs, device_meshes)
1499                .await;
1500        }
1501
1502        let result = if let Some(function) = function {
1503            // If a function was provided, use that to resolve the value.
1504            tokio::task::block_in_place(|| {
1505                self.call_python_fn_pytree(
1506                    cx,
1507                    function,
1508                    args_kwargs,
1509                    &mutates,
1510                    device_meshes,
1511                    HashMap::new(),
1512                )
1513            })
1514        } else {
1515            // If there's no function provided, there should be exactly one arg
1516            // and no kwargs.
1517            monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1518                let (args, kwargs) = args_kwargs
1519                    .to_python(py)
1520                    .map_err(|e| CallFunctionError::Error(e.into()))?;
1521                match (args.len(), kwargs.len()) {
1522                    (1, 0) => {
1523                        let arg = args.get_item(0).map_err(SerializablePyErr::from_fn(py))?;
1524                        arg.extract::<PyTree<Py<PyAny>>>()
1525                            .map_err(SerializablePyErr::from_fn(py))?
1526                            .try_into_map(|obj| {
1527                                let bound_obj = obj.bind(py);
1528                                if let Ok(ref_) = Ref::from_py_object(bound_obj) {
1529                                    self.ref_to_pyobject(&ref_)
1530                                } else {
1531                                    Ok(obj)
1532                                }
1533                            })
1534                    }
1535                    _ => Err(CallFunctionError::TooManyArgsForValue(
1536                        format!("args with {} elements", args.len()),
1537                        format!("kwargs with {} elements", kwargs.len()),
1538                    )),
1539                }
1540            })
1541        };
1542
1543        let value = match result {
1544            Ok(pyobject) => Ok(pyobject),
1545            Err(err) => {
1546                let err = self.report_seq_error(cx, seq, err).await?;
1547                for ref_ in mutates {
1548                    self.env.insert(ref_, Err(err.clone()));
1549                }
1550                Err(WorkerError {
1551                    backtrace: format!("{:?}", err),
1552                    worker_actor_id,
1553                })
1554            }
1555        };
1556
1557        // Actually send the value.
1558        // NOTE: respond_with_python_message is always true, so serialization is not needed
1559        // The controller will receive the value through send_value_python_message instead
1560        let result = match value {
1561            Ok(_value) => {
1562                // This code path is never executed since respond_with_python_message is true
1563                unreachable!(
1564                    "send_value should return early when respond_with_python_message is true"
1565                )
1566            }
1567            Err(e) => Err(e),
1568        };
1569        self.controller_actor.fetch_result(cx, seq, result).await?;
1570
1571        Ok(())
1572    }
1573
1574    async fn send_result_of_actor_call(
1575        &mut self,
1576        cx: &Context<Self>,
1577        params: ActorCallParams,
1578    ) -> anyhow::Result<()> {
1579        let seq = params.seq;
1580        let mutates = params.mutates.clone();
1581        self.try_define(cx, seq, vec![], &mutates, async |self| {
1582            let value = self.call_actor(cx, params).await?;
1583            let result = monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1584                pickle_python_result(py, value.into_bound(py), self.rank)
1585            })?;
1586            let result = wirevalue::Any::serialize(&result).unwrap();
1587            self.controller_actor
1588                .fetch_result(cx, seq, Ok(result))
1589                .await?;
1590            Ok(vec![])
1591        })
1592        .await
1593    }
1594
1595    async fn call_actor_method(
1596        &mut self,
1597        cx: &Context<Self>,
1598        params: ActorMethodParams,
1599    ) -> anyhow::Result<()> {
1600        let seq = params.call.seq;
1601        let mutates = params.call.mutates.clone();
1602        self.try_define(cx, seq, params.results, &mutates, async |self| {
1603            let result = self.call_actor(cx, params.call).await?;
1604            let result = monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1605                result
1606                    .into_bound(py)
1607                    .extract::<PyTree<Py<PyAny>>>()
1608                    .map_err(SerializablePyErr::from_fn(py))
1609            })?;
1610            Ok(result.into_leaves())
1611        })
1612        .await
1613    }
1614
1615    async fn define_recording(&mut self, _cx: &Context<Self>, recording: Ref) -> Result<()> {
1616        if self.active_recording.is_some() {
1617            bail!("different recording already active");
1618        }
1619        match self.recordings.entry(recording) {
1620            Entry::Occupied(_) => bail!("recording {:?} already defined", recording),
1621            Entry::Vacant(entry) => entry.insert(Recording::new()),
1622        };
1623        self.active_recording = Some(RecordingState::Defining {
1624            recording,
1625            defined_borrows: HashSet::new(),
1626        });
1627        Ok(())
1628    }
1629
1630    async fn finalize_recording(&mut self, _cx: &Context<Self>, recording: Ref) -> Result<()> {
1631        match self.active_recording {
1632            Some(RecordingState::Defining {
1633                recording: active_recording,
1634                ref defined_borrows,
1635            }) if active_recording == recording => {
1636                ensure!(
1637                    defined_borrows.is_empty(),
1638                    "all borrows created within recording must be dropped within recording"
1639                );
1640                self.active_recording = None;
1641            }
1642            _ => bail!("cannot finalize recording that isn't active"),
1643        }
1644        Ok(())
1645    }
1646
1647    async fn recording_formal(
1648        &mut self,
1649        _cx: &Context<Self>,
1650        result: Ref,
1651        argument_index: usize,
1652    ) -> Result<()> {
1653        match self.get_defining_recording() {
1654            Some((recording, _)) => {
1655                recording.messages.push(StreamMessage::RecordingFormal {
1656                    result,
1657                    argument_index,
1658                });
1659            }
1660            None => bail!("recording_formal called outside of recording"),
1661        };
1662        Ok(())
1663    }
1664
1665    async fn recording_result(
1666        &mut self,
1667        _cx: &Context<Self>,
1668        result: Ref,
1669        output_index: usize,
1670    ) -> Result<()> {
1671        match self.get_defining_recording() {
1672            Some((recording, _)) => {
1673                recording.messages.push(StreamMessage::RecordingResult {
1674                    result,
1675                    output_index,
1676                });
1677            }
1678            None => bail!("recording_result called outside of recording"),
1679        };
1680        Ok(())
1681    }
1682
1683    async fn call_recording(
1684        &mut self,
1685        cx: &Context<Self>,
1686        seq: Seq,
1687        recording: Ref,
1688        results: Vec<Ref>,
1689        actuals: Vec<Ref>,
1690    ) -> Result<()> {
1691        if self.active_recording.is_some() {
1692            bail!("cannot call recording while another recording is active");
1693        }
1694
1695        let messages = match self.recordings.get(&recording) {
1696            Some(recording) => recording
1697                .messages
1698                .iter()
1699                .map(|message| message.clone_for_recording())
1700                .collect::<Vec<_>>(),
1701            None => bail!("recording {:?} not found", recording),
1702        };
1703
1704        self.active_recording = Some(RecordingState::Running);
1705
1706        // Global error for all messages in the recording. The first time a message
1707        // fails in the recording, we set the error. We then need to propagate this
1708        // error to all of the refs mutated by the entire recording, as well as the
1709        // result refs.
1710        let mut error: Option<Arc<SeqError>> = None;
1711        // The set of all refs defined by this recording (excluding "results"),
1712        // which we need to ensure are deleted when the recording is done executing.
1713        let mut all_defined_refs = HashSet::new();
1714        // The set of all refs mutated by this recording. If there is an error with
1715        // any message, all of these refs need to have the correct error set.
1716        let mut all_mutated_refs = HashSet::new();
1717        // Map from the result ref of a RecordingFormal message to the associated
1718        // actual ref from "actuals". We need to track this in order to properly
1719        // handle recordings that mutate refs contained in "actuals" -- every
1720        // message in the recording that interacts with the recording inputs will
1721        // interact with the formal ref rather than the actual ref.
1722        let mut formal_to_actual_refs = HashMap::new();
1723        // clear any pre-existing error messages before recording started
1724        self.last_seq_error = None;
1725        for message in messages.into_iter() {
1726            let defined_refs = message.get_defined_refs();
1727            all_defined_refs.extend(defined_refs.clone());
1728
1729            let mutated_refs_with_formals = message.get_mutated_refs();
1730            all_mutated_refs.extend(mutated_refs_with_formals.iter().filter_map(|ref_| {
1731                match formal_to_actual_refs.get(ref_) {
1732                    Some(actual_ref) => Some(*actual_ref),
1733                    None => {
1734                        if all_defined_refs.contains(ref_) {
1735                            None
1736                        } else {
1737                            Some(*ref_)
1738                        }
1739                    }
1740                }
1741            }));
1742
1743            match message {
1744                StreamMessage::RecordingFormal {
1745                    result: formal_ref,
1746                    argument_index,
1747                } => match actuals.get(argument_index) {
1748                    None => bail!("recording_formal called with too few arguments"),
1749                    Some(actual_ref) => {
1750                        formal_to_actual_refs.insert(formal_ref, *actual_ref);
1751                        self.define_ref(formal_ref, *actual_ref)?;
1752                    }
1753                },
1754                StreamMessage::RecordingResult {
1755                    result: result_ref,
1756                    output_index,
1757                } => match results.get(output_index) {
1758                    None => bail!("recording_result called with too few results"),
1759                    Some(actual_result_ref) => {
1760                        self.define_ref(*actual_result_ref, result_ref)?;
1761                    }
1762                },
1763                StreamMessage::DeleteRefs(ref refs) => {
1764                    for ref_ in refs {
1765                        all_defined_refs.remove(ref_);
1766                    }
1767                    StreamMessageHandler::handle(self, cx, message).await?;
1768                }
1769                StreamMessage::CallFunction { .. } if error.is_some() => {
1770                    // CallFunction is expensive. If the recording already failed, then
1771                    // just update the necessary refs with the error. Most of the other
1772                    // message types need to run regardless because there are other actors
1773                    // that expect the call to happen (e.g., all of the borrow messages,
1774                    // pipe send/recv, send_tensor, reduce, etc.).
1775                    let error = error.clone().unwrap();
1776                    for ref_ in defined_refs.iter().chain(mutated_refs_with_formals.iter()) {
1777                        self.env.insert(*ref_, Err(error.clone()));
1778                    }
1779                }
1780                StreamMessage::BorrowLastUse { ref result, .. } => {
1781                    all_defined_refs.remove(result);
1782                    StreamMessageHandler::handle(self, cx, message).await?;
1783                }
1784                StreamMessage::Reduce {
1785                    local_tensor,
1786                    ref out,
1787                    ..
1788                } => {
1789                    // Reduce doesn't propagate errors to the result ref, so we need
1790                    // to check for existing errors on the input tensors and set the
1791                    // recording's error if necessary.
1792                    if error.is_none() {
1793                        let inputs_to_check = [Some(local_tensor), *out]
1794                            .iter()
1795                            .filter_map(|r| *r)
1796                            .collect::<Vec<_>>();
1797                        error = self.get_first_error(inputs_to_check.as_slice())?;
1798                    }
1799                    StreamMessageHandler::handle(self, cx, message).await?;
1800                }
1801                StreamMessage::SendTensor {
1802                    ref tensor,
1803                    ref to_rank,
1804                    ..
1805                } => {
1806                    // If this rank is sending a tensor (e.g., to_rank has a value),
1807                    // we need to check for existing errors on the input tensor, because
1808                    // the error is only propagated to the result ref when this rank
1809                    // is also receiving a tensor.
1810                    if to_rank.is_some() && error.is_none() {
1811                        error = self.get_first_error(&[*tensor])?;
1812                    }
1813                    StreamMessageHandler::handle(self, cx, message).await?;
1814                }
1815                _ => {
1816                    StreamMessageHandler::handle(self, cx, message).await?;
1817                }
1818            };
1819
1820            // It's not entirely trivial to determine whether a message "failed" or not.
1821            // For example, the CallFunction message can return Ok(..) if there is an error
1822            // in the underlying function call. But in that case, we would still want to
1823            // consider the recording call as "failed". Unlike in python, where we can just
1824            // wrap everything in try-except, in rust, we keep track of the last report SeqError, which
1825            // we clear before handling each recording message. If we see it is set, the
1826            // we know the recording has faild.
1827            match (&error, self.last_seq_error.take()) {
1828                (None, Some(seq_err)) => {
1829                    // Report failure to the controller.
1830                    self.controller_actor
1831                        .remote_function_failed(
1832                            cx,
1833                            seq,
1834                            WorkerError {
1835                                backtrace: format!("recording failed: {}", seq_err),
1836                                worker_actor_id: cx.self_addr().clone(),
1837                            },
1838                        )
1839                        .await?;
1840                    error = Some(seq_err)
1841                }
1842                _ => {}
1843            }
1844            // Continue processing the remaining stream messages regardless of error.
1845            // We need to do this partially for error propagation, but also because
1846            // certain messages (like borrows and reductions) need to run regardless
1847            // in order to prevent deadlocks.
1848        }
1849
1850        // Delete the formal refs and some subset of the RecordingResult refs. The
1851        // controller should have generated DeleteRefs messages for all other refs
1852        // defined by the recording.
1853        StreamMessageHandler::handle(
1854            self,
1855            cx,
1856            StreamMessage::DeleteRefs(all_defined_refs.into_iter().collect()),
1857        )
1858        .await?;
1859
1860        // Any refs mutated by the recording and all results should have the same error
1861        // (the original error that caused the recording to fail).
1862        if error.is_some() {
1863            for ref_ in results.iter().chain(all_mutated_refs.iter()) {
1864                self.env.insert(*ref_, Err(error.clone().unwrap()));
1865            }
1866        }
1867
1868        self.active_recording = None;
1869        Ok(())
1870    }
1871
1872    async fn set_ref_unit_tests_only(
1873        &mut self,
1874        _cx: &Context<Self>,
1875        reference: Ref,
1876        value: WireValue,
1877    ) -> Result<()> {
1878        let pyobj =
1879            monarch_with_gil_blocking(GilSite::StreamCompute, |py| -> PyResult<Py<PyAny>> {
1880                Ok(value.into_pyobject(py)?.unbind())
1881            })?;
1882        self.env.insert(reference, Ok(pyobj));
1883        Ok(())
1884    }
1885
1886    async fn set_tensor_ref_unit_tests_only(
1887        &mut self,
1888        _cx: &Context<Self>,
1889        reference: Ref,
1890        tensor_result: TensorCellResult,
1891    ) -> Result<()> {
1892        match tensor_result {
1893            Ok(tensor_cell) => {
1894                let pyobj = Self::tensor_to_pyobject(tensor_cell);
1895                self.env.insert(reference, Ok(pyobj));
1896            }
1897            Err(err) => {
1898                self.env.insert(reference, Err(err));
1899            }
1900        }
1901        Ok(())
1902    }
1903
1904    async fn get_ref_unit_tests_only(
1905        &mut self,
1906        _cx: &Context<Self>,
1907        reference: Ref,
1908    ) -> Result<Option<Result<WireValue, String>>> {
1909        use pyo3::types::PyBool;
1910        use pyo3::types::PyFloat;
1911        use pyo3::types::PyInt;
1912        use pyo3::types::PyList;
1913        use pyo3::types::PyNone;
1914        use pyo3::types::PyString;
1915        /// For testing only, doesn't support Tensor or TensorList.
1916        fn pyobject_to_wire(
1917            value: Result<Py<PyAny>, Arc<SeqError>>,
1918        ) -> Result<WireValue, Arc<SeqError>> {
1919            let pyobj = value?;
1920            monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1921                let bound = pyobj.bind(py);
1922                // Check bool before int since Python's bool is a subclass of int
1923                if bound.is_instance_of::<PyBool>() {
1924                    Ok(WireValue::Bool(bound.extract::<bool>().unwrap()))
1925                } else if bound.is_instance_of::<PyInt>() {
1926                    Ok(WireValue::Int(bound.extract::<i64>().unwrap()))
1927                } else if bound.is_instance_of::<PyList>() {
1928                    if let Ok(val) = bound.extract::<Vec<i64>>() {
1929                        Ok(WireValue::IntList(val))
1930                    } else {
1931                        Ok(WireValue::String(format!(
1932                            "unsupported list type: {:?}",
1933                            bound
1934                        )))
1935                    }
1936                } else if bound.is_instance_of::<PyFloat>() {
1937                    Ok(WireValue::Double(bound.extract::<f64>().unwrap()))
1938                } else if bound.is_instance_of::<PyString>() {
1939                    Ok(WireValue::String(bound.extract::<String>().unwrap()))
1940                } else if bound.is_instance_of::<PyNone>() {
1941                    Ok(WireValue::None(()))
1942                } else {
1943                    Ok(WireValue::String(format!(
1944                        "unsupported pyobject type: {:?}",
1945                        bound
1946                    )))
1947                }
1948            })
1949        }
1950        Ok(self.env.get(&reference).map(|pyobj| {
1951            pyobject_to_wire(monarch_with_gil_blocking(GilSite::StreamCompute, |_py| {
1952                pyobj.clone()
1953            }))
1954            .map_err(|err| err.to_string())
1955        }))
1956    }
1957
1958    async fn get_tensor_ref_unit_tests_only(
1959        &mut self,
1960        _cx: &Context<Self>,
1961        reference: Ref,
1962    ) -> Result<Option<TensorCellResult>> {
1963        match self.env.get(&reference) {
1964            Some(Ok(pyobj)) => monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1965                match Self::pyobject_to_tensor(py, pyobj) {
1966                    Ok(tensor) => Ok(Some(Ok(tensor.try_cpu().unwrap()))),
1967                    Err(e) => bail!("expected tensor, got extraction error: {:?}", e),
1968                }
1969            }),
1970            Some(Err(err)) => Ok(Some(Err(err.clone()))),
1971            None => Ok(None),
1972        }
1973    }
1974}
1975
1976#[cfg(all(test, fbcode_build))]
1977mod tests {
1978    use hyperactor::actor::ActorStatus;
1979    use hyperactor::context;
1980    use hyperactor::supervision::ActorSupervisionEvent;
1981    use monarch_messages::controller::ControllerMessage;
1982    use monarch_messages::worker::StreamCreationMode;
1983    use monarch_types::PickledPyObject;
1984    use monarch_types::UniqueId;
1985    use pyo3::IntoPyObjectExt;
1986    use timed_test::async_timed_test;
1987    use tokio::sync::watch;
1988    use torch_sys_cuda::nccl::UniqueIdExt;
1989    use torch_sys2::factory_float_tensor;
1990    use torch_sys2::testing::allclose;
1991
1992    use super::*;
1993    use crate::comm::CommParams;
1994    use crate::test_util;
1995
1996    #[allow(dead_code)]
1997    fn fake_seq_error(err: anyhow::Error) -> Arc<SeqError> {
1998        Arc::new(SeqError {
1999            seq: 0.into(),
2000            error: err,
2001        })
2002    }
2003
2004    struct TestSetup {
2005        proc: Proc,
2006        stream_actor: ActorHandle<StreamActor>,
2007        client: reference::Client,
2008        // Unused, but necessary, because proc needs a supervision
2009        // port -- otherwise an actor failure will cause a crash.
2010        #[allow(dead_code)]
2011        supervision_rx: PortReceiver<ActorSupervisionEvent>,
2012        #[allow(dead_code)]
2013        controller_rx: PortReceiver<ControllerMessage>,
2014        #[allow(dead_code)]
2015        controller_actor: reference::ActorRef<ControllerActor>,
2016        next_ref: Ref,
2017    }
2018
2019    impl TestSetup {
2020        async fn new() -> Result<Self> {
2021            Self::new_with_world_size(1).await
2022        }
2023
2024        async fn new_with_world_size(world_size: usize) -> Result<Self> {
2025            test_util::test_setup()?;
2026
2027            let proc = Proc::isolated();
2028            let (_, controller_actor, controller_rx) =
2029                proc.attach_actor::<ControllerActor, ControllerMessage>("controller")?;
2030            let client = proc.client("client");
2031            let (supervision_tx, supervision_rx) = client.open_port();
2032            proc.set_supervision_coordinator(supervision_tx.bind())?;
2033            let stream_actor = proc.spawn(StreamActor::new(StreamParams {
2034                world_size,
2035                rank: 0,
2036                creation_mode: StreamCreationMode::UseDefaultStream,
2037                id: 0.into(),
2038                device: Some(CudaDevice::new(0.into())),
2039                controller_actor: controller_actor.clone(),
2040                respond_with_python_message: false,
2041            }));
2042
2043            Ok(Self {
2044                proc,
2045                stream_actor,
2046                client,
2047                supervision_rx,
2048                controller_rx,
2049                controller_actor,
2050                next_ref: 0.into(),
2051            })
2052        }
2053
2054        fn next_ref(&mut self) -> Ref {
2055            let ref_ = self.next_ref;
2056            self.next_ref = Ref {
2057                id: self.next_ref.id + 1,
2058            };
2059            ref_
2060        }
2061
2062        async fn set_tensor(&mut self, reference: Ref, data: &[f32]) -> Result<()> {
2063            let tensor = TensorCell::new(factory_float_tensor(data, "cuda".parse().unwrap()));
2064            self.stream_actor
2065                .set_tensor_ref_unit_tests_only(&self.client, reference, Ok(tensor))
2066                .await
2067        }
2068
2069        async fn allclose(&mut self, reference: Ref, data: &[f32]) -> bool {
2070            let actual = self
2071                .stream_actor
2072                .get_tensor_ref_unit_tests_only(&self.client, reference)
2073                .await
2074                .unwrap()
2075                .unwrap()
2076                .unwrap();
2077
2078            // rustfmt-ignore
2079            allclose(
2080                &factory_float_tensor(data, "cpu".parse().unwrap()),
2081                &actual.borrow(),
2082            )
2083            .unwrap()
2084        }
2085
2086        #[allow(dead_code)]
2087        async fn validate_dependent_error(&mut self, reference: Ref, error: Arc<SeqError>) {
2088            let result_error = self
2089                .stream_actor
2090                .get_tensor_ref_unit_tests_only(&self.client, reference)
2091                .await
2092                .unwrap()
2093                .unwrap()
2094                .unwrap_err();
2095
2096            assert!(Arc::ptr_eq(&result_error, &error));
2097        }
2098    }
2099
2100    async fn assert_actor_failed_with_msg(
2101        status_rx: &mut watch::Receiver<ActorStatus>,
2102        expected_msg: String,
2103    ) {
2104        status_rx
2105            .wait_for(|s| matches!(s, ActorStatus::Failed(_)))
2106            .await
2107            .unwrap();
2108        let status = status_rx.borrow().clone();
2109        if let ActorStatus::Failed(msg) = status {
2110            assert!(msg.to_string().contains(&expected_msg));
2111        } else {
2112            panic!("expected ActorStatus::Failed, got {:?}", status);
2113        }
2114    }
2115
2116    async fn assert_refs_do_not_exist(test_setup: &TestSetup, refs: &[Ref]) {
2117        for ref_ in refs {
2118            assert!(
2119                test_setup
2120                    .stream_actor
2121                    .get_tensor_ref_unit_tests_only(&test_setup.client, *ref_)
2122                    .await
2123                    .unwrap()
2124                    .is_none()
2125            );
2126        }
2127    }
2128
2129    #[allow(dead_code)]
2130    async fn fetch_result(
2131        cx: &impl context::Actor,
2132        stream_actor: ActorHandle<StreamActor>,
2133        seq: Seq,
2134        reference: Ref,
2135    ) {
2136        let ref_to_send = monarch_with_gil_blocking(GilSite::Test, |py| {
2137            PickledPyObject::pickle(&reference.into_bound_py_any(py).unwrap()).unwrap()
2138        });
2139
2140        stream_actor
2141            .send_value(
2142                cx,
2143                seq,
2144                stream_actor.actor_addr().clone(),
2145                Vec::new(),
2146                None,
2147                ArgsKwargs::from_wire_values(
2148                    vec![WireValue::PyObject(ref_to_send)],
2149                    HashMap::new(),
2150                )
2151                .unwrap(),
2152                HashMap::new(),
2153            )
2154            .await
2155            .unwrap()
2156    }
2157
2158    #[allow(dead_code)]
2159    async fn check_fetch_result_error(
2160        cx: &impl context::Actor,
2161        stream_actor: ActorHandle<StreamActor>,
2162        seq: Seq,
2163        reference: Ref,
2164        controller_rx: &mut PortReceiver<ControllerMessage>,
2165        expected_backtrace: &str,
2166    ) {
2167        fetch_result(cx, stream_actor, seq, reference).await;
2168
2169        let controller_msg = controller_rx.recv().await.unwrap();
2170        match controller_msg {
2171            ControllerMessage::FetchResult {
2172                seq: actual_seq,
2173                value: Err(err),
2174            } => {
2175                assert_eq!(actual_seq, seq);
2176                assert!(
2177                    err.backtrace.contains(expected_backtrace),
2178                    "backtrace did not contain {:?}: {:?}",
2179                    expected_backtrace,
2180                    err.backtrace
2181                );
2182            }
2183            _ => panic!("Unexpected controller message: {:?}", controller_msg),
2184        };
2185    }
2186
2187    #[allow(dead_code)]
2188    async fn check_fetch_result_value(
2189        cx: &impl context::Actor,
2190        stream_actor: ActorHandle<StreamActor>,
2191        seq: Seq,
2192        reference: Ref,
2193        controller_rx: &mut PortReceiver<ControllerMessage>,
2194    ) {
2195        fetch_result(cx, stream_actor, seq, reference).await;
2196
2197        let controller_msg = controller_rx.recv().await.unwrap();
2198        match controller_msg {
2199            ControllerMessage::FetchResult {
2200                value: Ok(_),
2201                seq: actual_seq,
2202            } => assert_eq!(seq, actual_seq),
2203            _ => panic!("Unexpected controller message: {:?}", controller_msg),
2204        };
2205    }
2206
2207    #[async_timed_test(timeout_secs = 60)]
2208    async fn test_define_recording_other_recording_active() -> Result<()> {
2209        let test_setup = TestSetup::new().await?;
2210        test_setup
2211            .stream_actor
2212            .define_recording(&test_setup.client, 0.into())
2213            .await?;
2214        test_setup
2215            .stream_actor
2216            .define_recording(&test_setup.client, 1.into())
2217            .await?;
2218        assert_actor_failed_with_msg(
2219            &mut test_setup.stream_actor.status(),
2220            "different recording already active".into(),
2221        )
2222        .await;
2223        Ok(())
2224    }
2225
2226    #[async_timed_test(timeout_secs = 60)]
2227    async fn test_define_recording_already_defined() -> Result<()> {
2228        let test_setup = TestSetup::new().await?;
2229        test_setup
2230            .stream_actor
2231            .define_recording(&test_setup.client, 0.into())
2232            .await?;
2233        test_setup
2234            .stream_actor
2235            .finalize_recording(&test_setup.client, 0.into())
2236            .await?;
2237        test_setup
2238            .stream_actor
2239            .define_recording(&test_setup.client, 0.into())
2240            .await?;
2241        assert_actor_failed_with_msg(
2242            &mut test_setup.stream_actor.status(),
2243            "already defined".into(),
2244        )
2245        .await;
2246        Ok(())
2247    }
2248
2249    #[async_timed_test(timeout_secs = 60)]
2250    async fn test_finalize_recording_other_recording_active() -> Result<()> {
2251        let test_setup = TestSetup::new().await?;
2252        test_setup
2253            .stream_actor
2254            .define_recording(&test_setup.client, 0.into())
2255            .await?;
2256        test_setup
2257            .stream_actor
2258            .finalize_recording(&test_setup.client, 1.into())
2259            .await?;
2260        assert_actor_failed_with_msg(
2261            &mut test_setup.stream_actor.status(),
2262            "cannot finalize recording that isn't active".into(),
2263        )
2264        .await;
2265        Ok(())
2266    }
2267
2268    #[async_timed_test(timeout_secs = 60)]
2269    async fn test_recording_formal_outside_recording() -> Result<()> {
2270        let test_setup = TestSetup::new().await?;
2271        test_setup
2272            .stream_actor
2273            .recording_formal(&test_setup.client, 0.into(), 0)
2274            .await?;
2275        assert_actor_failed_with_msg(
2276            &mut test_setup.stream_actor.status(),
2277            "recording_formal called outside of recording".into(),
2278        )
2279        .await;
2280        Ok(())
2281    }
2282
2283    #[async_timed_test(timeout_secs = 60)]
2284    async fn test_recording_result_outside_recording() -> Result<()> {
2285        let test_setup = TestSetup::new().await?;
2286        test_setup
2287            .stream_actor
2288            .recording_result(&test_setup.client, 0.into(), 0)
2289            .await?;
2290        assert_actor_failed_with_msg(
2291            &mut test_setup.stream_actor.status(),
2292            "recording_result called outside of recording".into(),
2293        )
2294        .await;
2295        Ok(())
2296    }
2297
2298    #[async_timed_test(timeout_secs = 60)]
2299    async fn test_call_recording_other_recording_active() -> Result<()> {
2300        let test_setup = TestSetup::new().await?;
2301        test_setup
2302            .stream_actor
2303            .define_recording(&test_setup.client, 0.into())
2304            .await?;
2305        test_setup
2306            .stream_actor
2307            .call_recording(&test_setup.client, 0.into(), 0.into(), vec![], vec![])
2308            .await?;
2309        assert_actor_failed_with_msg(
2310            &mut test_setup.stream_actor.status(),
2311            "cannot call recording while another recording is active".into(),
2312        )
2313        .await;
2314        Ok(())
2315    }
2316
2317    #[async_timed_test(timeout_secs = 60)]
2318    async fn test_call_recording_not_found() -> Result<()> {
2319        let test_setup = TestSetup::new().await?;
2320        test_setup
2321            .stream_actor
2322            .call_recording(&test_setup.client, 0.into(), 0.into(), vec![], vec![])
2323            .await?;
2324        assert_actor_failed_with_msg(&mut test_setup.stream_actor.status(), "not found".into())
2325            .await;
2326        Ok(())
2327    }
2328
2329    #[async_timed_test(timeout_secs = 60)]
2330    async fn test_recording_formal_too_few_arguments() -> Result<()> {
2331        let test_setup = TestSetup::new().await?;
2332
2333        test_setup
2334            .stream_actor
2335            .define_recording(&test_setup.client, 0.into())
2336            .await?;
2337
2338        test_setup
2339            .stream_actor
2340            .recording_formal(&test_setup.client, 1.into(), 0)
2341            .await?;
2342
2343        test_setup
2344            .stream_actor
2345            .finalize_recording(&test_setup.client, 0.into())
2346            .await?;
2347
2348        test_setup
2349            .stream_actor
2350            .call_recording(&test_setup.client, 0.into(), 0.into(), vec![], vec![])
2351            .await?;
2352
2353        assert_actor_failed_with_msg(
2354            &mut test_setup.stream_actor.status(),
2355            "recording_formal called with too few arguments".into(),
2356        )
2357        .await;
2358        Ok(())
2359    }
2360
2361    #[async_timed_test(timeout_secs = 60)]
2362    async fn test_recording_result_too_few_results() -> Result<()> {
2363        let test_setup = TestSetup::new().await?;
2364
2365        test_setup
2366            .stream_actor
2367            .define_recording(&test_setup.client, 0.into())
2368            .await?;
2369
2370        test_setup
2371            .stream_actor
2372            .recording_result(&test_setup.client, 1.into(), 0)
2373            .await?;
2374
2375        test_setup
2376            .stream_actor
2377            .finalize_recording(&test_setup.client, 0.into())
2378            .await?;
2379
2380        test_setup
2381            .stream_actor
2382            .call_recording(&test_setup.client, 0.into(), 0.into(), vec![], vec![])
2383            .await?;
2384
2385        assert_actor_failed_with_msg(
2386            &mut test_setup.stream_actor.status(),
2387            "recording_result called with too few results".into(),
2388        )
2389        .await;
2390        Ok(())
2391    }
2392
2393    #[async_timed_test(timeout_secs = 60)]
2394    async fn test_basic_call_recording() -> Result<()> {
2395        let mut test_setup = TestSetup::new().await?;
2396
2397        // Define a recording equivalent to:
2398        // def f(x, y):
2399        //   return y, x
2400        test_setup
2401            .stream_actor
2402            .define_recording(&test_setup.client, 0.into())
2403            .await?;
2404
2405        let formal0_ref = 1.into();
2406        let formal0_index = 1;
2407        test_setup
2408            .stream_actor
2409            .recording_formal(&test_setup.client, formal0_ref, formal0_index)
2410            .await?;
2411
2412        let formal1_ref = 2.into();
2413        let formal1_index = 0;
2414        test_setup
2415            .stream_actor
2416            .recording_formal(&test_setup.client, formal1_ref, formal1_index)
2417            .await?;
2418
2419        let result0_ref = formal0_ref;
2420        let result0_index = 0;
2421        test_setup
2422            .stream_actor
2423            .recording_result(&test_setup.client, result0_ref, result0_index)
2424            .await?;
2425
2426        let result1_ref = formal1_ref;
2427        let result1_index = 1;
2428        test_setup
2429            .stream_actor
2430            .recording_result(&test_setup.client, result1_ref, result1_index)
2431            .await?;
2432
2433        test_setup
2434            .stream_actor
2435            .finalize_recording(&test_setup.client, 0.into())
2436            .await?;
2437
2438        let actual0_ref = 3.into();
2439        test_setup.set_tensor(actual0_ref, &[1.0, 2.0, 3.0]).await?;
2440
2441        let actual1_ref = 4.into();
2442        test_setup.set_tensor(actual1_ref, &[4.0, 5.0]).await?;
2443
2444        // Call the recording with valid tensors for the actual inputs,
2445        // and store the results in refs 5 and 6.
2446        let actual_result0_ref = 5.into();
2447        let actual_result1_ref = 6.into();
2448        test_setup
2449            .stream_actor
2450            .call_recording(
2451                &test_setup.client,
2452                0.into(),
2453                0.into(),
2454                vec![actual_result0_ref, actual_result1_ref],
2455                vec![actual0_ref, actual1_ref],
2456            )
2457            .await?;
2458
2459        // Ensure the results are correct.
2460        assert!(test_setup.allclose(actual_result0_ref, &[4.0, 5.0]).await);
2461        assert!(
2462            test_setup
2463                .allclose(actual_result1_ref, &[1.0, 2.0, 3.0])
2464                .await
2465        );
2466
2467        // Ensure the temporary refs associated with the formals/results have
2468        // been deleted.
2469        assert_refs_do_not_exist(&test_setup, &[formal0_ref, formal1_ref]).await;
2470        Ok(())
2471    }
2472
2473    #[async_timed_test(timeout_secs = 60)]
2474    async fn test_request_status_in_recording() -> Result<()> {
2475        let test_setup = TestSetup::new().await?;
2476        test_setup
2477            .stream_actor
2478            .define_recording(&test_setup.client, 0.into())
2479            .await?;
2480        test_setup
2481            .stream_actor
2482            .request_status(&test_setup.client)
2483            .await
2484            .expect_err("request_status should have failed");
2485        assert_actor_failed_with_msg(
2486            &mut test_setup.stream_actor.status(),
2487            "request_status not allowed in recording".into(),
2488        )
2489        .await;
2490        Ok(())
2491    }
2492
2493    #[async_timed_test(timeout_secs = 60)]
2494    async fn test_init_comm_in_recording() -> Result<()> {
2495        let test_setup = TestSetup::new().await?;
2496        test_setup
2497            .stream_actor
2498            .define_recording(&test_setup.client, 0.into())
2499            .await?;
2500
2501        let dummy_comm = test_setup.proc.spawn(
2502            NcclCommActor::new(CommParams::New {
2503                device: CudaDevice::new(0.into()),
2504                unique_id: UniqueId::new_nccl()?,
2505                world_size: 1,
2506                rank: 0,
2507            })
2508            .await
2509            .unwrap(),
2510        );
2511
2512        test_setup
2513            .stream_actor
2514            .init_comm(&test_setup.client, dummy_comm)
2515            .await?;
2516        assert_actor_failed_with_msg(
2517            &mut test_setup.stream_actor.status(),
2518            "init_comm not allowed in recording".into(),
2519        )
2520        .await;
2521        Ok(())
2522    }
2523
2524    #[async_timed_test(timeout_secs = 60)]
2525    async fn test_borrow_create_duplicate_borrow() -> Result<()> {
2526        let mut test_setup = TestSetup::new().await?;
2527        test_setup
2528            .stream_actor
2529            .define_recording(&test_setup.client, 0.into())
2530            .await?;
2531
2532        let borrow_id = 1;
2533        let tensor_ref = test_setup.next_ref();
2534        let (first_use_sender, _first_use_receiver) = test_setup.client.open_port();
2535
2536        test_setup
2537            .stream_actor
2538            .borrow_create(
2539                &test_setup.client,
2540                borrow_id,
2541                tensor_ref,
2542                first_use_sender.clone(),
2543            )
2544            .await?;
2545
2546        test_setup
2547            .stream_actor
2548            .borrow_create(&test_setup.client, borrow_id, tensor_ref, first_use_sender)
2549            .await?;
2550
2551        assert_actor_failed_with_msg(
2552            &mut test_setup.stream_actor.status(),
2553            "duplicate borrow create in recording".into(),
2554        )
2555        .await;
2556
2557        Ok(())
2558    }
2559
2560    #[async_timed_test(timeout_secs = 60)]
2561    async fn test_borrow_drop_borrow_not_defined() -> Result<()> {
2562        let test_setup = TestSetup::new().await?;
2563        test_setup
2564            .stream_actor
2565            .define_recording(&test_setup.client, 0.into())
2566            .await?;
2567
2568        let borrow_id = 1;
2569        let (_last_use_sender, last_use_receiver) = test_setup.client.open_port();
2570
2571        test_setup
2572            .stream_actor
2573            .borrow_drop(
2574                &test_setup.client,
2575                borrow_id,
2576                Arc::new(Mutex::new(last_use_receiver)),
2577            )
2578            .await?;
2579
2580        assert_actor_failed_with_msg(
2581            &mut test_setup.stream_actor.status(),
2582            "borrow drop for borrow not defined in recording".into(),
2583        )
2584        .await;
2585
2586        Ok(())
2587    }
2588
2589    #[async_timed_test(timeout_secs = 60)]
2590    async fn test_borrow_not_dropped_before_finalize() -> Result<()> {
2591        let mut test_setup = TestSetup::new().await?;
2592        test_setup
2593            .stream_actor
2594            .define_recording(&test_setup.client, 0.into())
2595            .await?;
2596
2597        let borrow_id = 1;
2598        let tensor_ref = test_setup.next_ref();
2599        let (first_use_sender, _first_use_receiver) = test_setup.client.open_port();
2600
2601        test_setup
2602            .stream_actor
2603            .borrow_create(
2604                &test_setup.client,
2605                borrow_id,
2606                tensor_ref,
2607                first_use_sender.clone(),
2608            )
2609            .await?;
2610
2611        // Attempt to finalize the recording without dropping the borrow
2612        test_setup
2613            .stream_actor
2614            .finalize_recording(&test_setup.client, 0.into())
2615            .await?;
2616
2617        assert_actor_failed_with_msg(
2618            &mut test_setup.stream_actor.status(),
2619            "all borrows created within recording must be dropped within recording".into(),
2620        )
2621        .await;
2622
2623        Ok(())
2624    }
2625}