chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from .backpressure_policy import BackpressurePolicy
|
||||
from .concurrency_cap_backpressure_policy import ConcurrencyCapBackpressurePolicy
|
||||
from .downstream_capacity_backpressure_policy import (
|
||||
DownstreamCapacityBackpressurePolicy,
|
||||
)
|
||||
from .resource_budget_backpressure_policy import ResourceBudgetBackpressurePolicy
|
||||
from ray.data.context import DataContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ray.data._internal.execution.resource_manager import ResourceManager
|
||||
from ray.data._internal.execution.streaming_executor_state import Topology
|
||||
|
||||
# Default enabled backpressure policies and its config key.
|
||||
# Use `DataContext.set_config` to config it.
|
||||
ENABLED_BACKPRESSURE_POLICIES = [
|
||||
ConcurrencyCapBackpressurePolicy,
|
||||
ResourceBudgetBackpressurePolicy,
|
||||
DownstreamCapacityBackpressurePolicy,
|
||||
]
|
||||
ENABLED_BACKPRESSURE_POLICIES_CONFIG_KEY = "backpressure_policies.enabled"
|
||||
|
||||
|
||||
def get_backpressure_policies(
|
||||
data_context: DataContext,
|
||||
topology: "Topology",
|
||||
resource_manager: "ResourceManager",
|
||||
) -> List[BackpressurePolicy]:
|
||||
policies = data_context.get_config(
|
||||
ENABLED_BACKPRESSURE_POLICIES_CONFIG_KEY, ENABLED_BACKPRESSURE_POLICIES
|
||||
)
|
||||
|
||||
return [policy(data_context, topology, resource_manager) for policy in policies]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BackpressurePolicy",
|
||||
"ConcurrencyCapBackpressurePolicy",
|
||||
"DownstreamCapacityBackpressurePolicy",
|
||||
"ENABLED_BACKPRESSURE_POLICIES_CONFIG_KEY",
|
||||
"get_backpressure_policies",
|
||||
]
|
||||
@@ -0,0 +1,70 @@
|
||||
from abc import ABC
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ray.data.context import DataContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ray.data._internal.execution.interfaces.physical_operator import (
|
||||
PhysicalOperator,
|
||||
)
|
||||
from ray.data._internal.execution.resource_manager import ResourceManager
|
||||
from ray.data._internal.execution.streaming_executor_state import Topology
|
||||
|
||||
|
||||
class BackpressurePolicy(ABC):
|
||||
"""Interface for back pressure policies."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Human-readable name for UX/progress bar display.
|
||||
|
||||
Defaults to the class name. Subclasses can override for a custom name.
|
||||
"""
|
||||
return type(self).__name__
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_context: DataContext,
|
||||
topology: "Topology",
|
||||
resource_manager: "ResourceManager",
|
||||
):
|
||||
"""Initialize the backpressure policy.
|
||||
|
||||
Args:
|
||||
data_context: The data context.
|
||||
topology: The execution topology.
|
||||
resource_manager: The resource manager.
|
||||
"""
|
||||
self._data_context = data_context
|
||||
self._topology = topology
|
||||
self._resource_manager = resource_manager
|
||||
|
||||
def can_add_input(self, op: "PhysicalOperator") -> bool:
|
||||
"""Determine if we can add a new input to the operator. If returns False, the
|
||||
operator will be backpressured and will not be able to run new tasks.
|
||||
Used in `streaming_executor_state.py::select_operator_to_run()`.
|
||||
|
||||
Returns: True if we can add a new input to the operator, False otherwise.
|
||||
|
||||
Note, if multiple backpressure policies are enabled, the operator will be
|
||||
backpressured if any of the policies returns False.
|
||||
"""
|
||||
return True
|
||||
|
||||
def max_task_output_bytes_to_read(self, op: "PhysicalOperator") -> Optional[int]:
|
||||
"""Return the maximum bytes of pending task outputs can be read for
|
||||
the given operator. None means no limit.
|
||||
|
||||
This is used for output backpressure to limit how much data an operator
|
||||
can read from its running tasks.
|
||||
|
||||
Note, if multiple backpressure policies return non-None values for an operator,
|
||||
the minimum of those values will be used as the limit.
|
||||
|
||||
Args:
|
||||
op: The operator to get the limit for.
|
||||
|
||||
Returns:
|
||||
The maximum bytes that can be read, or None if no limit.
|
||||
"""
|
||||
return None
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
import logging
|
||||
import math
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from .backpressure_policy import BackpressurePolicy
|
||||
from .downstream_capacity_backpressure_policy import (
|
||||
get_available_object_store_budget_fraction,
|
||||
)
|
||||
from ray._common.utils import env_float
|
||||
from ray.data._internal.execution.operators.map_operator import MapOperator
|
||||
from ray.data._internal.execution.operators.task_pool_map_operator import (
|
||||
TaskPoolMapOperator,
|
||||
)
|
||||
from ray.util.annotations import Deprecated, RayDeprecationWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ray.data._internal.execution.interfaces.physical_operator import (
|
||||
PhysicalOperator,
|
||||
)
|
||||
from ray.data._internal.execution.operators.map_operator import MapOperator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@Deprecated(
|
||||
message="ConcurrencyCapBackpressurePolicy is deprecated and will be removed "
|
||||
"on or after Ray 2.59.",
|
||||
)
|
||||
class ConcurrencyCapBackpressurePolicy(BackpressurePolicy):
|
||||
"""A backpressure policy that caps the concurrency of each operator.
|
||||
This policy dynamically limits the number of concurrent tasks per operator
|
||||
based on the output queue growth rate.
|
||||
|
||||
- Maintain asymmetric EWMA of total enqueued output bytes as the
|
||||
typical level: `level`.
|
||||
- Maintain asymmetric EWMA of absolute residual vs the *previous* level as a
|
||||
scale proxy: `dev = EWMA(|q - level_prev|)`.
|
||||
- Define deadband: Deadband is the acceptable range of the output queue size
|
||||
around the typical level where the queue size is expected to stay stable.
|
||||
deadband [lower, upper] = [level - K_DEV*dev, level + K_DEV*dev].
|
||||
- If q > upper -> target cap = running - BACKOFF_FACTOR (back off)
|
||||
If q < lower -> target cap = running + RAMPUP_FACTOR (ramp up)
|
||||
Else -> target cap = running (hold)
|
||||
- Apply user-configured max concurrency cap, admit iff running < target cap.
|
||||
|
||||
NOTE: Only support setting concurrency cap for `TaskPoolMapOperator` for now.
|
||||
TODO(chengsu): Consolidate with actor scaling logic of `ActorPoolMapOperator`.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "ConcurrencyCap"
|
||||
|
||||
# Smoothing factor for the asymmetric EWMA (slow fall, faster rise).
|
||||
EWMA_ALPHA = env_float("RAY_DATA_CONCURRENCY_CAP_EWMA_ALPHA", 0.1)
|
||||
EWMA_ALPHA_UP = 1.0 - (1.0 - EWMA_ALPHA) ** 2 # fast rise
|
||||
# Deadband width in units of the EWMA absolute deviation estimate.
|
||||
K_DEV = env_float("RAY_DATA_CONCURRENCY_CAP_K_DEV", 1.0)
|
||||
# Factor to back off when the queue is too large.
|
||||
BACKOFF_FACTOR = env_float("RAY_DATA_CONCURRENCY_CAP_BACKOFF_FACTOR", 1)
|
||||
# Factor to ramp up when the queue is too small.
|
||||
RAMPUP_FACTOR = env_float("RAY_DATA_CONCURRENCY_CAP_RAMPUP_FACTOR", 1)
|
||||
# Threshold for per-Op object store budget (available) vs total
|
||||
# (available / total) ratio to enable dynamic output queue size backpressure.
|
||||
AVAILABLE_OBJECT_STORE_BUDGET_THRESHOLD = env_float(
|
||||
"RAY_DATA_CONCURRENCY_CAP_AVAILABLE_OBJECT_STORE_BUDGET_THRESHOLD", 0.1
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Configured per-operator caps (+inf if unset).
|
||||
self._concurrency_caps: Dict["PhysicalOperator", float] = {}
|
||||
|
||||
# EWMA state for level
|
||||
self._q_level_nbytes: Dict["PhysicalOperator", float] = defaultdict(float)
|
||||
|
||||
# EWMA state for dev
|
||||
self._q_level_dev: Dict["PhysicalOperator", float] = defaultdict(float)
|
||||
|
||||
# Per-operator cached threshold (bootstrapped from first sample).
|
||||
self._queue_level_thresholds: Dict["PhysicalOperator", int] = defaultdict(int)
|
||||
|
||||
# Last effective cap for change logs.
|
||||
self._last_effective_caps: Dict["PhysicalOperator", int] = {}
|
||||
|
||||
# Initialize caps from operators (infinite if unset)
|
||||
for op, _ in self._topology.items():
|
||||
if (
|
||||
isinstance(op, TaskPoolMapOperator)
|
||||
and op.get_max_concurrency_limit() is not None
|
||||
):
|
||||
self._concurrency_caps[op] = op.get_max_concurrency_limit()
|
||||
else:
|
||||
self._concurrency_caps[op] = float("inf")
|
||||
|
||||
# Whether to cap the concurrency of an operator based on its and downstream's queue size.
|
||||
self.enable_dynamic_output_queue_size_backpressure = (
|
||||
self._data_context.enable_dynamic_output_queue_size_backpressure
|
||||
)
|
||||
|
||||
if self.enable_dynamic_output_queue_size_backpressure:
|
||||
warnings.warn(
|
||||
"ConcurrencyCapBackpressurePolicy is deprecated and will be "
|
||||
"removed on or after Ray 2.59.",
|
||||
RayDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
dynamic_output_queue_size_backpressure_configs = ""
|
||||
if self.enable_dynamic_output_queue_size_backpressure:
|
||||
dynamic_output_queue_size_backpressure_configs = (
|
||||
f", EWMA_ALPHA={self.EWMA_ALPHA}, K_DEV={self.K_DEV}, "
|
||||
f"BACKOFF_FACTOR={self.BACKOFF_FACTOR}, RAMPUP_FACTOR={self.RAMPUP_FACTOR}, "
|
||||
f"AVAILABLE_OBJECT_STORE_BUDGET_THRESHOLD={self.AVAILABLE_OBJECT_STORE_BUDGET_THRESHOLD}"
|
||||
)
|
||||
logger.debug(
|
||||
f"ConcurrencyCapBackpressurePolicy caps: {self._concurrency_caps}, "
|
||||
f"enabled: {self.enable_dynamic_output_queue_size_backpressure}{dynamic_output_queue_size_backpressure_configs}"
|
||||
)
|
||||
|
||||
def _update_ewma_asymmetric(self, prev_value: float, sample: float) -> float:
|
||||
"""
|
||||
Update EWMA with asymmetric behavior: fast rise, slow fall.
|
||||
Args:
|
||||
prev_value: Previous EWMA value
|
||||
sample: New sample value
|
||||
|
||||
Returns:
|
||||
Updated EWMA value
|
||||
"""
|
||||
if prev_value <= 0:
|
||||
return sample
|
||||
|
||||
# fast rise if sample > prev_value, slow fall otherwise
|
||||
alpha = self.EWMA_ALPHA_UP if sample > prev_value else self.EWMA_ALPHA
|
||||
return (1 - alpha) * prev_value + alpha * sample
|
||||
|
||||
def _update_level_and_dev(self, op: "PhysicalOperator", q_bytes: int) -> None:
|
||||
"""Update EWMA level and dev (residual w.r.t. previous level)."""
|
||||
q = float(q_bytes)
|
||||
|
||||
level_prev = self._q_level_nbytes[op]
|
||||
dev_prev = self._q_level_dev[op]
|
||||
|
||||
# Deviation vs the previous level
|
||||
dev_sample = abs(q - level_prev) if level_prev > 0 else 0.0
|
||||
dev = self._update_ewma_asymmetric(dev_prev, dev_sample)
|
||||
|
||||
# Now update the level itself
|
||||
level = self._update_ewma_asymmetric(level_prev, q)
|
||||
|
||||
self._q_level_nbytes[op] = level
|
||||
self._q_level_dev[op] = dev
|
||||
|
||||
# For visibility, store the integer center of the band
|
||||
self._queue_level_thresholds[op] = max(1, int(level))
|
||||
|
||||
def can_add_input(self, op: "PhysicalOperator") -> bool:
|
||||
"""Return whether `op` may accept another input now."""
|
||||
num_tasks_running = op.metrics.num_tasks_running
|
||||
|
||||
# Skip dynamic backpressure if:
|
||||
# - Not a MapOperator
|
||||
# - Not eligible for Op for Backpressure
|
||||
# - Dynamic backpressure based on output queue size is disabled
|
||||
# - Downstream is a materializing op which requires full materialization
|
||||
if (
|
||||
not isinstance(op, MapOperator)
|
||||
or not self._resource_manager.is_op_eligible(op)
|
||||
or not self.enable_dynamic_output_queue_size_backpressure
|
||||
or self._resource_manager._is_blocking_materializing_op(op)
|
||||
):
|
||||
return num_tasks_running < self._concurrency_caps[op]
|
||||
|
||||
# For this Op, if the objectstore budget (available) to total
|
||||
# ratio is above threshold, skip dynamic output queue size backpressure.
|
||||
available_budget_fraction = get_available_object_store_budget_fraction(
|
||||
self._resource_manager, op, consider_downstream_ineligible_ops=True
|
||||
)
|
||||
if (
|
||||
available_budget_fraction is not None
|
||||
and available_budget_fraction > self.AVAILABLE_OBJECT_STORE_BUDGET_THRESHOLD
|
||||
):
|
||||
# If the objectstore budget (available) to total
|
||||
# ratio is above threshold, skip dynamic output queue size
|
||||
# backpressure, but still enforce the configured cap.
|
||||
return num_tasks_running < self._concurrency_caps[op]
|
||||
|
||||
# Current total queued bytes (this op + downstream)
|
||||
current_queue_size_bytes = self._resource_manager.get_mem_op_internal(
|
||||
op
|
||||
) + self._resource_manager.get_mem_op_outputs(
|
||||
op, include_ineligible_downstream=True
|
||||
)
|
||||
|
||||
# Update EWMA state (level & dev) and compute effective cap. Note that
|
||||
# we don't update the EWMA state if the objectstore budget (available) vs total
|
||||
# ratio is above threshold, because the level and dev adjusts quickly.
|
||||
self._update_level_and_dev(op, current_queue_size_bytes)
|
||||
effective_cap = self._effective_cap(
|
||||
op, num_tasks_running, current_queue_size_bytes
|
||||
)
|
||||
|
||||
last = self._last_effective_caps.get(op, None)
|
||||
if last != effective_cap:
|
||||
logger.debug(
|
||||
f"Cap change {op.name}: {last if last is not None else 'None'} -> "
|
||||
f"{effective_cap} (running={num_tasks_running}, queue={current_queue_size_bytes}, "
|
||||
f"thr={self._queue_level_thresholds[op]})"
|
||||
)
|
||||
self._last_effective_caps[op] = effective_cap
|
||||
|
||||
return num_tasks_running < effective_cap
|
||||
|
||||
def _effective_cap(
|
||||
self,
|
||||
op: "PhysicalOperator",
|
||||
num_tasks_running: int,
|
||||
current_queue_size_bytes: int,
|
||||
) -> int:
|
||||
"""A simple controller around EWMA level.
|
||||
Args:
|
||||
op: The operator to compute the effective cap for.
|
||||
num_tasks_running: The number of tasks currently running.
|
||||
current_queue_size_bytes: Current total queued bytes for this operator + downstream.
|
||||
Returns:
|
||||
The effective cap.
|
||||
"""
|
||||
cap_cfg = self._concurrency_caps[op]
|
||||
|
||||
level = float(self._q_level_nbytes[op])
|
||||
dev = max(1.0, float(self._q_level_dev[op]))
|
||||
upper = level + self.K_DEV * dev
|
||||
lower = level - self.K_DEV * dev
|
||||
|
||||
if current_queue_size_bytes > upper:
|
||||
# back off
|
||||
target = num_tasks_running - self.BACKOFF_FACTOR
|
||||
elif current_queue_size_bytes < lower:
|
||||
# ramp up
|
||||
target = num_tasks_running + self.RAMPUP_FACTOR
|
||||
else:
|
||||
# hold
|
||||
target = num_tasks_running
|
||||
|
||||
# Clamp to [1, configured_cap]
|
||||
target = max(1, target)
|
||||
if not math.isinf(cap_cfg):
|
||||
target = min(target, int(cap_cfg))
|
||||
return int(target)
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from .backpressure_policy import BackpressurePolicy
|
||||
from ray._common.utils import env_float
|
||||
from ray.data._internal.execution.resource_manager import (
|
||||
ResourceManager,
|
||||
)
|
||||
from ray.data.context import DataContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ray.data._internal.execution.interfaces.physical_operator import (
|
||||
PhysicalOperator,
|
||||
)
|
||||
from ray.data._internal.execution.streaming_executor_state import Topology
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_available_object_store_budget_fraction(
|
||||
resource_manager: "ResourceManager",
|
||||
op: "PhysicalOperator",
|
||||
consider_downstream_ineligible_ops: bool,
|
||||
) -> Optional[float]:
|
||||
"""Get available object store memory budget fraction for the operator.
|
||||
|
||||
Args:
|
||||
resource_manager: The resource manager to use.
|
||||
op: The operator to get the budget fraction for.
|
||||
consider_downstream_ineligible_ops: If True, include downstream ineligible
|
||||
ops in the calculation. If False, only consider this op's usage/budget.
|
||||
|
||||
Returns:
|
||||
The available budget fraction, or None if not available.
|
||||
"""
|
||||
op_usage = resource_manager.get_op_usage(
|
||||
op, include_ineligible_downstream=consider_downstream_ineligible_ops
|
||||
)
|
||||
op_budget = resource_manager.get_budget(op)
|
||||
if op_usage is None or op_budget is None:
|
||||
return None
|
||||
|
||||
total_usage = op_usage.object_store_memory
|
||||
|
||||
total_budget = op_budget.object_store_memory
|
||||
total_mem = total_usage + total_budget
|
||||
if total_mem == 0:
|
||||
return None
|
||||
|
||||
return total_budget / total_mem
|
||||
|
||||
|
||||
def get_utilized_object_store_budget_fraction(
|
||||
resource_manager: "ResourceManager",
|
||||
op: "PhysicalOperator",
|
||||
consider_downstream_ineligible_ops: bool,
|
||||
) -> Optional[float]:
|
||||
"""Get utilized object store memory budget fraction for the operator.
|
||||
|
||||
Args:
|
||||
resource_manager: The resource manager to use.
|
||||
op: The operator to get the utilized fraction for.
|
||||
consider_downstream_ineligible_ops: If True, include downstream ineligible
|
||||
ops in the calculation. If False, only consider this op's usage/budget.
|
||||
|
||||
Returns:
|
||||
The utilized budget fraction, or None if not available.
|
||||
"""
|
||||
available_fraction = get_available_object_store_budget_fraction(
|
||||
resource_manager,
|
||||
op,
|
||||
consider_downstream_ineligible_ops=consider_downstream_ineligible_ops,
|
||||
)
|
||||
if available_fraction is None:
|
||||
return None
|
||||
return 1 - available_fraction
|
||||
|
||||
|
||||
class DownstreamCapacityBackpressurePolicy(BackpressurePolicy):
|
||||
"""Backpressure policy based on downstream processing capacity.
|
||||
|
||||
To backpressure a given operator, use queue size build up / downstream capacity ratio.
|
||||
This ratio represents the upper limit of buffering in object store between pipeline stages
|
||||
to optimize for throughput.
|
||||
"""
|
||||
|
||||
# Threshold for per-Op object store budget utilization vs total
|
||||
# (utilization / total) ratio to enable downstream capacity backpressure.
|
||||
OBJECT_STORE_BUDGET_UTIL_THRESHOLD = env_float(
|
||||
"RAY_DATA_DOWNSTREAM_CAPACITY_OBJECT_STORE_BUDGET_UTIL_THRESHOLD", 0.5
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "DownstreamCapacity"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_context: DataContext,
|
||||
topology: "Topology",
|
||||
resource_manager: "ResourceManager",
|
||||
):
|
||||
super().__init__(data_context, topology, resource_manager)
|
||||
|
||||
self._backpressure_capacity_ratio = (
|
||||
self._data_context.downstream_capacity_backpressure_ratio
|
||||
)
|
||||
self._prev_should_backpressure: dict["PhysicalOperator", bool] = {}
|
||||
|
||||
if self._backpressure_capacity_ratio is not None:
|
||||
logger.debug(
|
||||
"DownstreamCapacityBackpressurePolicy enabled with backpressure "
|
||||
f"capacity ratio: {self._backpressure_capacity_ratio}"
|
||||
)
|
||||
|
||||
def _get_queue_size_bytes(self, op: "PhysicalOperator") -> int:
|
||||
"""Get the output current queue size
|
||||
(this operator + ineligible downstream operators) in bytes for the given operator.
|
||||
"""
|
||||
op_outputs_usage = self._topology[op].output_queue_bytes()
|
||||
# Also account the downstream ineligible operators' memory usage.
|
||||
op_outputs_usage += sum(
|
||||
self._resource_manager.get_op_usage(next_op).object_store_memory
|
||||
for next_op in self._resource_manager._get_downstream_ineligible_ops(op)
|
||||
)
|
||||
return op_outputs_usage
|
||||
|
||||
def _get_downstream_capacity_size_bytes(self, op: "PhysicalOperator") -> int:
|
||||
"""Get the downstream capacity size for the given operator.
|
||||
|
||||
Downstream capacity size is the sum of the pending task inputs of the
|
||||
downstream eligible operators.
|
||||
|
||||
If an output dependency is ineligible, skip it and recurse down to find
|
||||
eligible output dependencies. If there are no output dependencies,
|
||||
return external consumer bytes.
|
||||
"""
|
||||
if not op.output_dependencies:
|
||||
# No output dependencies, return external consumer bytes.
|
||||
return self._resource_manager.get_external_consumer_bytes()
|
||||
|
||||
total_capacity_size_bytes = 0
|
||||
for output_dependency in op.output_dependencies:
|
||||
if self._resource_manager.is_op_eligible(output_dependency):
|
||||
# Output dependency is eligible, add its pending task inputs.
|
||||
total_capacity_size_bytes += (
|
||||
output_dependency.metrics.obj_store_mem_pending_task_inputs or 0
|
||||
)
|
||||
else:
|
||||
# Output dependency is ineligible, recurse down to find eligible ops.
|
||||
total_capacity_size_bytes += self._get_downstream_capacity_size_bytes(
|
||||
output_dependency
|
||||
)
|
||||
return total_capacity_size_bytes
|
||||
|
||||
def _should_skip_backpressure(self, op: "PhysicalOperator") -> bool:
|
||||
"""Check if backpressure should be skipped for the operator.
|
||||
TODO(srinathk10): Extract this to common logic to skip invoking BackpressurePolicy.
|
||||
"""
|
||||
if self._backpressure_capacity_ratio is None:
|
||||
# Downstream capacity backpressure is disabled.
|
||||
return True
|
||||
|
||||
if not self._resource_manager.is_op_eligible(op):
|
||||
# Operator is not eligible for backpressure.
|
||||
return True
|
||||
|
||||
if self._resource_manager._is_blocking_materializing_op(op):
|
||||
# Operator is materializing, so no need to perform backpressure.
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _get_queue_ratio(self, op: "PhysicalOperator") -> float:
|
||||
"""Get queue/capacity ratio for the operator."""
|
||||
queue_size_bytes = self._get_queue_size_bytes(op)
|
||||
downstream_capacity_size_bytes = self._get_downstream_capacity_size_bytes(op)
|
||||
if downstream_capacity_size_bytes == 0:
|
||||
# No downstream capacity to backpressure against, so no backpressure.
|
||||
return 0
|
||||
return queue_size_bytes / downstream_capacity_size_bytes
|
||||
|
||||
def _should_apply_backpressure(self, op: "PhysicalOperator") -> bool:
|
||||
"""Check if backpressure should be applied for the operator.
|
||||
|
||||
Returns True if backpressure should be applied, False otherwise.
|
||||
"""
|
||||
if self._should_skip_backpressure(op):
|
||||
return False
|
||||
|
||||
utilized_budget_fraction = get_utilized_object_store_budget_fraction(
|
||||
self._resource_manager, op, consider_downstream_ineligible_ops=True
|
||||
)
|
||||
queue_ratio = self._get_queue_ratio(op)
|
||||
if (
|
||||
utilized_budget_fraction is not None
|
||||
and utilized_budget_fraction <= self.OBJECT_STORE_BUDGET_UTIL_THRESHOLD
|
||||
):
|
||||
# Utilized budget fraction is below threshold, so should skip backpressure.
|
||||
result = False
|
||||
else:
|
||||
# Apply backpressure if queue ratio exceeds the threshold.
|
||||
result = queue_ratio > self._backpressure_capacity_ratio
|
||||
|
||||
prev = self._prev_should_backpressure.get(op)
|
||||
if prev != result:
|
||||
queue_size_bytes = self._get_queue_size_bytes(op)
|
||||
downstream_capacity_bytes = self._get_downstream_capacity_size_bytes(op)
|
||||
logger.debug(
|
||||
f"Backpressure change {op.name}: {prev} -> {result} "
|
||||
f"(queue_ratio={queue_ratio:.2f}, {queue_size_bytes=}, "
|
||||
f"{downstream_capacity_bytes=}, {utilized_budget_fraction=})"
|
||||
)
|
||||
self._prev_should_backpressure[op] = result
|
||||
|
||||
return result
|
||||
|
||||
def can_add_input(self, op: "PhysicalOperator") -> bool:
|
||||
"""Determine if we can add input to the operator based on
|
||||
downstream capacity.
|
||||
"""
|
||||
return not self._should_apply_backpressure(op)
|
||||
|
||||
def max_task_output_bytes_to_read(self, op: "PhysicalOperator") -> Optional[int]:
|
||||
"""Return the maximum bytes of pending task outputs can be read for
|
||||
the given operator. None means no limit."""
|
||||
if self._should_apply_backpressure(op):
|
||||
return 0
|
||||
return None
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from .backpressure_policy import BackpressurePolicy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ray.data._internal.execution.interfaces.physical_operator import (
|
||||
PhysicalOperator,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResourceBudgetBackpressurePolicy(BackpressurePolicy):
|
||||
"""A backpressure policy based on resource budgets in ResourceManager."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "ResourceBudget"
|
||||
|
||||
def can_add_input(self, op: "PhysicalOperator") -> bool:
|
||||
if self._resource_manager._op_resource_allocator is not None:
|
||||
return self._resource_manager._op_resource_allocator.can_submit_new_task(op)
|
||||
|
||||
return True
|
||||
|
||||
def max_task_output_bytes_to_read(self, op: "PhysicalOperator") -> Optional[int]:
|
||||
"""Determine maximum bytes to read based on the resource budgets.
|
||||
|
||||
Args:
|
||||
op: The operator to get the limit for.
|
||||
|
||||
Returns:
|
||||
The maximum bytes that can be read, or None if no limit.
|
||||
"""
|
||||
return self._resource_manager.max_task_output_bytes_to_read(op)
|
||||
Reference in New Issue
Block a user