1022 lines
37 KiB
Python
1022 lines
37 KiB
Python
import collections
|
||
import copy
|
||
import gc
|
||
import itertools
|
||
import logging
|
||
import os
|
||
import queue
|
||
import sys
|
||
import threading
|
||
import time
|
||
from multiprocessing import TimeoutError
|
||
from typing import Any, Callable, Dict, Hashable, Iterable, List, Optional, Tuple
|
||
|
||
import ray
|
||
from ray._common.usage import usage_lib
|
||
from ray.util import log_once
|
||
|
||
try:
|
||
from joblib._parallel_backends import SafeFunction
|
||
from joblib.parallel import BatchedCalls, parallel_backend
|
||
except ImportError:
|
||
BatchedCalls = None
|
||
parallel_backend = None
|
||
SafeFunction = None
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
RAY_ADDRESS_ENV = "RAY_ADDRESS"
|
||
|
||
|
||
def _put_in_dict_registry(
|
||
obj: Any, registry_hashable: Dict[Hashable, ray.ObjectRef]
|
||
) -> ray.ObjectRef:
|
||
if obj not in registry_hashable:
|
||
ret = ray.put(obj)
|
||
registry_hashable[obj] = ret
|
||
else:
|
||
ret = registry_hashable[obj]
|
||
return ret
|
||
|
||
|
||
def _put_in_list_registry(
|
||
obj: Any, registry: List[Tuple[Any, ray.ObjectRef]]
|
||
) -> ray.ObjectRef:
|
||
try:
|
||
ret = next((ref for o, ref in registry if o is obj))
|
||
except StopIteration:
|
||
ret = ray.put(obj)
|
||
registry.append((obj, ret))
|
||
return ret
|
||
|
||
|
||
def ray_put_if_needed(
|
||
obj: Any,
|
||
registry: Optional[List[Tuple[Any, ray.ObjectRef]]] = None,
|
||
registry_hashable: Optional[Dict[Hashable, ray.ObjectRef]] = None,
|
||
) -> ray.ObjectRef:
|
||
"""ray.put obj in object store if it's not an ObjRef and bigger than 100 bytes,
|
||
with support for list and dict registries"""
|
||
if isinstance(obj, ray.ObjectRef) or sys.getsizeof(obj) < 100:
|
||
return obj
|
||
ret = obj
|
||
if registry_hashable is not None:
|
||
try:
|
||
ret = _put_in_dict_registry(obj, registry_hashable)
|
||
except TypeError:
|
||
if registry is not None:
|
||
ret = _put_in_list_registry(obj, registry)
|
||
elif registry is not None:
|
||
ret = _put_in_list_registry(obj, registry)
|
||
return ret
|
||
|
||
|
||
def ray_get_if_needed(obj: Any) -> Any:
|
||
"""If obj is an ObjectRef, do ray.get, otherwise return obj"""
|
||
if isinstance(obj, ray.ObjectRef):
|
||
return ray.get(obj)
|
||
return obj
|
||
|
||
|
||
if BatchedCalls is not None:
|
||
|
||
class RayBatchedCalls(BatchedCalls):
|
||
"""Joblib's BatchedCalls with basic Ray object store management
|
||
|
||
This functionality is provided through the put_items_in_object_store,
|
||
which uses external registries (list and dict) containing objects
|
||
and their ObjectRefs."""
|
||
|
||
def put_items_in_object_store(
|
||
self,
|
||
registry: Optional[List[Tuple[Any, ray.ObjectRef]]] = None,
|
||
registry_hashable: Optional[Dict[Hashable, ray.ObjectRef]] = None,
|
||
):
|
||
"""Puts all applicable (kw)args in self.items in object store
|
||
|
||
Takes two registries - list for unhashable objects and dict
|
||
for hashable objects. The registries are a part of a Pool object.
|
||
The method iterates through all entries in items list (usually,
|
||
there will be only one, but the number depends on joblib Parallel
|
||
settings) and puts all of the args and kwargs into the object
|
||
store, updating the registries.
|
||
If an arg or kwarg is already in a registry, it will not be
|
||
put again, and instead, the cached object ref will be used."""
|
||
new_items = []
|
||
for func, args, kwargs in self.items:
|
||
args = [
|
||
ray_put_if_needed(arg, registry, registry_hashable) for arg in args
|
||
]
|
||
kwargs = {
|
||
k: ray_put_if_needed(v, registry, registry_hashable)
|
||
for k, v in kwargs.items()
|
||
}
|
||
new_items.append((func, args, kwargs))
|
||
self.items = new_items
|
||
|
||
def __call__(self):
|
||
# Exactly the same as in BatchedCalls, with the
|
||
# difference being that it gets args and kwargs from
|
||
# object store (which have been put in there by
|
||
# put_items_in_object_store)
|
||
|
||
# Set the default nested backend to self._backend but do
|
||
# not set the change the default number of processes to -1
|
||
with parallel_backend(self._backend, n_jobs=self._n_jobs):
|
||
return [
|
||
func(
|
||
*[ray_get_if_needed(arg) for arg in args],
|
||
**{k: ray_get_if_needed(v) for k, v in kwargs.items()},
|
||
)
|
||
for func, args, kwargs in self.items
|
||
]
|
||
|
||
def __reduce__(self):
|
||
# Exactly the same as in BatchedCalls, with the
|
||
# difference being that it returns RayBatchedCalls
|
||
# instead
|
||
if self._reducer_callback is not None:
|
||
self._reducer_callback()
|
||
# no need pickle the callback.
|
||
return (
|
||
RayBatchedCalls,
|
||
(self.items, (self._backend, self._n_jobs), None, self._pickle_cache),
|
||
)
|
||
|
||
else:
|
||
RayBatchedCalls = None
|
||
|
||
|
||
# Helper function to divide a by b and round the result up.
|
||
def div_round_up(a, b):
|
||
return -(-a // b)
|
||
|
||
|
||
class PoolTaskError(Exception):
|
||
def __init__(self, underlying):
|
||
self.underlying = underlying
|
||
|
||
|
||
class ResultThread(threading.Thread):
|
||
"""Thread that collects results from distributed actors.
|
||
|
||
It winds down when either:
|
||
- A pre-specified number of objects has been processed
|
||
- When the END_SENTINEL (submitted through self.add_object_ref())
|
||
has been received and all objects received before that have been
|
||
processed.
|
||
|
||
Initialize the thread with total_object_refs = float('inf') to wait for the
|
||
END_SENTINEL.
|
||
|
||
Args:
|
||
object_refs: ObjectRefs to Ray Actor calls.
|
||
Thread tracks whether they are ready. More ObjectRefs may be added
|
||
with add_object_ref (or _add_object_ref internally) until the object
|
||
count reaches total_object_refs.
|
||
single_result: Should be True if the thread is managing function
|
||
with a single result (like apply_async). False if the thread is managing
|
||
a function with a List of results.
|
||
callback: called only once at the end of the thread
|
||
if no results were errors. If single_result=True, and result is
|
||
not an error, callback is invoked with the result as the only
|
||
argument. If single_result=False, callback is invoked with
|
||
a list of all the results as the only argument.
|
||
error_callback: called only once on the first result
|
||
that errors. Should take an Exception as the only argument.
|
||
If no result errors, this callback is not called.
|
||
total_object_refs: Number of ObjectRefs that this thread
|
||
expects to be ready. May be more than len(object_refs) since
|
||
more ObjectRefs can be submitted after the thread starts.
|
||
If None, defaults to len(object_refs). If float("inf"), thread runs
|
||
until END_SENTINEL (submitted through self.add_object_ref())
|
||
has been received and all objects received before that have
|
||
been processed.
|
||
"""
|
||
|
||
END_SENTINEL = None
|
||
|
||
def __init__(
|
||
self,
|
||
object_refs: list,
|
||
single_result: bool = False,
|
||
callback: callable = None,
|
||
error_callback: callable = None,
|
||
total_object_refs: Optional[int] = None,
|
||
):
|
||
threading.Thread.__init__(self, daemon=True)
|
||
self._got_error = False
|
||
self._object_refs = []
|
||
self._num_ready = 0
|
||
self._results = []
|
||
self._ready_index_queue = queue.Queue()
|
||
self._single_result = single_result
|
||
self._callback = callback
|
||
self._error_callback = error_callback
|
||
self._total_object_refs = total_object_refs or len(object_refs)
|
||
self._indices = {}
|
||
# Thread-safe queue used to add ObjectRefs to fetch after creating
|
||
# this thread (used to lazily submit for imap and imap_unordered).
|
||
self._new_object_refs = queue.Queue()
|
||
for object_ref in object_refs:
|
||
self._add_object_ref(object_ref)
|
||
|
||
def _add_object_ref(self, object_ref):
|
||
self._indices[object_ref] = len(self._object_refs)
|
||
self._object_refs.append(object_ref)
|
||
self._results.append(None)
|
||
|
||
def add_object_ref(self, object_ref):
|
||
self._new_object_refs.put(object_ref)
|
||
|
||
def run(self):
|
||
unready = copy.copy(self._object_refs)
|
||
aggregated_batch_results = []
|
||
|
||
# Run for a specific number of objects if self._total_object_refs is finite.
|
||
# Otherwise, process all objects received prior to the stop signal, given by
|
||
# self.add_object(END_SENTINEL).
|
||
while self._num_ready < self._total_object_refs:
|
||
# Get as many new IDs from the queue as possible without blocking,
|
||
# unless we have no IDs to wait on, in which case we block.
|
||
ready_id = None
|
||
while ready_id is None:
|
||
try:
|
||
block = len(unready) == 0
|
||
new_object_ref = self._new_object_refs.get(block=block)
|
||
if new_object_ref is self.END_SENTINEL:
|
||
# Receiving the END_SENTINEL object is the signal to stop.
|
||
# Store the total number of objects.
|
||
self._total_object_refs = len(self._object_refs)
|
||
else:
|
||
self._add_object_ref(new_object_ref)
|
||
unready.append(new_object_ref)
|
||
except queue.Empty:
|
||
# queue.Empty means no result was retrieved if block=False.
|
||
pass
|
||
|
||
# Check if any of the available IDs are done. The timeout is required
|
||
# here to periodically check for new IDs from self._new_object_refs.
|
||
# NOTE(edoakes): the choice of a 100ms timeout here is arbitrary. Too
|
||
# low of a timeout would cause higher overhead from busy spinning and
|
||
# too high would cause higher tail latency to fetch the first result in
|
||
# some cases.
|
||
ready, unready = ray.wait(unready, num_returns=1, timeout=0.1)
|
||
if len(ready) > 0:
|
||
ready_id = ready[0]
|
||
|
||
try:
|
||
batch = ray.get(ready_id)
|
||
except ray.exceptions.RayError as e:
|
||
batch = [e]
|
||
|
||
# The exception callback is called only once on the first result
|
||
# that errors. If no result errors, it is never called.
|
||
if not self._got_error:
|
||
for result in batch:
|
||
if isinstance(result, Exception):
|
||
self._got_error = True
|
||
if self._error_callback is not None:
|
||
self._error_callback(result)
|
||
break
|
||
else:
|
||
aggregated_batch_results.append(result)
|
||
|
||
self._num_ready += 1
|
||
self._results[self._indices[ready_id]] = batch
|
||
self._ready_index_queue.put(self._indices[ready_id])
|
||
|
||
# The regular callback is called only once on the entire List of
|
||
# results as long as none of the results were errors. If any results
|
||
# were errors, the regular callback is never called; instead, the
|
||
# exception callback is called on the first erroring result.
|
||
#
|
||
# This callback is called outside the while loop to ensure that it's
|
||
# called on the entire list of results– not just a single batch.
|
||
if not self._got_error and self._callback is not None:
|
||
if not self._single_result:
|
||
self._callback(aggregated_batch_results)
|
||
else:
|
||
# On a thread handling a function with a single result
|
||
# (e.g. apply_async), we call the callback on just that result
|
||
# instead of on a list encaspulating that result
|
||
self._callback(aggregated_batch_results[0])
|
||
|
||
def got_error(self):
|
||
# Should only be called after the thread finishes.
|
||
return self._got_error
|
||
|
||
def result(self, index):
|
||
# Should only be called on results that are ready.
|
||
return self._results[index]
|
||
|
||
def results(self):
|
||
# Should only be called after the thread finishes.
|
||
return self._results
|
||
|
||
def next_ready_index(self, timeout=None):
|
||
try:
|
||
return self._ready_index_queue.get(timeout=timeout)
|
||
except queue.Empty:
|
||
# queue.Queue signals a timeout by raising queue.Empty.
|
||
raise TimeoutError
|
||
|
||
|
||
class AsyncResult:
|
||
"""An asynchronous interface to task results.
|
||
|
||
This should not be constructed directly.
|
||
"""
|
||
|
||
def __init__(
|
||
self, chunk_object_refs, callback=None, error_callback=None, single_result=False
|
||
):
|
||
self._single_result = single_result
|
||
self._result_thread = ResultThread(
|
||
chunk_object_refs, single_result, callback, error_callback
|
||
)
|
||
self._result_thread.start()
|
||
|
||
def wait(self, timeout: Optional[float] = None):
|
||
"""
|
||
Returns once the result is ready or the timeout expires (does not
|
||
raise TimeoutError).
|
||
|
||
Args:
|
||
timeout: timeout in milliseconds.
|
||
"""
|
||
|
||
self._result_thread.join(timeout)
|
||
|
||
def get(self, timeout=None):
|
||
self.wait(timeout)
|
||
if self._result_thread.is_alive():
|
||
raise TimeoutError
|
||
|
||
results = []
|
||
for batch in self._result_thread.results():
|
||
for result in batch:
|
||
if isinstance(result, PoolTaskError):
|
||
raise result.underlying
|
||
elif isinstance(result, Exception):
|
||
raise result
|
||
results.extend(batch)
|
||
|
||
if self._single_result:
|
||
return results[0]
|
||
|
||
return results
|
||
|
||
def ready(self):
|
||
"""
|
||
Returns true if the result is ready, else false if the tasks are still
|
||
running.
|
||
"""
|
||
|
||
return not self._result_thread.is_alive()
|
||
|
||
def successful(self):
|
||
"""
|
||
Returns true if none of the submitted tasks errored, else false. Should
|
||
only be called once the result is ready (can be checked using `ready`).
|
||
"""
|
||
|
||
if not self.ready():
|
||
raise ValueError(f"{self!r} not ready")
|
||
return not self._result_thread.got_error()
|
||
|
||
|
||
class IMapIterator:
|
||
"""Base class for OrderedIMapIterator and UnorderedIMapIterator."""
|
||
|
||
def __init__(self, pool, func, iterable, chunksize=None):
|
||
self._pool = pool
|
||
self._func = func
|
||
self._next_chunk_index = 0
|
||
self._finished_iterating = False
|
||
# List of bools indicating if the given chunk is ready or not for all
|
||
# submitted chunks. Ordering mirrors that in the in the ResultThread.
|
||
self._submitted_chunks = []
|
||
self._ready_objects = collections.deque()
|
||
self._iterator = iter(iterable)
|
||
if isinstance(iterable, collections.abc.Iterator):
|
||
# Got iterator (which has no len() function).
|
||
# Make default chunksize 1 instead of using _calculate_chunksize().
|
||
# Indicate unknown queue length, requiring explicit stopping.
|
||
self._chunksize = chunksize or 1
|
||
result_list_size = float("inf")
|
||
else:
|
||
self._chunksize = chunksize or pool._calculate_chunksize(iterable)
|
||
result_list_size = div_round_up(len(iterable), chunksize)
|
||
|
||
self._result_thread = ResultThread([], total_object_refs=result_list_size)
|
||
self._result_thread.start()
|
||
|
||
for _ in range(len(self._pool._actor_pool)):
|
||
self._submit_next_chunk()
|
||
|
||
def _submit_next_chunk(self):
|
||
# The full iterable has already been submitted, so no-op.
|
||
if self._finished_iterating:
|
||
return
|
||
|
||
actor_index = len(self._submitted_chunks) % len(self._pool._actor_pool)
|
||
chunk_iterator = itertools.islice(self._iterator, self._chunksize)
|
||
|
||
# Check whether we have run out of samples.
|
||
# This consumes the original iterator, so we convert to a list and back
|
||
chunk_list = list(chunk_iterator)
|
||
if len(chunk_list) < self._chunksize:
|
||
# Reached end of self._iterator
|
||
self._finished_iterating = True
|
||
if len(chunk_list) == 0:
|
||
# Nothing to do, return.
|
||
return
|
||
chunk_iterator = iter(chunk_list)
|
||
|
||
new_chunk_id = self._pool._submit_chunk(
|
||
self._func, chunk_iterator, self._chunksize, actor_index
|
||
)
|
||
self._submitted_chunks.append(False)
|
||
# Wait for the result
|
||
self._result_thread.add_object_ref(new_chunk_id)
|
||
# If we submitted the final chunk, notify the result thread
|
||
if self._finished_iterating:
|
||
self._result_thread.add_object_ref(ResultThread.END_SENTINEL)
|
||
|
||
def __iter__(self):
|
||
return self
|
||
|
||
def __next__(self):
|
||
return self.next()
|
||
|
||
def next(self):
|
||
# Should be implemented by subclasses.
|
||
raise NotImplementedError
|
||
|
||
|
||
class OrderedIMapIterator(IMapIterator):
|
||
"""Iterator to the results of tasks submitted using `imap`.
|
||
|
||
The results are returned in the same order that they were submitted, even
|
||
if they don't finish in that order. Only one batch of tasks per actor
|
||
process is submitted at a time - the rest are submitted as results come in.
|
||
|
||
Should not be constructed directly.
|
||
"""
|
||
|
||
def next(self, timeout=None):
|
||
if len(self._ready_objects) == 0:
|
||
if self._finished_iterating and (
|
||
self._next_chunk_index == len(self._submitted_chunks)
|
||
):
|
||
# Finish when all chunks have been dispatched and processed
|
||
# Notify the calling process that the work is done.
|
||
raise StopIteration
|
||
|
||
# This loop will break when the next index in order is ready or
|
||
# self._result_thread.next_ready_index() raises a timeout.
|
||
index = -1
|
||
while index != self._next_chunk_index:
|
||
start = time.time()
|
||
index = self._result_thread.next_ready_index(timeout=timeout)
|
||
self._submit_next_chunk()
|
||
self._submitted_chunks[index] = True
|
||
if timeout is not None:
|
||
timeout = max(0, timeout - (time.time() - start))
|
||
|
||
while (
|
||
self._next_chunk_index < len(self._submitted_chunks)
|
||
and self._submitted_chunks[self._next_chunk_index]
|
||
):
|
||
for result in self._result_thread.result(self._next_chunk_index):
|
||
self._ready_objects.append(result)
|
||
self._next_chunk_index += 1
|
||
|
||
return self._ready_objects.popleft()
|
||
|
||
|
||
class UnorderedIMapIterator(IMapIterator):
|
||
"""Iterator to the results of tasks submitted using `imap`.
|
||
|
||
The results are returned in the order that they finish. Only one batch of
|
||
tasks per actor process is submitted at a time - the rest are submitted as
|
||
results come in.
|
||
|
||
Should not be constructed directly.
|
||
"""
|
||
|
||
def next(self, timeout=None):
|
||
if len(self._ready_objects) == 0:
|
||
if self._finished_iterating and (
|
||
self._next_chunk_index == len(self._submitted_chunks)
|
||
):
|
||
# Finish when all chunks have been dispatched and processed
|
||
# Notify the calling process that the work is done.
|
||
raise StopIteration
|
||
|
||
index = self._result_thread.next_ready_index(timeout=timeout)
|
||
self._submit_next_chunk()
|
||
|
||
for result in self._result_thread.result(index):
|
||
self._ready_objects.append(result)
|
||
self._next_chunk_index += 1
|
||
|
||
return self._ready_objects.popleft()
|
||
|
||
|
||
@ray.remote(num_cpus=0)
|
||
class PoolActor:
|
||
"""Actor used to process tasks submitted to a Pool."""
|
||
|
||
def __init__(self, initializer=None, initargs=None):
|
||
if initializer:
|
||
initargs = initargs or ()
|
||
initializer(*initargs)
|
||
|
||
def ping(self):
|
||
# Used to wait for this actor to be initialized.
|
||
pass
|
||
|
||
def run_batch(self, func, batch):
|
||
results = []
|
||
for args, kwargs in batch:
|
||
args = args or ()
|
||
kwargs = kwargs or {}
|
||
try:
|
||
results.append(func(*args, **kwargs))
|
||
except Exception as e:
|
||
results.append(PoolTaskError(e))
|
||
return results
|
||
|
||
|
||
# https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing.pool
|
||
class Pool:
|
||
"""A pool of actor processes that is used to process tasks in parallel.
|
||
|
||
Args:
|
||
processes: number of actor processes to start in the pool. Defaults to
|
||
the number of cores in the Ray cluster if one is already running,
|
||
otherwise the number of cores on this machine.
|
||
initializer: function to be run in each actor when it starts up.
|
||
initargs: iterable of arguments to the initializer function.
|
||
maxtasksperchild: maximum number of tasks to run in each actor process.
|
||
After a process has executed this many tasks, it will be killed and
|
||
replaced with a new one.
|
||
context: Accepted for ``multiprocessing.Pool`` API compatibility but
|
||
ignored; Ray controls process initialization. A warning is logged
|
||
if a non-None value is supplied.
|
||
ray_address: address of the Ray cluster to run on. If None, a new local
|
||
Ray cluster will be started on this machine. Otherwise, this will
|
||
be passed to `ray.init()` to connect to a running cluster. This may
|
||
also be specified using the `RAY_ADDRESS` environment variable.
|
||
ray_remote_args: arguments used to configure the Ray Actors making up
|
||
the pool. See :func:`ray.remote` for details.
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
processes: Optional[int] = None,
|
||
initializer: Optional[Callable] = None,
|
||
initargs: Optional[Iterable] = None,
|
||
maxtasksperchild: Optional[int] = None,
|
||
context: Any = None,
|
||
ray_address: Optional[str] = None,
|
||
ray_remote_args: Optional[Dict[str, Any]] = None,
|
||
):
|
||
usage_lib.record_library_usage("util.multiprocessing.Pool")
|
||
|
||
self._closed = False
|
||
self._initializer = initializer
|
||
self._initargs = initargs
|
||
self._maxtasksperchild = maxtasksperchild or -1
|
||
self._actor_deletion_ids = []
|
||
self._registry: List[Tuple[Any, ray.ObjectRef]] = []
|
||
self._registry_hashable: Dict[Hashable, ray.ObjectRef] = {}
|
||
self._current_index = 0
|
||
self._ray_remote_args = ray_remote_args or {}
|
||
self._pool_actor = None
|
||
|
||
if context and log_once("context_argument_warning"):
|
||
logger.warning(
|
||
"The 'context' argument is not supported using "
|
||
"ray. Please refer to the documentation for how "
|
||
"to control ray initialization."
|
||
)
|
||
|
||
processes = self._init_ray(processes, ray_address)
|
||
self._start_actor_pool(processes)
|
||
|
||
def _init_ray(self, processes=None, ray_address=None):
|
||
# Initialize ray. If ray is already initialized, we do nothing.
|
||
# Else, the priority is:
|
||
# ray_address argument > RAY_ADDRESS > start new local cluster.
|
||
if not ray.is_initialized():
|
||
# Cluster mode.
|
||
if ray_address is None and (
|
||
RAY_ADDRESS_ENV in os.environ
|
||
or ray._private.utils.read_ray_address() is not None
|
||
):
|
||
init_kwargs = {}
|
||
if os.environ.get(RAY_ADDRESS_ENV) == "local":
|
||
init_kwargs["num_cpus"] = processes
|
||
ray.init(**init_kwargs)
|
||
elif ray_address is not None:
|
||
init_kwargs = {}
|
||
if ray_address == "local":
|
||
init_kwargs["num_cpus"] = processes
|
||
ray.init(address=ray_address, **init_kwargs)
|
||
# Local mode.
|
||
else:
|
||
ray.init(num_cpus=processes)
|
||
|
||
ray_cpus = int(ray._private.state.cluster_resources()["CPU"])
|
||
if processes is None:
|
||
processes = ray_cpus
|
||
if processes <= 0:
|
||
raise ValueError("Processes in the pool must be >0.")
|
||
if ray_cpus < processes:
|
||
raise ValueError(
|
||
"Tried to start a pool with {} processes on an "
|
||
"existing ray cluster, but there are only {} "
|
||
"CPUs in the ray cluster.".format(processes, ray_cpus)
|
||
)
|
||
|
||
return processes
|
||
|
||
def _start_actor_pool(self, processes):
|
||
self._pool_actor = None
|
||
self._actor_pool = [self._new_actor_entry() for _ in range(processes)]
|
||
ray.get([actor.ping.remote() for actor, _ in self._actor_pool])
|
||
|
||
def _wait_for_stopping_actors(self, timeout=None):
|
||
if len(self._actor_deletion_ids) == 0:
|
||
return
|
||
if timeout is not None:
|
||
timeout = float(timeout)
|
||
|
||
_, deleting = ray.wait(
|
||
self._actor_deletion_ids,
|
||
num_returns=len(self._actor_deletion_ids),
|
||
timeout=timeout,
|
||
)
|
||
self._actor_deletion_ids = deleting
|
||
|
||
def _stop_actor(self, actor):
|
||
# Check and clean up any outstanding IDs corresponding to deletions.
|
||
self._wait_for_stopping_actors(timeout=0.0)
|
||
# The deletion task will block until the actor has finished executing
|
||
# all pending tasks.
|
||
self._actor_deletion_ids.append(actor.__ray_terminate__.remote())
|
||
|
||
def _new_actor_entry(self):
|
||
# NOTE(edoakes): The initializer function can't currently be used to
|
||
# modify the global namespace (e.g., import packages or set globals)
|
||
# due to a limitation in cloudpickle.
|
||
# Cache the PoolActor with options
|
||
if not self._pool_actor:
|
||
self._pool_actor = PoolActor.options(**self._ray_remote_args)
|
||
return (self._pool_actor.remote(self._initializer, self._initargs), 0)
|
||
|
||
def _next_actor_index(self):
|
||
if self._current_index == len(self._actor_pool) - 1:
|
||
self._current_index = 0
|
||
else:
|
||
self._current_index += 1
|
||
return self._current_index
|
||
|
||
# Batch should be a list of tuples: (args, kwargs).
|
||
def _run_batch(self, actor_index, func, batch):
|
||
actor, count = self._actor_pool[actor_index]
|
||
object_ref = actor.run_batch.remote(func, batch)
|
||
count += 1
|
||
assert self._maxtasksperchild == -1 or count <= self._maxtasksperchild
|
||
if count == self._maxtasksperchild:
|
||
self._stop_actor(actor)
|
||
actor, count = self._new_actor_entry()
|
||
self._actor_pool[actor_index] = (actor, count)
|
||
return object_ref
|
||
|
||
def apply(
|
||
self,
|
||
func: Callable,
|
||
args: Optional[Tuple] = None,
|
||
kwargs: Optional[Dict] = None,
|
||
):
|
||
"""Run the given function on a random actor process and return the
|
||
result synchronously.
|
||
|
||
Args:
|
||
func: function to run.
|
||
args: optional arguments to the function.
|
||
kwargs: optional keyword arguments to the function.
|
||
|
||
Returns:
|
||
The result.
|
||
"""
|
||
|
||
return self.apply_async(func, args, kwargs).get()
|
||
|
||
def apply_async(
|
||
self,
|
||
func: Callable,
|
||
args: Optional[Tuple] = None,
|
||
kwargs: Optional[Dict] = None,
|
||
callback: Callable[[Any], None] = None,
|
||
error_callback: Callable[[Exception], None] = None,
|
||
):
|
||
"""Run the given function on a random actor process and return an
|
||
asynchronous interface to the result.
|
||
|
||
Args:
|
||
func: function to run.
|
||
args: optional arguments to the function.
|
||
kwargs: optional keyword arguments to the function.
|
||
callback: callback to be executed on the result once it is finished
|
||
only if it succeeds.
|
||
error_callback: callback to be executed the result once it is
|
||
finished only if the task errors. The exception raised by the
|
||
task will be passed as the only argument to the callback.
|
||
|
||
Returns:
|
||
AsyncResult containing the result.
|
||
"""
|
||
|
||
self._check_running()
|
||
func = self._convert_to_ray_batched_calls_if_needed(func)
|
||
object_ref = self._run_batch(self._next_actor_index(), func, [(args, kwargs)])
|
||
return AsyncResult([object_ref], callback, error_callback, single_result=True)
|
||
|
||
def _convert_to_ray_batched_calls_if_needed(self, func: Callable) -> Callable:
|
||
"""Convert joblib's BatchedCalls to RayBatchedCalls for ObjectRef caching.
|
||
|
||
This converts joblib's BatchedCalls callable, which is a collection of
|
||
functions with their args and kwargs to be ran sequentially in an
|
||
Actor, to a RayBatchedCalls callable, which provides identical
|
||
functionality in addition to a method which ensures that common
|
||
args and kwargs are put into the object store just once, saving time
|
||
and memory. That method is then ran.
|
||
|
||
If func is not a BatchedCalls instance, it is returned without changes.
|
||
|
||
The ObjectRefs are cached inside two registries (_registry and
|
||
_registry_hashable), which are common for the entire Pool and are
|
||
cleaned on close."""
|
||
if RayBatchedCalls is None:
|
||
return func
|
||
orginal_func = func
|
||
# SafeFunction is a Python 2 leftover and can be
|
||
# safely removed.
|
||
if isinstance(func, SafeFunction):
|
||
func = func.func
|
||
if isinstance(func, BatchedCalls):
|
||
func = RayBatchedCalls(
|
||
func.items,
|
||
(func._backend, func._n_jobs),
|
||
func._reducer_callback,
|
||
func._pickle_cache,
|
||
)
|
||
# go through all the items and replace args and kwargs with
|
||
# ObjectRefs, caching them in registries
|
||
func.put_items_in_object_store(self._registry, self._registry_hashable)
|
||
else:
|
||
func = orginal_func
|
||
return func
|
||
|
||
def _calculate_chunksize(self, iterable):
|
||
chunksize, extra = divmod(len(iterable), len(self._actor_pool) * 4)
|
||
if extra:
|
||
chunksize += 1
|
||
return chunksize
|
||
|
||
def _submit_chunk(self, func, iterator, chunksize, actor_index, unpack_args=False):
|
||
chunk = []
|
||
while len(chunk) < chunksize:
|
||
try:
|
||
args = next(iterator)
|
||
if not unpack_args:
|
||
args = (args,)
|
||
chunk.append((args, {}))
|
||
except StopIteration:
|
||
break
|
||
|
||
# Nothing to submit. The caller should prevent this.
|
||
assert len(chunk) > 0
|
||
|
||
return self._run_batch(actor_index, func, chunk)
|
||
|
||
def _chunk_and_run(self, func, iterable, chunksize=None, unpack_args=False):
|
||
if not hasattr(iterable, "__len__"):
|
||
iterable = list(iterable)
|
||
|
||
if chunksize is None:
|
||
chunksize = self._calculate_chunksize(iterable)
|
||
|
||
iterator = iter(iterable)
|
||
chunk_object_refs = []
|
||
while len(chunk_object_refs) * chunksize < len(iterable):
|
||
actor_index = len(chunk_object_refs) % len(self._actor_pool)
|
||
chunk_object_refs.append(
|
||
self._submit_chunk(
|
||
func, iterator, chunksize, actor_index, unpack_args=unpack_args
|
||
)
|
||
)
|
||
|
||
return chunk_object_refs
|
||
|
||
def _map_async(
|
||
self,
|
||
func,
|
||
iterable,
|
||
chunksize=None,
|
||
unpack_args=False,
|
||
callback=None,
|
||
error_callback=None,
|
||
):
|
||
self._check_running()
|
||
object_refs = self._chunk_and_run(
|
||
func, iterable, chunksize=chunksize, unpack_args=unpack_args
|
||
)
|
||
return AsyncResult(object_refs, callback, error_callback)
|
||
|
||
def map(self, func: Callable, iterable: Iterable, chunksize: Optional[int] = None):
|
||
"""Run the given function on each element in the iterable round-robin
|
||
on the actor processes and return the results synchronously.
|
||
|
||
Args:
|
||
func: function to run.
|
||
iterable: iterable of objects to be passed as the sole argument to
|
||
func.
|
||
chunksize: number of tasks to submit as a batch to each actor
|
||
process. If unspecified, a suitable chunksize will be chosen.
|
||
|
||
Returns:
|
||
A list of results.
|
||
"""
|
||
|
||
return self._map_async(
|
||
func, iterable, chunksize=chunksize, unpack_args=False
|
||
).get()
|
||
|
||
def map_async(
|
||
self,
|
||
func: Callable,
|
||
iterable: Iterable,
|
||
chunksize: Optional[int] = None,
|
||
callback: Callable[[List], None] = None,
|
||
error_callback: Callable[[Exception], None] = None,
|
||
):
|
||
"""Run the given function on each element in the iterable round-robin
|
||
on the actor processes and return an asynchronous interface to the
|
||
results.
|
||
|
||
Args:
|
||
func: function to run.
|
||
iterable: iterable of objects to be passed as the only argument to
|
||
func.
|
||
chunksize: number of tasks to submit as a batch to each actor
|
||
process. If unspecified, a suitable chunksize will be chosen.
|
||
callback: Will only be called if none of the results were errors,
|
||
and will only be called once after all results are finished.
|
||
A Python List of all the finished results will be passed as the
|
||
only argument to the callback.
|
||
error_callback: callback executed on the first errored result.
|
||
The Exception raised by the task will be passed as the only
|
||
argument to the callback.
|
||
|
||
Returns:
|
||
AsyncResult
|
||
"""
|
||
return self._map_async(
|
||
func,
|
||
iterable,
|
||
chunksize=chunksize,
|
||
unpack_args=False,
|
||
callback=callback,
|
||
error_callback=error_callback,
|
||
)
|
||
|
||
def starmap(self, func, iterable, chunksize=None):
|
||
"""Same as `map`, but unpacks each element of the iterable as the
|
||
arguments to func like: [func(*args) for args in iterable].
|
||
"""
|
||
|
||
return self._map_async(
|
||
func, iterable, chunksize=chunksize, unpack_args=True
|
||
).get()
|
||
|
||
def starmap_async(
|
||
self,
|
||
func: Callable,
|
||
iterable: Iterable,
|
||
callback: Callable[[List], None] = None,
|
||
error_callback: Callable[[Exception], None] = None,
|
||
):
|
||
"""Same as `map_async`, but unpacks each element of the iterable as the
|
||
arguments to func like: [func(*args) for args in iterable].
|
||
"""
|
||
|
||
return self._map_async(
|
||
func,
|
||
iterable,
|
||
unpack_args=True,
|
||
callback=callback,
|
||
error_callback=error_callback,
|
||
)
|
||
|
||
def imap(self, func: Callable, iterable: Iterable, chunksize: Optional[int] = 1):
|
||
"""Same as `map`, but only submits one batch of tasks to each actor
|
||
process at a time.
|
||
|
||
This can be useful if the iterable of arguments is very large or each
|
||
task's arguments consumes a large amount of resources.
|
||
|
||
The results are returned in the order corresponding to their arguments
|
||
in the iterable.
|
||
|
||
Args:
|
||
func: Function to apply to each element of ``iterable``.
|
||
iterable: Iterable of arguments to ``func``.
|
||
chunksize: Number of elements to send to each worker per batch.
|
||
|
||
Returns:
|
||
OrderedIMapIterator
|
||
"""
|
||
|
||
self._check_running()
|
||
return OrderedIMapIterator(self, func, iterable, chunksize=chunksize)
|
||
|
||
def imap_unordered(
|
||
self, func: Callable, iterable: Iterable, chunksize: Optional[int] = 1
|
||
):
|
||
"""Same as `map`, but only submits one batch of tasks to each actor
|
||
process at a time.
|
||
|
||
This can be useful if the iterable of arguments is very large or each
|
||
task's arguments consumes a large amount of resources.
|
||
|
||
The results are returned in the order that they finish.
|
||
|
||
Args:
|
||
func: Function to apply to each element of ``iterable``.
|
||
iterable: Iterable of arguments to ``func``.
|
||
chunksize: Number of elements to send to each worker per batch.
|
||
|
||
Returns:
|
||
UnorderedIMapIterator
|
||
"""
|
||
|
||
self._check_running()
|
||
return UnorderedIMapIterator(self, func, iterable, chunksize=chunksize)
|
||
|
||
def _check_running(self):
|
||
if self._closed:
|
||
raise ValueError("Pool not running")
|
||
|
||
def __enter__(self):
|
||
self._check_running()
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
self.terminate()
|
||
|
||
def close(self):
|
||
"""Close the pool.
|
||
|
||
Prevents any more tasks from being submitted on the pool but allows
|
||
outstanding work to finish.
|
||
"""
|
||
|
||
self._registry.clear()
|
||
self._registry_hashable.clear()
|
||
for actor, _ in self._actor_pool:
|
||
self._stop_actor(actor)
|
||
self._closed = True
|
||
gc.collect()
|
||
|
||
def terminate(self):
|
||
"""Close the pool.
|
||
|
||
Prevents any more tasks from being submitted on the pool and stops
|
||
outstanding work.
|
||
"""
|
||
|
||
if not self._closed:
|
||
self.close()
|
||
for actor, _ in self._actor_pool:
|
||
ray.kill(actor)
|
||
|
||
def join(self):
|
||
"""Wait for the actors in a closed pool to exit.
|
||
|
||
If the pool was closed using `close`, this will return once all
|
||
outstanding work is completed.
|
||
|
||
If the pool was closed using `terminate`, this will return quickly.
|
||
"""
|
||
|
||
if not self._closed:
|
||
raise ValueError("Pool is still running")
|
||
self._wait_for_stopping_actors()
|