chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -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",
]
+147
View File
@@ -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)}"
)