def _task_table(self, task_id): """Fetch and parse the task table information for a single task ID. Args: task_id_binary: A string of bytes with the task ID to get information about. Returns: A dictionary with information about the task ID in question. """ message = self._execute_command(task_id, "RAY.TABLE_LOOKUP", ray.gcs_utils.TablePrefix.RAYLET_TASK, "", task_id.id()) gcs_entries = ray.gcs_utils.GcsTableEntry.GetRootAsGcsTableEntry( message, 0) assert gcs_entries.EntriesLength() == 1 task_table_message = ray.gcs_utils.Task.GetRootAsTask( gcs_entries.Entries(0), 0) execution_spec = task_table_message.TaskExecutionSpec() task_spec = task_table_message.TaskSpecification() task_spec = ray.raylet.task_from_string(task_spec) function_descriptor_list = task_spec.function_descriptor_list() function_descriptor = FunctionDescriptor.from_bytes_list( function_descriptor_list) task_spec_info = { "DriverID": binary_to_hex(task_spec.driver_id().id()), "TaskID": binary_to_hex(task_spec.task_id().id()), "ParentTaskID": binary_to_hex(task_spec.parent_task_id().id()), "ParentCounter": task_spec.parent_counter(), "ActorID": binary_to_hex(task_spec.actor_id().id()), "ActorCreationID": binary_to_hex( task_spec.actor_creation_id().id()), "ActorCreationDummyObjectID": binary_to_hex( task_spec.actor_creation_dummy_object_id().id()), "ActorCounter": task_spec.actor_counter(), "Args": task_spec.arguments(), "ReturnObjectIDs": task_spec.returns(), "RequiredResources": task_spec.required_resources(), "FunctionID": binary_to_hex(function_descriptor.function_id.id()), "FunctionHash": binary_to_hex(function_descriptor.function_hash), "ModuleName": function_descriptor.module_name, "ClassName": function_descriptor.class_name, "FunctionName": function_descriptor.function_name, } return { "ExecutionSpec": { "Dependencies": [ execution_spec.Dependencies(i) for i in range(execution_spec.DependenciesLength()) ], "LastTimestamp": execution_spec.LastTimestamp(), "NumForwards": execution_spec.NumForwards() }, "TaskSpec": task_spec_info }
def __init__(self, function, num_cpus, num_gpus, resources, num_return_vals, max_calls): self._function = function self._function_descriptor = FunctionDescriptor.from_function(function) self._function_name = ( self._function.__module__ + '.' + self._function.__name__) self._num_cpus = (DEFAULT_REMOTE_FUNCTION_CPUS if num_cpus is None else num_cpus) self._num_gpus = num_gpus self._resources = resources self._num_return_vals = (DEFAULT_REMOTE_FUNCTION_NUM_RETURN_VALS if num_return_vals is None else num_return_vals) self._max_calls = (DEFAULT_REMOTE_FUNCTION_MAX_CALLS if max_calls is None else max_calls) ray.signature.check_signature_supported(self._function) self._function_signature = ray.signature.extract_signature( self._function) # # Export the function. worker = ray.worker.get_global_worker() worker.function_actor_manager.export(self)
def _remote(self, args=None, kwargs=None, num_cpus=None, num_gpus=None, memory=None, object_store_memory=None, resources=None, is_direct_call=None, max_concurrency=None, name=None, detached=False, is_asyncio=False): """Create an actor. This method allows more flexibility than the remote method because resource requirements can be specified and override the defaults in the decorator. Args: args: The arguments to forward to the actor constructor. kwargs: The keyword arguments to forward to the actor constructor. num_cpus: The number of CPUs required by the actor creation task. num_gpus: The number of GPUs required by the actor creation task. memory: Restrict the heap memory usage of this actor. object_store_memory: Restrict the object store memory used by this actor when creating objects. resources: The custom resources required by the actor creation task. is_direct_call: Use direct actor calls. max_concurrency: The max number of concurrent calls to allow for this actor. This only works with direct actor calls. The max concurrency defaults to 1 for threaded execution, and 1000 for asyncio execution. Note that the execution order is not guaranteed when max_concurrency > 1. name: The globally unique name for the actor. detached: Whether the actor should be kept alive after driver exits. is_asyncio: Turn on async actor calls. This only works with direct actor calls. Returns: A handle to the newly created actor. """ if args is None: args = [] if kwargs is None: kwargs = {} if is_direct_call is None: is_direct_call = ray_constants.direct_call_enabled() if max_concurrency is None: if is_asyncio: max_concurrency = 1000 else: max_concurrency = 1 if max_concurrency > 1 and not is_direct_call: raise ValueError( "setting max_concurrency requires is_direct_call=True") if max_concurrency < 1: raise ValueError("max_concurrency must be >= 1") if is_asyncio and not is_direct_call: raise ValueError( "Setting is_asyncio requires is_direct_call=True.") worker = ray.worker.get_global_worker() if worker.mode is None: raise Exception("Actors cannot be created before ray.init() " "has been called.") meta = self.__ray_metadata__ if detached and name is None: raise Exception("Detached actors must be named. " "Please use Actor._remote(name='some_name') " "to associate the name.") # Check whether the name is already taken. if name is not None: try: ray.experimental.get_actor(name) except ValueError: # name is not taken, expected. pass else: raise ValueError( "The name {name} is already taken. Please use " "a different name or get existing actor using " "ray.experimental.get_actor('{name}')".format(name=name)) # Set the actor's default resources if not already set. First three # conditions are to check that no resources were specified in the # decorator. Last three conditions are to check that no resources were # specified when _remote() was called. if (meta.num_cpus is None and meta.num_gpus is None and meta.resources is None and num_cpus is None and num_gpus is None and resources is None): # In the default case, actors acquire no resources for # their lifetime, and actor methods will require 1 CPU. cpus_to_use = ray_constants.DEFAULT_ACTOR_CREATION_CPU_SIMPLE actor_method_cpu = ray_constants.DEFAULT_ACTOR_METHOD_CPU_SIMPLE else: # If any resources are specified (here or in decorator), then # all resources are acquired for the actor's lifetime and no # resources are associated with methods. cpus_to_use = (ray_constants.DEFAULT_ACTOR_CREATION_CPU_SPECIFIED if meta.num_cpus is None else meta.num_cpus) actor_method_cpu = ray_constants.DEFAULT_ACTOR_METHOD_CPU_SPECIFIED function_name = "__init__" function_descriptor = FunctionDescriptor( meta.modified_class.__module__, function_name, meta.modified_class.__name__) # Do not export the actor class or the actor if run in LOCAL_MODE # Instead, instantiate the actor locally and add it to the worker's # dictionary if worker.mode == ray.LOCAL_MODE: actor_id = ActorID.from_random() worker.actors[actor_id] = meta.modified_class( *copy.deepcopy(args), **copy.deepcopy(kwargs)) else: # Export the actor. if (meta.last_export_session_and_job != worker.current_session_and_job): # If this actor class was not exported in this session and job, # we need to export this function again, because current GCS # doesn't have it. meta.last_export_session_and_job = ( worker.current_session_and_job) worker.function_actor_manager.export_actor_class( meta.modified_class, meta.actor_method_names) resources = ray.utils.resources_from_resource_arguments( cpus_to_use, meta.num_gpus, meta.memory, meta.object_store_memory, meta.resources, num_cpus, num_gpus, memory, object_store_memory, resources) # If the actor methods require CPU resources, then set the required # placement resources. If actor_placement_resources is empty, then # the required placement resources will be the same as resources. actor_placement_resources = {} assert actor_method_cpu in [0, 1] if actor_method_cpu == 1: actor_placement_resources = resources.copy() actor_placement_resources["CPU"] += 1 function_signature = meta.method_signatures[function_name] creation_args = signature.flatten_args(function_signature, args, kwargs) actor_id = worker.core_worker.create_actor( function_descriptor.get_function_descriptor_list(), creation_args, meta.max_reconstructions, resources, actor_placement_resources, is_direct_call, max_concurrency, detached, is_asyncio) actor_handle = ActorHandle(actor_id, meta.modified_class.__module__, meta.class_name, meta.actor_method_names, meta.method_decorators, meta.method_signatures, meta.actor_method_num_return_vals, actor_method_cpu, worker.current_session_and_job, original_handle=True) if name is not None: ray.experimental.register_actor(name, actor_handle) return actor_handle
def _remote(self, args=None, kwargs=None, num_return_vals=None, is_direct_call=None, num_cpus=None, num_gpus=None, memory=None, object_store_memory=None, resources=None): """Submit the remote function for execution.""" worker = ray.worker.get_global_worker() worker.check_connected() # If this function was not exported in this session and job, we need to # export this function again, because the current GCS doesn't have it. if self._last_export_session_and_job != worker.current_session_and_job: # There is an interesting question here. If the remote function is # used by a subsequent driver (in the same script), should the # second driver pickle the function again? If yes, then the remote # function definition can differ in the second driver (e.g., if # variables in its closure have changed). We probably want the # behavior of the remote function in the second driver to be # independent of whether or not the function was invoked by the # first driver. This is an argument for repickling the function, # which we do here. self._pickled_function = pickle.dumps(self._function) self._function_descriptor = FunctionDescriptor.from_function( self._function, self._pickled_function) self._function_descriptor_list = ( self._function_descriptor.get_function_descriptor_list()) self._last_export_session_and_job = worker.current_session_and_job worker.function_actor_manager.export(self) kwargs = {} if kwargs is None else kwargs args = [] if args is None else args if num_return_vals is None: num_return_vals = self._num_return_vals if is_direct_call is None: is_direct_call = self.direct_call_enabled resources = ray.utils.resources_from_resource_arguments( self._num_cpus, self._num_gpus, self._memory, self._object_store_memory, self._resources, num_cpus, num_gpus, memory, object_store_memory, resources) def invocation(args, kwargs): if not args and not kwargs and not self._function_signature: list_args = [] else: list_args = ray.signature.flatten_args( self._function_signature, args, kwargs) if worker.mode == ray.worker.LOCAL_MODE: object_ids = worker.local_mode_manager.execute( self._function, self._function_descriptor, args, kwargs, num_return_vals) else: object_ids = worker.core_worker.submit_task( self._function_descriptor_list, list_args, num_return_vals, is_direct_call, resources) if len(object_ids) == 1: return object_ids[0] elif len(object_ids) > 1: return object_ids if self._decorator is not None: invocation = self._decorator(invocation) return invocation(args, kwargs)
def _task_table(self, task_id): """Fetch and parse the task table information for a single task ID. Args: task_id: A task ID to get information about. Returns: A dictionary with information about the task ID in question. """ assert isinstance(task_id, ray.TaskID) message = self._execute_command( task_id, "RAY.TABLE_LOOKUP", gcs_utils.TablePrefix.Value("RAYLET_TASK"), "", task_id.binary()) if message is None: return {} gcs_entries = gcs_utils.GcsEntry.FromString(message) assert len(gcs_entries.entries) == 1 task_table_data = gcs_utils.TaskTableData.FromString( gcs_entries.entries[0]) task_table_message = gcs_utils.Task.GetRootAsTask( task_table_data.task, 0) execution_spec = task_table_message.TaskExecutionSpec() task_spec = task_table_message.TaskSpecification() task = ray._raylet.Task.from_string(task_spec) function_descriptor_list = task.function_descriptor_list() function_descriptor = FunctionDescriptor.from_bytes_list( function_descriptor_list) task_spec_info = { "JobID": task.job_id().hex(), "TaskID": task.task_id().hex(), "ParentTaskID": task.parent_task_id().hex(), "ParentCounter": task.parent_counter(), "ActorID": (task.actor_id().hex()), "ActorCreationID": task.actor_creation_id().hex(), "ActorCreationDummyObjectID": (task.actor_creation_dummy_object_id().hex()), "ActorCounter": task.actor_counter(), "Args": task.arguments(), "ReturnObjectIDs": task.returns(), "RequiredResources": task.required_resources(), "FunctionID": function_descriptor.function_id.hex(), "FunctionHash": binary_to_hex(function_descriptor.function_hash), "ModuleName": function_descriptor.module_name, "ClassName": function_descriptor.class_name, "FunctionName": function_descriptor.function_name, } return { "ExecutionSpec": { "Dependencies": [ execution_spec.Dependencies(i) for i in range(execution_spec.DependenciesLength()) ], "LastTimestamp": execution_spec.LastTimestamp(), "NumForwards": execution_spec.NumForwards() }, "TaskSpec": task_spec_info }
def _actor_method_call(self, method_name, args=None, kwargs=None, num_return_vals=None): """Method execution stub for an actor handle. This is the function that executes when `actor.method_name.remote(*args, **kwargs)` is called. Instead of executing locally, the method is packaged as a task and scheduled to the remote actor instance. Args: method_name: The name of the actor method to execute. args: A list of arguments for the actor method. kwargs: A dictionary of keyword arguments for the actor method. dependency: The object ID that this method is dependent on. Defaults to None, for no dependencies. Most tasks should pass in the dummy object returned by the preceding task. Some tasks, such as checkpoint and terminate methods, have no dependencies. Returns: object_ids: A list of object IDs returned by the remote actor method. """ worker = ray.worker.get_global_worker() worker.check_connected() function_signature = self._ray_method_signatures[method_name] if args is None: args = [] if kwargs is None: kwargs = {} args = signature.extend_args(function_signature, args, kwargs) # Execute functions locally if Ray is run in LOCAL_MODE # Copy args to prevent the function from mutating them. if worker.mode == ray.LOCAL_MODE: return getattr(worker.actors[self._ray_actor_id], method_name)(*copy.deepcopy(args)) is_actor_checkpoint_method = (method_name == "__ray_checkpoint__") function_descriptor = FunctionDescriptor(self._ray_module_name, method_name, self._ray_class_name) with self._ray_actor_lock: object_ids = worker.submit_task( function_descriptor, args, actor_id=self._ray_actor_id, actor_handle_id=self._ray_actor_handle_id, actor_counter=self._ray_actor_counter, is_actor_checkpoint_method=is_actor_checkpoint_method, actor_creation_dummy_object_id=( self._ray_actor_creation_dummy_object_id), execution_dependencies=[self._ray_actor_cursor], new_actor_handles=self._ray_new_actor_handles, # We add one for the dummy return ID. num_return_vals=num_return_vals + 1, resources={"CPU": self._ray_actor_method_cpus}, placement_resources={}, driver_id=self._ray_actor_driver_id, ) # Update the actor counter and cursor to reflect the most recent # invocation. self._ray_actor_counter += 1 # The last object returned is the dummy object that should be # passed in to the next actor method. Do not return it to the user. self._ray_actor_cursor = object_ids.pop() # We have notified the backend of the new actor handles to expect # since the last task was submitted, so clear the list. self._ray_new_actor_handles = [] if len(object_ids) == 1: object_ids = object_ids[0] elif len(object_ids) == 0: object_ids = None return object_ids
def _remote(self, args, kwargs, num_cpus=None, num_gpus=None, resources=None): """Create an actor. This method allows more flexibility than the remote method because resource requirements can be specified and override the defaults in the decorator. Args: args: The arguments to forward to the actor constructor. kwargs: The keyword arguments to forward to the actor constructor. num_cpus: The number of CPUs required by the actor creation task. num_gpus: The number of GPUs required by the actor creation task. resources: The custom resources required by the actor creation task. Returns: A handle to the newly created actor. """ worker = ray.worker.get_global_worker() if worker.mode is None: raise Exception("Actors cannot be created before ray.init() " "has been called.") actor_id = ObjectID(_random_string()) # The actor cursor is a dummy object representing the most recent # actor method invocation. For each subsequent method invocation, # the current cursor should be added as a dependency, and then # updated to reflect the new invocation. actor_cursor = None # Do not export the actor class or the actor if run in LOCAL_MODE # Instead, instantiate the actor locally and add it to the worker's # dictionary if worker.mode == ray.LOCAL_MODE: worker.actors[actor_id] = self._modified_class( *copy.deepcopy(args), **copy.deepcopy(kwargs)) else: # Export the actor. if not self._exported: worker.function_actor_manager.export_actor_class( self._modified_class, self._actor_method_names, self._checkpoint_interval) self._exported = True resources = ray.utils.resources_from_resource_arguments( self._num_cpus, self._num_gpus, self._resources, num_cpus, num_gpus, resources) # If the actor methods require CPU resources, then set the required # placement resources. If actor_placement_resources is empty, then # the required placement resources will be the same as resources. actor_placement_resources = {} assert self._actor_method_cpus in [0, 1] if self._actor_method_cpus == 1: actor_placement_resources = resources.copy() actor_placement_resources["CPU"] += 1 if args is None: args = [] if kwargs is None: kwargs = {} function_name = "__init__" function_signature = self._method_signatures[function_name] creation_args = signature.extend_args(function_signature, args, kwargs) function_descriptor = FunctionDescriptor( self._modified_class.__module__, function_name, self._modified_class.__name__) [actor_cursor] = worker.submit_task( function_descriptor, creation_args, actor_creation_id=actor_id, max_actor_reconstructions=self._max_reconstructions, num_return_vals=1, resources=resources, placement_resources=actor_placement_resources) actor_handle = ActorHandle( actor_id, self._modified_class.__module__, self._class_name, actor_cursor, self._actor_method_names, self._method_signatures, self._actor_method_num_return_vals, actor_cursor, self._actor_method_cpus, worker.task_driver_id) # We increment the actor counter by 1 to account for the actor creation # task. actor_handle._ray_actor_counter += 1 return actor_handle
def _task_table(self, task_id): """Fetch and parse the task table information for a single task ID. Args: task_id_binary: A string of bytes with the task ID to get information about. Returns: A dictionary with information about the task ID in question. """ message = self._execute_command(task_id, "RAY.TABLE_LOOKUP", ray.gcs_utils.TablePrefix.RAYLET_TASK, "", task_id.id()) gcs_entries = ray.gcs_utils.GcsTableEntry.GetRootAsGcsTableEntry( message, 0) assert gcs_entries.EntriesLength() == 1 task_table_message = ray.gcs_utils.Task.GetRootAsTask( gcs_entries.Entries(0), 0) execution_spec = task_table_message.TaskExecutionSpec() task_spec = task_table_message.TaskSpecification() task_spec = ray.raylet.task_from_string(task_spec) function_descriptor_list = task_spec.function_descriptor_list() function_descriptor = FunctionDescriptor.from_bytes_list( function_descriptor_list) task_spec_info = { "DriverID": binary_to_hex(task_spec.driver_id().id()), "TaskID": binary_to_hex(task_spec.task_id().id()), "ParentTaskID": binary_to_hex(task_spec.parent_task_id().id()), "ParentCounter": task_spec.parent_counter(), "ActorID": binary_to_hex(task_spec.actor_id().id()), "ActorCreationID": binary_to_hex(task_spec.actor_creation_id().id()), "ActorCreationDummyObjectID": binary_to_hex(task_spec.actor_creation_dummy_object_id().id()), "ActorCounter": task_spec.actor_counter(), "Args": task_spec.arguments(), "ReturnObjectIDs": task_spec.returns(), "RequiredResources": task_spec.required_resources(), "FunctionID": binary_to_hex(function_descriptor.function_id.id()), "FunctionHash": binary_to_hex(function_descriptor.function_hash), "ModuleName": function_descriptor.module_name, "ClassName": function_descriptor.class_name, "FunctionName": function_descriptor.function_name, } return { "ExecutionSpec": { "Dependencies": [ execution_spec.Dependencies(i) for i in range(execution_spec.DependenciesLength()) ], "LastTimestamp": execution_spec.LastTimestamp(), "NumForwards": execution_spec.NumForwards() }, "TaskSpec": task_spec_info }
def _remote(self, args=None, kwargs=None, num_cpus=None, num_gpus=None, resources=None): """Create an actor. This method allows more flexibility than the remote method because resource requirements can be specified and override the defaults in the decorator. Args: args: The arguments to forward to the actor constructor. kwargs: The keyword arguments to forward to the actor constructor. num_cpus: The number of CPUs required by the actor creation task. num_gpus: The number of GPUs required by the actor creation task. resources: The custom resources required by the actor creation task. Returns: A handle to the newly created actor. """ if args is None: args = [] if kwargs is None: kwargs = {} worker = ray.worker.get_global_worker() if worker.mode is None: raise Exception("Actors cannot be created before ray.init() " "has been called.") actor_id = ActorID(_random_string()) # The actor cursor is a dummy object representing the most recent # actor method invocation. For each subsequent method invocation, # the current cursor should be added as a dependency, and then # updated to reflect the new invocation. actor_cursor = None # Set the actor's default resources if not already set. First three # conditions are to check that no resources were specified in the # decorator. Last three conditions are to check that no resources were # specified when _remote() was called. if (self._num_cpus is None and self._num_gpus is None and self._resources is None and num_cpus is None and num_gpus is None and resources is None): # In the default case, actors acquire no resources for # their lifetime, and actor methods will require 1 CPU. cpus_to_use = ray_constants.DEFAULT_ACTOR_CREATION_CPU_SIMPLE actor_method_cpu = ray_constants.DEFAULT_ACTOR_METHOD_CPU_SIMPLE else: # If any resources are specified (here or in decorator), then # all resources are acquired for the actor's lifetime and no # resources are associated with methods. cpus_to_use = (ray_constants.DEFAULT_ACTOR_CREATION_CPU_SPECIFIED if self._num_cpus is None else self._num_cpus) actor_method_cpu = ray_constants.DEFAULT_ACTOR_METHOD_CPU_SPECIFIED # Do not export the actor class or the actor if run in LOCAL_MODE # Instead, instantiate the actor locally and add it to the worker's # dictionary if worker.mode == ray.LOCAL_MODE: worker.actors[actor_id] = self._modified_class( *copy.deepcopy(args), **copy.deepcopy(kwargs)) else: # Export the actor. if not self._exported: worker.function_actor_manager.export_actor_class( self._modified_class, self._actor_method_names) self._exported = True resources = ray.utils.resources_from_resource_arguments( cpus_to_use, self._num_gpus, self._resources, num_cpus, num_gpus, resources) # If the actor methods require CPU resources, then set the required # placement resources. If actor_placement_resources is empty, then # the required placement resources will be the same as resources. actor_placement_resources = {} assert actor_method_cpu in [0, 1] if actor_method_cpu == 1: actor_placement_resources = resources.copy() actor_placement_resources["CPU"] += 1 function_name = "__init__" function_signature = self._method_signatures[function_name] creation_args = signature.extend_args(function_signature, args, kwargs) function_descriptor = FunctionDescriptor( self._modified_class.__module__, function_name, self._modified_class.__name__) [actor_cursor] = worker.submit_task( function_descriptor, creation_args, actor_creation_id=actor_id, max_actor_reconstructions=self._max_reconstructions, num_return_vals=1, resources=resources, placement_resources=actor_placement_resources) assert isinstance(actor_cursor, ObjectID) actor_handle = ActorHandle( actor_id, self._modified_class.__module__, self._class_name, actor_cursor, self._actor_method_names, self._method_signatures, self._actor_method_num_return_vals, actor_cursor, actor_method_cpu, worker.task_driver_id) # We increment the actor counter by 1 to account for the actor creation # task. actor_handle._ray_actor_counter += 1 return actor_handle
def _actor_method_call(self, method_name, args=None, kwargs=None, num_return_vals=None): """Method execution stub for an actor handle. This is the function that executes when `actor.method_name.remote(*args, **kwargs)` is called. Instead of executing locally, the method is packaged as a task and scheduled to the remote actor instance. Args: method_name: The name of the actor method to execute. args: A list of arguments for the actor method. kwargs: A dictionary of keyword arguments for the actor method. num_return_vals (int): The number of return values for the method. Returns: object_ids: A list of object IDs returned by the remote actor method. """ worker = ray.worker.get_global_worker() worker.check_connected() function_signature = self._ray_method_signatures[method_name] if args is None: args = [] if kwargs is None: kwargs = {} args = signature.extend_args(function_signature, args, kwargs) function_descriptor = FunctionDescriptor(self._ray_module_name, method_name, self._ray_class_name) if worker.mode == ray.LOCAL_MODE: function = getattr(worker.actors[self._ray_actor_id], method_name) object_ids = worker.local_mode_manager.execute( function, function_descriptor, args, num_return_vals) else: with self._ray_actor_lock: object_ids = worker.submit_task( function_descriptor, args, actor_id=self._ray_actor_id, actor_handle_id=self._ray_actor_handle_id, actor_counter=self._ray_actor_counter, actor_creation_dummy_object_id=( self._ray_actor_creation_dummy_object_id), previous_actor_task_dummy_object_id=self._ray_actor_cursor, new_actor_handles=self._ray_new_actor_handles, # We add one for the dummy return ID. num_return_vals=num_return_vals + 1, resources={"CPU": self._ray_actor_method_cpus}, placement_resources={}, job_id=self._ray_actor_job_id, ) # Update the actor counter and cursor to reflect the most # recent invocation. self._ray_actor_counter += 1 # The last object returned is the dummy object that should be # passed in to the next actor method. Do not return it to the # user. self._ray_actor_cursor = object_ids.pop() # We have notified the backend of the new actor handles to # expect since the last task was submitted, so clear the list. self._ray_new_actor_handles = [] if len(object_ids) == 1: object_ids = object_ids[0] elif len(object_ids) == 0: object_ids = None return object_ids