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) -> Option<&Stream> {
634        self.cuda_stream
635            .get_or_init(|| {
636                self.device.map(|device| 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            })
643            .as_ref()
644    }
645
646    fn ref_to_pyobject(&self, ref_: &Ref) -> Result<Py<PyAny>, CallFunctionError> {
647        let pyobject = self
648            .env
649            .get(ref_)
650            .ok_or_else(|| CallFunctionError::RefNotFound(*ref_))?;
651        match pyobject {
652            Ok(val) => Ok(val.clone()),
653            Err(err) => Err(CallFunctionError::DependentError(err.clone())),
654        }
655    }
656
657    async fn report_seq_error(
658        &mut self,
659        cx: &Context<'_, Self>,
660        seq: Seq,
661        error: CallFunctionError,
662    ) -> Result<Arc<SeqError>, anyhow::Error> {
663        match error {
664            CallFunctionError::DependentError(root) => Ok(root),
665            CallFunctionError::Error(e) => {
666                if self.active_recording.is_none() {
667                    let worker_error = WorkerError {
668                        backtrace: format!("{e}"),
669                        worker_actor_id: cx.self_addr().clone(),
670                    };
671                    tracing::info!("Propagating remote function error to client: {worker_error}");
672                    self.controller_actor
673                        .remote_function_failed(cx, seq, worker_error)
674                        .await?
675                }
676                let err = Arc::new(SeqError { seq, error: e });
677                self.last_seq_error = Some(err.clone());
678                Ok(err)
679            }
680        }
681    }
682
683    async fn try_define<F>(
684        &mut self,
685        cx: &Context<'_, Self>,
686        seq: Seq,
687        result_refs: Vec<Option<Ref>>,
688        mutates: &Vec<Ref>,
689        f: F,
690    ) -> Result<()>
691    where
692        F: AsyncFnOnce(&mut Self) -> Result<Vec<Py<PyAny>>, CallFunctionError>,
693    {
694        let actual_results = f(self).await;
695        // Check if the expected number of returns is correct, otherwise convert
696        // into an error.
697        let op_results = actual_results.and_then(|actual_results| {
698            if result_refs.len() == actual_results.len() {
699                Ok(actual_results
700                    .into_iter()
701                    .zip(result_refs.iter())
702                    .filter_map(|(result, ref_)| ref_.map(|ref_| (ref_, result)))
703                    .collect::<Vec<(Ref, Py<PyAny>)>>())
704            } else {
705                Err(CallFunctionError::UnexpectedNumberOfReturns(
706                    result_refs.len(),
707                    actual_results.len(),
708                ))
709            }
710        });
711
712        // Propagate the results (either the actual values or an error) to the
713        // right entries in the global env mapping.
714        match op_results {
715            Ok(op_results) => {
716                for (ref_, pyobject) in op_results.into_iter() {
717                    let prev = self.env.insert(ref_, Ok(pyobject));
718                    assert!(prev.is_none(), "Duplicate write to reference: {:?}", ref_);
719                }
720            }
721            Err(err) => {
722                let err = self.report_seq_error(cx, seq, err).await?;
723                for ref_ in result_refs {
724                    match ref_ {
725                        Some(ref_) => {
726                            let prev = self.env.insert(ref_, Err(err.clone()));
727                            assert!(prev.is_none(), "Duplicate write to reference: {:?}", ref_);
728                        }
729                        None => {}
730                    }
731                }
732                for ref_ in mutates {
733                    self.env.insert(*ref_, Err(err.clone()));
734                }
735            }
736        }
737        Ok(())
738    }
739
740    fn call_python_fn<'py>(
741        &mut self,
742        py: Python<'py>,
743        _cx: &Context<Self>,
744        function: Option<ResolvableFunction>,
745        args_kwargs: ArgsKwargs,
746        _mutates: &[Ref],
747        device_meshes: HashMap<Ref, DeviceMesh>,
748        remote_process_groups: HashMap<
749            Ref,
750            (DeviceMesh, Vec<String>, Arc<ActorHandle<NcclCommActor>>),
751        >,
752    ) -> Result<Bound<'py, PyAny>, CallFunctionError> {
753        let (args_tuple, kwargs_dict) = args_kwargs
754            .to_python(py)
755            .map_err(|e| CallFunctionError::Error(e.into()))?;
756        let function = function
757            .map(|function| {
758                function.resolve(py).map_err(|e| {
759                    CallFunctionError::InvalidRemoteFunction(format!(
760                        "failed to resolve function {}: {}",
761                        function,
762                        SerializablePyErr::from(py, &e)
763                    ))
764                })
765            })
766            .transpose()?;
767
768        let remote_process_groups = remote_process_groups
769            .into_iter()
770            .map(|(gref, (_mesh, _dims, _comm))| {
771                let group = match self.remote_process_groups.entry(gref) {
772                    Entry::Occupied(ent) => ent.get().clone_ref(py),
773                    Entry::Vacant(_ent) => {
774                        panic!("no longer implemented");
775                    }
776                };
777                PyResult::Ok((gref, group))
778            })
779            .collect::<Result<HashMap<_, _>, _>>()
780            .map_err(SerializablePyErr::from_fn(py))?;
781
782        let resolve = |val: Bound<'py, PyAny>| {
783            val.extract::<PyTree<Py<PyAny>>>()
784                .map_err(SerializablePyErr::from_fn(py))?
785                .try_into_map(|obj| {
786                    Ok(if let Ok(ref_) = Ref::from_py_object(obj.bind(py)) {
787                        if let Some(mesh) = device_meshes.get(&ref_) {
788                            PyArg::Object(
789                                Py::new(py, mesh.clone())
790                                    .map_err(SerializablePyErr::from_fn(py))?
791                                    .into(),
792                            )
793                        } else if let Some(pg) = remote_process_groups.get(&ref_) {
794                            PyArg::Object(pg.clone_ref(py))
795                        } else {
796                            let pyobj = self.ref_to_pyobject(&ref_)?;
797                            PyArg::Object(pyobj)
798                        }
799                    } else {
800                        PyArg::Object(obj)
801                    })
802                })
803        };
804
805        // Resolve args and kwargs
806        let py_args: Vec<PyTree<PyArg>> = args_tuple
807            .iter()
808            .map(&resolve)
809            .collect::<Result<_, CallFunctionError>>()?;
810
811        let py_kwargs: HashMap<String, PyTree<PyArg>> = kwargs_dict
812            .iter()
813            .map(|(k, v)| {
814                let key = k
815                    .extract::<String>()
816                    .map_err(SerializablePyErr::from_fn(py))?;
817                let value = resolve(v)?;
818                Ok((key, value))
819            })
820            .collect::<Result<_, CallFunctionError>>()?;
821
822        // Call function.
823        // Use custom subscriber to route Worker messages to stdout.
824        let scoped_subscriber = Subscriber::builder().with_writer(std::io::stdout).finish();
825        let result: Bound<'_, PyAny> =
826            tracing::subscriber::with_default(scoped_subscriber, || {
827                // TODO(agallagher): The args/kwargs conversion traits generate
828                // the appropriate types here, but they get casted to `PyAny`.
829                // It'd be nice to make `TryToPy<PyAny>Unsafe` take a template
830                // arg for the converted py object to avoid this downcast.
831                // SAFETY: Tensor operations were unsafe because we were tracking their references
832                // like Rust objects (single mutable reference or many immutable references). We are
833                // removing this functionality in upcoming patches, so we use the unsafe version here
834                // until that happens.
835                let args = unsafe { py_args.try_to_object_unsafe(py) }
836                    .map_err(SerializablePyErr::from_fn(py))?;
837                // SAFETY: Same as above - reference tracking functionality is being removed.
838                let kwargs = &unsafe { py_kwargs.try_to_object_unsafe(py) }
839                    .map_err(SerializablePyErr::from_fn(py))?;
840
841                if let Some(function) = function {
842                    function
843                        .call(args, Some(kwargs))
844                        .map_err(SerializablePyErr::from_fn(py))
845                } else {
846                    Ok(args.get_item(0).unwrap())
847                }
848            })?;
849        Ok(result)
850    }
851
852    fn call_python_fn_pytree(
853        &mut self,
854        cx: &hyperactor::Context<Self>,
855        function: ResolvableFunction,
856        args_kwargs: ArgsKwargs,
857        mutates: &[Ref],
858        device_meshes: HashMap<Ref, DeviceMesh>,
859        remote_process_groups: HashMap<
860            Ref,
861            (DeviceMesh, Vec<String>, Arc<ActorHandle<NcclCommActor>>),
862        >,
863    ) -> Result<PyTree<Py<PyAny>>, CallFunctionError> {
864        monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
865            let result = self.call_python_fn(
866                py,
867                cx,
868                Some(function),
869                args_kwargs,
870                mutates,
871                device_meshes,
872                remote_process_groups,
873            )?;
874            Ok(PyTree::<Py<PyAny>>::extract_bound(&result)
875                .map_err(SerializablePyErr::from_fn(py))?)
876        })
877    }
878    /// Retrieve `ref_` or create a fake value with the provided factory if it
879    /// is an error. We use this for collective calls, where even if there was
880    /// an upstream failure, we still have participate in the collective to
881    /// avoid deadlocking the other ranks. It's okay to just put a nonsense
882    /// value here of the correct shape; the controller will have been notified
883    /// of the upstream failure and will know to ignore everything dependent on
884    /// it.
885    fn get_or_fake_on_err(&self, ref_: Ref, factory: &Factory) -> Result<TensorCell> {
886        let pyobject = self
887            .env
888            .get(&ref_)
889            .ok_or_else(|| anyhow!("tensor not found in stream: {ref_:#?}"))?;
890
891        match pyobject {
892            Ok(val) => monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
893                Self::pyobject_to_tensor(py, val)
894                    .map_err(|pyerr| anyhow::Error::from(SerializablePyErr::from(py, &pyerr)))
895            }),
896            Err(_) => {
897                let t = factory_zeros(&factory.size, factory.dtype, factory.layout, factory.device);
898                Ok(TensorCell::new(t))
899            }
900        }
901    }
902
903    fn get_defining_recording(&mut self) -> Option<(&mut Recording, &mut HashSet<u64>)> {
904        self.active_recording
905            .as_mut()
906            .and_then(|state| match state {
907                RecordingState::Defining {
908                    recording,
909                    defined_borrows,
910                } => {
911                    match self.recordings.get_mut(recording) {
912                        Some(recording) => Some((recording, defined_borrows)),
913                        // Panic, because this would be a logic error in the program.
914                        None => panic!("recording not found: {:?}", recording),
915                    }
916                }
917                RecordingState::Running => None,
918            })
919    }
920
921    fn get_first_error(&self, refs: &[Ref]) -> Result<Option<Arc<SeqError>>> {
922        for ref_ in refs {
923            let rvalue_or_err = self
924                .env
925                .get(ref_)
926                .ok_or_else(|| anyhow!("tensor not found in stream: {ref_:#?}"))?;
927            if let Err(err) = rvalue_or_err {
928                return Ok(Some(err.clone()));
929            }
930        }
931        Ok(None)
932    }
933    async fn send_value_python_message(
934        &mut self,
935        cx: &hyperactor::Context<'_, Self>,
936        seq: Seq,
937        mutates: Vec<Ref>,
938        function: Option<ResolvableFunction>,
939        args_kwargs: ArgsKwargs,
940        device_meshes: HashMap<Ref, DeviceMesh>,
941    ) -> Result<()> {
942        let rank = self.rank;
943        self.try_define(cx, seq, vec![], &vec![], async |self_| {
944            let python_message = monarch_with_gil_blocking(
945                GilSite::StreamCompute,
946                |py| -> Result<PythonMessage, CallFunctionError> {
947                    let python_result = tokio::task::block_in_place(|| {
948                        self_.call_python_fn(
949                            py,
950                            cx,
951                            function,
952                            args_kwargs,
953                            &mutates,
954                            device_meshes,
955                            HashMap::new(),
956                        )
957                    })?;
958                    pickle_python_result(py, python_result, rank).map_err(CallFunctionError::Error)
959                },
960            )?;
961            let ser = wirevalue::Any::serialize(&python_message).unwrap();
962            self_
963                .controller_actor
964                .fetch_result(cx, seq, Ok(ser))
965                .await?;
966            Ok(vec![])
967        })
968        .await
969    }
970    fn define_ref(&mut self, dest: Ref, src: Ref) -> Result<(), anyhow::Error> {
971        let rvalue = self
972            .env
973            .get(&src)
974            .ok_or_else(|| CallFunctionError::RefNotFound(src))?;
975        self.env.insert(
976            dest,
977            monarch_with_gil_blocking(GilSite::StreamCompute, |_py| rvalue.clone()),
978        );
979        Ok(())
980    }
981    async fn call_actor(
982        &mut self,
983        cx: &Context<'_, Self>,
984        params: ActorCallParams,
985    ) -> Result<Py<PyAny>, CallFunctionError> {
986        let local_state: Result<Vec<Py<PyAny>>> =
987            monarch_with_gil_blocking(GilSite::StreamCompute, |_py| {
988                params
989                    .local_state
990                    .into_iter()
991                    .map(|elem| {
992                        let pyobj = self.ref_to_pyobject(&elem)?;
993                        Ok(pyobj.into_any())
994                    })
995                    .collect()
996            });
997
998        let (send, recv) = cx.open_once_port();
999        let state = LocalState {
1000            response_port: send,
1001            state: local_state?,
1002        };
1003        let x: u64 = params.seq.into();
1004        let message = LocalStateBrokerMessage::Set(x as usize, state);
1005
1006        let broker = BrokerId::new(params.broker_id).resolve(cx).await;
1007        broker.post(cx, message);
1008        let result = recv
1009            .recv()
1010            .await
1011            .map_err(|e| CallFunctionError::Error(e.into()))?;
1012
1013        result.map_err(|pyerr| anyhow::Error::msg(pyerr.to_string()).into())
1014    }
1015}
1016
1017#[async_trait]
1018#[handle(StreamMessage)]
1019impl StreamMessageHandler for StreamActor {
1020    async fn call_function(
1021        &mut self,
1022        cx: &Context<Self>,
1023        params: CallFunctionParams,
1024        device_meshes: HashMap<Ref, DeviceMesh>,
1025        remote_process_groups: HashMap<
1026            Ref,
1027            (DeviceMesh, Vec<String>, Arc<ActorHandle<NcclCommActor>>),
1028        >,
1029    ) -> Result<()> {
1030        if let Some((recording, _)) = self.get_defining_recording() {
1031            recording.messages.push(StreamMessage::CallFunction(
1032                params,
1033                device_meshes,
1034                remote_process_groups,
1035            ));
1036            return Ok(());
1037        }
1038
1039        params.function.panic_if_requested();
1040        self.try_define(
1041            cx,
1042            params.seq,
1043            params.results,
1044            &params.mutates,
1045            async |self| {
1046                tokio::task::block_in_place(|| {
1047                    self.call_python_fn_pytree(
1048                        cx,
1049                        params.function,
1050                        params.args_kwargs,
1051                        &params.mutates,
1052                        device_meshes,
1053                        remote_process_groups,
1054                    )
1055                    .map(|results| results.into_leaves())
1056                })
1057            },
1058        )
1059        .await?;
1060        Ok(())
1061    }
1062
1063    async fn borrow_create(
1064        &mut self,
1065        cx: &Context<Self>,
1066        borrow: u64,
1067        tensor: Ref,
1068        first_use_sender: PortHandle<(Option<Event>, TensorCellResult)>,
1069    ) -> Result<()> {
1070        if let Some((recording, defined_borrows)) = self.get_defining_recording() {
1071            recording.messages.push(StreamMessage::BorrowCreate {
1072                borrow,
1073                tensor,
1074                first_use_sender,
1075            });
1076            ensure!(
1077                defined_borrows.insert(borrow),
1078                "duplicate borrow create in recording"
1079            );
1080            return Ok(());
1081        }
1082
1083        let pyobj_result = self
1084            .env
1085            .get(&tensor)
1086            .ok_or_else(|| anyhow!("invalid reference for borrow_create: {:#?}", tensor))?;
1087
1088        let result = match pyobj_result {
1089            Ok(pyobj) => monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1090                Ok(Self::pyobject_to_tensor(py, pyobj).unwrap())
1091            }),
1092            Err(e) => Err(e.clone()),
1093        };
1094
1095        let event = self.cuda_stream().map(|stream| stream.record_event(None));
1096        first_use_sender.post(cx, (event, result));
1097        Ok(())
1098    }
1099
1100    async fn borrow_first_use(
1101        &mut self,
1102        _cx: &Context<Self>,
1103        borrow: u64,
1104        result: Ref,
1105        first_use_receiver: Arc<Mutex<PortReceiver<(Option<Event>, TensorCellResult)>>>,
1106    ) -> Result<()> {
1107        if let Some((recording, _)) = self.get_defining_recording() {
1108            recording.messages.push(StreamMessage::BorrowFirstUse {
1109                borrow,
1110                result,
1111                first_use_receiver: first_use_receiver.clone(),
1112            });
1113            return Ok(());
1114        }
1115
1116        let (first_use_event, cell) =
1117            first_use_receiver
1118                .lock()
1119                .await
1120                .recv()
1121                .await
1122                .map_err(|err| {
1123                    anyhow!(
1124                        "failed receiving first use event for borrow {:?}: {:?}",
1125                        borrow,
1126                        err
1127                    )
1128                })?;
1129
1130        if let Some(stream) = self.cuda_stream() {
1131            stream.wait_event(
1132                &mut first_use_event.expect("sent borrow to CUDA stream, expected a CUDA event"),
1133            );
1134        }
1135        match cell {
1136            Ok(cell) => {
1137                let pyobj = Self::tensor_to_pyobject(cell);
1138                self.env.insert(result, Ok(pyobj));
1139            }
1140            Err(err) => {
1141                self.env.insert(result, Err(err.clone()));
1142            }
1143        }
1144        Ok(())
1145    }
1146
1147    async fn borrow_last_use(
1148        &mut self,
1149        cx: &Context<Self>,
1150        borrow: u64,
1151        result: Ref,
1152        last_use_sender: PortHandle<(Option<Event>, TensorCellResult)>,
1153    ) -> Result<()> {
1154        if let Some((recording, _)) = self.get_defining_recording() {
1155            recording.messages.push(StreamMessage::BorrowLastUse {
1156                borrow,
1157                result,
1158                last_use_sender,
1159            });
1160            return Ok(());
1161        }
1162
1163        let event = self.cuda_stream().map(|stream| stream.record_event(None));
1164        let pyobj_or_err = self.env.remove(&result).ok_or(anyhow!(
1165            "Invalid reference for borrow_last_use: {result:#?}"
1166        ))?;
1167        let tensor = match pyobj_or_err {
1168            Ok(pyobj) => Ok(monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1169                Self::pyobject_to_tensor(py, &pyobj).unwrap()
1170            })),
1171            Err(e) => Err(e),
1172        };
1173
1174        last_use_sender.post(cx, (event, tensor));
1175        Ok(())
1176    }
1177
1178    async fn borrow_drop(
1179        &mut self,
1180        _cx: &Context<Self>,
1181        borrow: u64,
1182        last_use_receiver: Arc<Mutex<PortReceiver<(Option<Event>, TensorCellResult)>>>,
1183    ) -> Result<()> {
1184        if let Some((recording, defined_borrows)) = self.get_defining_recording() {
1185            recording.messages.push(StreamMessage::BorrowDrop {
1186                borrow,
1187                last_use_receiver: last_use_receiver.clone(),
1188            });
1189            ensure!(
1190                defined_borrows.remove(&borrow),
1191                "borrow drop for borrow not defined in recording"
1192            );
1193            return Ok(());
1194        }
1195
1196        // The borrowed cell isn't used directly, but we still want to receive it here
1197        // so that the underlying tensor isn't dropped until after we synchronize the
1198        // CUDA streams.
1199        let (last_use_event, _cell) =
1200            last_use_receiver.lock().await.recv().await.map_err(|err| {
1201                anyhow!(
1202                    "failed receiving last use event for borrow {:?}: {:?}",
1203                    borrow,
1204                    err
1205                )
1206            })?;
1207
1208        if let Some(stream) = self.cuda_stream() {
1209            stream.wait_event(
1210                &mut last_use_event.expect("sent borrow to CUDA stream, expected a CUDA event"),
1211            );
1212        }
1213        // let the cell drop.
1214        Ok(())
1215    }
1216
1217    async fn delete_refs(&mut self, _cx: &Context<Self>, refs: Vec<Ref>) -> Result<()> {
1218        if let Some((recording, _)) = self.get_defining_recording() {
1219            recording.messages.push(StreamMessage::DeleteRefs(refs));
1220            return Ok(());
1221        }
1222
1223        for ref_ in refs.iter() {
1224            self.env.remove(ref_);
1225        }
1226        Ok(())
1227    }
1228
1229    async fn request_status(&mut self, _cx: &Context<Self>) -> Result<()> {
1230        if self.get_defining_recording().is_some() {
1231            bail!("request_status not allowed in recording");
1232        }
1233
1234        Ok(())
1235    }
1236
1237    async fn init_comm(
1238        &mut self,
1239        _cx: &Context<Self>,
1240        comm: ActorHandle<NcclCommActor>,
1241    ) -> Result<()> {
1242        if self.get_defining_recording().is_some() {
1243            bail!("init_comm not allowed in recording");
1244        }
1245
1246        self.comm = Some(comm);
1247        Ok(())
1248    }
1249
1250    async fn reduce(
1251        &mut self,
1252        cx: &Context<Self>,
1253        comm: Arc<ActorHandle<NcclCommActor>>,
1254        dim_size: i64,
1255        result: Ref,
1256        local_tensor: Ref,
1257        factory: Factory,
1258        reduction: Reduction,
1259        scatter: bool,
1260        in_place: bool,
1261        out: Option<Ref>,
1262    ) -> Result<()> {
1263        if let Some((recording, _)) = self.get_defining_recording() {
1264            recording.messages.push(StreamMessage::Reduce {
1265                comm,
1266                dim_size,
1267                result,
1268                local_tensor,
1269                factory,
1270                reduction,
1271                scatter,
1272                in_place,
1273                out,
1274            });
1275            return Ok(());
1276        }
1277
1278        let stream = self
1279            .cuda_stream()
1280            .expect("reductions not yet supported for non-CUDA workers")
1281            .clone();
1282        let input_cell = self.get_or_fake_on_err(local_tensor, &factory)?;
1283        let out_cell = out
1284            .map(|out| self.get_or_fake_on_err(out, &factory))
1285            .transpose()?;
1286        let output_cell = match reduction {
1287            Reduction::Stack => {
1288                if scatter {
1289                    let output_cell = if in_place {
1290                        input_cell.clone()
1291                    } else {
1292                        out_cell.unwrap_or({
1293                            let borrow = input_cell.try_borrow().map_err(|e| anyhow!("{e:?}"))?;
1294                            let cloned = deep_clone(&borrow);
1295                            TensorCell::new(cloned)
1296                        })
1297                    };
1298                    comm.all_to_all_single(cx, output_cell.clone(), input_cell, stream)
1299                        .await?;
1300                    output_cell
1301                } else {
1302                    ensure!(
1303                        !in_place,
1304                        "in-place, non-scatter not supported for stack reduce"
1305                    );
1306
1307                    let output_cell = out_cell.unwrap_or({
1308                        // In Python, this would be [dim_size, *factory.sizes]
1309                        let sizes = [&[dim_size][..], &factory.size[..]].concat();
1310                        let output =
1311                            factory_empty(&sizes, factory.dtype, factory.layout, factory.device);
1312                        TensorCell::new(output)
1313                    });
1314
1315                    comm.all_gather_into_tensor(cx, output_cell.clone(), input_cell, stream)
1316                        .await?;
1317                    output_cell
1318                }
1319            }
1320            Reduction::ReduceOp(op) => {
1321                if scatter {
1322                    ensure!(!in_place, "in-place, scatter not supported for reduce");
1323
1324                    let output_cell = out_cell.unwrap_or({
1325                        let output = factory_empty(
1326                            &factory.size[1..],
1327                            factory.dtype,
1328                            factory.layout,
1329                            factory.device,
1330                        );
1331                        TensorCell::new(output)
1332                    });
1333                    comm.reduce_scatter_tensor(cx, output_cell.clone(), input_cell, op, stream)
1334                        .await?;
1335                    output_cell
1336                } else {
1337                    let output_cell = if in_place {
1338                        input_cell.clone()
1339                    } else {
1340                        out_cell.map_or(
1341                            {
1342                                let borrow =
1343                                    input_cell.try_borrow().map_err(|e| anyhow!("{e:?}"))?;
1344                                let cloned = deep_clone(&borrow);
1345                                Ok(TensorCell::new(cloned))
1346                            },
1347                            |out_cell| -> Result<_, anyhow::Error> {
1348                                let mut out_borrow =
1349                                    out_cell.try_borrow_mut().map_err(|e| anyhow!("{e:?}"))?;
1350                                let in_borrow =
1351                                    input_cell.try_borrow().map_err(|e| anyhow!("{e:?}"))?;
1352                                out_borrow.copy_(&in_borrow);
1353                                drop(out_borrow);
1354                                Ok(out_cell)
1355                            },
1356                        )?
1357                    };
1358
1359                    comm.all_reduce(cx, output_cell.clone(), op, stream).await?;
1360                    output_cell
1361                }
1362            }
1363        };
1364
1365        let pyobj = Self::tensor_to_pyobject(output_cell);
1366        self.env.insert(result, Ok(pyobj));
1367        Ok(())
1368    }
1369
1370    async fn send_tensor(
1371        &mut self,
1372        cx: &Context<Self>,
1373        result: Ref,
1374        from_rank: Option<usize>,
1375        to_rank: Option<usize>,
1376        tensor: Ref,
1377        factory: Factory,
1378        comm: Option<Arc<ActorHandle<NcclCommActor>>>,
1379    ) -> Result<()> {
1380        if let Some((recording, _)) = self.get_defining_recording() {
1381            recording.messages.push(StreamMessage::SendTensor {
1382                result,
1383                from_rank,
1384                to_rank,
1385                tensor,
1386                factory,
1387                comm,
1388            });
1389            return Ok(());
1390        }
1391
1392        if to_rank.is_none() && from_rank.is_none() {
1393            bail!("tried to send tensor without a to/from rank");
1394        }
1395
1396        // Value is local, so we do not have to actually send it.
1397        if from_rank == to_rank {
1398            let input_cell: &std::result::Result<Py<PyAny>, Arc<SeqError>> = self
1399                .env
1400                .get(&tensor)
1401                .ok_or_else(|| anyhow!("tensor not found in stream: {tensor:#?}"))?;
1402            let output_cell: Result<Py<PyAny>, Arc<SeqError>> = match input_cell {
1403                Ok(pyobj) => {
1404                    monarch_with_gil_blocking(
1405                        GilSite::StreamCompute,
1406                        |py| -> Result<Py<PyAny>, Arc<SeqError>> {
1407                            let input_tensor = Self::pyobject_to_tensor(py, pyobj).unwrap();
1408                            // We create a defensive copy here to prevent mutations on
1409                            // the input tensor from affecting output tensor.
1410                            // Should we copy if input ref == output ref?
1411                            // Should we support copy-on-write to avoid unnecessary copy?
1412                            let borrow = input_tensor.try_borrow().unwrap();
1413                            let cloned = deep_clone(&borrow);
1414                            let cloned_cell = TensorCell::new(cloned);
1415                            Ok(Self::tensor_to_pyobject(cloned_cell))
1416                        },
1417                    )
1418                }
1419                Err(err) => Err(err.clone()),
1420            };
1421            self.env.insert(result, output_cell);
1422            return Ok(());
1423        }
1424
1425        let comm = comm.context("send_tensor requires backend comm")?;
1426
1427        let mut messages = Vec::new();
1428
1429        if let Some(to_rank) = to_rank {
1430            let input_cell = self.get_or_fake_on_err(tensor, &factory)?;
1431            messages.push(CommMessage::Send(
1432                input_cell,
1433                to_rank.try_into().unwrap(),
1434                self.cuda_stream()
1435                    .expect("tried to send_tensor on non-cuda stream")
1436                    .clone(),
1437                cx.open_once_port().0,
1438            ));
1439        }
1440
1441        if let Some(from_rank) = from_rank {
1442            let output_cell = TensorCell::new(factory_empty(
1443                &factory.size,
1444                factory.dtype,
1445                factory.layout,
1446                factory.device,
1447            ));
1448            messages.push(CommMessage::Recv(
1449                output_cell.clone(),
1450                from_rank.try_into().unwrap(),
1451                self.cuda_stream()
1452                    .expect("tried to send_tensor on non-cuda stream")
1453                    .clone(),
1454                cx.open_once_port().0,
1455            ));
1456            let pyobj = Self::tensor_to_pyobject(output_cell);
1457            self.env.insert(result, Ok(pyobj));
1458        }
1459
1460        comm.group(
1461            cx,
1462            messages,
1463            self.cuda_stream()
1464                .expect("tried to send_tensor on non-cuda stream")
1465                .clone(),
1466        )
1467        .await?;
1468        Ok(())
1469    }
1470
1471    async fn send_value(
1472        &mut self,
1473        cx: &Context<Self>,
1474        seq: Seq,
1475        worker_actor_id: reference::ActorAddr,
1476        mutates: Vec<Ref>,
1477        function: Option<ResolvableFunction>,
1478        args_kwargs: ArgsKwargs,
1479        device_meshes: HashMap<Ref, DeviceMesh>,
1480    ) -> Result<()> {
1481        if self.respond_with_python_message {
1482            return self
1483                .send_value_python_message(cx, seq, mutates, function, args_kwargs, device_meshes)
1484                .await;
1485        }
1486
1487        let result = if let Some(function) = function {
1488            // If a function was provided, use that to resolve the value.
1489            tokio::task::block_in_place(|| {
1490                self.call_python_fn_pytree(
1491                    cx,
1492                    function,
1493                    args_kwargs,
1494                    &mutates,
1495                    device_meshes,
1496                    HashMap::new(),
1497                )
1498            })
1499        } else {
1500            // If there's no function provided, there should be exactly one arg
1501            // and no kwargs.
1502            monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1503                let (args, kwargs) = args_kwargs
1504                    .to_python(py)
1505                    .map_err(|e| CallFunctionError::Error(e.into()))?;
1506                match (args.len(), kwargs.len()) {
1507                    (1, 0) => {
1508                        let arg = args.get_item(0).map_err(SerializablePyErr::from_fn(py))?;
1509                        arg.extract::<PyTree<Py<PyAny>>>()
1510                            .map_err(SerializablePyErr::from_fn(py))?
1511                            .try_into_map(|obj| {
1512                                let bound_obj = obj.bind(py);
1513                                if let Ok(ref_) = Ref::from_py_object(bound_obj) {
1514                                    self.ref_to_pyobject(&ref_)
1515                                } else {
1516                                    Ok(obj)
1517                                }
1518                            })
1519                    }
1520                    _ => Err(CallFunctionError::TooManyArgsForValue(
1521                        format!("args with {} elements", args.len()),
1522                        format!("kwargs with {} elements", kwargs.len()),
1523                    )),
1524                }
1525            })
1526        };
1527
1528        let value = match result {
1529            Ok(pyobject) => Ok(pyobject),
1530            Err(err) => {
1531                let err = self.report_seq_error(cx, seq, err).await?;
1532                for ref_ in mutates {
1533                    self.env.insert(ref_, Err(err.clone()));
1534                }
1535                Err(WorkerError {
1536                    backtrace: format!("{:?}", err),
1537                    worker_actor_id,
1538                })
1539            }
1540        };
1541
1542        // Actually send the value.
1543        // NOTE: respond_with_python_message is always true, so serialization is not needed
1544        // The controller will receive the value through send_value_python_message instead
1545        let result = match value {
1546            Ok(_value) => {
1547                // This code path is never executed since respond_with_python_message is true
1548                unreachable!(
1549                    "send_value should return early when respond_with_python_message is true"
1550                )
1551            }
1552            Err(e) => Err(e),
1553        };
1554        self.controller_actor.fetch_result(cx, seq, result).await?;
1555
1556        Ok(())
1557    }
1558
1559    async fn send_result_of_actor_call(
1560        &mut self,
1561        cx: &Context<Self>,
1562        params: ActorCallParams,
1563    ) -> anyhow::Result<()> {
1564        let seq = params.seq;
1565        let mutates = params.mutates.clone();
1566        self.try_define(cx, seq, vec![], &mutates, async |self| {
1567            let value = self.call_actor(cx, params).await?;
1568            let result = monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1569                pickle_python_result(py, value.into_bound(py), self.rank)
1570            })?;
1571            let result = wirevalue::Any::serialize(&result).unwrap();
1572            self.controller_actor
1573                .fetch_result(cx, seq, Ok(result))
1574                .await?;
1575            Ok(vec![])
1576        })
1577        .await
1578    }
1579
1580    async fn call_actor_method(
1581        &mut self,
1582        cx: &Context<Self>,
1583        params: ActorMethodParams,
1584    ) -> anyhow::Result<()> {
1585        let seq = params.call.seq;
1586        let mutates = params.call.mutates.clone();
1587        self.try_define(cx, seq, params.results, &mutates, async |self| {
1588            let result = self.call_actor(cx, params.call).await?;
1589            let result = monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1590                PyTree::<Py<PyAny>>::extract_bound(&result.into_bound(py))
1591                    .map_err(SerializablePyErr::from_fn(py))
1592            })?;
1593            Ok(result.into_leaves())
1594        })
1595        .await
1596    }
1597
1598    async fn define_recording(&mut self, _cx: &Context<Self>, recording: Ref) -> Result<()> {
1599        if self.active_recording.is_some() {
1600            bail!("different recording already active");
1601        }
1602        match self.recordings.entry(recording) {
1603            Entry::Occupied(_) => bail!("recording {:?} already defined", recording),
1604            Entry::Vacant(entry) => entry.insert(Recording::new()),
1605        };
1606        self.active_recording = Some(RecordingState::Defining {
1607            recording,
1608            defined_borrows: HashSet::new(),
1609        });
1610        Ok(())
1611    }
1612
1613    async fn finalize_recording(&mut self, _cx: &Context<Self>, recording: Ref) -> Result<()> {
1614        match self.active_recording {
1615            Some(RecordingState::Defining {
1616                recording: active_recording,
1617                ref defined_borrows,
1618            }) if active_recording == recording => {
1619                ensure!(
1620                    defined_borrows.is_empty(),
1621                    "all borrows created within recording must be dropped within recording"
1622                );
1623                self.active_recording = None;
1624            }
1625            _ => bail!("cannot finalize recording that isn't active"),
1626        }
1627        Ok(())
1628    }
1629
1630    async fn recording_formal(
1631        &mut self,
1632        _cx: &Context<Self>,
1633        result: Ref,
1634        argument_index: usize,
1635    ) -> Result<()> {
1636        match self.get_defining_recording() {
1637            Some((recording, _)) => {
1638                recording.messages.push(StreamMessage::RecordingFormal {
1639                    result,
1640                    argument_index,
1641                });
1642            }
1643            None => bail!("recording_formal called outside of recording"),
1644        };
1645        Ok(())
1646    }
1647
1648    async fn recording_result(
1649        &mut self,
1650        _cx: &Context<Self>,
1651        result: Ref,
1652        output_index: usize,
1653    ) -> Result<()> {
1654        match self.get_defining_recording() {
1655            Some((recording, _)) => {
1656                recording.messages.push(StreamMessage::RecordingResult {
1657                    result,
1658                    output_index,
1659                });
1660            }
1661            None => bail!("recording_result called outside of recording"),
1662        };
1663        Ok(())
1664    }
1665
1666    async fn call_recording(
1667        &mut self,
1668        cx: &Context<Self>,
1669        seq: Seq,
1670        recording: Ref,
1671        results: Vec<Ref>,
1672        actuals: Vec<Ref>,
1673    ) -> Result<()> {
1674        if self.active_recording.is_some() {
1675            bail!("cannot call recording while another recording is active");
1676        }
1677
1678        let messages = match self.recordings.get(&recording) {
1679            Some(recording) => recording
1680                .messages
1681                .iter()
1682                .map(|message| message.clone_for_recording())
1683                .collect::<Vec<_>>(),
1684            None => bail!("recording {:?} not found", recording),
1685        };
1686
1687        self.active_recording = Some(RecordingState::Running);
1688
1689        // Global error for all messages in the recording. The first time a message
1690        // fails in the recording, we set the error. We then need to propagate this
1691        // error to all of the refs mutated by the entire recording, as well as the
1692        // result refs.
1693        let mut error: Option<Arc<SeqError>> = None;
1694        // The set of all refs defined by this recording (excluding "results"),
1695        // which we need to ensure are deleted when the recording is done executing.
1696        let mut all_defined_refs = HashSet::new();
1697        // The set of all refs mutated by this recording. If there is an error with
1698        // any message, all of these refs need to have the correct error set.
1699        let mut all_mutated_refs = HashSet::new();
1700        // Map from the result ref of a RecordingFormal message to the associated
1701        // actual ref from "actuals". We need to track this in order to properly
1702        // handle recordings that mutate refs contained in "actuals" -- every
1703        // message in the recording that interacts with the recording inputs will
1704        // interact with the formal ref rather than the actual ref.
1705        let mut formal_to_actual_refs = HashMap::new();
1706        // clear any pre-existing error messages before recording started
1707        self.last_seq_error = None;
1708        for message in messages.into_iter() {
1709            let defined_refs = message.get_defined_refs();
1710            all_defined_refs.extend(defined_refs.clone());
1711
1712            let mutated_refs_with_formals = message.get_mutated_refs();
1713            all_mutated_refs.extend(mutated_refs_with_formals.iter().filter_map(|ref_| {
1714                match formal_to_actual_refs.get(ref_) {
1715                    Some(actual_ref) => Some(*actual_ref),
1716                    None => {
1717                        if all_defined_refs.contains(ref_) {
1718                            None
1719                        } else {
1720                            Some(*ref_)
1721                        }
1722                    }
1723                }
1724            }));
1725
1726            match message {
1727                StreamMessage::RecordingFormal {
1728                    result: formal_ref,
1729                    argument_index,
1730                } => match actuals.get(argument_index) {
1731                    None => bail!("recording_formal called with too few arguments"),
1732                    Some(actual_ref) => {
1733                        formal_to_actual_refs.insert(formal_ref, *actual_ref);
1734                        self.define_ref(formal_ref, *actual_ref)?;
1735                    }
1736                },
1737                StreamMessage::RecordingResult {
1738                    result: result_ref,
1739                    output_index,
1740                } => match results.get(output_index) {
1741                    None => bail!("recording_result called with too few results"),
1742                    Some(actual_result_ref) => {
1743                        self.define_ref(*actual_result_ref, result_ref)?;
1744                    }
1745                },
1746                StreamMessage::DeleteRefs(ref refs) => {
1747                    for ref_ in refs {
1748                        all_defined_refs.remove(ref_);
1749                    }
1750                    StreamMessageHandler::handle(self, cx, message).await?;
1751                }
1752                StreamMessage::CallFunction { .. } if error.is_some() => {
1753                    // CallFunction is expensive. If the recording already failed, then
1754                    // just update the necessary refs with the error. Most of the other
1755                    // message types need to run regardless because there are other actors
1756                    // that expect the call to happen (e.g., all of the borrow messages,
1757                    // pipe send/recv, send_tensor, reduce, etc.).
1758                    let error = error.clone().unwrap();
1759                    for ref_ in defined_refs.iter().chain(mutated_refs_with_formals.iter()) {
1760                        self.env.insert(*ref_, Err(error.clone()));
1761                    }
1762                }
1763                StreamMessage::BorrowLastUse { ref result, .. } => {
1764                    all_defined_refs.remove(result);
1765                    StreamMessageHandler::handle(self, cx, message).await?;
1766                }
1767                StreamMessage::Reduce {
1768                    local_tensor,
1769                    ref out,
1770                    ..
1771                } => {
1772                    // Reduce doesn't propagate errors to the result ref, so we need
1773                    // to check for existing errors on the input tensors and set the
1774                    // recording's error if necessary.
1775                    if error.is_none() {
1776                        let inputs_to_check = [Some(local_tensor), *out]
1777                            .iter()
1778                            .filter_map(|r| *r)
1779                            .collect::<Vec<_>>();
1780                        error = self.get_first_error(inputs_to_check.as_slice())?;
1781                    }
1782                    StreamMessageHandler::handle(self, cx, message).await?;
1783                }
1784                StreamMessage::SendTensor {
1785                    ref tensor,
1786                    ref to_rank,
1787                    ..
1788                } => {
1789                    // If this rank is sending a tensor (e.g., to_rank has a value),
1790                    // we need to check for existing errors on the input tensor, because
1791                    // the error is only propagated to the result ref when this rank
1792                    // is also receiving a tensor.
1793                    if to_rank.is_some() && error.is_none() {
1794                        error = self.get_first_error(&[*tensor])?;
1795                    }
1796                    StreamMessageHandler::handle(self, cx, message).await?;
1797                }
1798                _ => {
1799                    StreamMessageHandler::handle(self, cx, message).await?;
1800                }
1801            };
1802
1803            // It's not entirely trivial to determine whether a message "failed" or not.
1804            // For example, the CallFunction message can return Ok(..) if there is an error
1805            // in the underlying function call. But in that case, we would still want to
1806            // consider the recording call as "failed". Unlike in python, where we can just
1807            // wrap everything in try-except, in rust, we keep track of the last report SeqError, which
1808            // we clear before handling each recording message. If we see it is set, the
1809            // we know the recording has faild.
1810            match (&error, self.last_seq_error.take()) {
1811                (None, Some(seq_err)) => {
1812                    // Report failure to the controller.
1813                    self.controller_actor
1814                        .remote_function_failed(
1815                            cx,
1816                            seq,
1817                            WorkerError {
1818                                backtrace: format!("recording failed: {}", &seq_err),
1819                                worker_actor_id: cx.self_addr().clone(),
1820                            },
1821                        )
1822                        .await?;
1823                    error = Some(seq_err)
1824                }
1825                _ => {}
1826            }
1827            // Continue processing the remaining stream messages regardless of error.
1828            // We need to do this partially for error propagation, but also because
1829            // certain messages (like borrows and reductions) need to run regardless
1830            // in order to prevent deadlocks.
1831        }
1832
1833        // Delete the formal refs and some subset of the RecordingResult refs. The
1834        // controller should have generated DeleteRefs messages for all other refs
1835        // defined by the recording.
1836        StreamMessageHandler::handle(
1837            self,
1838            cx,
1839            StreamMessage::DeleteRefs(all_defined_refs.into_iter().collect()),
1840        )
1841        .await?;
1842
1843        // Any refs mutated by the recording and all results should have the same error
1844        // (the original error that caused the recording to fail).
1845        if error.is_some() {
1846            for ref_ in results.iter().chain(all_mutated_refs.iter()) {
1847                self.env.insert(*ref_, Err(error.clone().unwrap()));
1848            }
1849        }
1850
1851        self.active_recording = None;
1852        Ok(())
1853    }
1854
1855    async fn set_ref_unit_tests_only(
1856        &mut self,
1857        _cx: &Context<Self>,
1858        reference: Ref,
1859        value: WireValue,
1860    ) -> Result<()> {
1861        let pyobj =
1862            monarch_with_gil_blocking(GilSite::StreamCompute, |py| -> PyResult<Py<PyAny>> {
1863                Ok(value.into_pyobject(py)?.unbind())
1864            })?;
1865        self.env.insert(reference, Ok(pyobj));
1866        Ok(())
1867    }
1868
1869    async fn set_tensor_ref_unit_tests_only(
1870        &mut self,
1871        _cx: &Context<Self>,
1872        reference: Ref,
1873        tensor_result: TensorCellResult,
1874    ) -> Result<()> {
1875        match tensor_result {
1876            Ok(tensor_cell) => {
1877                let pyobj = Self::tensor_to_pyobject(tensor_cell);
1878                self.env.insert(reference, Ok(pyobj));
1879            }
1880            Err(err) => {
1881                self.env.insert(reference, Err(err));
1882            }
1883        }
1884        Ok(())
1885    }
1886
1887    async fn get_ref_unit_tests_only(
1888        &mut self,
1889        _cx: &Context<Self>,
1890        reference: Ref,
1891    ) -> Result<Option<Result<WireValue, String>>> {
1892        use pyo3::types::PyBool;
1893        use pyo3::types::PyFloat;
1894        use pyo3::types::PyInt;
1895        use pyo3::types::PyList;
1896        use pyo3::types::PyNone;
1897        use pyo3::types::PyString;
1898        /// For testing only, doesn't support Tensor or TensorList.
1899        fn pyobject_to_wire(
1900            value: Result<Py<PyAny>, Arc<SeqError>>,
1901        ) -> Result<WireValue, Arc<SeqError>> {
1902            let pyobj = value?;
1903            monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1904                let bound = pyobj.bind(py);
1905                // Check bool before int since Python's bool is a subclass of int
1906                if bound.is_instance_of::<PyBool>() {
1907                    Ok(WireValue::Bool(bound.extract::<bool>().unwrap()))
1908                } else if bound.is_instance_of::<PyInt>() {
1909                    Ok(WireValue::Int(bound.extract::<i64>().unwrap()))
1910                } else if bound.is_instance_of::<PyList>() {
1911                    if let Ok(val) = bound.extract::<Vec<i64>>() {
1912                        Ok(WireValue::IntList(val))
1913                    } else {
1914                        Ok(WireValue::String(format!(
1915                            "unsupported list type: {:?}",
1916                            bound
1917                        )))
1918                    }
1919                } else if bound.is_instance_of::<PyFloat>() {
1920                    Ok(WireValue::Double(bound.extract::<f64>().unwrap()))
1921                } else if bound.is_instance_of::<PyString>() {
1922                    Ok(WireValue::String(bound.extract::<String>().unwrap()))
1923                } else if bound.is_instance_of::<PyNone>() {
1924                    Ok(WireValue::None(()))
1925                } else {
1926                    Ok(WireValue::String(format!(
1927                        "unsupported pyobject type: {:?}",
1928                        bound
1929                    )))
1930                }
1931            })
1932        }
1933        Ok(self.env.get(&reference).map(|pyobj| {
1934            pyobject_to_wire(monarch_with_gil_blocking(GilSite::StreamCompute, |_py| {
1935                pyobj.clone()
1936            }))
1937            .map_err(|err| err.to_string())
1938        }))
1939    }
1940
1941    async fn get_tensor_ref_unit_tests_only(
1942        &mut self,
1943        _cx: &Context<Self>,
1944        reference: Ref,
1945    ) -> Result<Option<TensorCellResult>> {
1946        match self.env.get(&reference) {
1947            Some(Ok(pyobj)) => monarch_with_gil_blocking(GilSite::StreamCompute, |py| {
1948                match Self::pyobject_to_tensor(py, pyobj) {
1949                    Ok(tensor) => Ok(Some(Ok(tensor.try_cpu().unwrap()))),
1950                    Err(e) => bail!("expected tensor, got extraction error: {:?}", e),
1951                }
1952            }),
1953            Some(Err(err)) => Ok(Some(Err(err.clone()))),
1954            None => Ok(None),
1955        }
1956    }
1957}
1958
1959#[cfg(all(test, fbcode_build))]
1960mod tests {
1961    use hyperactor::actor::ActorStatus;
1962    use hyperactor::context;
1963    use hyperactor::supervision::ActorSupervisionEvent;
1964    use monarch_messages::controller::ControllerMessage;
1965    use monarch_messages::worker::StreamCreationMode;
1966    use monarch_types::PickledPyObject;
1967    use monarch_types::UniqueId;
1968    use pyo3::IntoPyObjectExt;
1969    use timed_test::async_timed_test;
1970    use tokio::sync::watch;
1971    use torch_sys_cuda::nccl::UniqueIdExt;
1972    use torch_sys2::factory_float_tensor;
1973    use torch_sys2::testing::allclose;
1974
1975    use super::*;
1976    use crate::comm::CommParams;
1977    use crate::test_util;
1978
1979    #[allow(dead_code)]
1980    fn fake_seq_error(err: anyhow::Error) -> Arc<SeqError> {
1981        Arc::new(SeqError {
1982            seq: 0.into(),
1983            error: err,
1984        })
1985    }
1986
1987    struct TestSetup {
1988        proc: Proc,
1989        stream_actor: ActorHandle<StreamActor>,
1990        client: reference::Client,
1991        // Unused, but necessary, because proc needs a supervision
1992        // port -- otherwise an actor failure will cause a crash.
1993        #[allow(dead_code)]
1994        supervision_rx: PortReceiver<ActorSupervisionEvent>,
1995        #[allow(dead_code)]
1996        controller_rx: PortReceiver<ControllerMessage>,
1997        #[allow(dead_code)]
1998        controller_actor: reference::ActorRef<ControllerActor>,
1999        next_ref: Ref,
2000    }
2001
2002    impl TestSetup {
2003        async fn new() -> Result<Self> {
2004            Self::new_with_world_size(1).await
2005        }
2006
2007        async fn new_with_world_size(world_size: usize) -> Result<Self> {
2008            test_util::test_setup()?;
2009
2010            let proc = Proc::isolated();
2011            let (_, controller_actor, controller_rx) =
2012                proc.attach_actor::<ControllerActor, ControllerMessage>("controller")?;
2013            let client = proc.client("client");
2014            let (supervision_tx, supervision_rx) = client.open_port();
2015            proc.set_supervision_coordinator(supervision_tx.bind())?;
2016            let stream_actor = proc.spawn(StreamActor::new(StreamParams {
2017                world_size,
2018                rank: 0,
2019                creation_mode: StreamCreationMode::UseDefaultStream,
2020                id: 0.into(),
2021                device: Some(CudaDevice::new(0.into())),
2022                controller_actor: controller_actor.clone(),
2023                respond_with_python_message: false,
2024            }));
2025
2026            Ok(Self {
2027                proc,
2028                stream_actor,
2029                client,
2030                supervision_rx,
2031                controller_rx,
2032                controller_actor,
2033                next_ref: 0.into(),
2034            })
2035        }
2036
2037        fn next_ref(&mut self) -> Ref {
2038            let ref_ = self.next_ref;
2039            self.next_ref = Ref {
2040                id: self.next_ref.id + 1,
2041            };
2042            ref_
2043        }
2044
2045        async fn set_tensor(&mut self, reference: Ref, data: &[f32]) -> Result<()> {
2046            let tensor = TensorCell::new(factory_float_tensor(data, "cuda".parse().unwrap()));
2047            self.stream_actor
2048                .set_tensor_ref_unit_tests_only(&self.client, reference, Ok(tensor))
2049                .await
2050        }
2051
2052        async fn allclose(&mut self, reference: Ref, data: &[f32]) -> bool {
2053            let actual = self
2054                .stream_actor
2055                .get_tensor_ref_unit_tests_only(&self.client, reference)
2056                .await
2057                .unwrap()
2058                .unwrap()
2059                .unwrap();
2060
2061            // rustfmt-ignore
2062            allclose(
2063                &factory_float_tensor(data, "cpu".parse().unwrap()),
2064                &actual.borrow(),
2065            )
2066            .unwrap()
2067        }
2068
2069        #[allow(dead_code)]
2070        async fn validate_dependent_error(&mut self, reference: Ref, error: Arc<SeqError>) {
2071            let result_error = self
2072                .stream_actor
2073                .get_tensor_ref_unit_tests_only(&self.client, reference)
2074                .await
2075                .unwrap()
2076                .unwrap()
2077                .unwrap_err();
2078
2079            assert!(Arc::ptr_eq(&result_error, &error));
2080        }
2081    }
2082
2083    async fn assert_actor_failed_with_msg(
2084        status_rx: &mut watch::Receiver<ActorStatus>,
2085        expected_msg: String,
2086    ) {
2087        status_rx
2088            .wait_for(|s| matches!(s, ActorStatus::Failed(_)))
2089            .await
2090            .unwrap();
2091        let status = status_rx.borrow().clone();
2092        if let ActorStatus::Failed(msg) = status {
2093            assert!(msg.to_string().contains(&expected_msg));
2094        } else {
2095            panic!("expected ActorStatus::Failed, got {:?}", status);
2096        }
2097    }
2098
2099    async fn assert_refs_do_not_exist(test_setup: &TestSetup, refs: &[Ref]) {
2100        for ref_ in refs {
2101            assert!(
2102                test_setup
2103                    .stream_actor
2104                    .get_tensor_ref_unit_tests_only(&test_setup.client, *ref_)
2105                    .await
2106                    .unwrap()
2107                    .is_none()
2108            );
2109        }
2110    }
2111
2112    #[allow(dead_code)]
2113    async fn fetch_result(
2114        cx: &impl context::Actor,
2115        stream_actor: ActorHandle<StreamActor>,
2116        seq: Seq,
2117        reference: Ref,
2118    ) {
2119        let ref_to_send = monarch_with_gil_blocking(GilSite::Test, |py| {
2120            PickledPyObject::pickle(&reference.into_bound_py_any(py).unwrap()).unwrap()
2121        });
2122
2123        stream_actor
2124            .send_value(
2125                cx,
2126                seq,
2127                stream_actor.actor_addr().clone(),
2128                Vec::new(),
2129                None,
2130                ArgsKwargs::from_wire_values(
2131                    vec![WireValue::PyObject(ref_to_send)],
2132                    HashMap::new(),
2133                )
2134                .unwrap(),
2135                HashMap::new(),
2136            )
2137            .await
2138            .unwrap()
2139    }
2140
2141    #[allow(dead_code)]
2142    async fn check_fetch_result_error(
2143        cx: &impl context::Actor,
2144        stream_actor: ActorHandle<StreamActor>,
2145        seq: Seq,
2146        reference: Ref,
2147        controller_rx: &mut PortReceiver<ControllerMessage>,
2148        expected_backtrace: &str,
2149    ) {
2150        fetch_result(cx, stream_actor, seq, reference).await;
2151
2152        let controller_msg = controller_rx.recv().await.unwrap();
2153        match controller_msg {
2154            ControllerMessage::FetchResult {
2155                seq: actual_seq,
2156                value: Err(err),
2157            } => {
2158                assert_eq!(actual_seq, seq);
2159                assert!(
2160                    err.backtrace.contains(expected_backtrace),
2161                    "backtrace did not contain {:?}: {:?}",
2162                    expected_backtrace,
2163                    err.backtrace
2164                );
2165            }
2166            _ => panic!("Unexpected controller message: {:?}", controller_msg),
2167        };
2168    }
2169
2170    #[allow(dead_code)]
2171    async fn check_fetch_result_value(
2172        cx: &impl context::Actor,
2173        stream_actor: ActorHandle<StreamActor>,
2174        seq: Seq,
2175        reference: Ref,
2176        controller_rx: &mut PortReceiver<ControllerMessage>,
2177    ) {
2178        fetch_result(cx, stream_actor, seq, reference).await;
2179
2180        let controller_msg = controller_rx.recv().await.unwrap();
2181        match controller_msg {
2182            ControllerMessage::FetchResult {
2183                value: Ok(_),
2184                seq: actual_seq,
2185            } => assert_eq!(seq, actual_seq),
2186            _ => panic!("Unexpected controller message: {:?}", controller_msg),
2187        };
2188    }
2189
2190    #[async_timed_test(timeout_secs = 60)]
2191    async fn test_define_recording_other_recording_active() -> Result<()> {
2192        let test_setup = TestSetup::new().await?;
2193        test_setup
2194            .stream_actor
2195            .define_recording(&test_setup.client, 0.into())
2196            .await?;
2197        test_setup
2198            .stream_actor
2199            .define_recording(&test_setup.client, 1.into())
2200            .await?;
2201        assert_actor_failed_with_msg(
2202            &mut test_setup.stream_actor.status(),
2203            "different recording already active".into(),
2204        )
2205        .await;
2206        Ok(())
2207    }
2208
2209    #[async_timed_test(timeout_secs = 60)]
2210    async fn test_define_recording_already_defined() -> Result<()> {
2211        let test_setup = TestSetup::new().await?;
2212        test_setup
2213            .stream_actor
2214            .define_recording(&test_setup.client, 0.into())
2215            .await?;
2216        test_setup
2217            .stream_actor
2218            .finalize_recording(&test_setup.client, 0.into())
2219            .await?;
2220        test_setup
2221            .stream_actor
2222            .define_recording(&test_setup.client, 0.into())
2223            .await?;
2224        assert_actor_failed_with_msg(
2225            &mut test_setup.stream_actor.status(),
2226            "already defined".into(),
2227        )
2228        .await;
2229        Ok(())
2230    }
2231
2232    #[async_timed_test(timeout_secs = 60)]
2233    async fn test_finalize_recording_other_recording_active() -> Result<()> {
2234        let test_setup = TestSetup::new().await?;
2235        test_setup
2236            .stream_actor
2237            .define_recording(&test_setup.client, 0.into())
2238            .await?;
2239        test_setup
2240            .stream_actor
2241            .finalize_recording(&test_setup.client, 1.into())
2242            .await?;
2243        assert_actor_failed_with_msg(
2244            &mut test_setup.stream_actor.status(),
2245            "cannot finalize recording that isn't active".into(),
2246        )
2247        .await;
2248        Ok(())
2249    }
2250
2251    #[async_timed_test(timeout_secs = 60)]
2252    async fn test_recording_formal_outside_recording() -> Result<()> {
2253        let test_setup = TestSetup::new().await?;
2254        test_setup
2255            .stream_actor
2256            .recording_formal(&test_setup.client, 0.into(), 0)
2257            .await?;
2258        assert_actor_failed_with_msg(
2259            &mut test_setup.stream_actor.status(),
2260            "recording_formal called outside of recording".into(),
2261        )
2262        .await;
2263        Ok(())
2264    }
2265
2266    #[async_timed_test(timeout_secs = 60)]
2267    async fn test_recording_result_outside_recording() -> Result<()> {
2268        let test_setup = TestSetup::new().await?;
2269        test_setup
2270            .stream_actor
2271            .recording_result(&test_setup.client, 0.into(), 0)
2272            .await?;
2273        assert_actor_failed_with_msg(
2274            &mut test_setup.stream_actor.status(),
2275            "recording_result called outside of recording".into(),
2276        )
2277        .await;
2278        Ok(())
2279    }
2280
2281    #[async_timed_test(timeout_secs = 60)]
2282    async fn test_call_recording_other_recording_active() -> Result<()> {
2283        let test_setup = TestSetup::new().await?;
2284        test_setup
2285            .stream_actor
2286            .define_recording(&test_setup.client, 0.into())
2287            .await?;
2288        test_setup
2289            .stream_actor
2290            .call_recording(&test_setup.client, 0.into(), 0.into(), vec![], vec![])
2291            .await?;
2292        assert_actor_failed_with_msg(
2293            &mut test_setup.stream_actor.status(),
2294            "cannot call recording while another recording is active".into(),
2295        )
2296        .await;
2297        Ok(())
2298    }
2299
2300    #[async_timed_test(timeout_secs = 60)]
2301    async fn test_call_recording_not_found() -> Result<()> {
2302        let test_setup = TestSetup::new().await?;
2303        test_setup
2304            .stream_actor
2305            .call_recording(&test_setup.client, 0.into(), 0.into(), vec![], vec![])
2306            .await?;
2307        assert_actor_failed_with_msg(&mut test_setup.stream_actor.status(), "not found".into())
2308            .await;
2309        Ok(())
2310    }
2311
2312    #[async_timed_test(timeout_secs = 60)]
2313    async fn test_recording_formal_too_few_arguments() -> Result<()> {
2314        let test_setup = TestSetup::new().await?;
2315
2316        test_setup
2317            .stream_actor
2318            .define_recording(&test_setup.client, 0.into())
2319            .await?;
2320
2321        test_setup
2322            .stream_actor
2323            .recording_formal(&test_setup.client, 1.into(), 0)
2324            .await?;
2325
2326        test_setup
2327            .stream_actor
2328            .finalize_recording(&test_setup.client, 0.into())
2329            .await?;
2330
2331        test_setup
2332            .stream_actor
2333            .call_recording(&test_setup.client, 0.into(), 0.into(), vec![], vec![])
2334            .await?;
2335
2336        assert_actor_failed_with_msg(
2337            &mut test_setup.stream_actor.status(),
2338            "recording_formal called with too few arguments".into(),
2339        )
2340        .await;
2341        Ok(())
2342    }
2343
2344    #[async_timed_test(timeout_secs = 60)]
2345    async fn test_recording_result_too_few_results() -> Result<()> {
2346        let test_setup = TestSetup::new().await?;
2347
2348        test_setup
2349            .stream_actor
2350            .define_recording(&test_setup.client, 0.into())
2351            .await?;
2352
2353        test_setup
2354            .stream_actor
2355            .recording_result(&test_setup.client, 1.into(), 0)
2356            .await?;
2357
2358        test_setup
2359            .stream_actor
2360            .finalize_recording(&test_setup.client, 0.into())
2361            .await?;
2362
2363        test_setup
2364            .stream_actor
2365            .call_recording(&test_setup.client, 0.into(), 0.into(), vec![], vec![])
2366            .await?;
2367
2368        assert_actor_failed_with_msg(
2369            &mut test_setup.stream_actor.status(),
2370            "recording_result called with too few results".into(),
2371        )
2372        .await;
2373        Ok(())
2374    }
2375
2376    #[async_timed_test(timeout_secs = 60)]
2377    async fn test_basic_call_recording() -> Result<()> {
2378        let mut test_setup = TestSetup::new().await?;
2379
2380        // Define a recording equivalent to:
2381        // def f(x, y):
2382        //   return y, x
2383        test_setup
2384            .stream_actor
2385            .define_recording(&test_setup.client, 0.into())
2386            .await?;
2387
2388        let formal0_ref = 1.into();
2389        let formal0_index = 1;
2390        test_setup
2391            .stream_actor
2392            .recording_formal(&test_setup.client, formal0_ref, formal0_index)
2393            .await?;
2394
2395        let formal1_ref = 2.into();
2396        let formal1_index = 0;
2397        test_setup
2398            .stream_actor
2399            .recording_formal(&test_setup.client, formal1_ref, formal1_index)
2400            .await?;
2401
2402        let result0_ref = formal0_ref;
2403        let result0_index = 0;
2404        test_setup
2405            .stream_actor
2406            .recording_result(&test_setup.client, result0_ref, result0_index)
2407            .await?;
2408
2409        let result1_ref = formal1_ref;
2410        let result1_index = 1;
2411        test_setup
2412            .stream_actor
2413            .recording_result(&test_setup.client, result1_ref, result1_index)
2414            .await?;
2415
2416        test_setup
2417            .stream_actor
2418            .finalize_recording(&test_setup.client, 0.into())
2419            .await?;
2420
2421        let actual0_ref = 3.into();
2422        test_setup.set_tensor(actual0_ref, &[1.0, 2.0, 3.0]).await?;
2423
2424        let actual1_ref = 4.into();
2425        test_setup.set_tensor(actual1_ref, &[4.0, 5.0]).await?;
2426
2427        // Call the recording with valid tensors for the actual inputs,
2428        // and store the results in refs 5 and 6.
2429        let actual_result0_ref = 5.into();
2430        let actual_result1_ref = 6.into();
2431        test_setup
2432            .stream_actor
2433            .call_recording(
2434                &test_setup.client,
2435                0.into(),
2436                0.into(),
2437                vec![actual_result0_ref, actual_result1_ref],
2438                vec![actual0_ref, actual1_ref],
2439            )
2440            .await?;
2441
2442        // Ensure the results are correct.
2443        assert!(test_setup.allclose(actual_result0_ref, &[4.0, 5.0]).await);
2444        assert!(
2445            test_setup
2446                .allclose(actual_result1_ref, &[1.0, 2.0, 3.0])
2447                .await
2448        );
2449
2450        // Ensure the temporary refs associated with the formals/results have
2451        // been deleted.
2452        assert_refs_do_not_exist(&test_setup, &[formal0_ref, formal1_ref]).await;
2453        Ok(())
2454    }
2455
2456    #[async_timed_test(timeout_secs = 60)]
2457    async fn test_request_status_in_recording() -> Result<()> {
2458        let test_setup = TestSetup::new().await?;
2459        test_setup
2460            .stream_actor
2461            .define_recording(&test_setup.client, 0.into())
2462            .await?;
2463        test_setup
2464            .stream_actor
2465            .request_status(&test_setup.client)
2466            .await
2467            .expect_err("request_status should have failed");
2468        assert_actor_failed_with_msg(
2469            &mut test_setup.stream_actor.status(),
2470            "request_status not allowed in recording".into(),
2471        )
2472        .await;
2473        Ok(())
2474    }
2475
2476    #[async_timed_test(timeout_secs = 60)]
2477    async fn test_init_comm_in_recording() -> Result<()> {
2478        let test_setup = TestSetup::new().await?;
2479        test_setup
2480            .stream_actor
2481            .define_recording(&test_setup.client, 0.into())
2482            .await?;
2483
2484        let dummy_comm = test_setup.proc.spawn(
2485            NcclCommActor::new(CommParams::New {
2486                device: CudaDevice::new(0.into()),
2487                unique_id: UniqueId::new_nccl()?,
2488                world_size: 1,
2489                rank: 0,
2490            })
2491            .await
2492            .unwrap(),
2493        );
2494
2495        test_setup
2496            .stream_actor
2497            .init_comm(&test_setup.client, dummy_comm)
2498            .await?;
2499        assert_actor_failed_with_msg(
2500            &mut test_setup.stream_actor.status(),
2501            "init_comm not allowed in recording".into(),
2502        )
2503        .await;
2504        Ok(())
2505    }
2506
2507    #[async_timed_test(timeout_secs = 60)]
2508    async fn test_borrow_create_duplicate_borrow() -> Result<()> {
2509        let mut test_setup = TestSetup::new().await?;
2510        test_setup
2511            .stream_actor
2512            .define_recording(&test_setup.client, 0.into())
2513            .await?;
2514
2515        let borrow_id = 1;
2516        let tensor_ref = test_setup.next_ref();
2517        let (first_use_sender, _first_use_receiver) = test_setup.client.open_port();
2518
2519        test_setup
2520            .stream_actor
2521            .borrow_create(
2522                &test_setup.client,
2523                borrow_id,
2524                tensor_ref,
2525                first_use_sender.clone(),
2526            )
2527            .await?;
2528
2529        test_setup
2530            .stream_actor
2531            .borrow_create(&test_setup.client, borrow_id, tensor_ref, first_use_sender)
2532            .await?;
2533
2534        assert_actor_failed_with_msg(
2535            &mut test_setup.stream_actor.status(),
2536            "duplicate borrow create in recording".into(),
2537        )
2538        .await;
2539
2540        Ok(())
2541    }
2542
2543    #[async_timed_test(timeout_secs = 60)]
2544    async fn test_borrow_drop_borrow_not_defined() -> Result<()> {
2545        let test_setup = TestSetup::new().await?;
2546        test_setup
2547            .stream_actor
2548            .define_recording(&test_setup.client, 0.into())
2549            .await?;
2550
2551        let borrow_id = 1;
2552        let (_last_use_sender, last_use_receiver) = test_setup.client.open_port();
2553
2554        test_setup
2555            .stream_actor
2556            .borrow_drop(
2557                &test_setup.client,
2558                borrow_id,
2559                Arc::new(Mutex::new(last_use_receiver)),
2560            )
2561            .await?;
2562
2563        assert_actor_failed_with_msg(
2564            &mut test_setup.stream_actor.status(),
2565            "borrow drop for borrow not defined in recording".into(),
2566        )
2567        .await;
2568
2569        Ok(())
2570    }
2571
2572    #[async_timed_test(timeout_secs = 60)]
2573    async fn test_borrow_not_dropped_before_finalize() -> Result<()> {
2574        let mut test_setup = TestSetup::new().await?;
2575        test_setup
2576            .stream_actor
2577            .define_recording(&test_setup.client, 0.into())
2578            .await?;
2579
2580        let borrow_id = 1;
2581        let tensor_ref = test_setup.next_ref();
2582        let (first_use_sender, _first_use_receiver) = test_setup.client.open_port();
2583
2584        test_setup
2585            .stream_actor
2586            .borrow_create(
2587                &test_setup.client,
2588                borrow_id,
2589                tensor_ref,
2590                first_use_sender.clone(),
2591            )
2592            .await?;
2593
2594        // Attempt to finalize the recording without dropping the borrow
2595        test_setup
2596            .stream_actor
2597            .finalize_recording(&test_setup.client, 0.into())
2598            .await?;
2599
2600        assert_actor_failed_with_msg(
2601            &mut test_setup.stream_actor.status(),
2602            "all borrows created within recording must be dropped within recording".into(),
2603        )
2604        .await;
2605
2606        Ok(())
2607    }
2608}