[xlang] Cross language serialize ActorHandle (#7134)

This commit is contained in:
fyrestone
2020-02-17 20:44:56 +08:00
committed by GitHub
parent b079787c59
commit a6b8bd47b0
27 changed files with 498 additions and 197 deletions
+187 -122
View File
@@ -140,6 +140,83 @@ class ActorMethod:
hardref=True)
class ActorClassMethodMetadata(object):
"""Metadata for all methods in an actor class. This data can be cached.
Attributes:
methods: The actor methods.
decorators: Optional decorators that should be applied to the
method invocation function before invoking the actor methods. These
can be set by attaching the attribute
"__ray_invocation_decorator__" to the actor method.
signatures: The signatures of the methods.
num_return_vals: The default number of return values for
each actor method.
"""
_cache = {} # This cache will be cleared in ray.disconnect()
def __init__(self):
class_name = type(self).__name__
raise Exception("{} can not be constructed directly, "
"instead of running '{}()', try '{}.create()'".format(
class_name, class_name, class_name))
@classmethod
def reset_cache(cls):
cls._cache.clear()
@classmethod
def create(cls, modified_class, actor_creation_function_descriptor):
# Try to create an instance from cache.
cached_meta = cls._cache.get(actor_creation_function_descriptor)
if cached_meta is not None:
return cached_meta
# Create an instance without __init__ called.
self = cls.__new__(cls)
actor_methods = inspect.getmembers(modified_class,
ray.utils.is_function_or_method)
self.methods = dict(actor_methods)
# Extract the signatures of each of the methods. This will be used
# to catch some errors if the methods are called with inappropriate
# arguments.
self.decorators = {}
self.signatures = {}
self.num_return_vals = {}
for method_name, method in actor_methods:
# Whether or not this method requires binding of its first
# argument. For class and static methods, we do not want to bind
# the first argument, but we do for instance methods
is_bound = (ray.utils.is_class_method(method)
or ray.utils.is_static_method(modified_class,
method_name))
# Print a warning message if the method signature is not
# supported. We don't raise an exception because if the actor
# inherits from a class that has a method whose signature we
# don't support, there may not be much the user can do about it.
self.signatures[method_name] = signature.extract_signature(
method, ignore_first=not is_bound)
# Set the default number of return values for this method.
if hasattr(method, "__ray_num_return_vals__"):
self.num_return_vals[method_name] = (
method.__ray_num_return_vals__)
else:
self.num_return_vals[method_name] = (
ray_constants.DEFAULT_ACTOR_METHOD_NUM_RETURN_VALS)
if hasattr(method, "__ray_invocation_decorator__"):
self.decorators[method_name] = (
method.__ray_invocation_decorator__)
# Update cache.
cls._cache[actor_creation_function_descriptor] = self
return self
class ActorClassMetadata:
"""Metadata for an actor class.
@@ -164,15 +241,7 @@ class ActorClassMetadata:
export the remote function again. It is imperfect in the sense that
the actor class definition could be exported multiple times by
different workers.
actor_methods: The actor methods.
method_decorators: Optional decorators that should be applied to the
method invocation function before invoking the actor methods. These
can be set by attaching the attribute
"__ray_invocation_decorator__" to the actor method.
method_signatures: The signatures of the methods.
actor_method_names: The names of the actor methods.
actor_method_num_return_vals: The default number of return values for
each actor method.
method_meta: The actor method metadata.
"""
def __init__(self, language, modified_class,
@@ -193,58 +262,8 @@ class ActorClassMetadata:
self.object_store_memory = object_store_memory
self.resources = resources
self.last_export_session_and_job = None
self.actor_methods = inspect.getmembers(
self.modified_class, ray.utils.is_function_or_method)
self.actor_method_names = [
method_name for method_name, _ in self.actor_methods
]
constructor_name = "__init__"
if not self.is_cross_language and \
constructor_name not in self.actor_method_names:
# Add __init__ if it does not exist.
# Actor creation will be executed with __init__ together.
# Assign an __init__ function will avoid many checks later on.
def __init__(self):
pass
self.modified_class.__init__ = __init__
self.actor_method_names.append(constructor_name)
self.actor_methods.append((constructor_name, __init__))
# Extract the signatures of each of the methods. This will be used
# to catch some errors if the methods are called with inappropriate
# arguments.
self.method_decorators = {}
self.method_signatures = {}
self.actor_method_num_return_vals = {}
for method_name, method in self.actor_methods:
# Whether or not this method requires binding of its first
# argument. For class and static methods, we do not want to bind
# the first argument, but we do for instance methods
is_bound = (ray.utils.is_class_method(method)
or ray.utils.is_static_method(self.modified_class,
method_name))
# Print a warning message if the method signature is not
# supported. We don't raise an exception because if the actor
# inherits from a class that has a method whose signature we
# don't support, there may not be much the user can do about it.
self.method_signatures[method_name] = signature.extract_signature(
method, ignore_first=not is_bound)
# Set the default number of return values for this method.
if hasattr(method, "__ray_num_return_vals__"):
self.actor_method_num_return_vals[method_name] = (
method.__ray_num_return_vals__)
else:
self.actor_method_num_return_vals[method_name] = (
ray_constants.DEFAULT_ACTOR_METHOD_NUM_RETURN_VALS)
if hasattr(method, "__ray_invocation_decorator__"):
self.method_decorators[method_name] = (
method.__ray_invocation_decorator__)
self.method_meta = ActorClassMethodMetadata.create(
modified_class, actor_creation_function_descriptor)
class ActorClass:
@@ -321,8 +340,9 @@ class ActorClass:
# Construct the base object.
self = DerivedActorClass.__new__(DerivedActorClass)
# Actor creation function descriptor.
actor_creation_function_descriptor = PythonFunctionDescriptor(
modified_class.__module__, "__init__", modified_class.__name__)
actor_creation_function_descriptor = \
PythonFunctionDescriptor.from_class(
modified_class.__ray_actor_class__)
self.__ray_metadata__ = ActorClassMetadata(
Language.PYTHON, modified_class,
@@ -517,7 +537,7 @@ class ActorClass:
worker.function_actor_manager.export_actor_class(
meta.modified_class,
meta.actor_creation_function_descriptor,
meta.actor_method_names)
meta.method_meta.methods.keys())
resources = ray.utils.resources_from_resource_arguments(
cpus_to_use, meta.num_gpus, meta.memory,
@@ -536,23 +556,30 @@ class ActorClass:
creation_args = cross_language.format_args(
worker, args, kwargs)
else:
function_signature = meta.method_signatures["__init__"]
function_signature = meta.method_meta.signatures["__init__"]
creation_args = signature.flatten_args(function_signature,
args, kwargs)
actor_id = worker.core_worker.create_actor(
meta.language, meta.actor_creation_function_descriptor,
creation_args, meta.max_reconstructions, resources,
actor_placement_resources, is_direct_call, max_concurrency,
detached, is_asyncio)
meta.language,
meta.actor_creation_function_descriptor,
creation_args,
meta.max_reconstructions,
resources,
actor_placement_resources,
is_direct_call,
max_concurrency,
detached,
is_asyncio,
# Store actor_method_cpu in actor handle's extension data.
extension_data=str(actor_method_cpu))
actor_handle = ActorHandle(
meta.language,
actor_id,
meta.method_decorators,
meta.method_signatures,
meta.actor_method_num_return_vals,
meta.method_meta.decorators,
meta.method_meta.signatures,
meta.method_meta.num_return_vals,
actor_method_cpu,
meta.is_cross_language,
meta.actor_creation_function_descriptor,
worker.current_session_and_job,
original_handle=True)
@@ -600,7 +627,6 @@ class ActorHandle:
method_signatures,
method_num_return_vals,
actor_method_cpus,
is_cross_language,
actor_creation_function_descriptor,
session_and_job,
original_handle=False):
@@ -612,7 +638,7 @@ class ActorHandle:
self._ray_method_num_return_vals = method_num_return_vals
self._ray_actor_method_cpus = actor_method_cpus
self._ray_session_and_job = session_and_job
self._ray_is_cross_language = is_cross_language
self._ray_is_cross_language = language != Language.PYTHON
self._ray_actor_creation_function_descriptor = \
actor_creation_function_descriptor
self._ray_function_descriptor = {}
@@ -700,6 +726,21 @@ class ActorHandle:
if not self._ray_is_cross_language:
raise AttributeError("'{}' object has no attribute '{}'".format(
type(self).__name__, item))
if item in ["__ray_terminate__", "__ray_checkpoint__"]:
class FakeActorMethod(object):
def __call__(self, *args, **kwargs):
raise Exception(
"Actor methods cannot be called directly. Instead "
"of running 'object.{}()', try 'object.{}.remote()'.".
format(item, item))
def remote(self, *args, **kwargs):
logger.warning(
"Actor method {} is not supported by cross language."
.format(item))
return FakeActorMethod()
return ActorMethod(
self,
@@ -779,24 +820,27 @@ class ActorHandle:
"""
worker = ray.worker.get_global_worker()
worker.check_connected()
state = {
"actor_language": self._ray_actor_language,
# Local mode just uses the actor ID.
"core_handle": worker.core_worker.serialize_actor_handle(
self._ray_actor_id)
if hasattr(worker, "core_worker") else self._ray_actor_id,
"method_decorators": self._ray_method_decorators,
"method_signatures": self._ray_method_signatures,
"method_num_return_vals": self._ray_method_num_return_vals,
"actor_method_cpus": self._ray_actor_method_cpus,
"is_cross_language": self._ray_is_cross_language,
"actor_creation_function_descriptor": self.
_ray_actor_creation_function_descriptor,
}
if hasattr(worker, "core_worker"):
# Non-local mode
state = worker.core_worker.serialize_actor_handle(self)
else:
# Local mode
state = {
"actor_language": self._ray_actor_language,
"actor_id": self._ray_actor_id,
"method_decorators": self._ray_method_decorators,
"method_signatures": self._ray_method_signatures,
"method_num_return_vals": self._ray_method_num_return_vals,
"actor_method_cpus": self._ray_actor_method_cpus,
"actor_creation_function_descriptor": self.
_ray_actor_creation_function_descriptor,
}
return state
def _deserialization_helper(self, state, ray_forking):
@classmethod
def _deserialization_helper(cls, state, ray_forking):
"""This is defined in order to make pickling work.
Args:
@@ -807,33 +851,35 @@ class ActorHandle:
worker = ray.worker.get_global_worker()
worker.check_connected()
self.__init__(
# TODO(swang): Accessing the worker's current task ID is not
# thread-safe.
# Local mode just uses the actor ID.
state["actor_language"],
worker.core_worker.deserialize_and_register_actor_handle(
state["core_handle"])
if hasattr(worker, "core_worker") else state["core_handle"],
state["method_decorators"],
state["method_signatures"],
state["method_num_return_vals"],
state["actor_method_cpus"],
state["is_cross_language"],
state["actor_creation_function_descriptor"],
worker.current_session_and_job)
if hasattr(worker, "core_worker"):
# Non-local mode
return worker.core_worker.deserialize_and_register_actor_handle(
state)
else:
# Local mode
return cls(
# TODO(swang): Accessing the worker's current task ID is not
# thread-safe.
state["actor_language"],
state["actor_id"],
state["method_decorators"],
state["method_signatures"],
state["method_num_return_vals"],
state["actor_method_cpus"],
state["actor_creation_function_descriptor"],
worker.current_session_and_job)
def __getstate__(self):
def __reduce__(self):
"""This code path is used by pickling but not by Ray forking."""
return self._serialization_helper(False)
def __setstate__(self, state):
"""This code path is used by pickling but not by Ray forking."""
return self._deserialization_helper(state, False)
state = self._serialization_helper(False)
return ActorHandle._deserialization_helper, (state, False)
def make_actor(cls, num_cpus, num_gpus, memory, object_store_memory, resources,
max_reconstructions):
def modify_class(cls):
# cls has been modified.
if hasattr(cls, "__ray_actor_class__"):
return cls
# Give an error if cls is an old-style class.
if not issubclass(cls, object):
raise TypeError(
@@ -846,18 +892,11 @@ def make_actor(cls, num_cpus, num_gpus, memory, object_store_memory, resources,
"A checkpointable actor class should implement all abstract "
"methods in the `Checkpointable` interface.")
if max_reconstructions is None:
max_reconstructions = 0
if not (ray_constants.NO_RECONSTRUCTION <= max_reconstructions <=
ray_constants.INFINITE_RECONSTRUCTION):
raise Exception("max_reconstructions must be in range [%d, %d]." %
(ray_constants.NO_RECONSTRUCTION,
ray_constants.INFINITE_RECONSTRUCTION))
# Modify the class to have an additional method that will be used for
# terminating the worker.
class Class(cls):
__ray_actor_class__ = cls # The original actor class
def __ray_terminate__(self):
worker = ray.worker.get_global_worker()
if worker.mode != ray.LOCAL_MODE:
@@ -880,6 +919,32 @@ def make_actor(cls, num_cpus, num_gpus, memory, object_store_memory, resources,
Class.__module__ = cls.__module__
Class.__name__ = cls.__name__
if not ray.utils.is_function_or_method(getattr(Class, "__init__", None)):
# Add __init__ if it does not exist.
# Actor creation will be executed with __init__ together.
# Assign an __init__ function will avoid many checks later on.
def __init__(self):
pass
Class.__init__ = __init__
return Class
def make_actor(cls, num_cpus, num_gpus, memory, object_store_memory, resources,
max_reconstructions):
Class = modify_class(cls)
if max_reconstructions is None:
max_reconstructions = 0
if not (ray_constants.NO_RECONSTRUCTION <= max_reconstructions <=
ray_constants.INFINITE_RECONSTRUCTION):
raise Exception("max_reconstructions must be in range [%d, %d]." %
(ray_constants.NO_RECONSTRUCTION,
ray_constants.INFINITE_RECONSTRUCTION))
return ActorClass._ray_from_modified_class(
Class, ActorClassID.from_random(), max_reconstructions, num_cpus,
num_gpus, memory, object_store_memory, resources)