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