1#![feature(duration_constructors)]
10#![allow(unsafe_op_in_unsafe_fn)]
13#![deny(clippy::disallowed_methods)]
14
15mod 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 PartialRecording {
121 last_index: usize,
123 commands: Vec<WorkerMessage>,
125 },
126
127 CompleteRecording {
129 streams: HashSet<StreamRef>,
131 },
132}
133
134#[derive(Debug)]
142#[hyperactor::spawnable]
143#[hyperactor::export(
144 handlers = [
145 WorkerMessage,
146 AssignRankMessage,
147 ],
148)]
149pub struct WorkerActor {
150 device: Option<CudaDevice>,
151 device_index: Option<i8>,
156 streams: HashMap<StreamRef, Arc<ActorHandle<StreamActor>>>,
157 device_meshes: HashMap<
160 Ref,
161 (
162 DeviceMesh,
163 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 remote_process_groups: HashMap<Ref, RemoteProcessGroupState>,
176 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 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 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 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 }
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 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#[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 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 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 ¶ms.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(¶ms.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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}