diff --git a/monarch_hyperactor/src/context.rs b/monarch_hyperactor/src/context.rs index af0aed9f9..b3dbbd2f1 100644 --- a/monarch_hyperactor/src/context.rs +++ b/monarch_hyperactor/src/context.rs @@ -112,6 +112,13 @@ pub struct PyInstance { pub(crate) rank: PyPoint, #[pyo3(get, set, name = "_children")] children: Option, + + #[pyo3(get, set, name = "name")] + name: String, + #[pyo3(get, set, name = "class_name")] + class_name: Option, + #[pyo3(get, set, name = "creator")] + creator: Option, } #[pymethods] @@ -171,6 +178,9 @@ impl> From for PyInstance { controller_controller: None, rank: PyPoint::new(0, Extent::unity().into()), children: None, + name: "root".to_string(), + class_name: None, + creator: None, } } } diff --git a/python/monarch/_src/actor/actor_mesh.py b/python/monarch/_src/actor/actor_mesh.py index 3ebe39c13..2b15d48a8 100644 --- a/python/monarch/_src/actor/actor_mesh.py +++ b/python/monarch/_src/actor/actor_mesh.py @@ -161,6 +161,12 @@ def proc(self) -> "ProcMesh": rank: Point proc_mesh: "ProcMesh" _controller_controller: "_ControllerController" + name: str # the name this actor was given on spawn + class_name: str # the fully qualified class name of the actor. + creator: Optional[ + "CreatorInstance" + ] # information about the actor who spawned this actor + # None if this actor is the spawning actor. # this property is used to hold the handles to actors and processes launched by this actor # in order to keep them alive until this actor exits. @@ -179,6 +185,47 @@ def _as_rust(self) -> HyInstance: def _as_py(ins: HyInstance) -> "Instance": return cast(Instance, ins) + def _as_creator(self) -> "CreatorInstance": + return CreatorInstance( + self.rank, + self.proc_mesh, + self.proc, + self.name, + self.class_name, + self.creator, + ) + + def __repr__(self) -> str: + return _qualified_name(self) + + +@dataclass +class CreatorInstance: + """ + An instance that can be serialized so it can be passed around + to describe the creation hierarchy of an actor instance. + """ + + rank: Point + proc_mesh: "ProcMesh" + proc: "ProcMesh" + name: str + class_name: Optional[str] + creator: Optional["CreatorInstance"] + + def __repr__(self) -> str: + return _qualified_name(self) + + +def _qualified_name(ins: "CreatorInstance | Instance | None") -> str: + names = [] + while ins: + class_prefix = "" if ins.class_name is None else f"{ins.class_name} " + rank_postfix = str(ins.rank) if len(ins.rank) > 0 else "" + names.append(f"<{class_prefix}{ins.name}{rank_postfix}>") + ins = ins.creator + return ".".join(reversed(names)) + @rust_struct("monarch_hyperactor::context::Context") class Context: @@ -881,8 +928,16 @@ async def handle( match method: case MethodSpecifier.Init(): ins = ctx.actor_instance - Class, ins.proc_mesh, ins._controller_controller, *args = args + ( + Class, + ins.proc_mesh, + ins._controller_controller, + ins.name, + ins.creator, + *args, + ) = args ins.rank = ctx.message_rank + ins.class_name = f"{Class.__module__}.{Class.__qualname__}" try: self.instance = Class(*args, **kwargs) self._maybe_exit_debugger() @@ -1183,6 +1238,7 @@ def _endpoint( def _create( cls, Class: Type[T], + name: str, actor_mesh: "PythonActorMesh", shape: Shape, proc_mesh: "ProcMesh", @@ -1205,7 +1261,18 @@ async def null_func(*_args: Iterable[Any], **_kwargs: Dict[str, Any]) -> None: None, False, ) - send(ep, (mesh._class, proc_mesh, controller_controller, *args), kwargs) + send( + ep, + ( + mesh._class, + proc_mesh, + controller_controller, + name, + context().actor_instance._as_creator(), + *args, + ), + kwargs, + ) return mesh diff --git a/python/monarch/_src/actor/proc_mesh.py b/python/monarch/_src/actor/proc_mesh.py index 9b382a2ad..e3cc151ac 100644 --- a/python/monarch/_src/actor/proc_mesh.py +++ b/python/monarch/_src/actor/proc_mesh.py @@ -463,6 +463,7 @@ def _spawn_nonblocking_on( actor_mesh = HyProcMeshV0.spawn_async(pm, name, _Actor) service = ActorMesh._create( Class, + name, actor_mesh, self._shape, self, diff --git a/python/monarch/_src/actor/python_extension_methods.py b/python/monarch/_src/actor/python_extension_methods.py index f3ff7d920..84829e4cf 100644 --- a/python/monarch/_src/actor/python_extension_methods.py +++ b/python/monarch/_src/actor/python_extension_methods.py @@ -25,9 +25,14 @@ def __call__(self, python_class: Type[T]) -> Type[T]: raise ValueError(f"mismatched type names {rust_name} != {python_name}") for name, implementation in python_class.__dict__.items(): if hasattr(self.rust_class, name): - # do not patch in the stub methods that - # are already defined by the rust implementation - continue + the_attr = getattr(self.rust_class, name) + is_object_default = name.startswith("__") and getattr( + the_attr, "__qualname__", "" + ).startswith("object.") + if not is_object_default: + # do not patch in the stub methods that + # are already defined by the rust implementation + continue if not callable(implementation) and not isinstance( implementation, property ): diff --git a/python/monarch/_src/actor/v1/proc_mesh.py b/python/monarch/_src/actor/v1/proc_mesh.py index 0e83c28ce..de39a9e0c 100644 --- a/python/monarch/_src/actor/v1/proc_mesh.py +++ b/python/monarch/_src/actor/v1/proc_mesh.py @@ -315,6 +315,7 @@ def _spawn_nonblocking_on( ) service = ActorMesh._create( Class, + name, actor_mesh, self._region.as_shape(), self, diff --git a/python/tests/test_python_actors.py b/python/tests/test_python_actors.py index 6fce69ce2..52f404ee9 100644 --- a/python/tests/test_python_actors.py +++ b/python/tests/test_python_actors.py @@ -22,7 +22,7 @@ import unittest.mock from tempfile import TemporaryDirectory from types import ModuleType -from typing import cast, Tuple +from typing import Any, cast, Tuple import monarch.actor import pytest @@ -1731,3 +1731,23 @@ def test_setup_async() -> None: counter.incr.call().get() # Make sure no errors occur in the meantime time.sleep(10) + + +class Named(Actor): + @endpoint + def report(self) -> Any: + return context().actor_instance.creator, str(context().actor_instance) + + +def test_instance_name(): + cr, result = ( + this_host() + .spawn_procs(per_host={"f": 2}) + .spawn("the_name", Named) + .slice(f=0) + .report.call_one() + .get() + ) + assert "test_python_actors.Named the_name{'f': 0/2}>" in result + assert cr.name == "root" + assert str(context().actor_instance) == ""