Files
2026-07-13 13:17:40 +08:00

148 lines
5.4 KiB
Python

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 = []