1use std::any::type_name;
10use std::collections::HashSet;
11use std::fmt;
12use std::hash::Hash;
13use std::ops::Deref;
14use std::sync::Arc;
15use std::time::Duration;
16
17use hyperactor::ActorAddr;
18use hyperactor::ActorRef;
19use hyperactor::Endpoint as _;
20use hyperactor::Handler;
21use hyperactor::ProcAddr;
22use hyperactor::RemoteMessage;
23use hyperactor::RemoteSpawn;
24use hyperactor::accum::StreamingReducerOpts;
25use hyperactor::actor::ActorStatus;
26use hyperactor::actor::remote::Remote;
27use hyperactor::context;
28use hyperactor::id::Label;
29use hyperactor::supervision::ActorSupervisionEvent;
30use hyperactor_config::CONFIG;
31use hyperactor_config::ConfigAttr;
32use hyperactor_config::attrs::declare_attrs;
33use hyperactor_telemetry::hash_to_u64;
34use ndslice::Extent;
35use ndslice::ViewExt as _;
36use ndslice::view;
37use ndslice::view::CollectMeshExt;
38use ndslice::view::Ranked;
39use ndslice::view::Region;
40use serde::Deserialize;
41use serde::Serialize;
42use typeuri::Named;
43
44use crate::ActorMesh;
45use crate::ActorMeshRef;
46use crate::Error;
47use crate::HostMeshRef;
48use crate::ValueMesh;
49use crate::host_mesh::GET_PROC_STATE_MAX_IDLE;
50use crate::host_mesh::host_agent::GetHostProcStates;
51use crate::host_mesh::host_agent::ProcState;
52use crate::host_mesh::mesh_to_rankedvalues_with_default;
53use crate::mesh_controller::ActorMeshControlPlane;
54use crate::mesh_controller::ActorMeshController;
55use crate::mesh_id::ActorMeshId;
56use crate::mesh_id::ProcMeshId;
57use crate::mesh_id::ResourceId;
58use crate::proc_agent;
59use crate::proc_agent::ActorState;
60use crate::proc_agent::ProcAgent;
61use crate::resource;
62use crate::resource::GetRankStatus;
63use crate::resource::Status;
64use crate::supervision::MeshFailure;
65
66declare_attrs! {
67 @meta(CONFIG = ConfigAttr::new(
70 Some("HYPERACTOR_MESH_ACTOR_SPAWN_MAX_IDLE".to_string()),
71 Some("actor_spawn_max_idle".to_string()),
72 ))
73 pub attr ACTOR_SPAWN_MAX_IDLE: Duration = Duration::from_secs(30);
74
75 @meta(CONFIG = ConfigAttr::new(
78 Some("HYPERACTOR_MESH_GET_ACTOR_STATE_MAX_IDLE".to_string()),
79 Some("get_actor_state_max_idle".to_string()),
80 ))
81 pub attr GET_ACTOR_STATE_MAX_IDLE: Duration = Duration::from_secs(30);
82}
83
84pub fn telemetry_actor_mesh_id(proc_mesh_id: &ProcMeshId, actor_mesh_id: &ActorMeshId) -> u64 {
86 hash_to_u64(&(proc_mesh_id, actor_mesh_id))
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
91pub struct ProcRef {
92 proc_id: ProcAddr,
93 create_rank: usize,
95 agent: ActorRef<ProcAgent>,
97}
98
99impl ProcRef {
100 pub fn new(proc_id: ProcAddr, create_rank: usize, agent: ActorRef<ProcAgent>) -> Self {
102 Self {
103 proc_id,
104 create_rank,
105 agent,
106 }
107 }
108
109 pub fn proc_addr(&self) -> &ProcAddr {
110 &self.proc_id
111 }
112
113 pub(crate) fn actor_addr(&self, id: &ActorMeshId) -> ActorAddr {
114 self.proc_id.actor_addr_uid(id.uid().clone())
115 }
116}
117
118#[derive(Debug)]
120pub struct ProcMesh {
121 #[allow(dead_code)]
122 id: ProcMeshId,
123 current_ref: ProcMeshRef,
124 controller: Option<ActorRef<crate::mesh_controller::ProcMeshController>>,
125}
126
127impl ProcMesh {
128 pub(crate) fn create(
129 id: ProcMeshId,
130 extent: Extent,
131 hosts: HostMeshRef,
132 ranks: Vec<ProcRef>,
133 ) -> crate::Result<Self> {
134 let region = extent.into();
135 let ranks = Arc::new(ranks);
136
137 if let Some(first) = ranks.first() {
141 crate::global_context::set_global_supervision_sink(
142 first.agent.port::<ActorSupervisionEvent>(),
143 );
144 }
145
146 let current_ref = ProcMeshRef::new(id.clone(), region, ranks, Some(hosts)).unwrap();
147
148 {
150 let name_str = id.to_string();
151 let mesh_id_hash = hash_to_u64(&id);
152
153 let hm = current_ref
154 .host_mesh
155 .as_ref()
156 .expect("ProcMesh always has a host mesh");
157 let parent_mesh_id = hash_to_u64(hm.id());
158 let parent_view_json = serde_json::to_string(hm.region())
159 .unwrap_or_else(|e| format!("encountered error when serializing region: {}", e));
160
161 hyperactor_telemetry::notify_mesh_created(hyperactor_telemetry::MeshEvent {
162 id: mesh_id_hash,
163 timestamp: std::time::SystemTime::now(),
164 class: "Proc".to_string(),
165 given_name: id
166 .display_label()
167 .map(|l| l.as_str())
168 .unwrap_or("unnamed")
169 .to_string(),
170 full_name: name_str,
171 shape_json: serde_json::to_string(¤t_ref.region.extent()).unwrap_or_default(),
172 parent_mesh_id: Some(parent_mesh_id),
173 parent_view_json: Some(parent_view_json),
174 });
175
176 let now = std::time::SystemTime::now();
179 for rank in current_ref.ranks.iter() {
180 let actor_addr = rank.agent.actor_addr();
181
182 hyperactor_telemetry::notify_actor_created(hyperactor_telemetry::ActorEvent {
183 id: hyperactor_telemetry::hash_to_u64(actor_addr.id()),
184 timestamp: now,
185 mesh_id: mesh_id_hash,
186 rank: rank.create_rank as u64,
187 full_name: actor_addr.to_string(),
188 display_name: None,
189 });
190 }
191 }
192
193 Ok(Self {
194 id,
195 current_ref,
196 controller: None,
197 })
198 }
199
200 pub(crate) fn set_controller(
202 &mut self,
203 controller: Option<ActorRef<crate::mesh_controller::ProcMeshController>>,
204 ) {
205 self.controller = controller;
206 }
207
208 pub async fn stop(&mut self, cx: &impl context::Actor, reason: String) -> anyhow::Result<()> {
219 if let Some(controller) = self.controller.take() {
220 let id = self.id.resource_id().clone();
221 controller.post(
222 cx,
223 resource::Stop {
224 id: id.clone(),
225 reason,
226 },
227 );
228
229 let (port, mut rx) = cx.mailbox().open_port();
234 controller.post(
235 cx,
236 resource::GetState::<resource::mesh::State<()>> {
237 id: id.clone(),
238 reply: port.bind(),
239 },
240 );
241
242 let statuses = rx.recv().await?;
243 let Some(state) = &statuses.state else {
244 anyhow::bail!(
245 "non-existent state in GetState reply from controller: {}",
246 controller.actor_addr()
247 );
248 };
249 let all_stopped = state.statuses.values().all(|s| s.is_terminating());
255 if !all_stopped {
256 anyhow::bail!(
257 "proc mesh {} not all procs reached terminating state after stop: {:?}",
258 id,
259 state.statuses,
260 );
261 }
262 return Ok(());
263 }
264
265 let region = self.region.clone();
266 let procs = self.current_ref.proc_ids().collect::<Vec<ProcAddr>>();
267 self.current_ref
270 .host_mesh
271 .as_ref()
272 .expect("ProcMesh always has a host mesh")
273 .stop_proc_mesh(cx, &self.id, procs, region, reason)
274 .await
275 .map(|_| ())
276 .map_err(anyhow::Error::from)
277 }
278}
279
280impl fmt::Display for ProcMesh {
281 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 write!(f, "{}", self.current_ref)
283 }
284}
285
286impl Deref for ProcMesh {
287 type Target = ProcMeshRef;
288
289 fn deref(&self) -> &Self::Target {
290 &self.current_ref
291 }
292}
293
294impl Drop for ProcMesh {
295 fn drop(&mut self) {
296 tracing::info!(
297 name = "ProcMeshStatus",
298 proc_mesh = %self.id,
299 status = "Dropped",
300 );
301 }
302}
303
304#[derive(Debug, Clone, Named, Serialize, Deserialize)]
315pub struct ProcMeshRef {
316 id: ProcMeshId,
317 region: Region,
318 ranks: Arc<Vec<ProcRef>>,
319 proc_agent_mesh: ActorMeshRef<ProcAgent>,
329 host_mesh: Option<HostMeshRef>,
331}
332wirevalue::register_type!(ProcMeshRef);
333
334impl PartialEq for ProcMeshRef {
337 fn eq(&self, other: &Self) -> bool {
338 self.id == other.id
339 && self.region == other.region
340 && self.ranks == other.ranks
341 && self.host_mesh == other.host_mesh
342 }
343}
344
345impl Eq for ProcMeshRef {}
346
347impl std::hash::Hash for ProcMeshRef {
348 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
349 self.id.hash(state);
350 self.region.hash(state);
351 self.ranks.hash(state);
352 self.host_mesh.hash(state);
353 }
354}
355
356impl ProcMeshRef {
357 #[allow(clippy::result_large_err)]
359 fn new(
360 id: ProcMeshId,
361 region: Region,
362 ranks: Arc<Vec<ProcRef>>,
363 host_mesh: Option<HostMeshRef>,
364 ) -> crate::Result<Self> {
365 if ranks.is_empty() {
366 return Err(crate::Error::ConfigurationError(anyhow::anyhow!(
367 "empty proc meshes are not supported"
368 )));
369 }
370 if region.num_ranks() != ranks.len() {
371 return Err(crate::Error::InvalidRankCardinality {
372 expected: region.num_ranks(),
373 actual: ranks.len(),
374 });
375 }
376 let proc_agent_mesh = Self::proc_agent_mesh_ref(&id, ®ion, &ranks)?;
377 Ok(Self {
378 id,
379 region,
380 ranks,
381 proc_agent_mesh,
382 host_mesh,
383 })
384 }
385
386 pub fn new_singleton(id: ProcMeshId, proc_ref: ProcRef) -> crate::Result<Self> {
390 let region: Region = Extent::unity().into();
391 let ranks = Arc::new(vec![proc_ref]);
392 let proc_agent_mesh = Self::proc_agent_mesh_ref(&id, ®ion, &ranks)?;
393 Ok(Self {
394 id,
395 region,
396 ranks,
397 proc_agent_mesh,
398 host_mesh: None,
399 })
400 }
401
402 pub fn id(&self) -> &ProcMeshId {
403 &self.id
404 }
405
406 pub fn host_mesh_id(&self) -> Option<&crate::mesh_id::HostMeshId> {
407 self.host_mesh.as_ref().map(|h| h.id())
408 }
409
410 pub fn hosts(&self) -> Option<&HostMeshRef> {
412 self.host_mesh.as_ref()
413 }
414
415 pub(crate) fn agent_mesh(&self) -> &ActorMeshRef<ProcAgent> {
416 &self.proc_agent_mesh
417 }
418
419 fn proc_agent_mesh_ref(
420 proc_mesh_id: &ProcMeshId,
421 region: &Region,
422 ranks: &[ProcRef],
423 ) -> crate::Result<ActorMeshRef<ProcAgent>> {
424 let agent_label = ranks
425 .first()
426 .unwrap()
427 .agent
428 .actor_addr()
429 .label()
430 .cloned()
431 .unwrap_or_else(|| Label::new(proc_agent::PROC_AGENT_ACTOR_NAME).unwrap());
432 let id = ActorMeshId::singleton(agent_label);
433
434 let members = Arc::new(
435 ranks
436 .iter()
437 .map(|rank| rank.agent.actor_addr().clone())
438 .collect_mesh::<ValueMesh<_>>(region.clone())
439 .map_err(|error| crate::Error::ConfigurationError(error.into()))?,
440 );
441
442 Ok(ActorMeshRef::new(
443 id,
444 Some(proc_mesh_id.clone()),
445 region.clone(),
446 None,
447 members,
448 ))
449 }
450
451 pub async fn actor_states(
453 &self,
454 cx: &impl context::Actor,
455 id: ActorMeshId,
456 ) -> crate::Result<ValueMesh<resource::State<ActorState>>> {
457 self.actor_states_with_keepalive(cx, id, None).await
458 }
459
460 pub(crate) async fn actor_states_with_keepalive(
466 &self,
467 cx: &impl context::Actor,
468 id: ActorMeshId,
469 keepalive: Option<std::time::SystemTime>,
470 ) -> crate::Result<ValueMesh<resource::State<ActorState>>> {
471 let (port, mut rx) = cx.mailbox().open_port::<resource::State<ActorState>>();
472 let mut port = port.bind();
473 port.return_undeliverable(false);
476 let get_state = resource::GetState::<ActorState> {
479 id: id.resource_id().clone(),
480 reply: port,
481 };
482 if let Some(expires_after) = keepalive {
483 self.proc_agent_mesh.cast(
484 cx,
485 resource::KeepaliveGetState {
486 expires_after,
487 get_state,
488 },
489 )?;
490 } else {
491 self.proc_agent_mesh.cast(cx, get_state)?;
492 }
493 let expected = self.ranks.len();
494 let mut states = Vec::with_capacity(expected);
495 let timeout = hyperactor_config::global::get(GET_ACTOR_STATE_MAX_IDLE);
496 for _ in 0..expected {
497 let state = tokio::time::timeout(timeout, rx.recv()).await;
503 if let Ok(state) = state {
504 let state = state?;
506 match state.state {
507 Some(ref inner) => {
508 states.push((inner.create_rank, state));
509 }
510 None => {
511 return Err(Error::NotExist(state.id));
512 }
513 }
514 } else {
515 tracing::error!(
516 "timeout waiting for a message after {:?} from proc mesh agent in mesh {}",
517 timeout,
518 self.proc_agent_mesh
519 );
520 let all_ranks = (0..self.ranks.len()).collect::<HashSet<_>>();
523 let completed_ranks = states.iter().map(|(rank, _)| *rank).collect::<HashSet<_>>();
524 let mut leftover_ranks = all_ranks.difference(&completed_ranks).collect::<Vec<_>>();
525 assert_eq!(leftover_ranks.len(), expected - states.len());
526 while states.len() < expected {
527 let rank = *leftover_ranks
528 .pop()
529 .expect("leftover ranks should not be empty");
530 let agent = self.proc_agent_mesh.get(rank).expect("agent should exist");
531 let agent_id = agent.actor_addr().clone();
532 states.push((
533 rank,
535 resource::State {
536 id: id.resource_id().clone(),
537 status: resource::Status::Timeout(timeout),
538 generation: u64::MAX,
543 timestamp: std::time::SystemTime::now(),
544 state: Some(ActorState {
545 actor_id: agent_id.clone(),
546 create_rank: rank,
547 supervision_events: vec![ActorSupervisionEvent::new(
548 agent_id,
549 None,
550 ActorStatus::generic_failure(format!(
551 "timeout waiting for message from proc mesh agent while querying for \"{}\". The process likely crashed",
552 id,
553 )),
554 None,
555 )],
556 }),
557 },
558 ));
559 }
560 break;
561 }
562 }
563 states.sort_by_key(|(rank, _)| *rank);
567 let vm = states
568 .into_iter()
569 .map(|(_, state)| state)
570 .collect_mesh::<ValueMesh<_>>(self.region.clone())?;
571 Ok(vm)
572 }
573
574 #[allow(clippy::result_large_err)]
587 pub async fn states(
588 &self,
589 cx: &impl context::Actor,
590 keepalive: Option<std::time::SystemTime>,
591 ) -> crate::Result<Option<ValueMesh<resource::State<ProcState>>>> {
592 let Some(host_mesh) = self.host_mesh.as_ref() else {
594 return Ok(None);
595 };
596 let region = self.region.clone();
597 let timeout = hyperactor_config::global::get(GET_PROC_STATE_MAX_IDLE);
598
599 let template: ValueMesh<resource::State<ProcState>> = self
603 .ranks
604 .iter()
605 .map(|proc_ref| resource::State {
606 id: ResourceId::new(
607 proc_ref.proc_id.uid().clone(),
608 proc_ref.proc_id.label().cloned(),
609 ),
610 status: resource::Status::Timeout(timeout),
611 state: None,
612 generation: 0,
613 timestamp: std::time::SystemTime::now(),
614 })
615 .collect_mesh::<ValueMesh<_>>(region.clone())?;
616 let fallback = template.clone();
618
619 let (port, rx) = cx.mailbox().open_accum_port_opts(
624 template,
625 StreamingReducerOpts {
626 max_update_interval: Some(Duration::from_millis(50)),
627 initial_update_interval: None,
628 },
629 );
630
631 host_mesh.agent_mesh().cast(
632 cx,
633 GetHostProcStates {
634 proc_mesh_id: self.id.clone(),
635 region: region.clone(),
636 keepalive,
637 reply: port.bind(),
638 },
639 )?;
640
641 let mesh =
646 match resource::wait_mesh(rx, timeout, fallback, |s: &resource::State<ProcState>| {
647 !matches!(s.status, resource::Status::Timeout(_))
648 })
649 .await
650 {
651 Ok(mesh) | Err(mesh) => mesh,
652 };
653
654 Ok(Some(mesh))
655 }
656
657 pub(crate) fn proc_ids(&self) -> impl Iterator<Item = ProcAddr> {
659 self.ranks.iter().map(|proc_ref| proc_ref.proc_id.clone())
660 }
661
662 pub async fn spawn<A: RemoteSpawn, C: context::Actor>(
672 &self,
673 cx: &C,
674 name: &str,
675 params: &A::Params,
676 ) -> crate::Result<ActorMesh<A>>
677 where
678 A::Params: RemoteMessage,
679 C::A: Handler<MeshFailure>,
680 {
681 let id = ActorMeshId::instance(Label::strip(name));
683 self.spawn_with_name(cx, id, params, None, false).await
684 }
685
686 pub async fn spawn_service<A: RemoteSpawn, C: context::Actor>(
694 &self,
695 cx: &C,
696 name: &str,
697 params: &A::Params,
698 ) -> crate::Result<ActorMesh<A>>
699 where
700 A::Params: RemoteMessage,
701 C::A: Handler<MeshFailure>,
702 {
703 let id = ActorMeshId::singleton(Label::strip(name));
704 self.spawn_with_name(cx, id, params, None, false).await
705 }
706
707 #[hyperactor::instrument(fields(
725 host_mesh=self.host_mesh_id().map(|id| id.to_string()),
726 proc_mesh=self.id.to_string(),
727 actor_name=name.to_string(),
728 ))]
729 pub async fn spawn_with_name<A: RemoteSpawn, C: context::Actor>(
730 &self,
731 cx: &C,
732 name: ActorMeshId,
733 params: &A::Params,
734 supervision_display_name: Option<String>,
735 is_system_actor: bool,
736 ) -> crate::Result<ActorMesh<A>>
737 where
738 A::Params: RemoteMessage,
739 C::A: Handler<MeshFailure>,
740 {
741 tracing::info!(
742 name = "ProcMeshStatus",
743 status = "ActorMesh::Spawn::Attempt",
744 );
745 tracing::info!(name = "ActorMeshStatus", status = "Spawn::Attempt");
746 let result = self
747 .spawn_with_name_inner(cx, name, params, supervision_display_name, is_system_actor)
748 .await;
749 match &result {
750 Ok(_) => {
751 tracing::info!(
752 name = "ProcMeshStatus",
753 status = "ActorMesh::Spawn::Success",
754 );
755 tracing::info!(name = "ActorMeshStatus", status = "Spawn::Success");
756 }
757 Err(error) => {
758 tracing::error!(name = "ProcMeshStatus", status = "ActorMesh::Spawn::Failed", %error);
759 tracing::error!(name = "ActorMeshStatus", status = "Spawn::Failed", %error);
760 }
761 }
762 result
763 }
764
765 async fn spawn_with_name_inner<A: RemoteSpawn, C: context::Actor>(
766 &self,
767 cx: &C,
768 actor_mesh_id: ActorMeshId,
769 params: &A::Params,
770 supervision_display_name: Option<String>,
771 is_system_actor: bool,
772 ) -> crate::Result<ActorMesh<A>>
773 where
774 C::A: Handler<MeshFailure>,
775 {
776 let remote = Remote::collect();
777 let actor_type = remote
781 .name_of::<A>()
782 .ok_or(Error::ActorTypeNotRegistered(type_name::<A>().to_string()))?
783 .to_string();
784
785 let serialized_params = bincode::serde::encode_to_vec(params, bincode::config::legacy())?;
786 self.proc_agent_mesh.cast(
787 cx,
788 resource::CreateOrUpdate::<proc_agent::ActorSpec> {
789 id: actor_mesh_id.resource_id().clone(),
790 rank: Default::default(),
791 spec: proc_agent::ActorSpec {
792 actor_type: actor_type.clone(),
793 params_data: serialized_params.clone(),
794 },
795 },
796 )?;
797
798 let region = self.region().clone();
799 let (port, rx) = cx.mailbox().open_accum_port_opts(
809 crate::StatusMesh::from_single(region.clone(), Status::NotExist),
812 StreamingReducerOpts {
813 max_update_interval: Some(Duration::from_millis(50)),
814 initial_update_interval: None,
815 },
816 );
817
818 let mut reply = port.bind();
819 reply.return_undeliverable(false);
822 self.proc_agent_mesh.cast(
825 cx,
826 resource::GetRankStatus {
827 id: actor_mesh_id.resource_id().clone(),
828 reply,
829 },
830 )?;
831
832 let start_time = tokio::time::Instant::now();
833
834 let statuses = match GetRankStatus::wait(
843 rx,
844 self.ranks.len(),
845 hyperactor_config::global::get(ACTOR_SPAWN_MAX_IDLE),
846 region.clone(), )
848 .await
849 {
850 Ok(statuses) => {
851 let has_terminating = statuses.values().any(|s| s.is_terminating());
855 if !has_terminating {
856 Ok(statuses)
857 } else {
858 let legacy = mesh_to_rankedvalues_with_default(
859 &statuses,
860 Status::NotExist,
861 Status::is_not_exist,
862 self.ranks.len(),
863 );
864 Err(Error::ActorSpawnError { statuses: legacy })
865 }
866 }
867 Err(complete) => {
868 let elapsed = start_time.elapsed();
871 let legacy = mesh_to_rankedvalues_with_default(
872 &complete,
873 Status::Timeout(elapsed),
874 Status::is_not_exist,
875 self.ranks.len(),
876 );
877 Err(Error::ActorSpawnError { statuses: legacy })
878 }
879 }?;
880
881 let actor_mesh_members = Arc::new(
882 self.ranks
883 .iter()
884 .map(|rank| rank.actor_addr(&actor_mesh_id))
885 .collect_mesh::<ValueMesh<_>>(self.region().clone())
886 .map_err(|error| crate::Error::ConfigurationError(error.into()))?,
887 );
888
889 let mut mesh = ActorMesh::new(
890 self.clone(),
891 actor_mesh_id.clone(),
892 None,
893 actor_mesh_members,
894 );
895 if !is_system_actor {
898 let controller: ActorMeshController<A> = ActorMeshController::new(
901 ActorMeshControlPlane::new(mesh.deref().clone(), self.clone()),
902 supervision_display_name.clone(),
903 Some(cx.instance().port().bind()),
904 statuses,
905 );
906 let controller_name = format!(
911 "{}_{}",
912 crate::mesh_controller::ACTOR_MESH_CONTROLLER_NAME,
913 mesh.id()
914 );
915 let controller = cx.spawn_with_label(&controller_name, controller);
916 mesh.set_controller(Some(controller.bind()));
919 }
920 {
922 let id_str = mesh.id().to_string();
923
924 let parent_mesh_id_hash = hash_to_u64(self.id());
926 let mesh_id_hash = telemetry_actor_mesh_id(self.id(), mesh.id());
927
928 hyperactor_telemetry::notify_mesh_created(hyperactor_telemetry::MeshEvent {
929 id: mesh_id_hash,
930 timestamp: std::time::SystemTime::now(),
931 class: supervision_display_name
932 .as_deref()
933 .and_then(python_class_from_supervision_name)
934 .unwrap_or(actor_type),
935 given_name: mesh
936 .id()
937 .display_label()
938 .map(|l| l.as_str())
939 .unwrap_or("unnamed")
940 .to_string(),
941 full_name: id_str,
942 shape_json: serde_json::to_string(&self.region().extent()).unwrap_or_default(),
943 parent_mesh_id: Some(parent_mesh_id_hash),
944 parent_view_json: serde_json::to_string(self.region()).ok(),
945 });
946
947 let now = std::time::SystemTime::now();
951 for (rank, proc_ref) in self.ranks.iter().enumerate() {
952 let display_name = supervision_display_name.as_ref().map(|sdn| {
953 let point = self.region().extent().point_of_rank(rank).unwrap();
954 crate::actor_display_name(sdn, &point)
955 });
956 let actor_addr = proc_ref.actor_addr(&actor_mesh_id);
957 hyperactor_telemetry::notify_actor_created(hyperactor_telemetry::ActorEvent {
958 id: hyperactor_telemetry::hash_to_u64(actor_addr.id()),
959 timestamp: now,
960 mesh_id: mesh_id_hash,
961 rank: rank as u64,
962 full_name: actor_addr.to_string(),
963 display_name,
964 });
965 }
966 }
967
968 Ok(mesh)
969 }
970
971 #[hyperactor::instrument(fields(
973 host_mesh = self.host_mesh_id().map(|id| id.to_string()),
974 proc_mesh = self.id.to_string(),
975 actor_mesh = actor_mesh_id.to_string(),
976 ))]
977 pub(crate) async fn stop_actor_by_id(
978 &self,
979 cx: &impl context::Actor,
980 actor_mesh_id: ActorMeshId,
981 reason: String,
982 ) -> crate::Result<ValueMesh<Status>> {
983 tracing::info!(name = "ProcMeshStatus", status = "ActorMesh::Stop::Attempt");
984 tracing::info!(name = "ActorMeshStatus", status = "Stop::Attempt");
985 let result = self.stop_actor_by_id_inner(cx, actor_mesh_id, reason).await;
986 match &result {
987 Ok(_) => {
988 tracing::info!(name = "ProcMeshStatus", status = "ActorMesh::Stop::Success");
989 tracing::info!(name = "ActorMeshStatus", status = "Stop::Success");
990 }
991 Err(error) => {
992 tracing::error!(name = "ProcMeshStatus", status = "ActorMesh::Stop::Failed", %error);
993 tracing::error!(name = "ActorMeshStatus", status = "Stop::Failed", %error);
994 }
995 }
996 result
997 }
998
999 async fn stop_actor_by_id_inner(
1000 &self,
1001 cx: &impl context::Actor,
1002 actor_mesh_id: ActorMeshId,
1003 reason: String,
1004 ) -> crate::Result<ValueMesh<Status>> {
1005 let region = self.region().clone();
1006 self.proc_agent_mesh.cast(
1007 cx,
1008 resource::Stop {
1009 id: actor_mesh_id.resource_id().clone(),
1010 reason,
1011 },
1012 )?;
1013
1014 let (port, rx) = cx.mailbox().open_accum_port_opts(
1024 crate::StatusMesh::from_single(region.clone(), Status::NotExist),
1027 StreamingReducerOpts {
1028 max_update_interval: Some(Duration::from_millis(50)),
1029 initial_update_interval: None,
1030 },
1031 );
1032 self.proc_agent_mesh.cast(
1036 cx,
1037 resource::WaitRankStatus {
1038 id: actor_mesh_id.resource_id().clone(),
1039 min_status: Status::Stopped,
1040 reply: port.bind(),
1041 },
1042 )?;
1043 let start_time = tokio::time::Instant::now();
1044
1045 let max_idle_time = hyperactor_config::global::get(ACTOR_SPAWN_MAX_IDLE);
1047 match GetRankStatus::wait(
1048 rx,
1049 self.ranks.len(),
1050 max_idle_time,
1051 region.clone(), )
1053 .await
1054 {
1055 Ok(statuses) => {
1056 let all_stopped = statuses.values().all(|s| s.is_terminating());
1060 if all_stopped {
1061 Ok(statuses)
1062 } else {
1063 let legacy = mesh_to_rankedvalues_with_default(
1064 &statuses,
1065 Status::NotExist,
1066 Status::is_not_exist,
1067 self.ranks.len(),
1068 );
1069 Err(Error::ActorStopError { statuses: legacy })
1070 }
1071 }
1072 Err(complete) => {
1073 let legacy = mesh_to_rankedvalues_with_default(
1076 &complete,
1077 Status::Timeout(start_time.elapsed()),
1078 Status::is_not_exist,
1079 self.ranks.len(),
1080 );
1081 Err(Error::ActorStopError { statuses: legacy })
1082 }
1083 }
1084 }
1085}
1086
1087impl fmt::Display for ProcMeshRef {
1088 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1089 write!(f, "{}{{{}}}", self.id, self.region)
1090 }
1091}
1092
1093impl view::Ranked for ProcMeshRef {
1094 type Item = ProcRef;
1095
1096 fn region(&self) -> &Region {
1097 &self.region
1098 }
1099
1100 fn get(&self, rank: usize) -> Option<&Self::Item> {
1101 self.ranks.get(rank)
1102 }
1103}
1104
1105impl view::RankedSliceable for ProcMeshRef {
1106 fn sliced(&self, region: Region) -> Self {
1111 debug_assert!(region.is_subset(view::Ranked::region(self)));
1112 let ranks = self
1113 .region()
1114 .remap(®ion)
1115 .unwrap()
1116 .map(|index| self.get(index).unwrap().clone())
1117 .collect::<Vec<_>>();
1118
1119 Self {
1120 id: self.id.clone(),
1121 proc_agent_mesh: self.proc_agent_mesh.sliced(region.clone()),
1122 region,
1123 ranks: Arc::new(ranks),
1124 host_mesh: self.host_mesh.clone(),
1125 }
1126 }
1127}
1128
1129fn python_class_from_supervision_name(sdn: &str) -> Option<String> {
1145 let inner = sdn.rsplit_once('<')?.1.strip_suffix('>')?;
1146 let qualified = inner.split_whitespace().next()?;
1147 let class_name = qualified.rsplit_once('.')?.1;
1148 Some(format!("Python<{class_name}>"))
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 #[cfg(fbcode_build)]
1154 use std::ops::Deref;
1155 #[cfg(fbcode_build)]
1156 use std::time::Duration;
1157
1158 #[cfg(fbcode_build)]
1159 use hyperactor::Instance;
1160 #[cfg(fbcode_build)]
1161 use hyperactor::config::ENABLE_DEST_ACTOR_REORDERING_BUFFER;
1162 #[cfg(fbcode_build)]
1163 use ndslice::ViewExt as _;
1164 #[cfg(fbcode_build)]
1165 use ndslice::extent;
1166 #[cfg(fbcode_build)]
1167 use timed_test::assert_no_process_leak;
1168 #[cfg(fbcode_build)]
1169 use timed_test::async_timed_test;
1170 #[cfg(fbcode_build)]
1171 use uuid::Uuid;
1172
1173 #[cfg(fbcode_build)]
1174 use crate::ActorMesh;
1175 #[cfg(fbcode_build)]
1176 use crate::comm::ENABLE_NATIVE_V1_CASTING;
1177 #[cfg(fbcode_build)]
1178 use crate::host_mesh::PROC_SPAWN_MAX_IDLE;
1179 #[cfg(fbcode_build)]
1180 use crate::resource::RankedValues;
1181 #[cfg(fbcode_build)]
1182 use crate::resource::Status;
1183 #[cfg(fbcode_build)]
1184 use crate::testactor;
1185 #[cfg(fbcode_build)]
1186 use crate::testing;
1187
1188 #[cfg(fbcode_build)]
1189 async fn execute_spawn_actor() {
1190 hyperactor_telemetry::initialize_logging(hyperactor_telemetry::DefaultTelemetryClock {});
1191
1192 let instance = testing::instance();
1193
1194 let mut hm = testing::host_mesh(2).await;
1195 let proc_mesh = hm
1196 .spawn(&instance, "test", extent!(gpus = 1), None, None)
1197 .await
1198 .unwrap();
1199 let actor_mesh = proc_mesh.spawn(instance, "test", &()).await.unwrap();
1200 testactor::assert_mesh_shape(actor_mesh).await;
1201
1202 let _ = hm.shutdown(instance).await;
1203 }
1204
1205 #[async_timed_test(timeout_secs = 120)]
1206 #[cfg(fbcode_build)]
1207 async fn test_spawn_actor_v1_casting() {
1208 let config = hyperactor_config::global::lock();
1209 let _guard = config.override_key(ENABLE_NATIVE_V1_CASTING, true);
1210 let _guard2 = config.override_key(ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1211 let _guard3 = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
1212 let _guard4 = config.override_key(
1213 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1214 Duration::from_secs(120),
1215 );
1216 execute_spawn_actor().await;
1217 }
1218
1219 #[async_timed_test(timeout_secs = 120)]
1220 #[cfg(fbcode_build)]
1221 async fn test_spawn_actor_v1_casting_p2p() {
1222 let config = hyperactor_config::global::lock();
1223 let _guard = config.override_key(ENABLE_NATIVE_V1_CASTING, true);
1224 let _guard2 = config.override_key(ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1225 let _guard3 = config.override_key(crate::config::V1_CAST_POINT_TO_POINT_THRESHOLD, 1024);
1226 let _guard4 = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
1227 let _guard5 = config.override_key(
1228 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1229 Duration::from_secs(120),
1230 );
1231 execute_spawn_actor().await;
1232 }
1233
1234 #[async_timed_test(timeout_secs = 120)]
1235 #[cfg(fbcode_build)]
1236 async fn test_spawn_actor_v0_casting() {
1237 let config = hyperactor_config::global::lock();
1238 let _guard = config.override_key(ENABLE_NATIVE_V1_CASTING, false);
1239 let _guard2 = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
1240 let _guard3 = config.override_key(
1241 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1242 Duration::from_secs(120),
1243 );
1244 execute_spawn_actor().await;
1245 }
1246
1247 #[cfg(fbcode_build)]
1251 async fn spawn_for_seq_test(
1252 cx: &Instance<testing::TestRootClient>,
1253 proc_mesh: &super::ProcMeshRef,
1254 ) -> ActorMesh<testactor::TestActor> {
1255 let actor_mesh: ActorMesh<testactor::TestActor> =
1256 proc_mesh.spawn(cx, "test", &()).await.unwrap();
1257
1258 let instance = cx
1259 .proc()
1260 .client(&format!("random_casts_{}", Uuid::now_v7()));
1261 let n = 1;
1262 for _ in 0..n {
1263 actor_mesh.cast(&instance, ()).unwrap();
1264 }
1265 println!(
1266 "did {} casts with sequencer session id {}",
1267 n,
1268 instance.sequencer().session_id()
1269 );
1270 actor_mesh
1271 }
1272
1273 #[async_timed_test(timeout_secs = 60)]
1274 #[cfg(fbcode_build)]
1275 async fn test_seq_from_same_sender_to_different_meshes() {
1276 let config = hyperactor_config::global::lock();
1277 let _guard = config.override_key(ENABLE_NATIVE_V1_CASTING, true);
1278 let _guard2 = config.override_key(ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1279 let _guard3 = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
1280 let _guard4 = config.override_key(
1281 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1282 Duration::from_secs(60),
1283 );
1284
1285 hyperactor_telemetry::initialize_logging_for_test();
1286 let instance = testing::instance();
1287 let session_id = instance.sequencer().session_id();
1288
1289 let mut hm = testing::host_mesh(2).await;
1290 let proc_mesh = hm
1291 .spawn(&instance, "test", extent!(gpus = 1), None, None)
1292 .await
1293 .unwrap();
1294 let proc_mesh_ref = proc_mesh.deref();
1295
1296 let handles = (0..3)
1300 .map(|_| {
1301 let proc_mesh_ref_clone = proc_mesh_ref.clone();
1302 tokio::spawn(async move {
1303 let actor_mesh = spawn_for_seq_test(instance, &proc_mesh_ref_clone).await;
1304 let expected_seqs = vec![1; 2];
1305 testactor::assert_casting_correctness(
1306 &actor_mesh,
1307 instance,
1308 Some((session_id, expected_seqs)),
1309 )
1310 .await;
1311 })
1312 })
1313 .collect::<Vec<_>>();
1314 futures::future::join_all(handles).await;
1315
1316 let _ = hm.shutdown(instance).await;
1317 }
1318
1319 #[async_timed_test(timeout_secs = 60)]
1322 #[cfg(fbcode_build)]
1323 async fn test_seq_from_same_sender_to_different_views() {
1324 let config = hyperactor_config::global::lock();
1325 let _guard = config.override_key(ENABLE_NATIVE_V1_CASTING, true);
1326 let _guard2 = config.override_key(ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1327 let _guard3 = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
1328 let _guard4 = config.override_key(
1329 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1330 Duration::from_secs(60),
1331 );
1332
1333 hyperactor_telemetry::initialize_logging_for_test();
1334
1335 let instance = testing::instance();
1336 let session_id = instance.sequencer().session_id();
1337
1338 let mut hm = testing::host_mesh(3).await;
1339 let proc_mesh = hm
1340 .spawn(&instance, "test", extent!(gpus = 1), None, None)
1341 .await
1342 .unwrap();
1343
1344 let actor_mesh = spawn_for_seq_test(instance, &proc_mesh).await;
1345
1346 let expected_seqs = vec![1; 3];
1348 testactor::assert_casting_correctness(
1349 &actor_mesh,
1350 instance,
1351 Some((session_id, expected_seqs)),
1352 )
1353 .await;
1354
1355 let sliced_actor_mesh = actor_mesh.range("hosts", 1..3).unwrap();
1357 let expected_seqs = vec![2; 2];
1359 testactor::assert_casting_correctness(
1360 &sliced_actor_mesh,
1361 instance,
1362 Some((session_id, expected_seqs)),
1363 )
1364 .await;
1365
1366 let sliced_actor_mesh = actor_mesh.range("hosts", 0..2).unwrap();
1368 let expected_seqs = vec![2, 3];
1372 testactor::assert_casting_correctness(
1373 &sliced_actor_mesh,
1374 instance,
1375 Some((session_id, expected_seqs)),
1376 )
1377 .await;
1378
1379 let _ = hm.shutdown(instance).await;
1380 }
1381
1382 #[async_timed_test(timeout_secs = 60)]
1383 #[cfg(fbcode_build)]
1384 async fn test_seq_from_different_senders() {
1385 let config = hyperactor_config::global::lock();
1386 let _guard = config.override_key(ENABLE_NATIVE_V1_CASTING, true);
1387 let _guard2 = config.override_key(ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1388 let _guard3 = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
1389 let _guard4 = config.override_key(
1390 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1391 Duration::from_secs(60),
1392 );
1393
1394 hyperactor_telemetry::initialize_logging_for_test();
1395
1396 use hyperactor::Proc;
1397 use hyperactor::channel::ChannelTransport;
1398
1399 let proc = Proc::direct(ChannelTransport::Unix.any(), "test_0".to_string()).unwrap();
1400 let instance = proc
1401 .actor_instance::<testing::TestRootClient>("test_client")
1402 .unwrap()
1403 .instance;
1404 let first_instance = proc
1405 .actor_instance::<testing::TestRootClient>("first_client")
1406 .unwrap()
1407 .instance;
1408 let second_instance = proc
1409 .actor_instance::<testing::TestRootClient>("second_client")
1410 .unwrap()
1411 .instance;
1412 let third_instance = proc
1413 .actor_instance::<testing::TestRootClient>("third_client")
1414 .unwrap()
1415 .instance;
1416
1417 let mut hm = testing::host_mesh(2).await;
1418 let proc_mesh = hm
1419 .spawn(&instance, "test", extent!(gpus = 1), None, None)
1420 .await
1421 .unwrap();
1422
1423 let actor_mesh = spawn_for_seq_test(&instance, &proc_mesh).await;
1424
1425 for inst in [&first_instance, &second_instance, &third_instance] {
1428 let expected_seqs = vec![1; 2];
1429 let session_id = inst.sequencer().session_id();
1430 testactor::assert_casting_correctness(
1431 &actor_mesh,
1432 inst,
1433 Some((session_id, expected_seqs)),
1434 )
1435 .await;
1436 }
1437
1438 let _ = hm.shutdown(&instance).await;
1439 }
1440
1441 #[cfg(fbcode_build)]
1442 #[assert_no_process_leak]
1443 #[tokio::test]
1444 async fn test_failing_spawn_actor() {
1445 hyperactor_telemetry::initialize_logging(hyperactor_telemetry::DefaultTelemetryClock {});
1446
1447 let config = hyperactor_config::global::lock();
1448 let _guard = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
1449 let _guard2 = config.override_key(
1450 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1451 Duration::from_secs(60),
1452 );
1453
1454 let instance = testing::instance();
1455
1456 let mut hm = testing::host_mesh(1).await;
1457 let proc_mesh = hm
1458 .spawn(&instance, "test", extent!(gpus = 1), None, None)
1459 .await
1460 .unwrap();
1461 let err = proc_mesh
1462 .spawn::<testactor::FailingCreateTestActor, Instance<testing::TestRootClient>>(
1463 instance,
1464 "testfail",
1465 &(),
1466 )
1467 .await
1468 .unwrap_err();
1469 let statuses = err.into_actor_spawn_error().unwrap();
1470 assert_eq!(
1471 statuses,
1472 RankedValues::from((0..1, Status::Failed("test failure".to_string()))),
1473 );
1474
1475 let _ = hm.shutdown(instance).await;
1476 }
1477
1478 #[async_timed_test(timeout_secs = 60)]
1479 #[cfg(fbcode_build)]
1480 async fn test_spawn_actor_on_proc_mesh_slice_only_spawns_slice_members() {
1481 let instance = testing::instance();
1482
1483 let mut hm = testing::host_mesh(2).await;
1484 let proc_mesh = hm
1485 .spawn(&instance, "test", extent!(gpus = 2), None, None)
1486 .await
1487 .unwrap();
1488 let host1 = proc_mesh.range("hosts", 1..2).unwrap();
1489 let actor_name = crate::mesh_id::ActorMeshId::instance(
1490 hyperactor::id::Label::new("slice_only").unwrap(),
1491 );
1492
1493 let actor_mesh = host1
1494 .spawn_with_name::<testactor::TestActor, _>(
1495 instance,
1496 actor_name.clone(),
1497 &(),
1498 None,
1499 false,
1500 )
1501 .await
1502 .unwrap();
1503 testactor::assert_casting_correctness(&actor_mesh, instance, None).await;
1504
1505 let slice_states = host1
1506 .actor_states(instance, actor_name.clone())
1507 .await
1508 .unwrap();
1509 assert_eq!(slice_states.extent(), host1.extent());
1510
1511 let err = proc_mesh
1512 .actor_states(instance, actor_name.clone())
1513 .await
1514 .unwrap_err();
1515 let expected_name = actor_name.into();
1516 match err {
1517 crate::Error::NotExist(name) if name == expected_name => {}
1518 other => panic!("expected NotExist for {expected_name}, got {other:?}"),
1519 }
1520
1521 let _ = hm.shutdown(instance).await;
1522 }
1523
1524 #[cfg(fbcode_build)]
1529 async fn assert_proc_states_match_slice<C: hyperactor::context::Actor>(
1530 mesh: &super::ProcMeshRef,
1531 cx: &C,
1532 ) {
1533 let mut expected: Vec<usize> = mesh.ranks.iter().map(|r| r.create_rank).collect();
1535 expected.sort();
1536
1537 let states = mesh
1538 .states(cx, None)
1539 .await
1540 .unwrap()
1541 .expect("host-backed mesh yields Some");
1542
1543 assert_eq!(states.extent(), mesh.extent());
1544
1545 let mut got: Vec<usize> = states
1546 .values()
1547 .map(|s| {
1548 assert!(
1549 !matches!(
1550 s.status,
1551 Status::NotExist | Status::Failed(_) | Status::Timeout(_)
1552 ),
1553 "rank should resolve to a live proc, got status {:?}",
1554 s.status
1555 );
1556 s.state.expect("live proc carries ProcState").create_rank
1557 })
1558 .collect();
1559
1560 got.sort();
1561
1562 assert_eq!(
1563 got, expected,
1564 "proc_states must return exactly this (sub)mesh's procs"
1565 );
1566 }
1567
1568 #[async_timed_test(timeout_secs = 60)]
1569 #[cfg(fbcode_build)]
1570 async fn test_proc_states_on_sliced_mesh() {
1571 let instance = testing::instance();
1572 let mut hm = testing::host_mesh(2).await;
1573 let proc_mesh = hm
1575 .spawn(&instance, "test", extent!(gpus = 4), None, None)
1576 .await
1577 .unwrap();
1578
1579 assert_proc_states_match_slice(&proc_mesh, instance).await;
1581 assert_proc_states_match_slice(&proc_mesh.range("gpus", 2..4).unwrap(), instance).await;
1583 assert_proc_states_match_slice(&proc_mesh.range("hosts", 1..2).unwrap(), instance).await;
1585 assert_proc_states_match_slice(
1587 &proc_mesh
1588 .range("gpus", 2..4)
1589 .unwrap()
1590 .range("hosts", 1..2)
1591 .unwrap(),
1592 instance,
1593 )
1594 .await;
1595
1596 let _ = hm.shutdown(instance).await;
1597 }
1598
1599 #[test]
1600 fn test_python_class_from_supervision_name() {
1601 use super::python_class_from_supervision_name;
1602
1603 assert_eq!(
1604 python_class_from_supervision_name("instance0.<my_module.MyWorker test_mesh>"),
1605 Some("Python<MyWorker>".to_string()),
1606 );
1607 assert_eq!(
1608 python_class_from_supervision_name(
1609 "instance0.<package.submodule.TrainingActor mesh_0>"
1610 ),
1611 Some("Python<TrainingActor>".to_string()),
1612 );
1613 assert_eq!(python_class_from_supervision_name("plain_name"), None,);
1615 assert_eq!(
1617 python_class_from_supervision_name("instance0.<NoModule mesh>"),
1618 None,
1619 );
1620 }
1621}