chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,796 @@
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from ray.data._internal.actor_autoscaler import (
|
||||
create_actor_autoscaler,
|
||||
)
|
||||
from ray.data._internal.cluster_autoscaler import create_cluster_autoscaler
|
||||
from ray.data._internal.execution import create_ranker
|
||||
from ray.data._internal.execution.backpressure_policy import (
|
||||
BackpressurePolicy,
|
||||
get_backpressure_policies,
|
||||
)
|
||||
from ray.data._internal.execution.block_ref_counter import BlockRefCounter
|
||||
from ray.data._internal.execution.dataset_state import DatasetState
|
||||
from ray.data._internal.execution.execution_callback import ExecutionCallback
|
||||
from ray.data._internal.execution.interfaces import (
|
||||
Executor,
|
||||
OutputIterator,
|
||||
PhysicalOperator,
|
||||
RefBundle,
|
||||
)
|
||||
from ray.data._internal.execution.metadata_fetcher import make_metadata_fetcher
|
||||
from ray.data._internal.execution.operators.base_physical_operator import (
|
||||
InternalQueueOperatorMixin,
|
||||
)
|
||||
from ray.data._internal.execution.operators.input_data_buffer import InputDataBuffer
|
||||
from ray.data._internal.execution.resource_manager import (
|
||||
ResourceManager,
|
||||
)
|
||||
from ray.data._internal.execution.streaming_executor_state import (
|
||||
OpState,
|
||||
OutputBackpressureGuard,
|
||||
Topology,
|
||||
build_streaming_topology,
|
||||
format_op_state_summary,
|
||||
process_completed_tasks,
|
||||
select_operator_to_run,
|
||||
update_operator_states,
|
||||
)
|
||||
from ray.data._internal.logging import (
|
||||
get_log_directory,
|
||||
register_dataset_logger,
|
||||
unregister_dataset_logger,
|
||||
)
|
||||
from ray.data._internal.metadata_exporter import (
|
||||
Topology as TopologyMetadata,
|
||||
sanitize_for_struct,
|
||||
)
|
||||
from ray.data._internal.operator_schema_exporter import (
|
||||
OperatorSchema,
|
||||
get_operator_schema_exporter,
|
||||
)
|
||||
from ray.data._internal.progress import get_progress_manager
|
||||
from ray.data._internal.stats import DatasetStats, Timer, _StatsManager
|
||||
from ray.data.context import OK_PREFIX, WARN_PREFIX, DataContext
|
||||
from ray.util.debug import log_once
|
||||
from ray.util.metrics import Gauge
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ray.data._internal.issue_detection.issue_detector_manager import (
|
||||
IssueDetectorManager,
|
||||
)
|
||||
from ray.data._internal.progress.base_progress import BaseExecutionProgressManager
|
||||
from ray.data.block import Schema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Interval for logging execution progress updates and operator metrics.
|
||||
DEBUG_LOG_INTERVAL_SECONDS = 5
|
||||
|
||||
# Maximum string/sequence length for DataContext logging. Set high to avoid truncation
|
||||
# while still protecting against pathological cases.
|
||||
DATA_CONTEXT_LOG_TRUNCATE_LENGTH = 10000
|
||||
|
||||
# Visible for testing.
|
||||
_num_shutdown = 0
|
||||
|
||||
|
||||
# Extra environment variables to log that don't start with RAY_DATA.
|
||||
_EXTRA_ENV_VARS_TO_LOG = (
|
||||
# We historically recommended users configure this value. If a Ray Data job uses
|
||||
# more object store memory than expected, it's worth checking how this environment
|
||||
# variable has been configured.
|
||||
"RAY_DEFAULT_OBJECT_STORE_MEMORY_PROPORTION",
|
||||
)
|
||||
|
||||
|
||||
def _log_ray_data_env_vars() -> None:
|
||||
env_vars = {
|
||||
k: v
|
||||
for k, v in os.environ.items()
|
||||
if k.startswith("RAY_DATA") or k in _EXTRA_ENV_VARS_TO_LOG
|
||||
}
|
||||
if env_vars:
|
||||
formatted = ", ".join(f"{k}={v}" for k, v in sorted(env_vars.items()))
|
||||
logger.debug(f"RAY_DATA environment variables: {formatted}")
|
||||
else:
|
||||
logger.debug("No RAY_DATA environment variables set.")
|
||||
|
||||
|
||||
class StreamingExecutor(Executor, threading.Thread):
|
||||
"""A streaming Dataset executor.
|
||||
|
||||
This implementation executes Dataset DAGs in a fully streamed way. It runs
|
||||
by setting up the operator topology, and then routing blocks through operators in
|
||||
a way that maximizes throughput under resource constraints.
|
||||
"""
|
||||
|
||||
UPDATE_METRICS_INTERVAL_S: float = 5.0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_context: DataContext,
|
||||
dataset_id: str = "unknown_dataset",
|
||||
):
|
||||
self._data_context = data_context
|
||||
self._ranker = create_ranker()
|
||||
self._start_time: Optional[float] = None
|
||||
self._initial_stats: Optional[DatasetStats] = None
|
||||
self._final_stats: Optional[DatasetStats] = None
|
||||
self._progress_manager: Optional["BaseExecutionProgressManager"] = None
|
||||
self._callbacks: List["ExecutionCallback"] = []
|
||||
|
||||
# The executor can be shutdown while still running.
|
||||
self._shutdown_lock = threading.RLock()
|
||||
self._execution_started = False
|
||||
self._shutdown = False
|
||||
|
||||
# Internal execution state shared across thread boundaries. We run the control
|
||||
# loop on a separate thread so that it doesn't become stalled between
|
||||
# generator `yield`s.
|
||||
self._topology: Optional[Topology] = None
|
||||
self._output_node: Optional[Tuple[PhysicalOperator, OpState]] = None
|
||||
self._backpressure_policies: List[BackpressurePolicy] = []
|
||||
self._op_schema: Dict[PhysicalOperator, Schema] = {}
|
||||
|
||||
self._dataset_id = dataset_id
|
||||
# Set by IssueDetectionExecutionCallback when issue detection is registered;
|
||||
# otherwise remains None. Access via the issue_detector_manager property.
|
||||
self._issue_detector_manager: Optional["IssueDetectorManager"] = None
|
||||
# Stores if an operator is completed,
|
||||
# used for marking when an op has just completed.
|
||||
self._has_op_completed: Optional[Dict[PhysicalOperator, bool]] = None
|
||||
self._max_errored_blocks = self._data_context.max_errored_blocks
|
||||
self._num_errored_blocks = 0
|
||||
|
||||
self._last_debug_log_time = 0
|
||||
|
||||
self._data_context.set_dataset_logger_id(
|
||||
register_dataset_logger(self._dataset_id)
|
||||
)
|
||||
|
||||
# This stores the last time we updated the metrics.
|
||||
# This allows us to update metrics on some interval,
|
||||
# by comparing it with the current timestamp.
|
||||
self._metrics_last_updated: float = 0.0
|
||||
|
||||
self._sched_loop_duration_s = Gauge(
|
||||
"data_sched_loop_duration_s",
|
||||
description="Duration of the scheduling loop in seconds",
|
||||
tag_keys=("dataset",),
|
||||
)
|
||||
|
||||
# Resolves pulled (block_ref, meta_ref) pairs into emitted RefBundles.
|
||||
# The threaded fetcher (default) fetches metadata on a background thread
|
||||
# so the scheduling loop never blocks on ``ray.get(meta_refs)``; the
|
||||
# inline fetcher reproduces the synchronous, master-identical path.
|
||||
self._metadata_fetcher = make_metadata_fetcher()
|
||||
|
||||
Executor.__init__(self, self._data_context.execution_options)
|
||||
thread_name = f"StreamingExecutor-{self._dataset_id}"
|
||||
threading.Thread.__init__(self, daemon=True, name=thread_name)
|
||||
|
||||
@property
|
||||
def issue_detector_manager(self) -> Optional["IssueDetectorManager"]:
|
||||
"""The issue detector manager, or None if issue detection isn't registered."""
|
||||
return self._issue_detector_manager
|
||||
|
||||
def execute(
|
||||
self,
|
||||
dag: PhysicalOperator,
|
||||
initial_stats: Optional[DatasetStats] = None,
|
||||
callbacks: Optional[List] = None,
|
||||
) -> OutputIterator:
|
||||
"""Executes the DAG using a streaming execution strategy.
|
||||
|
||||
We take an event-loop approach to scheduling. We block on the next scheduling
|
||||
event using `ray.wait`, updating operator state and dispatching new tasks.
|
||||
"""
|
||||
|
||||
if callbacks is not None:
|
||||
self._callbacks = callbacks
|
||||
else:
|
||||
self._callbacks = []
|
||||
|
||||
self._initial_stats = initial_stats
|
||||
self._start_time = time.perf_counter()
|
||||
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
_log_ray_data_env_vars()
|
||||
|
||||
if not isinstance(dag, InputDataBuffer):
|
||||
if self._data_context.print_on_execution_start:
|
||||
message = f"Starting execution of Dataset {self._dataset_id}."
|
||||
log_path = get_log_directory()
|
||||
if log_path is not None:
|
||||
message += f" Full logs are in {log_path}"
|
||||
logger.info(message)
|
||||
logger.info(
|
||||
f"Execution plan of Dataset {self._dataset_id}: {dag.dag_str}"
|
||||
)
|
||||
|
||||
# Log the full DataContext for traceability
|
||||
if logger.isEnabledFor(logging.DEBUG) and log_once(
|
||||
f"ray_data_log_context_{self._dataset_id}"
|
||||
):
|
||||
logger.debug(
|
||||
f"Data Context for dataset {self._dataset_id}:\n%s",
|
||||
sanitize_for_struct(
|
||||
self._data_context,
|
||||
truncate_length=DATA_CONTEXT_LOG_TRUNCATE_LENGTH,
|
||||
),
|
||||
)
|
||||
|
||||
# Setup the streaming DAG topology and start the runner thread.
|
||||
self._block_ref_counter = BlockRefCounter()
|
||||
self._topology = build_streaming_topology(
|
||||
dag, self._options, self._block_ref_counter
|
||||
)
|
||||
|
||||
self._resource_manager = ResourceManager(
|
||||
self._topology,
|
||||
self._options,
|
||||
lambda: self._cluster_autoscaler.get_total_resources(),
|
||||
self._data_context,
|
||||
self._block_ref_counter,
|
||||
)
|
||||
|
||||
# Constructed once per executor (not per scheduling iteration) so the
|
||||
# guard's idle-detection state accumulates across scheduling iterations.
|
||||
self._output_backpressure_guard = OutputBackpressureGuard(
|
||||
self._topology, self._resource_manager
|
||||
)
|
||||
|
||||
# Setup progress manager
|
||||
self._progress_manager = get_progress_manager(
|
||||
self._data_context,
|
||||
self._dataset_id,
|
||||
self._topology,
|
||||
self._options.verbose_progress,
|
||||
)
|
||||
self._progress_manager.start()
|
||||
|
||||
self._backpressure_policies = get_backpressure_policies(
|
||||
self._data_context, self._topology, self._resource_manager
|
||||
)
|
||||
self._cluster_autoscaler = create_cluster_autoscaler(
|
||||
self._topology,
|
||||
self._resource_manager,
|
||||
self._data_context,
|
||||
execution_id=self._dataset_id,
|
||||
)
|
||||
self._actor_autoscaler = create_actor_autoscaler(
|
||||
self._topology,
|
||||
self._resource_manager,
|
||||
config=self._data_context.autoscaling_config,
|
||||
)
|
||||
|
||||
self._has_op_completed = dict.fromkeys(self._topology, False)
|
||||
|
||||
self._output_node = dag, self._topology[dag]
|
||||
|
||||
op_to_id = {
|
||||
op: self._get_operator_id(op, i) for i, op in enumerate(self._topology)
|
||||
}
|
||||
_StatsManager.register_dataset_to_stats_actor(
|
||||
self._dataset_id,
|
||||
self._get_operator_tags(),
|
||||
TopologyMetadata.create_topology_metadata(dag, op_to_id),
|
||||
self._data_context,
|
||||
)
|
||||
for callback in self._callbacks:
|
||||
callback.before_execution_starts(self)
|
||||
|
||||
self.start()
|
||||
self._execution_started = True
|
||||
|
||||
return _ClosingIterator(self)
|
||||
|
||||
def __del__(self):
|
||||
# NOTE: Upon garbage-collection we're allowing running tasks
|
||||
# to be terminated asynchronously (ie avoid unnecessary
|
||||
# synchronization on their completion)
|
||||
self.shutdown(force=False)
|
||||
|
||||
def shutdown(self, force: bool, exception: Optional[Exception] = None):
|
||||
global _num_shutdown
|
||||
|
||||
with self._shutdown_lock:
|
||||
if not self._execution_started or self._shutdown:
|
||||
return
|
||||
|
||||
start = time.perf_counter()
|
||||
|
||||
status_detail = (
|
||||
f"failed with {exception}" if exception else "completed successfully"
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Shutting down executor for dataset {self._dataset_id} "
|
||||
f"({status_detail})"
|
||||
)
|
||||
|
||||
_num_shutdown += 1
|
||||
self._shutdown = True
|
||||
# Give the scheduling loop some time to finish processing.
|
||||
self.join(timeout=2.0)
|
||||
# Stop the metadata fetcher (after the loop thread that feeds it has
|
||||
# been joined). No-op for the inline fetcher.
|
||||
self._metadata_fetcher.stop()
|
||||
self._update_stats_metrics(
|
||||
state=DatasetState.FINISHED.name
|
||||
if exception is None
|
||||
else DatasetState.FAILED.name,
|
||||
force_update=True,
|
||||
)
|
||||
# Freeze the stats and save it.
|
||||
self._final_stats = self._generate_stats()
|
||||
stats_summary_string = self._final_stats.to_summary().to_string(
|
||||
include_parent=False
|
||||
)
|
||||
# Reset the scheduling loop duration gauge + resource manager budgets/usages.
|
||||
self._resource_manager.update_usages()
|
||||
self.update_metrics(0)
|
||||
if self._data_context.enable_auto_log_stats:
|
||||
logger.info(stats_summary_string)
|
||||
# Close the progress manager with a finishing message.
|
||||
if exception is None:
|
||||
desc = (
|
||||
f"{OK_PREFIX} Dataset {self._dataset_id} execution finished in "
|
||||
f"{self._final_stats.time_total_s:.2f} seconds"
|
||||
)
|
||||
else:
|
||||
desc = f"{WARN_PREFIX} Dataset {self._dataset_id} execution failed"
|
||||
|
||||
self._progress_manager.close_with_finishing_description(
|
||||
desc, exception is None
|
||||
)
|
||||
logger.info(desc)
|
||||
|
||||
timer = Timer()
|
||||
|
||||
for op in self._topology.keys():
|
||||
op.shutdown(timer, force=force)
|
||||
|
||||
self._clear_topology_queues_post_shutdown(force, exception)
|
||||
# Queues have been drained; any remaining Ray Core callbacks that fire
|
||||
# after this point should be no-ops.
|
||||
self._block_ref_counter.clear()
|
||||
|
||||
min_ = round(timer.min(), 3)
|
||||
max_ = round(timer.max(), 3)
|
||||
total = round(timer.get(), 3)
|
||||
logger.debug(
|
||||
f"Shut down operator hierarchy for dataset {self._dataset_id}"
|
||||
f" (min/max/total={min_}/{max_}/{total}s)"
|
||||
)
|
||||
|
||||
if exception is None:
|
||||
for callback in self._callbacks:
|
||||
callback.after_execution_succeeds(self)
|
||||
else:
|
||||
for callback in self._callbacks:
|
||||
callback.after_execution_fails(self, exception)
|
||||
|
||||
self._cluster_autoscaler.on_executor_shutdown()
|
||||
|
||||
dur = time.perf_counter() - start
|
||||
|
||||
logger.debug(
|
||||
f"Shut down executor for dataset {self._dataset_id} "
|
||||
f"(took {round(dur, 3)}s)"
|
||||
)
|
||||
|
||||
# Unregister should be called after all operators are shut down to
|
||||
# capture as many logs as possible.
|
||||
self._data_context.set_dataset_logger_id(
|
||||
unregister_dataset_logger(self._dataset_id)
|
||||
)
|
||||
|
||||
def _clear_topology_queues_post_shutdown(
|
||||
self, force: bool, exception: Optional[Exception] = None
|
||||
) -> None:
|
||||
"""Drain topology queues after operator shutdown (releases block refs)."""
|
||||
for op, state in self._topology.items():
|
||||
if isinstance(op, InternalQueueOperatorMixin):
|
||||
op.clear_internal_input_queue()
|
||||
op.clear_internal_output_queue()
|
||||
# Input queues alias upstream output queues; clears the DAG except the sink.
|
||||
for inqueue in state.input_queues:
|
||||
inqueue.clear()
|
||||
|
||||
output_op, _ = self._output_node
|
||||
# Clear sink output unless cooperative multi-split success (splits may still read).
|
||||
is_live_multi_split_sink = (
|
||||
output_op.num_output_splits() > 1 and not force and exception is None
|
||||
)
|
||||
if not is_live_multi_split_sink:
|
||||
self._topology[output_op].output_queue.clear()
|
||||
|
||||
def run(self):
|
||||
"""Run the control loop in a helper thread.
|
||||
|
||||
Results are returned via the output node's outqueue.
|
||||
"""
|
||||
exc: Optional[Exception] = None
|
||||
self._metadata_fetcher.start()
|
||||
try:
|
||||
# Run scheduling loop until complete.
|
||||
while True:
|
||||
# Use `perf_counter` rather than `process_time` to ensure we include
|
||||
# time spent on IO, like RPCs to Ray Core.
|
||||
t_start = time.perf_counter()
|
||||
continue_sched = self._scheduling_loop_step(self._topology)
|
||||
|
||||
sched_loop_duration = time.perf_counter() - t_start
|
||||
|
||||
self.update_metrics(sched_loop_duration)
|
||||
if self._initial_stats:
|
||||
self._initial_stats.streaming_exec_schedule_s.add(
|
||||
sched_loop_duration
|
||||
)
|
||||
|
||||
for callback in self._callbacks:
|
||||
callback.on_execution_step(self)
|
||||
if not continue_sched or self._shutdown:
|
||||
break
|
||||
except Exception as e:
|
||||
# Propagate it to the result iterator.
|
||||
exc = e
|
||||
finally:
|
||||
# Mark state of outputting operator as finished
|
||||
_, state = self._output_node
|
||||
state.mark_finished(exc)
|
||||
|
||||
def update_metrics(self, sched_loop_duration: int):
|
||||
self._sched_loop_duration_s.set(
|
||||
sched_loop_duration, tags={"dataset": self._dataset_id}
|
||||
)
|
||||
|
||||
def get_stats(self):
|
||||
"""Return the stats object for the streaming execution.
|
||||
|
||||
The stats object will be updated as streaming execution progresses.
|
||||
"""
|
||||
if self._final_stats:
|
||||
return self._final_stats
|
||||
else:
|
||||
return self._generate_stats()
|
||||
|
||||
def set_external_consumer_bytes(self, num_bytes: int) -> None:
|
||||
"""Set the bytes buffered by external consumers."""
|
||||
if self._resource_manager is not None:
|
||||
self._resource_manager.set_external_consumer_bytes(num_bytes)
|
||||
|
||||
def _generate_stats(self) -> DatasetStats:
|
||||
"""Create a new stats object reflecting execution status so far."""
|
||||
stats = self._initial_stats or DatasetStats(metadata={}, parent=None)
|
||||
for op in self._topology:
|
||||
if isinstance(op, InputDataBuffer):
|
||||
continue
|
||||
builder = stats.child_builder(op.name, override_start_time=self._start_time)
|
||||
stats = builder.build_multioperator(op.get_stats())
|
||||
stats.extra_metrics = op.metrics.as_dict(skip_internal_metrics=True)
|
||||
# Always assign a ``Timer`` so downstream consumers can call
|
||||
# ``.get()`` / ``.avg()`` / ``.max()`` / ``.percentile()``
|
||||
# unconditionally. When ``_initial_stats`` is absent we hand
|
||||
# back an empty Timer; zero-sample semantics yield 0 across all
|
||||
# four.
|
||||
stats.streaming_exec_schedule_s = (
|
||||
self._initial_stats.streaming_exec_schedule_s
|
||||
if self._initial_stats
|
||||
else Timer()
|
||||
)
|
||||
return stats
|
||||
|
||||
def _scheduling_loop_step(self, topology: Topology) -> bool:
|
||||
"""Run one step of the scheduling loop.
|
||||
|
||||
This runs a few general phases:
|
||||
1. Waiting for the next task completion using `ray.wait()`.
|
||||
2. Pulling completed refs into operator outqueues.
|
||||
3. Selecting and dispatching new inputs to operators.
|
||||
|
||||
Args:
|
||||
topology: The :class:`Topology` of operators being executed.
|
||||
|
||||
Returns:
|
||||
True if we should continue running the scheduling loop.
|
||||
"""
|
||||
self._resource_manager.update_usages()
|
||||
# Note: calling process_completed_tasks() is expensive since it incurs
|
||||
# ray.wait() overhead, so make sure to allow multiple dispatch per call for
|
||||
# greater parallelism.
|
||||
num_errored_blocks = process_completed_tasks(
|
||||
topology,
|
||||
self._backpressure_policies,
|
||||
self._max_errored_blocks,
|
||||
output_backpressure_guard=self._output_backpressure_guard,
|
||||
metadata_fetcher=self._metadata_fetcher,
|
||||
)
|
||||
if self._max_errored_blocks > 0:
|
||||
self._max_errored_blocks -= num_errored_blocks
|
||||
self._num_errored_blocks += num_errored_blocks
|
||||
|
||||
self._resource_manager.update_usages()
|
||||
# Dispatch as many operators as we can for completed tasks.
|
||||
self._report_current_usage()
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
op = select_operator_to_run(
|
||||
topology,
|
||||
self._resource_manager,
|
||||
self._backpressure_policies,
|
||||
# If consumer is idling (there's nothing for it to consume)
|
||||
# enforce liveness, ie that at least a single task gets scheduled
|
||||
ensure_liveness=self._consumer_idling(),
|
||||
ranker=self._ranker,
|
||||
)
|
||||
|
||||
if op is None:
|
||||
break
|
||||
|
||||
topology[op].dispatch_next_task()
|
||||
|
||||
self._resource_manager.update_usages()
|
||||
|
||||
i += 1
|
||||
if i % self._progress_manager.TOTAL_PROGRESS_REFRESH_EVERY_N_STEPS == 0:
|
||||
self._refresh_progress_manager(topology)
|
||||
|
||||
# Trigger autoscaling
|
||||
self._cluster_autoscaler.try_trigger_scaling()
|
||||
self._actor_autoscaler.try_trigger_scaling()
|
||||
|
||||
update_operator_states(topology)
|
||||
self._refresh_progress_manager(topology)
|
||||
|
||||
self._update_stats_metrics(state=DatasetState.RUNNING.name)
|
||||
if time.time() - self._last_debug_log_time >= DEBUG_LOG_INTERVAL_SECONDS:
|
||||
_log_op_metrics(topology)
|
||||
_debug_dump_topology(topology, self._resource_manager)
|
||||
self._last_debug_log_time = time.time()
|
||||
|
||||
for op, state in topology.items():
|
||||
# Export operator schema if it's updated
|
||||
if state._schema is not None and self._op_schema.get(op) != state._schema:
|
||||
self._op_schema[op] = state._schema
|
||||
self._export_operator_schema(op)
|
||||
|
||||
# Log metrics of newly completed operators.
|
||||
if not op.has_completed():
|
||||
op.refresh_state()
|
||||
elif not self._has_op_completed[op]:
|
||||
log_str = (
|
||||
f"Operator {op} completed. "
|
||||
f"Operator Metrics:\n{op._metrics.as_dict(skip_internal_metrics=True)}"
|
||||
)
|
||||
logger.debug(log_str)
|
||||
self._has_op_completed[op] = True
|
||||
self._validate_operator_queues_empty(op, state)
|
||||
|
||||
# Keep going until all operators run to completion.
|
||||
return not all(op.has_completed() for op in topology)
|
||||
|
||||
def _refresh_progress_manager(self, topology: Topology):
|
||||
# Update the progress manager to reflect scheduling decisions.
|
||||
if self._progress_manager:
|
||||
for op_state in topology.values():
|
||||
if not isinstance(op_state.op, InputDataBuffer):
|
||||
self._progress_manager.update_operator_progress(
|
||||
op_state, self._resource_manager
|
||||
)
|
||||
self._progress_manager.refresh()
|
||||
|
||||
def _consumer_idling(self) -> bool:
|
||||
"""Returns whether the user thread is blocked on topology execution."""
|
||||
_, state = self._output_node
|
||||
return len(state.output_queue) == 0
|
||||
|
||||
def _export_operator_schema(self, op: PhysicalOperator) -> None:
|
||||
schema = self._op_schema.get(op)
|
||||
operator_schema_exporter = get_operator_schema_exporter()
|
||||
if (
|
||||
operator_schema_exporter is not None
|
||||
and hasattr(schema, "names")
|
||||
and hasattr(schema, "types")
|
||||
):
|
||||
names = [str(n) for n in schema.names]
|
||||
types = [str(t) for t in schema.types]
|
||||
operator_schema = OperatorSchema(
|
||||
operator_uuid=op.id,
|
||||
schema_fields=dict(zip(names, types)),
|
||||
)
|
||||
operator_schema_exporter.export_operator_schema(operator_schema)
|
||||
|
||||
def _validate_operator_queues_empty(
|
||||
self, op: PhysicalOperator, state: OpState
|
||||
) -> None:
|
||||
"""Validate that all queues are empty when an operator completes.
|
||||
|
||||
Args:
|
||||
op: The completed operator to validate.
|
||||
state: The operator's execution state.
|
||||
"""
|
||||
error_msg = "Expected {} Queue for {} to be empty, but found {} bundles"
|
||||
|
||||
if isinstance(op, InternalQueueOperatorMixin):
|
||||
# 1) Check Internal Input Queue is empty
|
||||
assert op.internal_input_queue_num_blocks() == 0, error_msg.format(
|
||||
"Internal Input", op.name, op.internal_input_queue_num_blocks()
|
||||
)
|
||||
|
||||
# 2) Check Internal Output Queue is empty
|
||||
assert op.internal_output_queue_num_blocks() == 0, error_msg.format(
|
||||
"Internal Output",
|
||||
op.name,
|
||||
op.internal_output_queue_num_blocks(),
|
||||
)
|
||||
|
||||
# 3) Check that External Input Queue is empty
|
||||
for input_q in state.input_queues:
|
||||
assert len(input_q) == 0, error_msg.format(
|
||||
"External Input", op.name, len(input_q)
|
||||
)
|
||||
|
||||
def _report_current_usage(self) -> None:
|
||||
# running_usage is the amount of resources that have been requested but
|
||||
# not necessarily available
|
||||
# TODO(sofian) https://github.com/ray-project/ray/issues/47520
|
||||
# We need to split the reported resources into running, pending-scheduling,
|
||||
# pending-node-assignment.
|
||||
running_usage = self._resource_manager.get_global_running_usage()
|
||||
pending_usage = self._resource_manager.get_global_pending_usage()
|
||||
limits = self._resource_manager.get_global_limits()
|
||||
resources_status = (
|
||||
f"Active & requested resources: "
|
||||
f"{running_usage.cpu:.4g}/{limits.cpu:.4g} CPU, "
|
||||
)
|
||||
if running_usage.memory > 0:
|
||||
resources_status += (
|
||||
f"{running_usage.memory_str()}/{limits.memory_str()} memory, "
|
||||
)
|
||||
if running_usage.gpu > 0:
|
||||
resources_status += f"{running_usage.gpu:.4g}/{limits.gpu:.4g} GPU, "
|
||||
resources_status += (
|
||||
f"{running_usage.object_store_memory_str()}/"
|
||||
f"{limits.object_store_memory_str()} object store"
|
||||
)
|
||||
|
||||
# Only include pending section when there are pending resources.
|
||||
pending_parts = []
|
||||
if pending_usage.cpu:
|
||||
pending_parts.append(f"{pending_usage.cpu:.4g} CPU")
|
||||
if pending_usage.memory:
|
||||
pending_parts.append(f"{pending_usage.memory_str()} memory")
|
||||
if pending_usage.gpu:
|
||||
pending_parts.append(f"{pending_usage.gpu:.4g} GPU")
|
||||
if pending_parts:
|
||||
resources_status += f" (pending: {', '.join(pending_parts)})"
|
||||
|
||||
self._progress_manager.update_total_resource_status(resources_status)
|
||||
|
||||
def _get_operator_id(self, op: PhysicalOperator, topology_index: int) -> str:
|
||||
return f"{op.name}_{topology_index}"
|
||||
|
||||
def _get_operator_tags(self):
|
||||
"""Returns a list of operator tags."""
|
||||
return [
|
||||
f"{self._get_operator_id(op, i)}" for i, op in enumerate(self._topology)
|
||||
]
|
||||
|
||||
def _get_state_dict(self, state):
|
||||
last_op, last_state = list(self._topology.items())[-1]
|
||||
return {
|
||||
"state": state,
|
||||
"progress": last_state.num_completed_tasks,
|
||||
"total": last_op.num_outputs_total(),
|
||||
"total_rows": last_op.num_output_rows_total(),
|
||||
"end_time": time.time()
|
||||
if state in (DatasetState.FINISHED.name, DatasetState.FAILED.name)
|
||||
else None,
|
||||
"operators": {
|
||||
f"{self._get_operator_id(op, i)}": {
|
||||
"name": op.name,
|
||||
"progress": op_state.num_completed_tasks,
|
||||
"total": op.num_outputs_total(),
|
||||
"total_rows": op.num_output_rows_total(),
|
||||
"queued_blocks": op_state.total_enqueued_input_blocks(),
|
||||
"state": DatasetState.FINISHED.name
|
||||
if op.has_execution_finished()
|
||||
else state,
|
||||
}
|
||||
for i, (op, op_state) in enumerate(self._topology.items())
|
||||
},
|
||||
}
|
||||
|
||||
def _update_stats_metrics(self, state: str, force_update: bool = False):
|
||||
now = time.time()
|
||||
if (
|
||||
force_update
|
||||
or (now - self._metrics_last_updated) > self.UPDATE_METRICS_INTERVAL_S
|
||||
):
|
||||
_StatsManager.update_execution_metrics(
|
||||
self._dataset_id,
|
||||
[op.metrics for op in self._topology],
|
||||
self._get_operator_tags(),
|
||||
self._get_state_dict(state=state),
|
||||
)
|
||||
self._metrics_last_updated = now
|
||||
|
||||
|
||||
def _debug_dump_topology(topology: Topology, resource_manager: ResourceManager) -> None:
|
||||
"""Log current execution state for the topology for debugging.
|
||||
|
||||
Args:
|
||||
topology: The topology to debug.
|
||||
resource_manager: The resource manager for this topology.
|
||||
"""
|
||||
logger.debug("Execution Progress:")
|
||||
for i, (op, state) in enumerate(topology.items()):
|
||||
summary_str = format_op_state_summary(state, resource_manager, verbose=True)
|
||||
logger.debug(
|
||||
f"{i}: {op.name} - {summary_str}, "
|
||||
f"Blocks Outputted: {state.num_completed_tasks}/{op.num_outputs_total()}"
|
||||
)
|
||||
|
||||
|
||||
def _log_op_metrics(topology: Topology) -> None:
|
||||
"""Logs the metrics of each operator.
|
||||
|
||||
Args:
|
||||
topology: The topology to debug.
|
||||
"""
|
||||
log_str = "Operator Metrics:\n"
|
||||
for op in topology:
|
||||
metrics_dict = op.metrics.as_dict(skip_internal_metrics=True)
|
||||
log_str += f"{op.name}: {metrics_dict}\n"
|
||||
logger.debug(log_str)
|
||||
|
||||
|
||||
class _ClosingIterator(OutputIterator):
|
||||
"""Iterator automatically shutting down executor upon exhausting the
|
||||
iterable sequence.
|
||||
|
||||
NOTE: If this iterator isn't fully exhausted, executor still have to
|
||||
be closed manually by the caller!
|
||||
"""
|
||||
|
||||
def __init__(self, executor: StreamingExecutor):
|
||||
self._executor = executor
|
||||
|
||||
def get_next(self, output_split_idx: Optional[int] = None) -> RefBundle:
|
||||
try:
|
||||
op, state = self._executor._output_node
|
||||
bundle = state.get_output_blocking(output_split_idx)
|
||||
|
||||
# Update progress-bars
|
||||
if self._executor._progress_manager:
|
||||
self._executor._progress_manager.update_total_progress(
|
||||
bundle.num_rows() or 0, op.num_output_rows_total()
|
||||
)
|
||||
|
||||
return bundle
|
||||
|
||||
# Have to be BaseException to catch ``KeyboardInterrupt``
|
||||
#
|
||||
# NOTE: This also handles ``StopIteration``
|
||||
except BaseException as e:
|
||||
# Asynchronously shutdown the executor (ie avoid unnecessary
|
||||
# synchronization on tasks termination)
|
||||
self._executor.shutdown(
|
||||
force=False, exception=e if not isinstance(e, StopIteration) else None
|
||||
)
|
||||
raise
|
||||
|
||||
def __del__(self):
|
||||
# NOTE: Upon garbage-collection we're allowing running tasks
|
||||
# to be terminated asynchronously (ie avoid unnecessary
|
||||
# synchronization on their completion)
|
||||
self._executor.shutdown(force=False)
|
||||
Reference in New Issue
Block a user