234 lines
7.8 KiB
Python
234 lines
7.8 KiB
Python
import dataclasses
|
|
import logging
|
|
import time
|
|
import typing
|
|
from collections import defaultdict
|
|
from typing import Callable, Dict, List, Optional
|
|
|
|
from ray._common.utils import env_integer
|
|
from ray.data._internal.execution.operators.input_data_buffer import InputDataBuffer
|
|
from ray.data._internal.execution.operators.sub_progress import SubProgressBarMixin
|
|
from ray.data._internal.execution.streaming_executor_state import (
|
|
format_op_state_summary,
|
|
)
|
|
from ray.data._internal.progress.base_progress import (
|
|
BaseExecutionProgressManager,
|
|
BaseProgressBar,
|
|
NoopSubProgressBar,
|
|
)
|
|
from ray.data._internal.progress.utils import truncate_operator_name
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from ray.data._internal.execution.resource_manager import ResourceManager
|
|
from ray.data._internal.execution.streaming_executor_state import OpState, Topology
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class _LoggingMetrics:
|
|
name: str
|
|
desc: Optional[str]
|
|
completed: int
|
|
total: Optional[int]
|
|
|
|
|
|
class LoggingSubProgressBar(BaseProgressBar):
|
|
"""Thin wrapper to provide identical interface to the ProgressBar.
|
|
|
|
Internally passes relevant logging metrics to `LoggingExecutionProgressManager`.
|
|
Sub-progress is actually handled by Ray through operators, while operator-level
|
|
and total progress is handled by the `StreamingExecutor`. To ensure log-order,
|
|
this class helps to pass metric data to the progress manager so progress metrics
|
|
are logged centrally.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
total: Optional[int] = None,
|
|
max_name_length: int = 100,
|
|
):
|
|
"""Initialize sub-progress bar
|
|
|
|
Args:
|
|
name: name of sub-progress bar
|
|
total: total number of output rows. None for unknown.
|
|
max_name_length: maximum operator name length (unused).
|
|
"""
|
|
del max_name_length # unused
|
|
self._total = total
|
|
self._completed = 0
|
|
self._name = name
|
|
|
|
def set_description(self, name: str) -> None:
|
|
pass # unused
|
|
|
|
def get_description(self) -> str:
|
|
return "" # unused
|
|
|
|
def update(self, increment: int = 0, total: Optional[int] = None):
|
|
if total is not None:
|
|
self._total = total
|
|
self._completed += increment
|
|
|
|
def get_logging_metrics(self) -> _LoggingMetrics:
|
|
return _LoggingMetrics(
|
|
name=f" - {self._name}",
|
|
desc=None,
|
|
completed=self._completed,
|
|
total=self._total,
|
|
)
|
|
|
|
|
|
class LoggingExecutionProgressManager(BaseExecutionProgressManager):
|
|
"""Execution progress display for non-tty situations, preventing
|
|
spamming of progress reporting."""
|
|
|
|
# Refer to following issues for more context about this feature:
|
|
# https://github.com/ray-project/ray/issues/60083
|
|
# https://github.com/ray-project/ray/issues/57734
|
|
|
|
# This progress manager needs to refresh (log) based on elapsed time
|
|
# not scheduling steps. This elapsed time handling is done within
|
|
# this class.
|
|
TOTAL_PROGRESS_REFRESH_EVERY_N_STEPS = 1
|
|
|
|
# Time interval (seconds) in which progress is logged to console again.
|
|
LOG_REPORT_INTERVAL_SEC = env_integer("RAY_DATA_NON_TTY_PROGRESS_LOG_INTERVAL", 10)
|
|
|
|
def __init__(
|
|
self,
|
|
dataset_id: str,
|
|
topology: "Topology",
|
|
show_op_progress: bool,
|
|
verbose_progress: bool,
|
|
*,
|
|
_get_time: Callable[[], float] = time.time,
|
|
):
|
|
self._dataset_id = dataset_id
|
|
self._topology = topology
|
|
self._get_time = _get_time
|
|
self._last_log_time = self._get_time() - self.LOG_REPORT_INTERVAL_SEC
|
|
|
|
self._global_progress_metric = _LoggingMetrics(
|
|
name="Total Progress", desc=None, completed=0, total=None
|
|
)
|
|
self._op_progress_metrics: Dict["OpState", _LoggingMetrics] = {}
|
|
self._sub_progress_metrics: Dict[
|
|
"OpState", List[LoggingSubProgressBar]
|
|
] = defaultdict(list)
|
|
|
|
for state in self._topology.values():
|
|
op = state.op
|
|
if isinstance(op, InputDataBuffer):
|
|
continue
|
|
total = op.num_output_rows_total() or 1
|
|
|
|
contains_sub_progress_bars = isinstance(op, SubProgressBarMixin)
|
|
sub_progress_bar_enabled = show_op_progress and (
|
|
contains_sub_progress_bars or verbose_progress
|
|
)
|
|
|
|
if sub_progress_bar_enabled:
|
|
self._op_progress_metrics[state] = _LoggingMetrics(
|
|
name=truncate_operator_name(op.name, self.MAX_NAME_LENGTH),
|
|
desc=None,
|
|
completed=0,
|
|
total=total,
|
|
)
|
|
|
|
if not contains_sub_progress_bars:
|
|
continue
|
|
|
|
sub_pg_names = op.get_sub_progress_bar_names()
|
|
if sub_pg_names is None:
|
|
continue
|
|
for name in sub_pg_names:
|
|
if sub_progress_bar_enabled:
|
|
pg = LoggingSubProgressBar(
|
|
name=name, total=total, max_name_length=self.MAX_NAME_LENGTH
|
|
)
|
|
self._sub_progress_metrics[state].append(pg)
|
|
else:
|
|
pg = NoopSubProgressBar(
|
|
name=name, max_name_length=self.MAX_NAME_LENGTH
|
|
)
|
|
op.set_sub_progress_bar(name, pg)
|
|
|
|
# Management
|
|
def start(self):
|
|
# logging progress manager doesn't need separate start
|
|
pass
|
|
|
|
def refresh(self):
|
|
current_time = self._get_time()
|
|
if current_time - self._last_log_time < self.LOG_REPORT_INTERVAL_SEC:
|
|
return
|
|
self._last_log_time = current_time
|
|
|
|
# starting delimiter
|
|
firstline = f"======= Running Dataset: {self._dataset_id} ======="
|
|
lastline = "=" * len(firstline)
|
|
logger.info(firstline)
|
|
|
|
# log global progress
|
|
_log_global_progress(self._global_progress_metric)
|
|
|
|
# log operator-level progress
|
|
if len(self._op_progress_metrics.keys()) > 0:
|
|
logger.info("")
|
|
|
|
for opstate in self._topology.values():
|
|
metrics = self._op_progress_metrics.get(opstate)
|
|
if metrics is None:
|
|
continue
|
|
_log_op_or_sub_progress(metrics)
|
|
for pg in self._sub_progress_metrics[opstate]:
|
|
_log_op_or_sub_progress(pg.get_logging_metrics())
|
|
|
|
# finish logging
|
|
logger.info(lastline)
|
|
|
|
def close_with_finishing_description(self, desc: str, success: bool):
|
|
# We log in StreamingExecutor. No need for duplicate logging.
|
|
pass
|
|
|
|
# Total Progress
|
|
def update_total_progress(self, new_rows: int, total_rows: Optional[int]):
|
|
if total_rows is not None:
|
|
self._global_progress_metric.total = total_rows
|
|
self._global_progress_metric.completed += new_rows
|
|
|
|
def update_total_resource_status(self, resource_status: str):
|
|
self._global_progress_metric.desc = resource_status
|
|
|
|
# Operator Progress
|
|
def update_operator_progress(
|
|
self, opstate: "OpState", resource_manager: "ResourceManager"
|
|
):
|
|
op_metrics = self._op_progress_metrics.get(opstate)
|
|
if op_metrics is not None:
|
|
op_metrics.completed = opstate.op.metrics.row_outputs_taken
|
|
total = opstate.op.num_output_rows_total()
|
|
if total is not None:
|
|
op_metrics.total = total
|
|
op_metrics.desc = format_op_state_summary(opstate, resource_manager)
|
|
|
|
|
|
def _format_progress(m: _LoggingMetrics) -> str:
|
|
return f"{m.name}: {m.completed}/{m.total or '?'}"
|
|
|
|
|
|
def _log_global_progress(m: _LoggingMetrics):
|
|
logger.info(_format_progress(m))
|
|
if m.desc is not None:
|
|
logger.info(m.desc)
|
|
|
|
|
|
def _log_op_or_sub_progress(m: _LoggingMetrics):
|
|
logger.info(_format_progress(m))
|
|
if m.desc is not None:
|
|
logger.info(f" {m.desc}")
|