Skip to main content

monarch_tensor_worker/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9#![feature(duration_constructors)]
10// NOTE: Until https://github.com/PyO3/pyo3/pull/4674, `pyo3::pymethods` trigger
11// and unsafe-op-in-unsafe-fn warnings.
12#![allow(unsafe_op_in_unsafe_fn)]
13#![deny(clippy::disallowed_methods)]
14
15//! A `hyperactor`-based implementation of a PyTorch worker actor.
16//!
17//! The worker is responsible for executing PyTorch operations on a local
18//! device. It assumes it has exclusive access to device resources, and manages
19//! concurrency internally via device-specific constructs (CUDA stream, threads,
20//! etc.).
21//!
22//! This is a port of `monarch/python/controller/worker.py` but does have gaps due
23//! to drift that needs to be reconciled.
24//! This mainly includes:
25//! - Support for record and replay
26//! - debugger support
27//! - general drift in exisitng messages
28
29mod borrow;
30mod comm;
31pub mod device_mesh;
32pub mod stream;
33pub mod test_util;
34
35use std::collections::HashMap;
36use std::collections::HashSet;
37use std::collections::hash_map::Entry;
38use std::sync::Arc;
39
40use anyhow::Context;
41use anyhow::Result;
42use anyhow::anyhow;
43use anyhow::bail;
44use anyhow::ensure;
45use async_trait::async_trait;
46use borrow::Borrow;
47use comm::CommMessageClient;
48use comm::CommParams;
49use comm::NcclCommActor;
50use device_mesh::DeviceMesh;
51use futures::future::try_join_all;
52use hyperactor as reference;
53use hyperactor::Actor;
54use hyperactor::ActorEnvironment;
55use hyperactor::Handler;
56use hyperactor::RemoteSpawn;
57use hyperactor::actor::ActorHandle;
58use hyperactor::context;
59use hyperactor_mesh::casting::CastInfo;
60use itertools::Itertools;
61use monarch_gil::GilSite;
62use monarch_gil::monarch_with_gil_blocking;
63use monarch_hyperactor::shape::PyPoint;
64use monarch_messages::controller::ControllerActor;
65use monarch_messages::controller::ControllerMessageClient;
66use monarch_messages::controller::Seq;
67use monarch_messages::wire_value::WireValue;
68use monarch_messages::worker::ActorCallParams;
69use monarch_messages::worker::ActorMethodParams;
70use monarch_messages::worker::ArgsKwargs;
71use monarch_messages::worker::CallFunctionParams;
72use monarch_messages::worker::Factory;
73use monarch_messages::worker::Reduction;
74use monarch_messages::worker::Ref;
75use monarch_messages::worker::ResolvableFunction;
76use monarch_messages::worker::StreamCreationMode;
77use monarch_messages::worker::StreamRef;
78use monarch_messages::worker::WorkerMessage;
79use monarch_messages::worker::WorkerMessageHandler;
80use monarch_messages::worker::WorkerParams;
81use monarch_types::ReduceOp;
82use monarch_types::UniqueId;
83use ndslice::Slice;
84use pyo3::types::PyAnyMethods;
85use serde::Deserialize;
86use serde::Serialize;
87use sorted_vec::SortedVec;
88use stream::StreamActor;
89use stream::StreamMessageClient;
90use stream::StreamParams;
91use torch_sys2::CudaDevice;
92use torch_sys2::DeviceIndex;
93use torch_sys2::Layout;
94use torch_sys2::ScalarType;
95use torch_sys2::TensorCell;
96use torch_sys2::factory_zeros;
97use typeuri::Named;
98
99#[derive(Debug)]
100struct RemoteProcessGroupState {
101    device_mesh_ref: Ref,
102    dims: SortedVec<String>,
103    comms: HashMap<StreamRef, Arc<ActorHandle<NcclCommActor>>>,
104}
105
106impl RemoteProcessGroupState {
107    fn new(device_mesh_ref: Ref, dims: SortedVec<String>) -> Self {
108        Self {
109            device_mesh_ref,
110            dims,
111            comms: HashMap::new(),
112        }
113    }
114}
115
116#[derive(Debug)]
117enum Recording {
118    // In the process of receiving DefineRecording messages for this
119    // recording.
120    PartialRecording {
121        // The index of the last DefineRecording message received.
122        last_index: usize,
123        // The list of commands seen so far for this recording.
124        commands: Vec<WorkerMessage>,
125    },
126
127    // The recording is ready to be run.
128    CompleteRecording {
129        // The list of streams on which this recording is defined.
130        streams: HashSet<StreamRef>,
131    },
132}
133
134/// A PyTorch runtime instance, operating on a single accelerator device,
135/// controlled via hyperactor messaging.
136///
137/// Generally this is a thin multiplexer over a set of [`Stream`]s that do the
138/// real work.
139///
140/// See [`WorkerMessage`] for what it can do!
141#[derive(Debug)]
142#[hyperactor::spawnable]
143#[hyperactor::export(
144    handlers = [
145        WorkerMessage,
146        AssignRankMessage,
147    ],
148)]
149pub struct WorkerActor {
150    device: Option<CudaDevice>,
151    /// The device index this worker was spawned for, if any. The `CudaDevice` is
152    /// built from this in the `AssignRankMessage` handler -- after the GPU mask is
153    /// applied -- so we never hold a device before the mask nor query torch too
154    /// early (see `runtime_has_cuda` and the handler).
155    device_index: Option<i8>,
156    streams: HashMap<StreamRef, Arc<ActorHandle<StreamActor>>>,
157    /// Maps streams to the device mesh and a map of dim names to the concrete
158    /// communicator actor that represents the dimension for that stream.
159    device_meshes: HashMap<
160        Ref,
161        (
162            DeviceMesh,
163            // A map for comms for this mesh for a given pair of stream and dims.
164            HashMap<(StreamRef, SortedVec<String>), (usize, Arc<ActorHandle<NcclCommActor>>)>,
165        ),
166    >,
167    world_size: usize,
168    rank: usize,
169    borrows: HashMap<u64, Borrow>,
170    comm: Option<ActorHandle<NcclCommActor>>,
171    controller_actor: reference::ActorRef<ControllerActor>,
172    /// Remember the process groups "created" via `CreateRemoteProcessGroup` for
173    /// subsequent `CallFunction` calls, as this is where the actual allocation
174    /// will happen.
175    remote_process_groups: HashMap<Ref, RemoteProcessGroupState>,
176    /// The comm actor for each pair of streams that need to send/recv tensors.
177    send_recv_comms: HashMap<(StreamRef, StreamRef), Arc<ActorHandle<NcclCommActor>>>,
178    recordings: HashMap<Ref, Recording>,
179    defining_recording: Option<Ref>,
180    respond_with_python_message: bool,
181}
182
183impl WorkerActor {
184    fn runtime_has_cuda() -> bool {
185        // Whether this worker actually has a usable CUDA/ROCm device.
186        //
187        // Uses `torch.cuda.is_available()`, which calls `torch.cuda.device_count()`.
188        // c10 memoizes that count on the first call (c10/cuda/CUDAFunctions.cpp
189        // `device_count()`), reading the visible-device env vars at that moment, so
190        // this MUST only be called AFTER `_initialize_env` has applied the per-worker
191        // GPU mask (i.e. from the AssignRankMessage handler). Calling it during
192        // worker construction would latch the full host device count and every
193        // worker would keep seeing all GPUs after masking (observed on multi-GPU
194        // ROCm: `get_rng_state_all()` returned one state per host GPU). `remote_spawn`
195        // therefore never queries devices -- it just stores the spawn's device_index,
196        // and the `AssignRankMessage` handler builds the `CudaDevice` from it once the
197        // mask is applied.
198        monarch_with_gil_blocking(GilSite::WorkerInit, |py| {
199            py.import("torch")
200                .expect("torch must be importable in a worker")
201                .getattr("cuda")
202                .expect("torch.cuda attribute must exist")
203                .call_method0("is_available")
204                .expect("torch.cuda.is_available() must be callable")
205                .extract::<bool>()
206                .expect("torch.cuda.is_available() must return bool")
207        })
208    }
209
210    fn try_get_stream(&self, stream: StreamRef) -> Result<&Arc<ActorHandle<StreamActor>>> {
211        self.streams
212            .get(&stream)
213            .ok_or(anyhow::anyhow!("invalid stream id: {:#?}", stream))
214    }
215
216    async fn maybe_add_stream_to_recording(
217        &mut self,
218        cx: &impl context::Actor,
219        stream: StreamRef,
220    ) -> Result<()> {
221        // If we're defining a recording, add the stream to the list of streams that
222        // this recording uses, and call define_recording on the stream.
223        if let Some(defining_recording) = self.defining_recording {
224            let recording = self.recordings.get_mut(&defining_recording).unwrap();
225            let fut = match recording {
226                Recording::PartialRecording { .. } => panic!("unreachable, in theory"),
227                Recording::CompleteRecording { streams } => {
228                    streams.insert(stream).then(|| -> Result<_, anyhow::Error> {
229                        Ok(self
230                            .try_get_stream(stream)?
231                            .define_recording(cx, defining_recording))
232                    })
233                }
234            }
235            .transpose()?;
236            match fut {
237                Some(fut) => fut.await,
238                None => Ok(()),
239            }
240        } else {
241            Ok(())
242        }
243    }
244}
245
246impl Actor for WorkerActor {}
247
248#[async_trait]
249impl RemoteSpawn for WorkerActor {
250    type Params = WorkerParams;
251
252    async fn new(
253        WorkerParams {
254            world_size,
255            rank,
256            device_index,
257            controller_actor,
258        }: Self::Params,
259        _environment: &ActorEnvironment,
260    ) -> Result<Self> {
261        monarch_with_gil_blocking(GilSite::WorkerInit, |py| {
262            py.import("monarch.safe_torch").unwrap();
263        });
264        // Defer building the device to the `AssignRankMessage` handler, which runs
265        // after `_initialize_env` applies the per-worker GPU mask. We must not query
266        // torch here (any device query before the mask latches the unmasked host
267        // device count -- see `runtime_has_cuda`), and we must not hold a `CudaDevice`
268        // before the mask confirms one exists (a stale device would be handed to real
269        // HIP/CUDA calls if a device-using message ever arrived before AssignRank). So
270        // stash the index now and resolve it there.
271        Ok(Self {
272            device: None,
273            device_index,
274            streams: HashMap::new(),
275            device_meshes: HashMap::new(),
276            world_size,
277            rank,
278            borrows: HashMap::new(),
279            comm: None,
280            controller_actor,
281            remote_process_groups: HashMap::new(),
282            send_recv_comms: HashMap::new(),
283            recordings: HashMap::new(),
284            defining_recording: None,
285            respond_with_python_message: false,
286        })
287    }
288
289    // TODO: Exit the worker directly on any worker actor errors, with error exit code.
290}
291
292#[async_trait]
293impl Handler<AssignRankMessage> for WorkerActor {
294    async fn handle(
295        &mut self,
296        cx: &hyperactor::Context<Self>,
297        _: AssignRankMessage,
298    ) -> anyhow::Result<()> {
299        let point = cx.cast_point();
300        self.rank = point.rank();
301        self.respond_with_python_message = true;
302        monarch_with_gil_blocking(GilSite::WorkerInit, |py| {
303            let mesh_controller = py.import("monarch.mesh_controller").unwrap();
304            let p: PyPoint = point.into();
305            mesh_controller
306                .call_method1("_initialize_env", (p, cx.proc().proc_addr().to_string()))
307                .unwrap();
308        });
309        // The GPU mask is now applied, so it is safe to query torch and build the
310        // device. Resolve it from the stored index only if this worker actually has a
311        // usable device; otherwise leave it None.
312        self.device = if Self::runtime_has_cuda() {
313            self.device_index.map(|i| CudaDevice::new(DeviceIndex(i)))
314        } else {
315            None
316        };
317        Ok(())
318    }
319}
320
321/// Worker messages. These define the observable behavior of the worker, so the
322/// documentations here
323#[derive(Handler, Clone, Serialize, Deserialize, Debug, Named)]
324pub enum AssignRankMessage {
325    AssignRank(),
326}
327wirevalue::register_type!(AssignRankMessage);
328
329#[async_trait]
330impl Handler<WorkerMessage> for WorkerActor {
331    async fn handle(
332        &mut self,
333        cx: &hyperactor::Context<Self>,
334        message: WorkerMessage,
335    ) -> anyhow::Result<()> {
336        <Self as WorkerMessageHandler>::handle(self, cx, message).await
337    }
338}
339
340#[async_trait]
341impl WorkerMessageHandler for WorkerActor {
342    async fn backend_network_init(
343        &mut self,
344        cx: &hyperactor::Context<Self>,
345        unique_id: UniqueId,
346    ) -> Result<()> {
347        let Some(device) = self.device else {
348            return Ok(());
349        };
350        let comm = cx.spawn(
351            NcclCommActor::new(CommParams::New {
352                device,
353                unique_id,
354                world_size: self.world_size.try_into().unwrap(),
355                rank: self.rank.try_into().unwrap(),
356            })
357            .await?,
358        );
359
360        let tensor = factory_zeros(&[1], ScalarType::Float, Layout::Strided, device.into());
361        let cell = TensorCell::new(tensor);
362
363        comm.all_reduce(
364            cx,
365            cell,
366            ReduceOp::Sum,
367            torch_sys_cuda::cuda::Stream::get_current_stream()?,
368        )
369        .await?;
370
371        // TODO: this blocks forward progress of the the actor loop while we
372        // wait for the streams to catch up. Once we have a way of spawning
373        // tasks that the actor system can monitor in a non-blocking way, we
374        // should remove this.
375
376        // We need to be careful to initialize the streams in a consistent order
377        // across all workers to avoid NCCL deadlocks. Use the refs to provide
378        // this order, as a stream's ref will be the same across all workers.
379        let sorted_streams = self
380            .streams
381            .iter()
382            .sorted_by_key(|(k, _)| *k)
383            .map(|(_, v)| v.as_ref());
384
385        let mut splits = Vec::new();
386        for _ in 0..sorted_streams.len() {
387            // Do the split in this event loop, to provide a deterministic
388            // order.
389            splits.push(comm.split_all(cx).await?);
390        }
391        let _: Vec<()> = try_join_all(
392            sorted_streams
393                .into_iter()
394                .zip(splits)
395                .map(|(stream, split)| stream.init_comm(cx, split)),
396        )
397        .await?;
398
399        self.comm = Some(comm);
400
401        Ok(())
402    }
403
404    async fn backend_network_point_to_point_init(
405        &mut self,
406        cx: &hyperactor::Context<Self>,
407        from_stream: StreamRef,
408        to_stream: StreamRef,
409    ) -> Result<()> {
410        if !self.streams.contains_key(&from_stream) {
411            bail!("invalid from_stream id: {:#?}", from_stream);
412        }
413        if !self.streams.contains_key(&to_stream) {
414            bail!("invalid to_stream id: {:#?}", to_stream);
415        }
416        let Some(global_comm) = self.comm.as_ref() else {
417            return Ok(());
418        };
419        let comm = global_comm.split_all(cx).await?;
420        self.send_recv_comms
421            .insert((from_stream, to_stream), Arc::new(comm));
422        Ok(())
423    }
424
425    async fn call_function(
426        &mut self,
427        cx: &hyperactor::Context<Self>,
428        params: CallFunctionParams,
429    ) -> Result<()> {
430        let stream = self.try_get_stream(params.stream)?.clone();
431        self.maybe_add_stream_to_recording(cx, params.stream)
432            .await?;
433
434        let device_meshes = self
435            .device_meshes
436            .iter()
437            .map(|(k, v)| (*k, v.0.clone()))
438            .collect();
439
440        let mut remote_process_groups = HashMap::new();
441        for remote_process_group_ref in &params.remote_process_groups {
442            if let Some(state) = self.remote_process_groups.get(remote_process_group_ref) {
443                let dims_vec = state.dims.iter().cloned().collect();
444                let (device_mesh, _) = self
445                    .device_meshes
446                    .get(&state.device_mesh_ref)
447                    .ok_or_else(|| {
448                        anyhow::anyhow!("invalid device mesh id: {:#?}", state.device_mesh_ref)
449                    })?
450                    .clone();
451                let comm = state.comms
452                    .get(&params.stream)
453                    .ok_or_else(|| {
454                        anyhow::anyhow!("no comm found for remote process group {remote_process_group_ref:#?} stream {stream:#?}")
455                    })?
456                    .clone();
457                remote_process_groups
458                    .insert(*remote_process_group_ref, (device_mesh, dims_vec, comm));
459            }
460        }
461
462        stream
463            .call_function(cx, params, device_meshes, remote_process_groups)
464            .await?;
465
466        Ok(())
467    }
468
469    async fn command_group(
470        &mut self,
471        cx: &hyperactor::Context<Self>,
472        params: Vec<WorkerMessage>,
473    ) -> Result<()> {
474        for msg in params {
475            WorkerMessageHandler::handle(self, cx, msg).await?;
476        }
477        Ok(())
478    }
479
480    async fn create_stream(
481        &mut self,
482        cx: &hyperactor::Context<Self>,
483        result: StreamRef,
484        creation_mode: StreamCreationMode,
485    ) -> Result<()> {
486        let handle: ActorHandle<StreamActor> = cx.spawn(StreamActor::new(StreamParams {
487            world_size: self.world_size,
488            rank: self.rank,
489            creation_mode,
490            id: result,
491            device: self.device,
492            controller_actor: self.controller_actor.clone(),
493            respond_with_python_message: self.respond_with_python_message,
494        }));
495        self.streams.insert(result, Arc::new(handle));
496        Ok(())
497    }
498
499    async fn create_device_mesh(
500        &mut self,
501        _cx: &hyperactor::Context<Self>,
502        result: Ref,
503        names: Vec<String>,
504        ranks: Slice,
505    ) -> Result<()> {
506        self.device_meshes.insert(
507            result,
508            (DeviceMesh::new(names, ranks, self.rank)?, HashMap::new()),
509        );
510        Ok(())
511    }
512
513    async fn create_remote_process_group(
514        &mut self,
515        _cx: &hyperactor::Context<Self>,
516        result: Ref,
517        device_mesh: Ref,
518        dims: Vec<String>,
519    ) -> Result<()> {
520        self.device_meshes
521            .get(&device_mesh)
522            .with_context(|| format!("invalid device mesh id: {:#?}", device_mesh))?;
523        match self.remote_process_groups.entry(result) {
524            Entry::Vacant(ent) => ent.insert(RemoteProcessGroupState::new(
525                device_mesh,
526                SortedVec::from_unsorted(dims),
527            )),
528            Entry::Occupied(ent) => bail!("remote process group {:?} already create", ent.key()),
529        };
530        Ok(())
531    }
532
533    async fn borrow_create(
534        &mut self,
535        cx: &hyperactor::Context<Self>,
536        result: Ref,
537        borrow_id: u64,
538        tensor_ref: Ref,
539        from_stream: StreamRef,
540        to_stream: StreamRef,
541    ) -> Result<()> {
542        self.maybe_add_stream_to_recording(cx, from_stream).await?;
543        self.maybe_add_stream_to_recording(cx, to_stream).await?;
544        let from_stream = self.try_get_stream(from_stream)?.clone();
545        let to_stream = self.try_get_stream(to_stream)?.clone();
546
547        let borrow =
548            Borrow::create(cx, borrow_id, tensor_ref, result, from_stream, to_stream).await?;
549        self.borrows.insert(borrow_id, borrow);
550        Ok(())
551    }
552
553    async fn borrow_first_use(
554        &mut self,
555        cx: &hyperactor::Context<Self>,
556        borrow: u64,
557    ) -> Result<()> {
558        let borrow = self
559            .borrows
560            .get_mut(&borrow)
561            .ok_or_else(|| anyhow!("invalid borrow id: {:#?}", borrow))?;
562
563        borrow.first_use(cx).await?;
564        Ok(())
565    }
566
567    async fn borrow_last_use(&mut self, cx: &hyperactor::Context<Self>, borrow: u64) -> Result<()> {
568        let borrow = self
569            .borrows
570            .get_mut(&borrow)
571            .ok_or_else(|| anyhow::anyhow!("invalid borrow id: {:#?}", borrow))?;
572
573        borrow.last_use(cx).await?;
574        Ok(())
575    }
576
577    async fn borrow_drop(&mut self, cx: &hyperactor::Context<Self>, borrow_id: u64) -> Result<()> {
578        let borrow = self
579            .borrows
580            .get_mut(&borrow_id)
581            .ok_or_else(|| anyhow::anyhow!("invalid borrow id: {:#?}", borrow_id))?;
582
583        borrow.drop(cx).await?;
584        self.borrows.remove(&borrow_id);
585        Ok(())
586    }
587
588    async fn delete_refs(&mut self, cx: &hyperactor::Context<Self>, refs: Vec<Ref>) -> Result<()> {
589        // Fan the delete message to all streams.
590        // Check for errors.
591        // TODO: this blocks forward progress of the the actor loop while we
592        // wait for the streams to catch up. Once we have a way of spawning
593        // tasks that the actor system can monitor in a non-blocking way, we
594        // should remove this.
595        let _: Vec<()> = try_join_all(
596            self.streams
597                .values()
598                .map(|s| s.delete_refs(cx, refs.clone())),
599        )
600        .await?;
601        Ok(())
602    }
603
604    async fn request_status(
605        &mut self,
606        cx: &hyperactor::Context<Self>,
607        seq: Seq,
608        controller: bool,
609    ) -> Result<()> {
610        // TODO: this blocks forward progress of the the actor loop while we
611        // wait for the streams to catch up. Once we have a way of spawning
612        // tasks that the actor system can monitor in a non-blocking way, we
613        // should remove this.
614        let _: Vec<()> = try_join_all(
615            self.streams
616                .values()
617                .map(|stream| stream.request_status(cx)),
618        )
619        .await?;
620
621        ControllerMessageClient::status(
622            &self.controller_actor,
623            cx,
624            seq.next(),
625            cx.self_addr().clone(),
626            controller,
627        )
628        .await?;
629        Ok(())
630    }
631
632    async fn reduce(
633        &mut self,
634        cx: &hyperactor::Context<Self>,
635        result: Ref,
636        local_tensor: Ref,
637        factory: Factory,
638        source_mesh: Ref,
639        stream_ref: StreamRef,
640        dims: Vec<String>,
641        reduction: Reduction,
642        scatter: bool,
643        in_place: bool,
644        out: Option<Ref>,
645    ) -> Result<()> {
646        self.maybe_add_stream_to_recording(cx, stream_ref).await?;
647
648        // Sort for stable indexing.
649        let dims = SortedVec::from_unsorted(dims);
650        let stream = self.try_get_stream(stream_ref)?.clone();
651
652        let (_, comm_map) = self
653            .device_meshes
654            .get_mut(&source_mesh)
655            .ok_or_else(|| anyhow::anyhow!("invalid device mesh id: {:#?}", source_mesh))?;
656
657        let (size, comm) = comm_map
658            .get(&(stream_ref, dims.clone()))
659            .ok_or_else(|| anyhow::anyhow!("no comm found for stream {stream:#?}, dims {dims:#?}"))?
660            .clone();
661
662        stream
663            .reduce(
664                cx,
665                comm,
666                size.try_into()?,
667                result,
668                local_tensor,
669                factory,
670                reduction,
671                scatter,
672                in_place,
673                out,
674            )
675            .await?;
676
677        Ok(())
678    }
679
680    async fn send_tensor(
681        &mut self,
682        cx: &hyperactor::Context<Self>,
683        result: Ref,
684        from_ranks: Slice,
685        to_ranks: Slice,
686        tensor: Ref,
687        factory: Factory,
688        from_stream: StreamRef,
689        to_stream: StreamRef,
690    ) -> Result<()> {
691        let to_rank = from_ranks
692            .index(self.rank)
693            .map(|index| to_ranks.get(index).ok())
694            .ok()
695            .flatten();
696        let from_rank = to_ranks
697            .index(self.rank)
698            .map(|index| from_ranks.get(index).ok())
699            .ok()
700            .flatten();
701
702        let (stream, stream_ref) = if to_rank.is_none() {
703            (self.try_get_stream(to_stream)?.clone(), to_stream)
704        } else if from_rank.is_none() || from_stream == to_stream {
705            (self.try_get_stream(from_stream)?.clone(), from_stream)
706        } else {
707            unimplemented!(
708                "We haven't implemented to_mesh between streams if a rank participates as both a sender and receiver. \
709                It is possible, but would require the recv stream to send the output buffer tensor to the send stream and sync. \
710                Then the send stream would do the nccl op, and then sync with sending stream again."
711            );
712        };
713        let comm = if from_rank == to_rank {
714            None
715        } else {
716            Some(
717                self.send_recv_comms
718                    .get(&(from_stream, to_stream))
719                    .ok_or_else(|| {
720                        anyhow::anyhow!(
721                            "could not find stream to stream comm for: {:#?}",
722                            (from_stream, to_stream)
723                        )
724                    })?
725                    .clone(),
726            )
727        };
728
729        self.maybe_add_stream_to_recording(cx, stream_ref).await?;
730
731        stream
732            .send_tensor(cx, result, from_rank, to_rank, tensor, factory, comm)
733            .await?;
734
735        Ok(())
736    }
737
738    async fn exit(
739        &mut self,
740        cx: &hyperactor::Context<Self>,
741        error: Option<(Option<reference::ActorAddr>, String)>,
742    ) -> Result<()> {
743        for (_, stream) in self.streams.drain() {
744            stream.drain_and_stop("tensor worker exit cleanup")?;
745            Arc::into_inner(stream)
746                .expect("there should be no owners of this stream handle except the worker stream table")
747                .await;
748        }
749
750        let self_error_exit_code = std::env::var("MONARCH_TENSOR_WORKER_SELF_ERROR_EXIT_CODE")
751            .ok()
752            .and_then(|val| val.parse::<i32>().ok())
753            .unwrap_or(1);
754        let peer_error_exit_code = std::env::var("MONARCH_TENSOR_WORKER_PEER_ERROR_EXIT_CODE")
755            .ok()
756            .and_then(|val| val.parse::<i32>().ok())
757            .unwrap_or(1);
758
759        // Exit the worker process if there is an error.
760        let exit_code = match error {
761            Some((Some(actor_id), reason)) => {
762                tracing::error!(
763                    "stopping the worker, actor {} failed with error: {}",
764                    actor_id,
765                    reason
766                );
767                if cx.self_addr() == &actor_id {
768                    self_error_exit_code
769                } else {
770                    peer_error_exit_code
771                }
772            }
773            Some((None, reason)) => {
774                tracing::error!("stopping the worker, reason: {}", reason);
775                1
776            }
777            None => 0,
778        };
779
780        if exit_code != 0 {
781            tracing::info!("stopping the worker process, exit code: {}", exit_code);
782            std::process::exit(exit_code);
783        }
784        cx.stop("tensor worker exit")?;
785        Ok(())
786    }
787
788    async fn send_value(
789        &mut self,
790        cx: &hyperactor::Context<Self>,
791        seq: Seq,
792        destination: Option<Ref>,
793        mutates: Vec<Ref>,
794        function: Option<ResolvableFunction>,
795        args_kwargs: ArgsKwargs,
796        stream: StreamRef,
797    ) -> Result<()> {
798        // Resolve the stream.
799        let stream = self.try_get_stream(stream)?;
800
801        let device_meshes = if function.is_none() {
802            HashMap::new()
803        } else {
804            self.device_meshes
805                .iter()
806                .map(|(k, v)| (*k, v.0.clone()))
807                .collect()
808        };
809
810        if destination.is_some() {
811            panic!("send_value with pipe destination is no longer implemented")
812        }
813
814        // Resolve the value on the stream, then send the value back to the controller.
815        stream
816            .send_value(
817                cx,
818                seq,
819                cx.self_addr().clone(),
820                mutates,
821                function,
822                args_kwargs,
823                device_meshes,
824            )
825            .await
826    }
827
828    async fn send_result_of_actor_call(
829        &mut self,
830        cx: &hyperactor::Context<Self>,
831        params: ActorCallParams,
832    ) -> Result<()> {
833        let stream = self.try_get_stream(params.stream)?;
834        stream.send_result_of_actor_call(cx, params).await?;
835        Ok(())
836    }
837    async fn call_actor_method(
838        &mut self,
839        cx: &hyperactor::Context<Self>,
840        params: ActorMethodParams,
841    ) -> Result<()> {
842        let stream = self.try_get_stream(params.call.stream)?;
843        stream.call_actor_method(cx, params).await?;
844        Ok(())
845    }
846    async fn split_comm(
847        &mut self,
848        cx: &hyperactor::Context<Self>,
849        dims: Vec<String>,
850        device_mesh: Ref,
851        stream_ref: StreamRef,
852    ) -> Result<()> {
853        let Some(global_comm) = self.comm.as_ref() else {
854            return Ok(());
855        };
856        match self.device_meshes.get_mut(&device_mesh) {
857            Some((device_mesh, comm_map)) => {
858                // This rank is in the group to be split off. Split a new
859                // communicator for it off from the global communicator.
860                let stream = self
861                    .streams
862                    .get(&stream_ref)
863                    .ok_or_else(|| anyhow::anyhow!("invalid stream id: {:#?}", stream_ref))?;
864
865                let dims = SortedVec::from_unsorted(dims);
866
867                anyhow::ensure!(
868                    !comm_map.contains_key(&(stream_ref, dims.clone())),
869                    "comm already exists for stream {stream:#?}, dims {dims:#?}"
870                );
871                let ranks_for_group = device_mesh.get_ranks_for_dim_slice(&dims)?;
872                let size = ranks_for_group.len();
873                let split_comm = global_comm
874                    .split_from(
875                        cx,
876                        ranks_for_group
877                            .into_iter()
878                            .map(|v| v.try_into())
879                            .collect::<Result<Vec<_>, _>>()?,
880                    )
881                    .await?
882                    .context("split comm should include self rank")?;
883                comm_map.insert((stream_ref, dims), (size, Arc::new(split_comm)));
884            }
885            None => {
886                // This rank is not in the group to be split off. We still need to
887                // participate in the commSplit call, however.
888                global_comm.split_from(cx, vec![]).await?;
889            }
890        }
891        Ok(())
892    }
893
894    async fn split_comm_for_process_group(
895        &mut self,
896        cx: &hyperactor::Context<Self>,
897        remote_process_group_ref: Ref,
898        stream_ref: StreamRef,
899    ) -> Result<()> {
900        ensure!(
901            self.streams.contains_key(&stream_ref),
902            "invalid stream id: {:#?}",
903            stream_ref
904        );
905        let Some(global_comm) = self.comm.as_ref() else {
906            return Ok(());
907        };
908        let state = self
909            .remote_process_groups
910            .get_mut(&remote_process_group_ref)
911            .with_context(|| format!("invalid remote process group id: {:#?}", stream_ref))?;
912        match self.device_meshes.get_mut(&state.device_mesh_ref) {
913            Some((device_mesh, _)) => {
914                // This rank is in the group to be split off. Split a new
915                // communicator for it off from the global communicator.
916                let entry = match state.comms.entry(stream_ref) {
917                    Entry::Vacant(entry) => entry,
918                    Entry::Occupied(_) => bail!(
919                        "comm already exists for remote process group {:#?} on stream {:#?}",
920                        remote_process_group_ref,
921                        stream_ref,
922                    ),
923                };
924                let ranks_for_group = device_mesh.get_ranks_for_dim_slice(&state.dims)?;
925                let split_comm = global_comm
926                    .split_from(
927                        cx,
928                        ranks_for_group
929                            .into_iter()
930                            .map(|v| v.try_into())
931                            .collect::<Result<Vec<_>, _>>()?,
932                    )
933                    .await?
934                    .context("split comm should include self rank")?;
935                entry.insert(Arc::new(split_comm));
936            }
937            None => {
938                // This rank is not in the group to be split off. We still need to
939                // participate in the commSplit call, however.
940                global_comm.split_from(cx, vec![]).await?;
941            }
942        }
943        Ok(())
944    }
945
946    async fn pipe_recv(
947        &mut self,
948        _cx: &hyperactor::Context<Self>,
949        _seq: Seq,
950        _results: Vec<Option<Ref>>,
951        _pipe: Ref,
952        _stream: StreamRef,
953    ) -> Result<()> {
954        panic!("pipe_recv is no longer implemented")
955    }
956
957    async fn set_ref_unit_tests_only(
958        &mut self,
959        cx: &hyperactor::Context<Self>,
960        reference: Ref,
961        value: WireValue,
962        stream: StreamRef,
963    ) -> Result<()> {
964        let stream = self.try_get_stream(stream)?;
965
966        stream.set_ref_unit_tests_only(cx, reference, value).await
967    }
968
969    async fn get_ref_unit_tests_only(
970        &mut self,
971        cx: &hyperactor::Context<Self>,
972        ref_id: Ref,
973        stream: StreamRef,
974    ) -> Result<Option<Result<WireValue, String>>> {
975        let stream = self.try_get_stream(stream)?;
976        Ok(stream.get_ref_unit_tests_only(cx, ref_id).await?)
977    }
978
979    async fn define_recording(
980        &mut self,
981        cx: &hyperactor::Context<Self>,
982        result: Ref,
983        _nresults: usize,
984        _nformals: usize,
985        commands: Vec<WorkerMessage>,
986        ntotal_messages: usize,
987        index: usize,
988    ) -> Result<()> {
989        if self.defining_recording.is_some() && self.defining_recording.unwrap() != result {
990            bail!("already defining a different recording");
991        }
992        self.defining_recording = Some(result);
993
994        match self.recordings.entry(result) {
995            Entry::Vacant(entry) => {
996                ensure!(
997                    index == 0,
998                    "got DefineRecording message with (index = {:?}) > 0 for previously unseen recording",
999                    index
1000                );
1001                entry.insert(Recording::PartialRecording {
1002                    last_index: 0,
1003                    commands,
1004                });
1005            }
1006            Entry::Occupied(mut entry) => match entry.get_mut() {
1007                Recording::CompleteRecording { .. } => {
1008                    bail!("got DefineRecording message for already complete recording")
1009                }
1010                Recording::PartialRecording {
1011                    last_index,
1012                    commands: existing_commands,
1013                } => {
1014                    ensure!(
1015                        index == *last_index + 1,
1016                        "Got DefineRecording message with index = {:?}, but \
1017                            last seen index for recording is {:?}",
1018                        index,
1019                        last_index
1020                    );
1021                    *last_index = index;
1022                    existing_commands.extend(commands);
1023                }
1024            },
1025        };
1026
1027        if index < ntotal_messages - 1 {
1028            return Ok(());
1029        }
1030        let commands = match self.recordings.remove(&result).unwrap() {
1031            Recording::CompleteRecording { .. } => panic!("unreachable, in theory"),
1032            Recording::PartialRecording { commands, .. } => {
1033                self.recordings.insert(
1034                    result,
1035                    Recording::CompleteRecording {
1036                        streams: HashSet::new(),
1037                    },
1038                );
1039                commands
1040            }
1041        };
1042
1043        for command in commands {
1044            WorkerMessageHandler::handle(self, cx, command).await?;
1045        }
1046
1047        match self.recordings.get(&result).unwrap() {
1048            Recording::PartialRecording { .. } => panic!("unreachable, in theory"),
1049            Recording::CompleteRecording { streams, .. } => {
1050                for stream in streams {
1051                    self.try_get_stream(*stream)?
1052                        .finalize_recording(cx, result)
1053                        .await?;
1054                }
1055            }
1056        }
1057
1058        self.defining_recording = None;
1059        Ok(())
1060    }
1061
1062    async fn recording_formal(
1063        &mut self,
1064        cx: &hyperactor::Context<Self>,
1065        result: Ref,
1066        argument_index: usize,
1067        stream: StreamRef,
1068    ) -> Result<()> {
1069        ensure!(self.defining_recording.is_some());
1070        self.maybe_add_stream_to_recording(cx, stream).await?;
1071        self.try_get_stream(stream)?
1072            .recording_formal(cx, result, argument_index)
1073            .await
1074    }
1075
1076    async fn recording_result(
1077        &mut self,
1078        cx: &hyperactor::Context<Self>,
1079        result: Ref,
1080        output_index: usize,
1081        stream: StreamRef,
1082    ) -> Result<()> {
1083        ensure!(self.defining_recording.is_some());
1084        self.maybe_add_stream_to_recording(cx, stream).await?;
1085        self.try_get_stream(stream)?
1086            .recording_result(cx, result, output_index)
1087            .await
1088    }
1089
1090    async fn call_recording(
1091        &mut self,
1092        cx: &hyperactor::Context<Self>,
1093        seq: Seq,
1094        recording: Ref,
1095        results: Vec<Ref>,
1096        actuals: Vec<Ref>,
1097    ) -> Result<()> {
1098        ensure!(self.defining_recording.is_none());
1099        let recording_ref = recording;
1100        let recording = self.recordings.get(&recording).ok_or(anyhow::anyhow!(
1101            "could not find recording: {:#?}",
1102            recording
1103        ))?;
1104        match recording {
1105            Recording::PartialRecording { .. } => {
1106                bail!("cannot call recording because it is incomplete")
1107            }
1108            Recording::CompleteRecording { streams } => try_join_all(
1109                streams
1110                    .iter()
1111                    .map(|stream| self.try_get_stream(*stream))
1112                    .collect::<Result<Vec<_>>>()?
1113                    .into_iter()
1114                    .map(|stream| {
1115                        stream.call_recording(
1116                            cx,
1117                            seq,
1118                            recording_ref,
1119                            results.clone(),
1120                            actuals.clone(),
1121                        )
1122                    }),
1123            )
1124            .await
1125            .map(|_| ()),
1126        }
1127    }
1128}
1129
1130#[cfg(all(test, fbcode_build))]
1131mod tests {
1132    use std::assert_matches;
1133
1134    use anyhow::Result;
1135    use hyperactor::RemoteSpawn;
1136    use hyperactor::channel::ChannelAddr;
1137    use hyperactor::proc::Proc;
1138    use monarch_messages::controller::ControllerMessage;
1139    use monarch_messages::controller::WorkerError;
1140    use monarch_messages::worker::WorkerMessageClient;
1141    use monarch_types::PickledPyObject;
1142    use pyo3::prelude::*;
1143    use pyo3::types::PyList;
1144    use pyo3::types::PyString;
1145    use rand::RngExt as _;
1146    use rand::distr::Alphanumeric;
1147    use timed_test::async_timed_test;
1148    use torch_sys_cuda::nccl::UniqueIdExt;
1149
1150    use super::*;
1151    use crate::test_util::test_setup;
1152
1153    #[async_timed_test(timeout_secs = 60)]
1154    async fn basic_worker() -> Result<()> {
1155        test_setup()?;
1156
1157        let proc = Proc::isolated();
1158        let (client, controller_ref, mut controller_rx) = proc.attach_actor("controller").unwrap();
1159
1160        let worker_handle = proc.spawn(
1161            WorkerActor::new(
1162                WorkerParams {
1163                    world_size: 1,
1164                    rank: 0,
1165                    device_index: None,
1166                    controller_actor: controller_ref,
1167                },
1168                &ActorEnvironment::default(),
1169            )
1170            .await
1171            .unwrap(),
1172        );
1173        worker_handle
1174            .command_group(
1175                &client,
1176                vec![
1177                    WorkerMessage::CreateStream {
1178                        id: 1.into(),
1179                        stream_creation: StreamCreationMode::UseDefaultStream,
1180                    },
1181                    WorkerMessage::CallFunction(CallFunctionParams {
1182                        seq: 0.into(),
1183                        results: vec![Some(0.into())],
1184                        mutates: vec![],
1185                        function: "torch.ops.aten.ones.default".into(),
1186                        args_kwargs: ArgsKwargs::from_wire_values(
1187                            vec![WireValue::IntList(vec![2, 3])],
1188                            HashMap::new(),
1189                        )
1190                        .unwrap(),
1191                        stream: 1.into(),
1192                        remote_process_groups: vec![],
1193                    }),
1194                    WorkerMessage::CallFunction(CallFunctionParams {
1195                        seq: 2.into(),
1196                        results: vec![Some(Ref { id: 2 })],
1197                        mutates: vec![0.into()],
1198                        function: "torch.ops.aten.sub_.Scalar".into(),
1199                        args_kwargs: ArgsKwargs::from_wire_values(
1200                            vec![WireValue::Ref(0.into()), WireValue::Int(1)],
1201                            HashMap::new(),
1202                        )
1203                        .unwrap(),
1204                        stream: 1.into(),
1205                        remote_process_groups: vec![],
1206                    }),
1207                    WorkerMessage::CallFunction(CallFunctionParams {
1208                        seq: 3.into(),
1209                        results: vec![Some(Ref { id: 3 })],
1210                        mutates: vec![],
1211                        function: "torch.ops.aten.zeros.default".into(),
1212                        args_kwargs: ArgsKwargs::from_wire_values(
1213                            vec![WireValue::IntList(vec![2, 3])],
1214                            HashMap::new(),
1215                        )
1216                        .unwrap(),
1217                        stream: 1.into(),
1218                        remote_process_groups: vec![],
1219                    }),
1220                    WorkerMessage::CallFunction(CallFunctionParams {
1221                        seq: 4.into(),
1222                        results: vec![Some(Ref { id: 4 })],
1223                        mutates: vec![],
1224                        function: "torch.ops.aten.allclose.default".into(),
1225                        args_kwargs: ArgsKwargs::from_wire_values(
1226                            vec![WireValue::Ref(0.into()), WireValue::Ref(Ref { id: 3 })],
1227                            HashMap::new(),
1228                        )
1229                        .unwrap(),
1230                        stream: 1.into(),
1231                        remote_process_groups: vec![],
1232                    }),
1233                ],
1234            )
1235            .await
1236            .unwrap();
1237
1238        let result: bool = worker_handle
1239            .get_ref_unit_tests_only(&client, Ref { id: 4 }, 1.into())
1240            .await
1241            .unwrap()
1242            .unwrap()
1243            .unwrap()
1244            .try_into()
1245            .unwrap();
1246        worker_handle.drain_and_stop("test").unwrap();
1247        worker_handle.await;
1248        let error_responses = controller_rx.drain();
1249        assert!(
1250            error_responses.is_empty(),
1251            "Expected no error responses, got: {:#?}",
1252            error_responses
1253        );
1254        assert!(result);
1255
1256        Ok(())
1257    }
1258
1259    #[async_timed_test(timeout_secs = 60)]
1260    async fn error_sends_response() -> Result<()> {
1261        test_setup()?;
1262
1263        let proc = Proc::isolated();
1264        let (client, controller_ref, mut controller_rx) = proc.attach_actor("controller").unwrap();
1265
1266        let worker_handle = proc.spawn(
1267            WorkerActor::new(
1268                WorkerParams {
1269                    world_size: 1,
1270                    rank: 0,
1271                    device_index: None,
1272                    controller_actor: controller_ref,
1273                },
1274                &ActorEnvironment::default(),
1275            )
1276            .await
1277            .unwrap(),
1278        );
1279        worker_handle
1280            .command_group(
1281                &client,
1282                vec![
1283                    WorkerMessage::CreateStream {
1284                        id: 1.into(),
1285                        stream_creation: StreamCreationMode::UseDefaultStream,
1286                    },
1287                    WorkerMessage::CallFunction(CallFunctionParams {
1288                        seq: 0.into(),
1289                        results: vec![Some(0.into())],
1290                        mutates: vec![],
1291                        function: "torch.ops.aten.rand.default".into(),
1292                        args_kwargs: ArgsKwargs::from_wire_values(vec![], HashMap::new()).unwrap(),
1293                        stream: 1.into(),
1294                        remote_process_groups: vec![],
1295                    }),
1296                    WorkerMessage::Exit { error: None },
1297                ],
1298            )
1299            .await
1300            .unwrap();
1301
1302        worker_handle.drain_and_stop("test").unwrap();
1303        worker_handle.await;
1304        let response_message = controller_rx.recv().await.unwrap();
1305        match response_message {
1306            ControllerMessage::RemoteFunctionFailed {
1307                seq,
1308                error: WorkerError { backtrace: msg, .. },
1309            } => {
1310                assert_eq!(seq, 0.into());
1311                assert!(msg.contains("aten::rand() is missing value for argument 'size'"))
1312            }
1313            _ => panic!("unexpected response {:#?}", response_message),
1314        }
1315
1316        Ok(())
1317    }
1318
1319    #[async_timed_test(timeout_secs = 60)]
1320    async fn mutated_refs_are_updated_with_error() -> Result<()> {
1321        test_setup()?;
1322
1323        let proc = Proc::isolated();
1324        let (client, controller_ref, mut controller_rx) = proc.attach_actor("controller").unwrap();
1325
1326        let worker_handle = proc.spawn(
1327            WorkerActor::new(
1328                WorkerParams {
1329                    world_size: 1,
1330                    rank: 0,
1331                    device_index: None,
1332                    controller_actor: controller_ref,
1333                },
1334                &ActorEnvironment::default(),
1335            )
1336            .await
1337            .unwrap(),
1338        );
1339        worker_handle
1340            .command_group(
1341                &client,
1342                vec![
1343                    WorkerMessage::CreateStream {
1344                        id: 1.into(),
1345                        stream_creation: StreamCreationMode::UseDefaultStream,
1346                    },
1347                    WorkerMessage::SetRefUnitTestsOnly {
1348                        reference: 0.into(),
1349                        value: WireValue::Int(1),
1350                        stream: 1.into(),
1351                    },
1352                    WorkerMessage::CallFunction(CallFunctionParams {
1353                        seq: 0.into(),
1354                        results: vec![Some(Ref { id: 2 })],
1355                        mutates: vec![0.into()],
1356                        function: "i.dont.exist".into(),
1357                        args_kwargs: ArgsKwargs::from_wire_values(vec![], HashMap::new()).unwrap(),
1358                        stream: 1.into(),
1359                        remote_process_groups: vec![],
1360                    }),
1361                ],
1362            )
1363            .await
1364            .unwrap();
1365
1366        let result = worker_handle
1367            .get_ref_unit_tests_only(&client, 0.into(), 1.into())
1368            .await?;
1369
1370        // Stop/drain worker before asserts to avoid hangs.
1371        worker_handle.drain_and_stop("test").unwrap();
1372        worker_handle.await;
1373
1374        let mutated_ref = result
1375            .context("no such ref")?
1376            .err()
1377            .context("expected error")?;
1378        assert!(mutated_ref.contains("failed to resolve function"));
1379
1380        let responses = controller_rx.drain();
1381        assert_eq!(
1382            responses.len(),
1383            1,
1384            "Expected one response, got: {:#?}",
1385            responses
1386        );
1387        Ok(())
1388    }
1389
1390    #[async_timed_test(timeout_secs = 60)]
1391    async fn accessing_errored_dependency() -> Result<()> {
1392        test_setup()?;
1393
1394        let proc = Proc::isolated();
1395        let (client, controller_ref, mut controller_rx) = proc.attach_actor("controller").unwrap();
1396
1397        let worker_handle = proc.spawn(
1398            WorkerActor::new(
1399                WorkerParams {
1400                    world_size: 1,
1401                    rank: 0,
1402                    device_index: None,
1403                    controller_actor: controller_ref,
1404                },
1405                &ActorEnvironment::default(),
1406            )
1407            .await
1408            .unwrap(),
1409        );
1410        worker_handle
1411            .command_group(
1412                &client,
1413                vec![
1414                    WorkerMessage::CreateStream {
1415                        id: 1.into(),
1416                        stream_creation: StreamCreationMode::UseDefaultStream,
1417                    },
1418                    WorkerMessage::CallFunction(CallFunctionParams {
1419                        seq: 0.into(),
1420                        results: vec![Some(0.into())],
1421                        mutates: vec![],
1422                        function: "i.dont.exist".into(),
1423                        args_kwargs: ArgsKwargs::from_wire_values(vec![], HashMap::new()).unwrap(),
1424                        stream: 1.into(),
1425                        remote_process_groups: vec![],
1426                    }),
1427                    WorkerMessage::CallFunction(CallFunctionParams {
1428                        seq: 1.into(),
1429                        results: vec![Some(1.into())],
1430                        mutates: vec![],
1431                        function: "torch.ops.aten.sub_.Scalar".into(),
1432                        args_kwargs: ArgsKwargs::from_wire_values(
1433                            vec![WireValue::Ref(0.into())],
1434                            HashMap::new(),
1435                        )
1436                        .unwrap(),
1437                        stream: 1.into(),
1438                        remote_process_groups: vec![],
1439                    }),
1440                    WorkerMessage::Exit { error: None },
1441                ],
1442            )
1443            .await
1444            .unwrap();
1445
1446        worker_handle.drain_and_stop("test").unwrap();
1447        worker_handle.await;
1448
1449        let responses = controller_rx.drain();
1450        assert_eq!(
1451            responses.len(),
1452            1,
1453            "Expected one response, got: {:#?}",
1454            responses
1455        );
1456
1457        match &responses[0] {
1458            ControllerMessage::RemoteFunctionFailed { seq, .. } => {
1459                assert_eq!(seq, &0.into())
1460            }
1461            _ => panic!("unexpected response {:#?}", responses[0]),
1462        };
1463        Ok(())
1464    }
1465
1466    #[async_timed_test(timeout_secs = 60)]
1467    async fn py_remote_function_calls() -> Result<()> {
1468        test_setup()?;
1469
1470        let proc = Proc::isolated();
1471        let (client, controller_ref, mut controller_rx) = proc.attach_actor("controller").unwrap();
1472
1473        let worker_handle = proc.spawn(
1474            WorkerActor::new(
1475                WorkerParams {
1476                    world_size: 1,
1477                    rank: 0,
1478                    device_index: None,
1479                    controller_actor: controller_ref,
1480                },
1481                &ActorEnvironment::default(),
1482            )
1483            .await
1484            .unwrap(),
1485        );
1486        let (split_arg, sort_list, dim, layout, none, scalar, device, memory_format) =
1487            monarch_with_gil_blocking(GilSite::Test, |py| {
1488                let split_arg: PickledPyObject = PyString::new(py, "/fbs/fbc/foo/bar")
1489                    .into_any()
1490                    .try_into()?;
1491                let sort_list: PickledPyObject =
1492                    PyList::new(py, [65, 34, 79, 1, 5])?.into_any().try_into()?;
1493                let dim: PickledPyObject = PyString::new(py, "x").into_any().try_into()?;
1494                let layout: PickledPyObject = py.import("torch")?.getattr("strided")?.try_into()?;
1495                let none: PickledPyObject = py.None().into_any().into_bound(py).try_into()?;
1496                let scalar: PickledPyObject = py.import("torch")?.getattr("float32")?.try_into()?;
1497                let device: PickledPyObject = py
1498                    .import("torch")?
1499                    .getattr("device")?
1500                    .call1(("cuda:1",))?
1501                    .try_into()?;
1502                let memory_format: PickledPyObject = py
1503                    .import("torch")?
1504                    .getattr("contiguous_format")?
1505                    .try_into()?;
1506                PyResult::Ok((
1507                    split_arg,
1508                    sort_list,
1509                    dim,
1510                    layout,
1511                    none,
1512                    scalar,
1513                    device,
1514                    memory_format,
1515                ))
1516            })?;
1517
1518        worker_handle
1519            .command_group(
1520                &client,
1521                vec![
1522                    WorkerMessage::CreateStream {
1523                        id: 1.into(),
1524                        stream_creation: StreamCreationMode::UseDefaultStream,
1525                    },
1526                    WorkerMessage::CallFunction(CallFunctionParams {
1527                        seq: 0.into(),
1528                        results: vec![Some(0.into()), Some(Ref { id: 2 })],
1529                        mutates: vec![],
1530                        function: "os.path.split".into(),
1531                        args_kwargs: ArgsKwargs::from_wire_values(
1532                            vec![split_arg.into()],
1533                            HashMap::new(),
1534                        )
1535                        .unwrap(),
1536                        stream: 1.into(),
1537                        remote_process_groups: vec![],
1538                    }),
1539                    WorkerMessage::CallFunction(CallFunctionParams {
1540                        seq: 2.into(),
1541                        results: vec![Some(4.into()), None, None, None, None],
1542                        mutates: vec![],
1543                        function: "builtins.sorted".into(),
1544                        args_kwargs: ArgsKwargs::from_wire_values(
1545                            vec![sort_list.into()],
1546                            HashMap::new(),
1547                        )
1548                        .unwrap(),
1549                        stream: 1.into(),
1550                        remote_process_groups: vec![],
1551                    }),
1552                    WorkerMessage::CreateDeviceMesh {
1553                        result: 5.into(),
1554                        names: vec!["x".into()],
1555                        ranks: Slice::new(0, vec![2], vec![1]).unwrap(),
1556                    },
1557                    WorkerMessage::CallFunction(CallFunctionParams {
1558                        seq: 2.into(),
1559                        results: vec![Some(6.into())],
1560                        mutates: vec![],
1561                        function: "monarch.monarch_tensor_worker.test_utils.mesh_rank".into(),
1562                        args_kwargs: ArgsKwargs::from_wire_values(
1563                            vec![WireValue::Ref(Ref { id: 5 }), dim.into()],
1564                            HashMap::new(),
1565                        )
1566                        .unwrap(),
1567                        stream: 1.into(),
1568                        remote_process_groups: vec![],
1569                    }),
1570                    WorkerMessage::CallFunction(CallFunctionParams {
1571                        seq: 4.into(),
1572                        results: vec![Some(7.into())],
1573                        mutates: vec![],
1574                        function: "monarch.monarch_tensor_worker.test_utils.test_scalar_type"
1575                            .into(),
1576                        args_kwargs: ArgsKwargs::from_wire_values(
1577                            vec![scalar.into()],
1578                            HashMap::new(),
1579                        )
1580                        .unwrap(),
1581                        stream: 1.into(),
1582                        remote_process_groups: vec![],
1583                    }),
1584                    WorkerMessage::CallFunction(CallFunctionParams {
1585                        seq: 5.into(),
1586                        results: vec![Some(8.into())],
1587                        mutates: vec![],
1588                        function: "monarch.monarch_tensor_worker.test_utils.test_layout".into(),
1589                        args_kwargs: ArgsKwargs::from_wire_values(
1590                            vec![layout.into()],
1591                            HashMap::new(),
1592                        )
1593                        .unwrap(),
1594                        stream: 1.into(),
1595                        remote_process_groups: vec![],
1596                    }),
1597                    WorkerMessage::CallFunction(CallFunctionParams {
1598                        seq: 6.into(),
1599                        results: vec![Some(9.into())],
1600                        mutates: vec![],
1601                        function: "monarch.monarch_tensor_worker.test_utils.test_none".into(),
1602                        args_kwargs: ArgsKwargs::from_wire_values(
1603                            vec![none.into()],
1604                            HashMap::new(),
1605                        )
1606                        .unwrap(),
1607                        stream: 1.into(),
1608                        remote_process_groups: vec![],
1609                    }),
1610                    // Verify that a function that returns `None` matches up with an
1611                    // empty result list.
1612                    WorkerMessage::CallFunction(CallFunctionParams {
1613                        seq: 7.into(),
1614                        results: vec![None],
1615                        mutates: vec![],
1616                        function: "monarch.monarch_tensor_worker.test_utils.none".into(),
1617                        args_kwargs: ArgsKwargs::from_wire_values(vec![], HashMap::new()).unwrap(),
1618                        stream: 1.into(),
1619                        remote_process_groups: vec![],
1620                    }),
1621                    WorkerMessage::CallFunction(CallFunctionParams {
1622                        seq: 8.into(),
1623                        results: vec![Some(10.into())],
1624                        mutates: vec![],
1625                        function: "monarch.monarch_tensor_worker.test_utils.test_device".into(),
1626                        args_kwargs: ArgsKwargs::from_wire_values(
1627                            vec![device.into()],
1628                            HashMap::new(),
1629                        )
1630                        .unwrap(),
1631                        stream: 1.into(),
1632                        remote_process_groups: vec![],
1633                    }),
1634                    WorkerMessage::CallFunction(CallFunctionParams {
1635                        seq: 9.into(),
1636                        results: vec![Some(11.into())],
1637                        mutates: vec![],
1638                        function: "monarch.monarch_tensor_worker.test_utils.test_memory_format"
1639                            .into(),
1640                        args_kwargs: ArgsKwargs::from_wire_values(
1641                            vec![memory_format.into()],
1642                            HashMap::new(),
1643                        )
1644                        .unwrap(),
1645                        stream: 1.into(),
1646                        remote_process_groups: vec![],
1647                    }),
1648                    // Test that list of tests can be passes correctly
1649                    WorkerMessage::CallFunction(CallFunctionParams {
1650                        seq: 10.into(),
1651                        results: vec![Some(12.into())],
1652                        mutates: vec![],
1653                        function: "torch.ops.aten.ones.default".into(),
1654                        args_kwargs: ArgsKwargs::from_wire_values(
1655                            vec![WireValue::IntList(vec![2, 3])],
1656                            HashMap::new(),
1657                        )
1658                        .unwrap(),
1659                        stream: 1.into(),
1660                        remote_process_groups: vec![],
1661                    }),
1662                    WorkerMessage::CallFunction(CallFunctionParams {
1663                        seq: 11.into(),
1664                        results: vec![Some(13.into())],
1665                        mutates: vec![],
1666                        function: "torch.ops.aten.stack.default".into(),
1667                        args_kwargs: ArgsKwargs::from_wire_values(
1668                            vec![WireValue::RefList(vec![12.into(), 12.into()])],
1669                            HashMap::new(),
1670                        )
1671                        .unwrap(),
1672                        stream: 1.into(),
1673                        remote_process_groups: vec![],
1674                    }),
1675                ],
1676            )
1677            .await
1678            .unwrap();
1679
1680        let result1: String = worker_handle
1681            .get_ref_unit_tests_only(&client, 0.into(), 1.into())
1682            .await
1683            .unwrap()
1684            .unwrap()
1685            .unwrap()
1686            .try_into()
1687            .unwrap();
1688        let result2: String = worker_handle
1689            .get_ref_unit_tests_only(&client, 2.into(), 1.into())
1690            .await
1691            .unwrap()
1692            .unwrap()
1693            .unwrap()
1694            .try_into()
1695            .unwrap();
1696        let result3: i64 = worker_handle
1697            .get_ref_unit_tests_only(&client, 4.into(), 1.into())
1698            .await
1699            .unwrap()
1700            .unwrap()
1701            .unwrap()
1702            .try_into()
1703            .unwrap();
1704        let result4: i64 = worker_handle
1705            .get_ref_unit_tests_only(&client, 6.into(), 1.into())
1706            .await
1707            .unwrap()
1708            .unwrap()
1709            .unwrap()
1710            .try_into()
1711            .unwrap();
1712        worker_handle
1713            .get_ref_unit_tests_only(&client, 7.into(), 1.into())
1714            .await
1715            .unwrap()
1716            .unwrap()
1717            .unwrap();
1718
1719        worker_handle
1720            .get_ref_unit_tests_only(&client, 8.into(), 1.into())
1721            .await
1722            .unwrap()
1723            .unwrap()
1724            .unwrap();
1725
1726        assert_matches!(
1727            worker_handle
1728                .get_ref_unit_tests_only(&client, 9.into(), 1.into())
1729                .await
1730                .unwrap()
1731                .unwrap()
1732                .unwrap(),
1733            WireValue::None(()),
1734        );
1735        worker_handle
1736            .get_ref_unit_tests_only(&client, 10.into(), 1.into())
1737            .await
1738            .unwrap()
1739            .unwrap()
1740            .unwrap();
1741        worker_handle
1742            .get_ref_unit_tests_only(&client, 11.into(), 1.into())
1743            .await
1744            .unwrap()
1745            .unwrap()
1746            .unwrap();
1747
1748        worker_handle.drain_and_stop("test").unwrap();
1749        worker_handle.await;
1750        let error_responses = controller_rx.drain();
1751        assert!(
1752            error_responses.is_empty(),
1753            "Expected no error responses, got: {:#?}",
1754            error_responses
1755        );
1756
1757        assert_eq!(result1, "/fbs/fbc/foo");
1758        assert_eq!(result2, "bar");
1759        assert_eq!(result3, 1);
1760        assert_eq!(result4, 0);
1761
1762        Ok(())
1763    }
1764
1765    #[async_timed_test(timeout_secs = 60)]
1766    async fn delete_refs() -> Result<()> {
1767        test_setup()?;
1768
1769        let proc = Proc::isolated();
1770        let (client, controller_ref, _) = proc.attach_actor("controller").unwrap();
1771
1772        let worker_handle = proc.spawn(
1773            WorkerActor::new(
1774                WorkerParams {
1775                    world_size: 1,
1776                    rank: 0,
1777                    device_index: None,
1778                    controller_actor: controller_ref,
1779                },
1780                &ActorEnvironment::default(),
1781            )
1782            .await
1783            .unwrap(),
1784        );
1785        worker_handle
1786            .command_group(
1787                &client,
1788                vec![
1789                    WorkerMessage::CreateStream {
1790                        id: 0.into(),
1791                        stream_creation: StreamCreationMode::CreateNewStream,
1792                    },
1793                    WorkerMessage::CreateStream {
1794                        id: 1.into(),
1795                        stream_creation: StreamCreationMode::CreateNewStream,
1796                    },
1797                    WorkerMessage::SetRefUnitTestsOnly {
1798                        reference: Ref { id: 2 },
1799                        value: WireValue::Bool(false),
1800                        stream: 0.into(),
1801                    },
1802                    WorkerMessage::SetRefUnitTestsOnly {
1803                        reference: Ref { id: 3 },
1804                        value: WireValue::Bool(true),
1805                        stream: 0.into(),
1806                    },
1807                    WorkerMessage::SetRefUnitTestsOnly {
1808                        reference: Ref { id: 4 },
1809                        value: WireValue::Int(0),
1810                        stream: 1.into(),
1811                    },
1812                    WorkerMessage::DeleteRefs(vec![Ref { id: 2 }, Ref { id: 4 }]),
1813                ],
1814            )
1815            .await
1816            .unwrap();
1817
1818        let result: bool = worker_handle
1819            .get_ref_unit_tests_only(&client, Ref { id: 3 }, 0.into())
1820            .await
1821            .unwrap()
1822            .unwrap()
1823            .unwrap()
1824            .try_into()
1825            .unwrap();
1826        let fail_result = worker_handle
1827            .get_ref_unit_tests_only(&client, Ref { id: 4 }, 1.into())
1828            .await
1829            .unwrap();
1830
1831        worker_handle.drain_and_stop("test").unwrap();
1832        worker_handle.await;
1833
1834        assert!(result, "should be able to get a non-deleted ref");
1835        assert!(fail_result.is_none(), "should fail to get a deleted ref");
1836
1837        Ok(())
1838    }
1839
1840    #[async_timed_test(timeout_secs = 60)]
1841    async fn request_status() -> Result<()> {
1842        test_setup()?;
1843
1844        let proc = Proc::isolated();
1845        let (client, controller_ref, mut controller_rx) = proc.attach_actor("controller").unwrap();
1846
1847        let worker_handle = proc.spawn(
1848            WorkerActor::new(
1849                WorkerParams {
1850                    world_size: 1,
1851                    rank: 0,
1852                    device_index: None,
1853                    controller_actor: controller_ref,
1854                },
1855                &ActorEnvironment::default(),
1856            )
1857            .await
1858            .unwrap(),
1859        );
1860        worker_handle
1861            .command_group(
1862                &client,
1863                vec![
1864                    WorkerMessage::CreateStream {
1865                        id: 0.into(),
1866                        stream_creation: StreamCreationMode::CreateNewStream,
1867                    },
1868                    WorkerMessage::CreateStream {
1869                        id: 1.into(),
1870                        stream_creation: StreamCreationMode::CreateNewStream,
1871                    },
1872                ],
1873            )
1874            .await
1875            .unwrap();
1876
1877        for i in 0..100 {
1878            // call alternating functions on this stream.
1879            worker_handle
1880                .call_function(
1881                    &client,
1882                    CallFunctionParams {
1883                        seq: i.into(),
1884                        results: vec![Some(Ref { id: i + 2 })],
1885                        mutates: vec![],
1886                        function: "torch.ops.aten.ones.default".into(),
1887                        args_kwargs: ArgsKwargs::from_wire_values(
1888                            vec![WireValue::IntList(vec![2, 3])],
1889                            HashMap::new(),
1890                        )
1891                        .unwrap(),
1892                        stream: (i % 2).into(),
1893                        remote_process_groups: vec![],
1894                    },
1895                )
1896                .await
1897                .unwrap();
1898        }
1899
1900        worker_handle
1901            .request_status(&client, 100.into(), false)
1902            .await
1903            .unwrap();
1904
1905        worker_handle.drain_and_stop("test").unwrap();
1906        worker_handle.await;
1907
1908        let mut responses = controller_rx.drain();
1909        assert_eq!(
1910            responses.len(),
1911            1,
1912            "Expected one response, got: {:#?}",
1913            responses
1914        );
1915
1916        let response = responses.pop().unwrap();
1917        match response {
1918            ControllerMessage::Status { seq, .. } => {
1919                assert_eq!(seq, 101.into())
1920            }
1921            _ => panic!("unexpected response {:#?}", response),
1922        };
1923
1924        Ok(())
1925    }
1926
1927    #[async_timed_test(timeout_secs = 60)]
1928    async fn backend_network_init() {
1929        test_setup().unwrap();
1930        let proc = Proc::isolated();
1931        let (client, controller_ref, _) = proc.attach_actor("controller").unwrap();
1932
1933        let worker_handle1 = proc.spawn_with_label(
1934            "worker0",
1935            WorkerActor::new(
1936                WorkerParams {
1937                    world_size: 2,
1938                    rank: 0,
1939                    device_index: Some(0),
1940                    controller_actor: controller_ref.clone(),
1941                },
1942                &ActorEnvironment::default(),
1943            )
1944            .await
1945            .unwrap(),
1946        );
1947        let worker_handle2 = proc.spawn_with_label(
1948            "worker1",
1949            WorkerActor::new(
1950                WorkerParams {
1951                    world_size: 2,
1952                    rank: 1,
1953                    device_index: Some(1),
1954                    controller_actor: controller_ref,
1955                },
1956                &ActorEnvironment::default(),
1957            )
1958            .await
1959            .unwrap(),
1960        );
1961
1962        let unique_id = UniqueId::new_nccl().unwrap();
1963        worker_handle1
1964            .backend_network_init(&client, unique_id.clone())
1965            .await
1966            .unwrap();
1967        worker_handle2
1968            .backend_network_init(&client, unique_id)
1969            .await
1970            .unwrap();
1971
1972        worker_handle1.drain_and_stop("test").unwrap();
1973        worker_handle1.await;
1974        worker_handle2.drain_and_stop("test").unwrap();
1975        worker_handle2.await;
1976    }
1977
1978    #[allow(dead_code)]
1979    fn get_random_channel_addr() -> ChannelAddr {
1980        let random_string = rand::rng()
1981            .sample_iter(&Alphanumeric)
1982            .take(24)
1983            .map(char::from)
1984            .collect::<String>();
1985        format!("unix!@{random_string}").parse().unwrap()
1986    }
1987}