1#![feature(duration_constructors)]
10#![feature(exit_status_error)]
11#![allow(unsafe_op_in_unsafe_fn)]
14#![deny(clippy::disallowed_methods)]
15
16mod 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 PartialRecording {
124 last_index: usize,
126 commands: Vec<WorkerMessage>,
128 },
129
130 CompleteRecording {
132 streams: HashSet<StreamRef>,
134 },
135}
136
137#[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 device_meshes: HashMap<
158 Ref,
159 (
160 DeviceMesh,
161 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 remote_process_groups: HashMap<Ref, RemoteProcessGroupState>,
174 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 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 }
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#[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 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 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 ¶ms.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(¶ms.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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}