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,49 @@
from typing import TYPE_CHECKING, Iterator, Optional, Tuple
from ray.data._internal.execution.interfaces.ref_bundle import RefBundle
from ray.data._internal.stats import DatasetStats
from ray.data.context import DataContext
from ray.data.iterator import DataIterator
if TYPE_CHECKING:
from ray.data._internal.execution.streaming_executor import StreamingExecutor
from ray.data.dataset import Dataset, Schema
class DataIteratorImpl(DataIterator):
def __init__(
self,
base_dataset: "Dataset",
):
self._base_dataset = base_dataset
def __repr__(self) -> str:
return f"DataIterator({self._base_dataset})"
def _to_ref_bundle_iterator(
self,
) -> Tuple[
Iterator[RefBundle],
Optional[DatasetStats],
bool,
Optional["StreamingExecutor"],
]:
(
ref_bundles_iterator,
stats,
executor,
) = self._base_dataset._execute_to_iterator()
return ref_bundles_iterator, stats, False, executor
def stats(self) -> str:
return self._base_dataset.stats()
def schema(self) -> Optional["Schema"]:
return self._base_dataset.schema()
def get_context(self) -> DataContext:
return self._base_dataset.context
def _get_dataset_tag(self):
return self._base_dataset.get_dataset_id()
@@ -0,0 +1,596 @@
import logging
import threading
import time
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Set, Tuple
import ray
from ray.data._internal.execution.interfaces import (
NodeIdStr,
RefBundle,
)
from ray.data._internal.stats import DatasetStats
from ray.data.context import DataContext
from ray.data.iterator import DataIterator
from ray.types import ObjectRef
from ray.util.debug import log_once
if TYPE_CHECKING:
from ray.data.dataset import Dataset, Schema
logger = logging.getLogger(__name__)
BLOCKED_CLIENT_WARN_TIMEOUT = 30
class StreamSplitDataIterator(DataIterator):
"""Implements a collection of iterators over a shared data stream."""
YIELD_LOG_INTERVAL_S = 10
@staticmethod
def create(
base_dataset: "Dataset",
n: int,
locality_hints: Optional[List[NodeIdStr]],
) -> List["StreamSplitDataIterator"]:
"""Create a split iterator from the given base Dataset and options.
See also: `Dataset.streaming_split`.
"""
# To avoid deadlock, the concurrency on this actor must be set to at least `n`.
# We add 1 to the concurrency to allow for a shutdown_executor thread to run.
coord_actor = SplitCoordinator.options(
max_concurrency=n + 1,
label_selector={
ray._raylet.RAY_NODE_ID_KEY: ray.get_runtime_context().get_node_id()
},
).remote(base_dataset, n, locality_hints)
return [StreamSplitDataIterator(coord_actor, i, n) for i in range(n)]
def __init__(
self,
coord_actor: ray.actor.ActorHandle,
output_split_idx: int,
world_size: int,
):
self._coord_actor = coord_actor
self._output_split_idx = output_split_idx
self._world_size = world_size
self._iter_stats = DatasetStats(metadata={}, parent=None)
# Epoch this split is currently consuming. Set by ``gen_blocks``
# once ``start_epoch`` returns (on the async-prefetch filling
# worker thread); read and cleared by ``_on_iteration_end`` (on
# the consumer thread). The two threads access this without a
# lock — ordering is enforced by the protocol: ``start_epoch``
# must return before any item is yielded, which must happen
# before the consumer can exit and trigger ``_on_iteration_end``.
# Plain attribute access (no lock) so this iterator stays
# picklable, since users pass split iterators to ``@ray.remote``
# tasks.
self._active_epoch: Optional[int] = None
logger.debug(
f"StreamSplitDataIterator created: split={output_split_idx}, {world_size=}"
)
def _to_ref_bundle_iterator(
self,
) -> Tuple[Iterator[RefBundle], Optional[DatasetStats], bool, None]:
def gen_blocks() -> Iterator[RefBundle]:
logger.debug(f"Split {self._output_split_idx}: requesting new epoch.")
cur_epoch = ray.get(
self._coord_actor.start_epoch.remote(self._output_split_idx)
)
logger.debug(f"Split {self._output_split_idx}: epoch {cur_epoch} started")
self._active_epoch = cur_epoch
# Initial get with 0 prefetched bytes.
future: ObjectRef[Optional[RefBundle]] = self._coord_actor.get.remote(
cur_epoch, self._output_split_idx, 0
)
last_log_time = 0.0
while True:
block_ref_and_md: Optional[RefBundle] = ray.get(future)
if not block_ref_and_md:
break
else:
# Calculate prefetched bytes: BatchIterator's current
# prefetch plus the block we just received (which will
# be added to BatchIterator's sliding window when we
# yield it).
prefetched_bytes = (
self._iter_stats.iter_prefetched_bytes
+ block_ref_and_md.size_bytes()
)
future = self._coord_actor.get.remote(
cur_epoch,
self._output_split_idx,
prefetched_bytes,
)
yield RefBundle(
blocks=block_ref_and_md.blocks,
owns_blocks=False,
schema=block_ref_and_md.schema,
)
# Log dispatch progress.
now = time.time()
if now - last_log_time >= self.YIELD_LOG_INTERVAL_S:
last_log_time = now
logger.debug(
f"Split {self._output_split_idx} epoch "
f"{cur_epoch}: consumer resumed after yield"
)
logger.debug(f"Split {self._output_split_idx}: epoch {cur_epoch} exhausted")
# Return None for executor since StreamSplitDataIterator has its own
# mechanism for reporting prefetched bytes via SplitCoordinator.
return gen_blocks(), self._iter_stats, False, None
def _on_iteration_end(self, executor) -> None:
"""Fire ``notify_split_finished`` from the consumer's thread.
Runs synchronously on the consumer's thread from
``_iter_batches``'s ``finally`` (normal exhaustion, early
``break``, or exception), giving a deterministic shutdown path.
Putting this on ``gen_blocks``'s ``finally`` instead would not
work for early ``break``: ``gen_blocks`` runs inside
``make_async_gen``'s filling worker thread, which exits via the
``interrupted_event`` path without explicitly closing the
generator — its cleanup is then GC-bound and arbitrarily delayed
under CI load.
``executor`` is always ``None`` here — the executor lives on the
remote ``SplitCoordinator`` actor and is shut down there once
every split has finished.
"""
epoch = self._active_epoch
if epoch is None:
# Iteration never started, or already cleaned up.
return
self._active_epoch = None
self._coord_actor.notify_split_finished.remote(epoch, self._output_split_idx)
def stats(self) -> str:
"""Implements DataIterator."""
# Merge the locally recorded iter stats and the remotely recorded
# stream execution stats.
logger.debug(f"Split {self._output_split_idx}: fetching stats remote")
stats = ray.get(self._coord_actor.stats.remote())
summary = stats.to_summary()
summary.iter_stats = self._iter_stats.to_summary().iter_stats
summary.iter_stats.streaming_split_coord_time.add(
stats.streaming_split_coordinator_s.get()
)
return summary.to_string()
def schema(self) -> Optional["Schema"]:
"""Implements DataIterator."""
return ray.get(self._coord_actor.get_dataset_schema.remote())
def get_context(self) -> DataContext:
return ray.get(self._coord_actor.get_dataset_context.remote())
def world_size(self) -> int:
"""Returns the number of splits total."""
return self._world_size
def _get_dataset_tag(self):
return ray.get(self._coord_actor.get_dataset_tag.remote(self._output_split_idx))
@ray.remote(num_cpus=0)
class SplitCoordinator:
"""Coordinator actor for routing blocks to output splits.
This actor runs a streaming executor locally on its main thread. Clients can
retrieve results via actor calls running on other threads.
"""
DISPATCH_LOG_INTERVAL_S = 10
def __init__(
self,
dataset: "Dataset",
n: int,
locality_hints: Optional[List[NodeIdStr]],
):
# Set current DataContext.
# This needs to be a deep copy so that updates to the base dataset's
# context does not affect this process's global DataContext.
self._data_context = dataset.context.copy()
ray.data.DataContext._set_current(self._data_context)
self._base_dataset = dataset
self._n = n
self._locality_hints = locality_hints
self._lock = threading.RLock()
self._dataset_state_lock = threading.Lock()
self._schema = None
self._current_executor = None
# Guarded by self._lock.
self._next_bundle: Dict[int, RefBundle] = {}
# Number of splits that have not yet arrived at the next-epoch
# barrier. Decremented as each split calls ``start_epoch``;
# reset to ``n`` when an epoch starts.
self._num_unarrived_splits_at_barrier = n
# Splits that have finished consuming the current epoch (via
# natural exhaustion or early ``break``). Reset on each new
# epoch. Once every split has finished, the executor is shut
# down so it stops producing blocks into the object store.
# Guarded by self._lock.
self._finished_splits: Set[int] = set()
self._cur_epoch = -1
# Track prefetched bytes reported by each client (from BatchIterator).
# Guarded by self._lock.
self._client_prefetched_bytes: Dict[int, int] = {}
# Add a new stats field to track coordinator overhead
self._coordinator_overhead_s = 0.0
# Per-split row dispatch counters (reset each epoch in _barrier).
self._num_rows_dispatched: Dict[int, int] = dict.fromkeys(range(n), 0)
self._last_dispatch_log_time: float = 0.0
self._output_iterator = None
# Store the error raised from the `gen_epoch` call.
self._gen_epoch_error: Optional[Exception] = None
logger.debug(f"SplitCoordinator created: {n=}, {locality_hints=}")
def get_dataset_context(self) -> DataContext:
return self._data_context
def get_dataset_tag(self, output_split_idx: int) -> str:
return f"{self._base_dataset.get_dataset_id()}_split_{output_split_idx}"
def get_dataset_schema(self):
with self._dataset_state_lock:
if self._schema is not None:
return self._schema
if self._current_executor is not None and self._current_executor.is_alive():
raise RuntimeError(
"Cannot call schema() during active dataset execution. "
"Call schema() before or after iterating over the dataset, or call "
"schema() directly on the source Dataset object."
)
self._schema = self._base_dataset.schema()
return self._schema
def stats(self) -> DatasetStats:
"""Returns stats from the base dataset."""
if self._current_executor:
stats = self._current_executor.get_stats()
else:
stats = self._base_dataset._raw_stats()
# Set the tracked overhead time
stats.streaming_split_coordinator_s.add(self._coordinator_overhead_s)
return stats
def start_epoch(self, split_idx: int) -> str:
"""Called to start an epoch.
Args:
split_idx: The split index of the caller; used as the barrier key
so all split consumers synchronize before a new epoch starts.
Returns:
UUID for the epoch, which must be used when accessing results via get().
"""
# Wait for all clients to arrive at the barrier before starting a new epoch.
epoch_id = self._barrier(split_idx)
return epoch_id
def _try_start_new_epoch(self, starting_epoch: int):
with self._lock:
# This check gates that we start epoch only once
if self._cur_epoch == starting_epoch:
# Reset state
self._reset_state()
# Ratchet epoch
self._cur_epoch += 1
try:
# Force executor shutdown if present
if self._current_executor is not None:
self._current_executor.shutdown(force=True)
ds = self._base_dataset
# Re-execute dataset
self._current_executor = ds._create_executor()
self._output_iterator = ds._build_bundle_iterator(
self._current_executor
)
# Register the streaming split external consumers with the executor's resource manager.
self._current_executor.set_external_consumer_bytes(0)
logger.debug(
f"Starting epoch {self._cur_epoch} (all {self._n} clients "
"synced)."
)
except Exception as e:
logger.warning(
f"Error creating executor for epoch {self._cur_epoch}: {e}"
)
self._gen_epoch_error = e
if self._gen_epoch_error is not None:
# If there was an error when advancing to the next epoch,
# re-raise it for all threads.
raise self._gen_epoch_error
def _reset_state(self):
self._num_unarrived_splits_at_barrier = self._n
self._finished_splits.clear()
self._next_bundle.clear()
self._gen_epoch_error = None
# Reset per-split dispatch counters for the new epoch.
self._num_rows_dispatched = dict.fromkeys(range(self._n), 0)
def get(
self,
epoch_id: int,
output_split_idx: int,
client_prefetched_bytes: int = 0,
) -> Optional[RefBundle]:
"""Blocking get operation.
This is intended to be called concurrently from multiple clients.
Args:
epoch_id: The epoch ID from start_epoch().
output_split_idx: The output split index for this client.
client_prefetched_bytes: The prefetched bytes reported by the
client's BatchIterator, used for resource accounting.
Returns:
The next RefBundle for this split, or None if the epoch is done.
"""
start_time = time.perf_counter()
if epoch_id != self._cur_epoch:
raise ValueError(
"Invalid iterator: the dataset has moved on to another epoch."
)
returned_normally = False
try:
# Ensure there is at least one bundle.
with self._lock:
if output_split_idx in self._next_bundle:
next_bundle = self._next_bundle[output_split_idx]
else:
next_bundle = None
# Fetch next bundle if needed.
while next_bundle is None or not next_bundle.blocks:
# This is a BLOCKING call, so do it outside the lock.
next_bundle = self._output_iterator.get_next(output_split_idx)
schema = next_bundle.schema
last_entry = next_bundle.blocks[-1]
next_bundle = RefBundle(
blocks=next_bundle.blocks[:-1],
schema=next_bundle.schema,
owns_blocks=next_bundle.owns_blocks,
output_split_idx=next_bundle.output_split_idx,
)
# Accumulate any remaining blocks in next_bundle map as needed.
with self._lock:
self._next_bundle[output_split_idx] = next_bundle
if not next_bundle.blocks:
del self._next_bundle[output_split_idx]
# Update client prefetched bytes and report to resource manager.
self._client_prefetched_bytes[
output_split_idx
] = client_prefetched_bytes
self._report_prefetched_bytes_to_executor()
# Track per-split row dispatch count.
self._num_rows_dispatched[output_split_idx] += (
last_entry.metadata.num_rows if last_entry.metadata.num_rows else 0
)
num_rows_dispatched = self._num_rows_dispatched[output_split_idx]
self._maybe_log_dispatch(
split_idx=output_split_idx,
epoch_id=epoch_id,
num_rows_dispatched=num_rows_dispatched,
client_prefetched_bytes=client_prefetched_bytes,
)
returned_normally = True
return RefBundle(
[last_entry],
schema=schema,
owns_blocks=next_bundle.owns_blocks,
)
except StopIteration:
with self._lock:
num_rows = self._num_rows_dispatched[output_split_idx]
logger.debug(
f"Split {output_split_idx} epoch {epoch_id} finished, dispatched "
f"{num_rows} rows."
)
return None
except Exception as e:
with self._lock:
num_rows = self._num_rows_dispatched[output_split_idx]
logger.warning(
f"Split {output_split_idx} epoch {epoch_id} get() failed after "
f"{num_rows} rows: {e}"
)
raise
finally:
# Clear prefetched bytes on any exit (StopIteration or other
# exceptions) to avoid stale backpressure data.
if not returned_normally:
with self._lock:
self._client_prefetched_bytes[output_split_idx] = 0
self._report_prefetched_bytes_to_executor()
# Track overhead time in the instance variable
self._coordinator_overhead_s += time.perf_counter() - start_time
def _get_total_prefetched_bytes(self) -> int:
"""Get the total prefetched bytes including coordinator buffer and clients.
Must be called while holding self._lock.
"""
# Bytes buffered in the coordinator.
total = sum(bundle.size_bytes() for bundle in self._next_bundle.values())
# Bytes prefetched by each client's BatchIterator.
total += sum(self._client_prefetched_bytes.values())
return total
def _report_prefetched_bytes_to_executor(self) -> None:
"""Report total prefetched bytes to the executor's resource manager.
Must be called while holding self._lock.
"""
if self._current_executor is not None:
total_bytes = self._get_total_prefetched_bytes()
self._current_executor.set_external_consumer_bytes(total_bytes)
def get_client_prefetched_bytes(self) -> Dict[int, int]:
"""Get prefetched bytes for each client (for testing)."""
with self._lock:
return dict(self._client_prefetched_bytes)
def _is_executor_shutdown(self) -> bool:
"""Whether the current executor (if any) has been shut down.
For testing only.
"""
with self._lock:
executor = self._current_executor
return executor is not None and executor._shutdown
def _maybe_log_dispatch(
self,
*,
split_idx: int,
epoch_id: int,
num_rows_dispatched: int,
client_prefetched_bytes: int,
) -> None:
"""Log dispatch progress, throttled to once per interval.
The intention for throttling is to avoid overwhelming the logs with too many
messages.
"""
now = time.time()
with self._lock:
if now - self._last_dispatch_log_time < self.DISPATCH_LOG_INTERVAL_S:
return
self._last_dispatch_log_time = now
logger.debug(
f"Split {split_idx} epoch {epoch_id} returned block: "
f"{num_rows_dispatched=}, {client_prefetched_bytes=}"
)
def shutdown_executor(self):
"""Shuts down the internal data executor."""
logger.debug(f"Shutting down executor (epoch={self._cur_epoch}).")
with self._lock:
# Call shutdown on the executor
if self._current_executor is not None:
self._current_executor.shutdown(force=False)
def notify_split_finished(self, epoch_id: int, split_idx: int) -> None:
"""Called by a split iterator when it stops consuming for ``epoch_id``.
Triggered from ``_on_iteration_end`` on the consumer's thread on
normal exhaustion, early ``break``, or an exception. Clears this
split's prefetch state so resource accounting is accurate for the
remaining consumers, and shuts down the executor once every split
has finished the current epoch.
"""
executor_to_shutdown = None
with self._lock:
# ``notify_split_finished`` is fire-and-forget on the consumer
# side; in the time between the consumer firing it and the
# actor processing it, the other splits may have completed
# the current epoch and called ``start_epoch`` again, causing
# ``_try_start_new_epoch`` to advance ``_cur_epoch`` past
# ``epoch_id``. The state for the old epoch has already been
# cleared by ``_reset_state``, so there's nothing left to do.
if epoch_id != self._cur_epoch:
return
self._finished_splits.add(split_idx)
self._client_prefetched_bytes[split_idx] = 0
# Drop any blocks buffered for this split — the consumer won't
# read them and they'd otherwise pin memory until the next epoch.
self._next_bundle.pop(split_idx, None)
self._report_prefetched_bytes_to_executor()
if (
len(self._finished_splits) == self._n
and self._current_executor is not None
):
executor_to_shutdown = self._current_executor
# Shut down outside the lock: ``StreamingExecutor.shutdown`` joins
# the scheduling thread (up to 2s) and we don't want to block other
# coordinator calls in the meantime. ``shutdown`` is idempotent.
if executor_to_shutdown is not None:
logger.debug(
f"All splits finished epoch {epoch_id}; shutting down " "executor."
)
executor_to_shutdown.shutdown(force=True)
def _barrier(self, split_idx: int) -> int:
"""Arrive and block until the start of the given epoch."""
# Decrement and await all clients to arrive here.
with self._lock:
logger.debug(
f"Split {split_idx} arriving at barrier for epoch "
f"{self._cur_epoch + 1}."
)
starting_epoch = self._cur_epoch
self._num_unarrived_splits_at_barrier -= 1
start_time = time.time()
while (
self._cur_epoch == starting_epoch
and self._num_unarrived_splits_at_barrier != 0
):
if time.time() - start_time > BLOCKED_CLIENT_WARN_TIMEOUT:
if log_once(f"stream_split_blocked_{split_idx}_{starting_epoch}"):
logger.warning(
f"StreamSplitDataIterator(epoch={starting_epoch}, "
f"split={split_idx}) blocked waiting on other clients "
f"for more than {BLOCKED_CLIENT_WARN_TIMEOUT}s. All "
"clients must read from the DataIterator splits at "
"the same time. This warning will not be printed again "
"for this epoch."
)
time.sleep(0.1)
# Advance to the next epoch
self._try_start_new_epoch(starting_epoch)
if self._output_iterator is None:
raise ValueError(
"Invalid iterator: output iterator is not initialized. "
"This may indicate too many concurrent consumers."
)
if self._cur_epoch != starting_epoch + 1:
raise ValueError(
f"Invalid iterator: too many concurrent consumers detected. "
f"Expected epoch {starting_epoch + 1}, got {self._cur_epoch}."
)
return self._cur_epoch