chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from ray.air.execution.resources.fixed import FixedResourceManager
|
||||
from ray.air.execution.resources.placement_group import PlacementGroupResourceManager
|
||||
from ray.air.execution.resources.request import AcquiredResources, ResourceRequest
|
||||
from ray.air.execution.resources.resource_manager import ResourceManager
|
||||
|
||||
__all__ = [
|
||||
"ResourceRequest",
|
||||
"AcquiredResources",
|
||||
"ResourceManager",
|
||||
"FixedResourceManager",
|
||||
"PlacementGroupResourceManager",
|
||||
]
|
||||
@@ -0,0 +1,147 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import ray
|
||||
from ray import SCRIPT_MODE
|
||||
from ray.air.execution.resources.request import (
|
||||
AcquiredResources,
|
||||
RemoteRayEntity,
|
||||
ResourceRequest,
|
||||
)
|
||||
from ray.air.execution.resources.resource_manager import ResourceManager
|
||||
from ray.util.annotations import DeveloperAPI
|
||||
|
||||
# Avoid numerical errors by multiplying and subtracting with this number.
|
||||
# Compare: 0.99 - 0.33 = 0.65999... vs (0.99 * 1000 - 0.33 * 1000) / 1000 = 0.66
|
||||
_DIGITS = 100000
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
@dataclass
|
||||
class FixedAcquiredResources(AcquiredResources):
|
||||
bundles: List[Dict[str, float]]
|
||||
|
||||
def _annotate_remote_entity(
|
||||
self, entity: RemoteRayEntity, bundle: Dict[str, float], bundle_index: int
|
||||
) -> RemoteRayEntity:
|
||||
bundle = bundle.copy()
|
||||
num_cpus = bundle.pop("CPU", 0)
|
||||
num_gpus = bundle.pop("GPU", 0)
|
||||
memory = bundle.pop("memory", 0.0)
|
||||
|
||||
return entity.options(
|
||||
num_cpus=num_cpus,
|
||||
num_gpus=num_gpus,
|
||||
memory=memory,
|
||||
resources=bundle,
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class FixedResourceManager(ResourceManager):
|
||||
"""Fixed budget based resource manager.
|
||||
|
||||
This resource manager keeps track of a fixed set of resources. When resources
|
||||
are acquired, they are subtracted from the budget. When resources are freed,
|
||||
they are added back to the budget.
|
||||
|
||||
The resource manager still requires resources to be requested before they become
|
||||
available. However, because the resource requests are virtual, this will not
|
||||
trigger autoscaling.
|
||||
|
||||
Additionally, resources are not reserved on request, only on acquisition. Thus,
|
||||
acquiring a resource can change the availability of other requests. Note that
|
||||
this behavior may be changed in future implementations.
|
||||
|
||||
The fixed resource manager does not support placement strategies. Using
|
||||
``STRICT_SPREAD`` will result in an error. ``STRICT_PACK`` will succeed only
|
||||
within a placement group bundle. All other placement group arguments will be
|
||||
ignored.
|
||||
|
||||
Args:
|
||||
total_resources: Budget of resources to manage. Defaults to all available
|
||||
resources in the current task or all cluster resources (if outside a task).
|
||||
|
||||
"""
|
||||
|
||||
_resource_cls: AcquiredResources = FixedAcquiredResources
|
||||
|
||||
def __init__(self, total_resources: Optional[Dict[str, float]] = None):
|
||||
rtc = ray.get_runtime_context()
|
||||
|
||||
if not total_resources:
|
||||
if rtc.worker.mode in {None, SCRIPT_MODE}:
|
||||
total_resources = ray.cluster_resources()
|
||||
else:
|
||||
total_resources = rtc.get_assigned_resources()
|
||||
|
||||
# If we are in a placement group, all of our resources will be in a bundle
|
||||
# and thus fulfill requirements of STRICT_PACK - but only if child tasks
|
||||
# are captured by the pg.
|
||||
self._allow_strict_pack = (
|
||||
ray.util.get_current_placement_group() is not None
|
||||
and rtc.should_capture_child_tasks_in_placement_group
|
||||
)
|
||||
|
||||
self._total_resources = total_resources
|
||||
self._requested_resources = []
|
||||
self._used_resources = []
|
||||
|
||||
@property
|
||||
def _available_resources(self) -> Dict[str, float]:
|
||||
available_resources = self._total_resources.copy()
|
||||
|
||||
for used_resources in self._used_resources:
|
||||
all_resources = used_resources.required_resources
|
||||
for k, v in all_resources.items():
|
||||
available_resources[k] = (
|
||||
available_resources[k] * _DIGITS - v * _DIGITS
|
||||
) / _DIGITS
|
||||
return available_resources
|
||||
|
||||
def request_resources(self, resource_request: ResourceRequest):
|
||||
if resource_request.strategy == "STRICT_SPREAD" or (
|
||||
not self._allow_strict_pack and resource_request.strategy == "STRICT_PACK"
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"Requested a resource with placement strategy "
|
||||
f"{resource_request.strategy}, but this cannot be fulfilled by a "
|
||||
f"FixedResourceManager. In a nested setting, please set the inner "
|
||||
f"placement strategy to be less restrictive (i.e. no STRICT_ strategy)."
|
||||
)
|
||||
|
||||
self._requested_resources.append(resource_request)
|
||||
|
||||
def cancel_resource_request(self, resource_request: ResourceRequest):
|
||||
self._requested_resources.remove(resource_request)
|
||||
|
||||
def has_resources_ready(self, resource_request: ResourceRequest) -> bool:
|
||||
if resource_request not in self._requested_resources:
|
||||
return False
|
||||
|
||||
available_resources = self._available_resources
|
||||
all_resources = resource_request.required_resources
|
||||
for k, v in all_resources.items():
|
||||
if available_resources.get(k, 0.0) < v:
|
||||
return False
|
||||
return True
|
||||
|
||||
def acquire_resources(
|
||||
self, resource_request: ResourceRequest
|
||||
) -> Optional[AcquiredResources]:
|
||||
if not self.has_resources_ready(resource_request):
|
||||
return None
|
||||
|
||||
self._used_resources.append(resource_request)
|
||||
return self._resource_cls(
|
||||
bundles=resource_request.bundles, resource_request=resource_request
|
||||
)
|
||||
|
||||
def free_resources(self, acquired_resource: AcquiredResources):
|
||||
resources = acquired_resource.resource_request
|
||||
self._used_resources.remove(resources)
|
||||
|
||||
def clear(self):
|
||||
# Reset internal state
|
||||
self._requested_resources = []
|
||||
self._used_resources = []
|
||||
@@ -0,0 +1,214 @@
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
import ray
|
||||
from ray.air.execution.resources.request import (
|
||||
AcquiredResources,
|
||||
RemoteRayEntity,
|
||||
ResourceRequest,
|
||||
)
|
||||
from ray.air.execution.resources.resource_manager import ResourceManager
|
||||
from ray.util.annotations import DeveloperAPI
|
||||
from ray.util.placement_group import PlacementGroup, remove_placement_group
|
||||
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
@dataclass
|
||||
class PlacementGroupAcquiredResources(AcquiredResources):
|
||||
placement_group: PlacementGroup
|
||||
|
||||
def _annotate_remote_entity(
|
||||
self, entity: RemoteRayEntity, bundle: Dict[str, float], bundle_index: int
|
||||
) -> RemoteRayEntity:
|
||||
bundle = bundle.copy()
|
||||
num_cpus = bundle.pop("CPU", 0)
|
||||
num_gpus = bundle.pop("GPU", 0)
|
||||
memory = bundle.pop("memory", 0.0)
|
||||
|
||||
return entity.options(
|
||||
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
||||
placement_group=self.placement_group,
|
||||
placement_group_bundle_index=bundle_index,
|
||||
placement_group_capture_child_tasks=True,
|
||||
),
|
||||
num_cpus=num_cpus,
|
||||
num_gpus=num_gpus,
|
||||
memory=memory,
|
||||
resources=bundle,
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class PlacementGroupResourceManager(ResourceManager):
|
||||
"""Resource manager using placement groups as the resource backend.
|
||||
|
||||
This manager will use placement groups to fulfill resource requests. Requesting
|
||||
a resource will schedule the placement group. Acquiring a resource will
|
||||
return a ``PlacementGroupAcquiredResources`` that can be used to schedule
|
||||
Ray tasks and actors on the placement group. Freeing an acquired resource
|
||||
will destroy the associated placement group.
|
||||
|
||||
Ray core does not emit events when resources are available. Instead, the
|
||||
scheduling state has to be periodically updated.
|
||||
|
||||
Per default, placement group scheduling state is refreshed every time when
|
||||
resource state is inquired, but not more often than once every ``update_interval_s``
|
||||
seconds. Alternatively, staging futures can be retrieved (and awaited) with
|
||||
``get_resource_futures()`` and state update can be force with ``update_state()``.
|
||||
|
||||
Args:
|
||||
update_interval_s: Minimum interval in seconds between updating scheduling
|
||||
state of placement groups.
|
||||
|
||||
"""
|
||||
|
||||
_resource_cls: AcquiredResources = PlacementGroupAcquiredResources
|
||||
|
||||
def __init__(self, update_interval_s: float = 0.1):
|
||||
# Internally, the placement group lifecycle is like this:
|
||||
# - Resources are requested with ``request_resources()``
|
||||
# - A placement group is scheduled ("staged")
|
||||
# - A ``PlacementGroup.ready()`` future is scheduled ("staging future")
|
||||
# - We update the scheduling state when we need to
|
||||
# (e.g. when ``has_resources_ready()`` is called)
|
||||
# - When staging futures resolve, a placement group is moved from "staging"
|
||||
# to "ready"
|
||||
# - When a resource request is canceled, we remove a placement group from
|
||||
# "staging". If there are not staged placement groups
|
||||
# (because they are already "ready"), we remove one from "ready" instead.
|
||||
# - When a resource is acquired, the pg is removed from "ready" and moved
|
||||
# to "acquired"
|
||||
# - When a resource is freed, the pg is removed from "acquired" and destroyed
|
||||
|
||||
# Mapping of placement group to request
|
||||
self._pg_to_request: Dict[PlacementGroup, ResourceRequest] = {}
|
||||
|
||||
# PGs that are staged but not "ready", yet (i.e. not CREATED)
|
||||
self._request_to_staged_pgs: Dict[
|
||||
ResourceRequest, Set[PlacementGroup]
|
||||
] = defaultdict(set)
|
||||
|
||||
# PGs that are CREATED and can be used by tasks and actors
|
||||
self._request_to_ready_pgs: Dict[
|
||||
ResourceRequest, Set[PlacementGroup]
|
||||
] = defaultdict(set)
|
||||
|
||||
# Staging futures used to update internal state.
|
||||
# We keep a double mapping here for better lookup efficiency.
|
||||
self._staging_future_to_pg: Dict[ray.ObjectRef, PlacementGroup] = dict()
|
||||
self._pg_to_staging_future: Dict[PlacementGroup, ray.ObjectRef] = dict()
|
||||
|
||||
# Set of acquired PGs. We keep track of these here to make sure we
|
||||
# only free PGs that this manager managed.
|
||||
self._acquired_pgs: Set[PlacementGroup] = set()
|
||||
|
||||
# Minimum time between updates of the internal state
|
||||
self.update_interval_s = update_interval_s
|
||||
self._last_update = time.monotonic() - self.update_interval_s - 1
|
||||
|
||||
def get_resource_futures(self) -> List[ray.ObjectRef]:
|
||||
return list(self._staging_future_to_pg.keys())
|
||||
|
||||
def _maybe_update_state(self):
|
||||
now = time.monotonic()
|
||||
if now > self._last_update + self.update_interval_s:
|
||||
self.update_state()
|
||||
|
||||
def update_state(self):
|
||||
ready, not_ready = ray.wait(
|
||||
list(self._staging_future_to_pg.keys()),
|
||||
num_returns=len(self._staging_future_to_pg),
|
||||
timeout=0,
|
||||
)
|
||||
for future in ready:
|
||||
# Remove staging future
|
||||
pg = self._staging_future_to_pg.pop(future)
|
||||
self._pg_to_staging_future.pop(pg)
|
||||
# Fetch resource request
|
||||
request = self._pg_to_request[pg]
|
||||
# Remove from staging, add to ready
|
||||
self._request_to_staged_pgs[request].remove(pg)
|
||||
self._request_to_ready_pgs[request].add(pg)
|
||||
self._last_update = time.monotonic()
|
||||
|
||||
def request_resources(self, resource_request: ResourceRequest):
|
||||
pg = resource_request.to_placement_group()
|
||||
self._pg_to_request[pg] = resource_request
|
||||
self._request_to_staged_pgs[resource_request].add(pg)
|
||||
|
||||
future = pg.ready()
|
||||
self._staging_future_to_pg[future] = pg
|
||||
self._pg_to_staging_future[pg] = future
|
||||
|
||||
def cancel_resource_request(self, resource_request: ResourceRequest):
|
||||
if self._request_to_staged_pgs[resource_request]:
|
||||
pg = self._request_to_staged_pgs[resource_request].pop()
|
||||
|
||||
# PG was staging
|
||||
future = self._pg_to_staging_future.pop(pg)
|
||||
self._staging_future_to_pg.pop(future)
|
||||
|
||||
# Cancel the pg.ready task.
|
||||
# Otherwise, it will be pending node assignment forever.
|
||||
ray.cancel(future)
|
||||
else:
|
||||
# PG might be ready
|
||||
pg = self._request_to_ready_pgs[resource_request].pop()
|
||||
if not pg:
|
||||
raise RuntimeError(
|
||||
"Cannot cancel resource request: No placement group was "
|
||||
f"staged or is ready. Make sure to not cancel more resource "
|
||||
f"requests than you've created. Request: {resource_request}"
|
||||
)
|
||||
|
||||
self._pg_to_request.pop(pg)
|
||||
ray.util.remove_placement_group(pg)
|
||||
|
||||
def has_resources_ready(self, resource_request: ResourceRequest) -> bool:
|
||||
if not bool(len(self._request_to_ready_pgs[resource_request])):
|
||||
# Only update state if needed
|
||||
self._maybe_update_state()
|
||||
|
||||
return bool(len(self._request_to_ready_pgs[resource_request]))
|
||||
|
||||
def acquire_resources(
|
||||
self, resource_request: ResourceRequest
|
||||
) -> Optional[PlacementGroupAcquiredResources]:
|
||||
if not self.has_resources_ready(resource_request):
|
||||
return None
|
||||
|
||||
pg = self._request_to_ready_pgs[resource_request].pop()
|
||||
self._acquired_pgs.add(pg)
|
||||
|
||||
return self._resource_cls(placement_group=pg, resource_request=resource_request)
|
||||
|
||||
def free_resources(self, acquired_resource: PlacementGroupAcquiredResources):
|
||||
pg = acquired_resource.placement_group
|
||||
|
||||
self._acquired_pgs.remove(pg)
|
||||
remove_placement_group(pg)
|
||||
self._pg_to_request.pop(pg)
|
||||
|
||||
def clear(self):
|
||||
if not ray.is_initialized():
|
||||
return
|
||||
|
||||
for staged_pgs in self._request_to_staged_pgs.values():
|
||||
for staged_pg in staged_pgs:
|
||||
remove_placement_group(staged_pg)
|
||||
|
||||
for ready_pgs in self._request_to_ready_pgs.values():
|
||||
for ready_pg in ready_pgs:
|
||||
remove_placement_group(ready_pg)
|
||||
|
||||
for acquired_pg in self._acquired_pgs:
|
||||
remove_placement_group(acquired_pg)
|
||||
|
||||
# Reset internal state
|
||||
self.__init__(update_interval_s=self.update_interval_s)
|
||||
|
||||
def __del__(self):
|
||||
self.clear()
|
||||
@@ -0,0 +1,259 @@
|
||||
import abc
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from inspect import signature
|
||||
from typing import Dict, List, Union
|
||||
|
||||
import ray
|
||||
from ray.util import placement_group
|
||||
from ray.util.annotations import DeveloperAPI
|
||||
|
||||
RemoteRayEntity = Union[ray.remote_function.RemoteFunction, ray.actor.ActorClass]
|
||||
|
||||
|
||||
def _sum_bundles(bundles: List[Dict[str, float]]) -> Dict[str, float]:
|
||||
"""Sum all resources in a list of resource bundles.
|
||||
|
||||
Args:
|
||||
bundles: List of resource bundles.
|
||||
|
||||
Returns:
|
||||
Dict containing all resources summed up.
|
||||
"""
|
||||
resources = {}
|
||||
for bundle in bundles:
|
||||
for k, v in bundle.items():
|
||||
resources[k] = resources.get(k, 0) + v
|
||||
return resources
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class ResourceRequest:
|
||||
"""Request for resources.
|
||||
|
||||
This class is used to define a resource request. A resource request comprises one
|
||||
or more bundles of resources and instructions on the scheduling behavior.
|
||||
|
||||
The resource request can be submitted to a resource manager, which will
|
||||
schedule the resources. Depending on the resource backend, this may instruct
|
||||
Ray to scale up (autoscaling).
|
||||
|
||||
Resource requests are compatible with the most fine-grained low-level resource
|
||||
backend, which are Ray placement groups.
|
||||
|
||||
Args:
|
||||
bundles: A list of bundles which represent the resources requirements.
|
||||
E.g. ``[{"CPU": 1, "GPU": 1}]``.
|
||||
strategy: The scheduling strategy to acquire the bundles.
|
||||
|
||||
- "PACK": Packs Bundles into as few nodes as possible.
|
||||
- "SPREAD": Places Bundles across distinct nodes as even as possible.
|
||||
- "STRICT_PACK": Packs Bundles into one node. The group is
|
||||
not allowed to span multiple nodes.
|
||||
- "STRICT_SPREAD": Packs Bundles across distinct nodes.
|
||||
*args: Passed to the call of ``placement_group()``, if applicable.
|
||||
**kwargs: Passed to the call of ``placement_group()``, if applicable.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bundles: List[Dict[str, Union[int, float]]],
|
||||
strategy: str = "PACK",
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
if not bundles:
|
||||
raise ValueError("Cannot initialize a ResourceRequest with zero bundles.")
|
||||
|
||||
# Remove empty resource keys
|
||||
self._bundles = [
|
||||
{k: float(v) for k, v in bundle.items() if v != 0} for bundle in bundles
|
||||
]
|
||||
|
||||
# Check if the head bundle is empty (no resources defined or all resources
|
||||
# are 0 (and thus removed in the previous step)
|
||||
if not self._bundles[0]:
|
||||
# This is when the head bundle doesn't need resources.
|
||||
self._head_bundle_is_empty = True
|
||||
self._bundles.pop(0)
|
||||
|
||||
if not self._bundles:
|
||||
raise ValueError(
|
||||
"Cannot initialize a ResourceRequest with an empty head "
|
||||
"and zero worker bundles."
|
||||
)
|
||||
else:
|
||||
self._head_bundle_is_empty = False
|
||||
|
||||
self._strategy = strategy
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
self._hash = None
|
||||
self._bound = None
|
||||
|
||||
self._bind()
|
||||
|
||||
@property
|
||||
def head_bundle_is_empty(self):
|
||||
"""Returns True if head bundle is empty while child bundles
|
||||
need resources.
|
||||
|
||||
This is considered an internal API within Tune.
|
||||
"""
|
||||
return self._head_bundle_is_empty
|
||||
|
||||
@property
|
||||
@DeveloperAPI
|
||||
def head_cpus(self) -> float:
|
||||
"""Returns the number of cpus in the head bundle."""
|
||||
return 0.0 if self._head_bundle_is_empty else self._bundles[0].get("CPU", 0.0)
|
||||
|
||||
@property
|
||||
@DeveloperAPI
|
||||
def bundles(self) -> List[Dict[str, float]]:
|
||||
"""Returns a deep copy of resource bundles"""
|
||||
return deepcopy(self._bundles)
|
||||
|
||||
@property
|
||||
def required_resources(self) -> Dict[str, float]:
|
||||
"""Returns a dict containing the sums of all resources"""
|
||||
return _sum_bundles(self._bundles)
|
||||
|
||||
@property
|
||||
@DeveloperAPI
|
||||
def strategy(self) -> str:
|
||||
"""Returns the placement strategy"""
|
||||
return self._strategy
|
||||
|
||||
def _bind(self):
|
||||
"""Bind the args and kwargs to the `placement_group()` signature.
|
||||
|
||||
We bind the args and kwargs, so we can compare equality of two resource
|
||||
requests. The main reason for this is that the `placement_group()` API
|
||||
can evolve independently from the ResourceRequest API (e.g. adding new
|
||||
arguments). Then, `ResourceRequest(bundles, strategy, arg=arg)` should
|
||||
be the same as `ResourceRequest(bundles, strategy, arg)`.
|
||||
"""
|
||||
sig = signature(placement_group)
|
||||
try:
|
||||
self._bound = sig.bind(
|
||||
self._bundles, self._strategy, *self._args, **self._kwargs
|
||||
)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"Invalid definition for resource request. Please check "
|
||||
"that you passed valid arguments to the ResourceRequest "
|
||||
"object."
|
||||
) from exc
|
||||
|
||||
def to_placement_group(self):
|
||||
return placement_group(*self._bound.args, **self._bound.kwargs)
|
||||
|
||||
def __eq__(self, other: "ResourceRequest"):
|
||||
return (
|
||||
isinstance(other, ResourceRequest)
|
||||
and self._bound == other._bound
|
||||
and self.head_bundle_is_empty == other.head_bundle_is_empty
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
if not self._hash:
|
||||
# Cache hash
|
||||
self._hash = hash(
|
||||
json.dumps(
|
||||
{"args": self._bound.args, "kwargs": self._bound.kwargs},
|
||||
sort_keys=True,
|
||||
indent=0,
|
||||
ensure_ascii=True,
|
||||
)
|
||||
)
|
||||
return self._hash
|
||||
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
state.pop("_hash", None)
|
||||
state.pop("_bound", None)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
self._hash = None
|
||||
self._bound = None
|
||||
self._bind()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<ResourceRequest (_bound={self._bound}, "
|
||||
f"head_bundle_is_empty={self.head_bundle_is_empty})>"
|
||||
)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
@dataclass
|
||||
class AcquiredResources(abc.ABC):
|
||||
"""Base class for resources that have been acquired.
|
||||
|
||||
Acquired resources can be associated to Ray objects, which can then be
|
||||
scheduled using these resources.
|
||||
|
||||
Internally this can point e.g. to a placement group, a placement
|
||||
group bundle index, or just raw resources.
|
||||
|
||||
The main API is the `annotate_remote_entities` method. This will associate
|
||||
remote Ray objects (tasks and actors) with the acquired resources by setting
|
||||
the Ray remote options to use the acquired resources.
|
||||
"""
|
||||
|
||||
resource_request: ResourceRequest
|
||||
|
||||
def annotate_remote_entities(
|
||||
self, entities: List[RemoteRayEntity]
|
||||
) -> List[Union[RemoteRayEntity]]:
|
||||
"""Return remote ray entities (tasks/actors) to use the acquired resources.
|
||||
|
||||
The first entity will be associated with the first bundle, the second
|
||||
entity will be associated with the second bundle, etc.
|
||||
|
||||
Args:
|
||||
entities: Remote Ray entities to annotate with the acquired resources.
|
||||
|
||||
Returns:
|
||||
The list of annotated remote Ray entities.
|
||||
"""
|
||||
bundles = self.resource_request.bundles
|
||||
|
||||
# Also count the empty head bundle as a bundle
|
||||
num_bundles = len(bundles) + int(self.resource_request.head_bundle_is_empty)
|
||||
|
||||
if len(entities) > num_bundles:
|
||||
raise RuntimeError(
|
||||
f"The number of callables to annotate ({len(entities)}) cannot "
|
||||
f"exceed the number of available bundles ({num_bundles})."
|
||||
)
|
||||
|
||||
annotated = []
|
||||
|
||||
if self.resource_request.head_bundle_is_empty:
|
||||
# The empty head bundle is place on the first bundle index with empty
|
||||
# resources.
|
||||
annotated.append(
|
||||
self._annotate_remote_entity(entities[0], {}, bundle_index=0)
|
||||
)
|
||||
|
||||
# Shift the remaining entities
|
||||
entities = entities[1:]
|
||||
|
||||
for i, (entity, bundle) in enumerate(zip(entities, bundles)):
|
||||
annotated.append(
|
||||
self._annotate_remote_entity(entity, bundle, bundle_index=i)
|
||||
)
|
||||
|
||||
return annotated
|
||||
|
||||
def _annotate_remote_entity(
|
||||
self, entity: RemoteRayEntity, bundle: Dict[str, float], bundle_index: int
|
||||
) -> RemoteRayEntity:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,155 @@
|
||||
import abc
|
||||
from typing import List, Optional
|
||||
|
||||
import ray
|
||||
from ray.air.execution.resources.request import AcquiredResources, ResourceRequest
|
||||
from ray.util.annotations import DeveloperAPI
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class ResourceManager(abc.ABC):
|
||||
"""Resource manager interface.
|
||||
|
||||
A resource manager can be used to request resources from a Ray cluster and
|
||||
allocate them to remote Ray tasks or actors.
|
||||
|
||||
Resources have to be requested before they can be acquired.
|
||||
|
||||
Resources managed by the resource manager can be in three states:
|
||||
|
||||
1. "Requested": The resources have been requested but are not yet available to
|
||||
schedule remote Ray objects. The resource request may trigger autoscaling,
|
||||
and can be cancelled if no longer needed.
|
||||
2. "Ready": The requested resources are now available to schedule remote Ray
|
||||
objects. They can be acquired and subsequently used remote Ray objects.
|
||||
The resource request can still be cancelled if no longer needed.
|
||||
3. "Acquired": The resources have been acquired by a caller to use for scheduling
|
||||
remote Ray objects. Note that it is the responsibility of the caller to
|
||||
schedule the Ray objects with these resources.
|
||||
The associated resource request has been completed and can no longer be
|
||||
cancelled. The acquired resources can be freed by the resource manager when
|
||||
they are no longer used.
|
||||
|
||||
The flow is as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Create resource manager
|
||||
resource_manager = ResourceManager()
|
||||
|
||||
# Create resource request
|
||||
resource_request = ResourceRequest([{"CPU": 4}])
|
||||
|
||||
# Pass to resource manager
|
||||
resource_manager.request_resources(resource_request)
|
||||
|
||||
# Wait until ready
|
||||
while not resource_manager.has_resources_ready(resource_request):
|
||||
time.sleep(1)
|
||||
|
||||
# Once ready, acquire resources
|
||||
acquired_resource = resource_manager.acquire_resources(resource_request)
|
||||
|
||||
# Bind to remote task or actor
|
||||
annotated_remote_fn = acquired_resource.annotate_remote_entities(
|
||||
[remote_fn])
|
||||
|
||||
# Run remote function. This will use the acquired resources
|
||||
ray.get(annotated_remote_fn.remote())
|
||||
|
||||
# After using the resources, free
|
||||
resource_manager.free_resources(annotated_resources)
|
||||
|
||||
"""
|
||||
|
||||
def request_resources(self, resource_request: ResourceRequest):
|
||||
"""Request resources.
|
||||
|
||||
Depending on the backend, resources can trigger autoscaling. Requested
|
||||
resources can be ready or not ready. Once they are "ready", they can
|
||||
be acquired and used by remote Ray objects.
|
||||
|
||||
Resource requests can be cancelled anytime using ``cancel_resource_request()``.
|
||||
Once acquired, the resource request is removed. Acquired resources can be
|
||||
freed with ``free_resources()``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def cancel_resource_request(self, resource_request: ResourceRequest):
|
||||
"""Cancel resource request.
|
||||
|
||||
Resource requests can be cancelled anytime before a resource is acquired.
|
||||
Acquiring a resource will remove the associated resource request.
|
||||
Acquired resources can be freed with ``free_resources()``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def has_resources_ready(self, resource_request: ResourceRequest) -> bool:
|
||||
"""Returns True if resources for the given request are ready to be acquired."""
|
||||
raise NotImplementedError
|
||||
|
||||
def acquire_resources(
|
||||
self, resource_request: ResourceRequest
|
||||
) -> Optional[AcquiredResources]:
|
||||
"""Acquire resources. Returns None if resources are not ready to be acquired.
|
||||
|
||||
Acquiring resources will remove the associated resource request.
|
||||
Acquired resources can be returned with ``free_resources()``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def free_resources(self, acquired_resource: AcquiredResources):
|
||||
"""Free acquired resources from usage and return them to the resource manager.
|
||||
|
||||
Freeing resources will return the resources to the manager, but there are
|
||||
no guarantees about the tasks and actors scheduled on the resources. The caller
|
||||
should make sure that any references to tasks or actors scheduled on the
|
||||
resources have been removed before calling ``free_resources()``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_resource_futures(self) -> List[ray.ObjectRef]:
|
||||
"""Return futures for resources to await.
|
||||
|
||||
Depending on the backend, we use resource futures to determine availability
|
||||
of resources (e.g. placement groups) or resolution of requests.
|
||||
In this case, the futures can be awaited externally by the caller.
|
||||
|
||||
When a resource future resolved, the caller may call ``update_state()``
|
||||
to force the resource manager to update its internal state immediately.
|
||||
"""
|
||||
return []
|
||||
|
||||
def update_state(self):
|
||||
"""Update internal state of the resource manager.
|
||||
|
||||
The resource manager may have internal state that needs periodic updating.
|
||||
For instance, depending on the backend, resource futures can be awaited
|
||||
externally (with ``get_resource_futures()``).
|
||||
|
||||
If such a future resolved, the caller can instruct the resource
|
||||
manager to update its internal state immediately.
|
||||
"""
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
"""Reset internal state and clear all resources.
|
||||
|
||||
Calling this method will reset the resource manager to its initialization state.
|
||||
All resources will be removed.
|
||||
|
||||
Clearing the state will remove tracked resources from the manager, but there are
|
||||
no guarantees about the tasks and actors scheduled on the resources. The caller
|
||||
should make sure that any references to tasks or actors scheduled on the
|
||||
resources have been removed before calling ``clear()``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __reduce__(self):
|
||||
"""We disallow serialization.
|
||||
|
||||
Shared resource managers should live on an actor.
|
||||
"""
|
||||
raise ValueError(
|
||||
f"Resource managers cannot be serialized. Resource manager: {str(self)}"
|
||||
)
|
||||
Reference in New Issue
Block a user