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,43 @@
import logging
from typing import TYPE_CHECKING
from ray.data._internal.usage.collector import (
EnvInfo,
Issue,
LogicalOp,
OpConfig,
PipelinePerf,
PlanNode,
UsageInfo,
WorkloadInfo,
record_usage_info,
)
from ray.data._internal.usage.execution_callback import UsageCallback
if TYPE_CHECKING:
from ray.data._internal.logical.interfaces.logical_plan import LogicalPlan
logger = logging.getLogger(__name__)
def create_usage_callback(logical_plan: "LogicalPlan") -> UsageCallback:
"""Create the usage callback for an execution.
Factory method to return a ``UsageCallback`` object.
"""
return UsageCallback(logical_plan)
__all__ = [
"EnvInfo",
"Issue",
"LogicalOp",
"OpConfig",
"PipelinePerf",
"PlanNode",
"UsageCallback",
"UsageInfo",
"WorkloadInfo",
"create_usage_callback",
"record_usage_info",
]
@@ -0,0 +1,425 @@
"""Ray Data usage-stats collector.
Accumulates per-execution usage data (environment, workload description,
performance) and flushes it to GCS via ``record_extra_usage_tag``.
The usage payload for each execution is assembled by :class:`UsageCallback`
this module owns the process-global buffer of recent executions and the builder functions
collecting usage data.
"""
import hashlib
import importlib.metadata
import json
import logging
import os
import threading
from collections import OrderedDict
from dataclasses import asdict, dataclass, field
from functools import cache
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
import ray
from ray._common.usage.usage_lib import (
TagKey,
record_extra_usage_tag,
usage_stats_enabled,
)
from ray._private.internal_api import get_memory_info_reply, get_state_from_address
from ray._private.worker import global_worker
from ray.core.generated.gcs_pb2 import GcsNodeInfo
from ray.data._internal.logical.interfaces import LogicalOperator
from ray.data._internal.logical.operators import MapBatches
from ray.data._internal.usage.util import anonymize_op_name
from ray.data.block import VALID_BATCH_FORMATS, _apply_batch_format
if TYPE_CHECKING:
from ray.data._internal.execution.interfaces.physical_operator import (
PhysicalOperator,
)
from ray.data._internal.issue_detection.issue_detector import IssueType
from ray.data._internal.logical.interfaces.logical_plan import LogicalPlan
logger = logging.getLogger(__name__)
# Bounded timeout for the GCS get_all_node_info query used to count dead nodes.
_NODE_INFO_RPC_TIMEOUT_S = 5.0
@dataclass(frozen=True)
class OpConfig:
"""Configuration for an operator"""
batch_format: Optional[str] = None
@dataclass(frozen=True)
class LogicalOp:
"""An operator in the plan"""
usage_id: str
name: str
config: Optional[OpConfig] = None
@dataclass(frozen=True)
class PlanNode:
"""A node in the anonymized plan tree (one logical operator)."""
usage_id: str
op: str
inputs: List["PlanNode"] = field(default_factory=list)
@dataclass(frozen=True)
class WorkloadInfo:
"""The anonymized plan tree, a human-readable rendering of it, and the
per-op flat list (with config)."""
plan: PlanNode
plan_str: str
ops: List[LogicalOp]
@dataclass(frozen=True)
class EnvInfo:
pyarrow: Optional[str]
@dataclass(frozen=True)
class PipelinePerf:
bytes_spilled: Optional[int]
oom_kills: Optional[int] = None
unexpected_worker_kills: Optional[int] = None
node_deaths: Optional[int] = None
@dataclass(frozen=True)
class Issue:
"""An issue detected during execution, tied to an anonymized operator."""
issue_type: str
operator: str
@dataclass
class UsageInfo:
"""Per-execution usage payload: the entry buffered and flushed to GCS."""
id: str
started_at: float
env: EnvInfo
workload: WorkloadInfo
performance: Optional[PipelinePerf] = None
detected_issues: List[Issue] = field(default_factory=list)
# A callable that records config information for a logical operator.
OpConfigFn = Callable[[LogicalOperator], Optional[OpConfig]]
# A callable that returns process-wide environment info. Overridable so
# subclasses can collect richer env details.
EnvFn = Callable[[], EnvInfo]
# A callable that returns the anonymized name for a logical operator.
# Allows subclasses to add custom anonymization logic.
OpNameFn = Callable[[LogicalOperator], str]
# A callable that samples a cluster metric (spilled bytes, dead node
# count, ...)
MetricReader = Callable[[], Optional[int]]
# Bounded buffer of recent executions. OrderedDict so eviction picks the
# oldest-inserted entry
_MAX_EXECUTIONS_TO_TRACK = 100
# Module state. Mutations are serialized through ``_lock``.
_executions: "OrderedDict[str, UsageInfo]" = OrderedDict()
_lock = threading.Lock()
def usage_collection_disabled() -> bool:
"""True when the user has opted out of usage stats (via
``RAY_USAGE_STATS_ENABLED=0``, ``ray disable-usage-stats``, or
``~/.ray/config.json``) or when ``RAY_DATA_USAGE_DISABLED=1`` is set.
"""
return not usage_stats_enabled() or os.environ.get("RAY_DATA_USAGE_DISABLED") == "1"
def cluster_spilled_bytes() -> Optional[int]:
"""Cluster-wide cumulative spilled bytes from Ray core's store_stats.
Returns None on any failure — usage collection must never break execution.
"""
if not ray.is_initialized():
return None
try:
reply = get_memory_info_reply(
get_state_from_address(ray.get_runtime_context().gcs_address),
timeout_seconds=10.0,
)
return int(reply.store_stats.spilled_bytes_total)
except Exception:
logger.debug("Failed to read cluster spilled bytes", exc_info=True)
return None
def cluster_dead_node_count() -> Optional[int]:
"""Number of dead nodes in the GCS node table.
Queries GCS with a bounded timeout and a server-side DEAD state filter.
Returns None on any failure.
"""
if not ray.is_initialized():
return None
try:
gcs_client = global_worker.gcs_client # pyrefly: ignore[missing-attribute]
dead_nodes = gcs_client.get_all_node_info(
timeout=_NODE_INFO_RPC_TIMEOUT_S,
state_filter=GcsNodeInfo.GcsNodeState.DEAD,
)
return len(dead_nodes)
except Exception:
logger.debug("Failed to read cluster dead node count", exc_info=True)
return None
def compute_delta(start: Optional[int], end: Optional[int]) -> Optional[int]:
"""Non-negative delta between two cumulative samples. Returns None if
either sample is missing"""
if start is None or end is None:
return None
return max(0, end - start)
def record_usage_info(info: UsageInfo) -> None:
"""Buffer ``info`` (evicting the oldest entry when full) and flush the whole
buffer to GCS via ``record_extra_usage_tag``.
The callback calls this both before execution starts (so attempted
executions are captured even if the execution fails) and after it finishes
(to overwrite the same entry with performance and issue data).
Short-circuits when the user has opted out of Ray usage stats (via
``RAY_USAGE_STATS_ENABLED=0``, ``ray disable-usage-stats``, or
``~/.ray/config.json``) or when ``RAY_DATA_USAGE_DISABLED=1`` is set.
"""
if usage_collection_disabled():
return
try:
with _lock:
if (
info.id not in _executions
and len(_executions) >= _MAX_EXECUTIONS_TO_TRACK
):
_executions.popitem(last=False)
_executions[info.id] = info
payload = _serialize_locked()
record_extra_usage_tag(TagKey.DATA_USAGE, payload)
except Exception:
logger.debug("Failed to record usage info", exc_info=True)
def build_usage_id_map(
logical_plan: "LogicalPlan",
op_name_fn: Optional[OpNameFn] = None,
) -> Dict[int, str]:
"""Build the ``id(logical_op) -> usage_id`` map for a plan.
The IDs are computed based on the hash of the (post-order index, anonymized name) tuple. These are
used to identify logical ops after anonymization (i.e. MapBatches-<id1>, MapBatches-<id2>, etc.).
Short-circuits to an empty map when the user has opted out of usage stats:
without a recorded payload there is nothing for the IDs to reference.
"""
if usage_collection_disabled():
return {}
if op_name_fn is None:
op_name_fn = anonymize_op_name
try:
ordered_logical_ops: List[Tuple[LogicalOperator, str]] = []
_build_plan(logical_plan.dag, ordered_logical_ops, op_name_fn)
return {id(op): usage_id for op, usage_id in ordered_logical_ops}
except Exception:
logger.debug("Failed to build usage id map", exc_info=True)
return {}
def physical_op_name_with_id(
operator: "PhysicalOperator",
usage_id_map: Optional[Dict[int, str]] = None,
op_name_fn: Optional[OpNameFn] = None,
) -> str:
"""Anonymized name for a physical op. Fused ops join their constituent logical
ops with '->' to signal operator fusion. We need physical op name as
issues from the issue detector reference physical ops."""
if op_name_fn is None:
op_name_fn = anonymize_op_name
logical_ops = operator._logical_operators
if not logical_ops:
return "Unknown"
return "->".join(
_logical_op_name_with_id(op, usage_id_map, op_name_fn) for op in logical_ops
)
def _logical_op_name_with_id(
logical_op: LogicalOperator,
usage_id_map: Optional[Dict[int, str]] = None,
op_name_fn: OpNameFn = anonymize_op_name,
) -> str:
"""Logical op is formatted as ``<anonymized_name>-<usage_id>``. The usage ID map is populated before execution starts in the usage callback."""
name = op_name_fn(logical_op)
if usage_id_map:
# Correlate with the IDs assigned to the logical ops in the workload plan.
usage_id = usage_id_map.get(id(logical_op))
if usage_id is not None:
return f"{name}-{usage_id}"
return name
def collect_issues(
detected_issues: Optional[List[Tuple["IssueType", str]]],
) -> List[Issue]:
"""Convert (issue_type, operator) pairs into ``Issue`` records, mapping
each ``IssueType`` enum to its string value."""
if not detected_issues:
return []
return [
Issue(issue_type=issue_type.value, operator=operator)
for issue_type, operator in detected_issues
]
def _serialize_locked() -> str:
"""Serialize current state to JSON. Caller must hold ``_lock``."""
return json.dumps({"executions": [asdict(e) for e in _executions.values()]})
def collect_env() -> EnvInfo:
"""Process-wide environment info."""
return EnvInfo(pyarrow=_safe_version("pyarrow"))
def _safe_version(pkg: str) -> Optional[str]:
try:
return importlib.metadata.version(pkg)
except importlib.metadata.PackageNotFoundError:
return None
def collect_workload(
logical_plan: "LogicalPlan",
op_config_fn: Optional[OpConfigFn] = None,
op_name_fn: Optional[OpNameFn] = None,
) -> WorkloadInfo:
"""Collect the anonymized plan tree, indented text rendering, and per-op
config list in a single DAG walk.
``op_config_fn`` builds the per-op config; it defaults to ``collect_op_config``
and is overridable if subclasses need to extract custom config info.
``op_name_fn`` anonymizes each operator's name; it defaults to
``anonymize_op_name`` and is overridable the same way.
"""
if op_config_fn is None:
op_config_fn = collect_op_config
if op_name_fn is None:
op_name_fn = anonymize_op_name
dag = logical_plan.dag
ordered_logical_ops: List[Tuple[LogicalOperator, str]] = []
plan = _build_plan(dag, ordered_logical_ops, op_name_fn)
return WorkloadInfo(
plan=plan,
plan_str=_format_plan_str(dag, op_name_fn),
ops=_build_ops(ordered_logical_ops, op_config_fn, op_name_fn),
)
def _build_plan(
op: LogicalOperator,
ordered_logical_ops: List[Tuple[LogicalOperator, str]],
op_name_fn: OpNameFn,
) -> PlanNode:
"""Build the plan tree and record logical ops in post-order.
Deduplicates shared operator instances (e.g. ``ds.zip(ds)``), so each
operator is assigned a single usage_id even when reachable via multiple
plan branches.
"""
@cache
def _build_cached(op: LogicalOperator) -> PlanNode:
child_plans = [_build_cached(child) for child in op.input_dependencies]
name = op_name_fn(op)
usage_id = make_usage_op_id(len(ordered_logical_ops), name)
ordered_logical_ops.append((op, usage_id))
return PlanNode(usage_id=usage_id, op=name, inputs=child_plans)
return _build_cached(op)
def _build_ops(
ordered_logical_ops: List[Tuple[LogicalOperator, str]],
op_config_fn: OpConfigFn,
op_name_fn: OpNameFn,
) -> List[LogicalOp]:
"""Build the flat logical-op list from the canonical post-order traversal."""
ops: List[LogicalOp] = []
for op, usage_id in ordered_logical_ops:
name = op_name_fn(op)
ops.append(
LogicalOp(
usage_id=usage_id,
name=name,
config=op_config_fn(op),
)
)
return ops
def collect_op_config(op: LogicalOperator) -> Optional[OpConfig]:
# MapBatches is the only operator with a user-facing batch_format.
if not isinstance(op, MapBatches):
return None
batch_format = op.batch_format
if batch_format == "default":
batch_format = _apply_batch_format(batch_format)
if batch_format in VALID_BATCH_FORMATS:
return OpConfig(batch_format=batch_format)
logger.debug(f"Unexpected batch format: {batch_format!r}")
return OpConfig(batch_format="unknown")
def make_usage_op_id(index: int, name: str) -> str:
return hashlib.sha256(f"{index}:{name}".encode()).hexdigest()[:4]
def _format_plan_str(
op: LogicalOperator,
op_name_fn: OpNameFn = anonymize_op_name,
depth: int = 0,
) -> str:
"""Render the anonymized DAG as an indented tree, using ``op_name_fn`` to
avoid leaking UDF / datasource details.
"""
name = op_name_fn(op)
if depth == 0:
line = f"{name}\n"
else:
line = f"{' ' * ((depth - 1) * 3)}+- {name}\n"
for child in op.input_dependencies:
line += _format_plan_str(child, op_name_fn, depth + 1)
return line
def reset_for_testing() -> None:
"""Reset module state. Tests only."""
with _lock:
_executions.clear()
def get_executions() -> "OrderedDict[str, UsageInfo]":
"""Get the current executions. Tests only."""
with _lock:
return _executions.copy()
@@ -0,0 +1,169 @@
"""Execution-side usage-stats hook.
The callback is injected with the logical plan during planning. It
records the workload entry (DAG, env, configs) before execution starts, then
adds performance data and issues detected after execution
finishes, flushing the payload to GCS at execution start and end so attempted executions
are captured even if they fail.
"""
import logging
import time
import uuid
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
from ray.data._internal.execution.execution_callback import ExecutionCallback
from ray.data._internal.usage import collector, util
from ray.data._internal.usage.collector import (
OpConfig,
PipelinePerf,
UsageInfo,
WorkloadInfo,
)
if TYPE_CHECKING:
from ray.data._internal.execution.streaming_executor import StreamingExecutor
from ray.data._internal.issue_detection.issue_detector import IssueType
from ray.data._internal.logical.interfaces.logical_operator import LogicalOperator
from ray.data._internal.logical.interfaces.logical_plan import LogicalPlan
logger = logging.getLogger(__name__)
class UsageCallback(ExecutionCallback):
"""Records per-execution usage data."""
def __init__(self, logical_plan: "LogicalPlan"):
self._logical_plan = logical_plan
# Globally unique per-execution id, used for deduplicating executions for usage collection
self._execution_id = uuid.uuid4().hex
# id(logical_op) -> usage_id, built while assembling the payload and used
# to label operators so they reference the workload payload.
self._usage_id_map: Dict[int, str] = {}
# The workload tree and usage-id map derive only from the (immutable)
# logical plan, so they're computed once in the start, cached for the execution end
self._workload: Optional[WorkloadInfo] = None
self._started_at: Optional[float] = None
self._spilled_at_start: Optional[int] = None
self._spilled_at_end: Optional[int] = None
self._dead_nodes_at_start: Optional[int] = None
self._dead_nodes_at_end: Optional[int] = None
self._executor: Optional["StreamingExecutor"] = None
self._finished = False
# --- ExecutionCallback interface ---
def before_execution_starts(self, executor: "StreamingExecutor") -> None:
if collector.usage_collection_disabled():
return
try:
self._executor = executor
self.on_collection_start(executor)
collector.record_usage_info(self.build_usage_info())
except Exception:
logger.debug("Usage collection failed at start", exc_info=True)
def after_execution_succeeds(self, executor: "StreamingExecutor") -> None:
self._finish(executor, None)
def after_execution_fails(
self, executor: "StreamingExecutor", error: Exception
) -> None:
self._finish(executor, error)
def _finish(
self, executor: "StreamingExecutor", error: Optional[Exception]
) -> None:
if collector.usage_collection_disabled():
return
try:
self._executor = executor
self._finished = True
self.on_collection_end(executor, error)
collector.record_usage_info(self.build_usage_info())
except Exception:
logger.debug("Usage collection failed at finish", exc_info=True)
# --- extension surface ---
def collect_op_config(self, op: "LogicalOperator") -> Optional[OpConfig]:
"""Build the config entry for one operator in the workload payload."""
return collector.collect_op_config(op)
def anonymize_op_name(self, op: "LogicalOperator") -> str:
"""Anonymized name for one operator in the workload payload.
The default policy lives in
``ray.data._internal.usage.util.anonymize_op_name`` because it's a
utility shared with the legacy ``record_operators_usage`` path.
"""
return util.anonymize_op_name(op)
def on_collection_start(self, executor: "StreamingExecutor") -> None:
"""Called once before execution starts. Records start timing and the
cluster metric baselines used to compute per-execution deltas."""
self._started_at = time.time()
self._spilled_at_start = collector.cluster_spilled_bytes()
self._dead_nodes_at_start = collector.cluster_dead_node_count()
def on_collection_end(
self, executor: "StreamingExecutor", error: Optional[Exception]
) -> None:
"""Called once after execution succeeds or fails. Records the ending
cluster metric samples. ``error`` is the failure (or ``None`` on
success); subclasses may override to capture it."""
self._spilled_at_end = collector.cluster_spilled_bytes()
self._dead_nodes_at_end = collector.cluster_dead_node_count()
def build_usage_info(self) -> UsageInfo:
"""Assemble the usage collection payload for this execution."""
if self._workload is None:
self._usage_id_map = collector.build_usage_id_map(
self._logical_plan, self.anonymize_op_name
)
self._workload = collector.collect_workload(
self._logical_plan, self.collect_op_config, self.anonymize_op_name
)
performance = None
if self._finished:
performance = PipelinePerf(
bytes_spilled=collector.compute_delta(
self._spilled_at_start, self._spilled_at_end
),
node_deaths=collector.compute_delta(
self._dead_nodes_at_start, self._dead_nodes_at_end
),
)
# Both are populated before this runs: on_collection_start sets
# _started_at, and before_execution_starts/_finish set _executor.
assert self._started_at is not None
assert self._executor is not None
return UsageInfo(
id=self._execution_id,
started_at=self._started_at,
env=collector.collect_env(),
workload=self._workload,
performance=performance,
detected_issues=collector.collect_issues(
self._collect_detected_issues(self._executor)
),
)
def _collect_detected_issues(
self, executor: "StreamingExecutor"
) -> List[Tuple["IssueType", str]]:
# The manager is None when issue detection isn't registered.
manager = executor.issue_detector_manager
if manager is None:
return []
issues = (
(
issue_type,
collector.physical_op_name_with_id(
operator, self._usage_id_map, self.anonymize_op_name
),
)
for issue_type, operator in manager.get_detected_issues()
)
# Sort by the issue type's string value, then by the operator name.
return sorted(issues, key=lambda issue: (issue[0].value, issue[1]))
+81
View File
@@ -0,0 +1,81 @@
"""Utility functions for operator naming and logical-op usage recording for Ray Data telemetry.
"""
import json
import threading
from typing import Dict
from ray._common.usage.usage_lib import TagKey, record_extra_usage_tag
from ray.data._internal.logical.interfaces import LogicalOperator
from ray.data._internal.logical.operators import Read, ReadFiles, Write
# The dictionary for the operator name and count.
_recorded_operators = dict()
_recorded_operators_lock = threading.Lock()
def _is_builtin_cls(cls: type) -> bool:
"""Return True if ``cls`` is defined under the ``ray`` package.
Used to gate which operator / datasource / datasink class names are safe
to surface in telemetry. Anything outside ``ray.*`` is treated as
user-defined and anonymized.
"""
return (cls.__module__ or "").startswith("ray.")
def record_operators_usage(op: LogicalOperator):
"""Record logical operator usage with Ray telemetry."""
ops_dict = dict()
_collect_operators_to_dict(op, ops_dict)
ops_json_str = ""
with _recorded_operators_lock:
for op_name, count in ops_dict.items():
_recorded_operators.setdefault(op_name, 0)
_recorded_operators[op_name] += count
ops_json_str = json.dumps(_recorded_operators)
record_extra_usage_tag(TagKey.DATA_LOGICAL_OPS, ops_json_str)
def anonymize_op_name(op: LogicalOperator) -> str:
"""Return an op name suitable for usage collection.
Read/Write surface their datasource/datasink suffix (``ReadParquet``,
``WriteIceberg``) when the underlying class ships under ``ray.data.*``;
user-defined datasources/datasinks collapse to ``ReadCustom`` /
``WriteCustom``. ``ReadFiles`` (the V2 file-read op) surfaces its
format via ``datasource_name`` (e.g. ``ReadFilesParquetV2``) when the
scanner class is built-in; user-defined scanners collapse to
``ReadFilesCustom``. All other built-in operators emit their class
name (``Sort``, ``MapBatches``, ``Limit``, …); user-defined
``LogicalOperator`` subclasses collapse to ``Unknown``.
"""
if isinstance(op, Read):
if _is_builtin_cls(type(op.datasource)):
return f"Read{op.datasource.get_name()}"
return "ReadCustom"
if isinstance(op, Write):
sink = op.datasink_or_legacy_datasource
if _is_builtin_cls(type(sink)):
return f"Write{sink.get_name()}"
return "WriteCustom"
if isinstance(op, ReadFiles):
# Gate on the scanner class — the string ``datasource_name`` field
# could be set to anything by a user-defined V2 datasource, so it's
# not safe to surface on its own.
if _is_builtin_cls(type(op.scanner)):
return f"ReadFiles{op.datasource_name}"
return "ReadFilesCustom"
cls = type(op)
return cls.__name__ if _is_builtin_cls(cls) else "Unknown"
def _collect_operators_to_dict(op: LogicalOperator, ops_dict: Dict[str, int]):
"""Collect the logical operator name and count into `ops_dict`."""
for child in op.input_dependencies:
_collect_operators_to_dict(child, ops_dict)
op_name = anonymize_op_name(op)
ops_dict.setdefault(op_name, 0)
ops_dict[op_name] += 1