Files
2026-07-13 13:17:40 +08:00

126 lines
4.3 KiB
Python

import pickle
from concurrent.futures import ThreadPoolExecutor
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Union
import ray
from ray.data.block import Block, BlockAccessor, CallableClass
if TYPE_CHECKING:
from ray._raylet import StreamingGeneratorStats
from ray.data._internal.execution.interfaces import RefBundle
from ray.data.block import BlockMetadataWithSchema
def merge_label_selector(
ray_remote_args: Dict[str, Any],
ctx_label_selector: Optional[Dict[str, str]],
) -> Dict[str, Any]:
"""Merge a DataContext-level label_selector into ``ray_remote_args``.
Operator-level keys (already in ``ray_remote_args["label_selector"]``) win on
conflict so existing node-pin selectors are preserved. Returns a new dict;
the input is not mutated. If ``ctx_label_selector`` is falsy, returns the
input unchanged.
"""
if not ctx_label_selector:
return ray_remote_args
op_selector = ray_remote_args.get("label_selector") or {}
merged = {**ctx_label_selector, **op_selector}
out = dict(ray_remote_args)
out["label_selector"] = merged
return out
def make_ref_bundles(simple_data: List[List[Any]]) -> List["RefBundle"]:
"""Create ref bundles from a list of block data.
One bundle is created for each input block.
"""
import pandas as pd
import pyarrow as pa
from ray.data._internal.execution.interfaces import BlockEntry, RefBundle
output = []
for block in simple_data:
block = pd.DataFrame({"id": block})
output.append(
RefBundle(
[
BlockEntry(
ray.put(block),
BlockAccessor.for_block(block).get_metadata(),
)
],
owns_blocks=True,
schema=pa.lib.Schema.from_pandas(block, preserve_index=False),
)
)
return output
memory_units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]
def memory_string(num_bytes: float) -> str:
"""Return a human-readable memory string for the given amount of bytes."""
k = 0
while num_bytes >= 1024 and k < len(memory_units) - 1:
num_bytes /= 1024
k += 1
return f"{num_bytes:.1f}{memory_units[k]}"
def locality_string(locality_hits: int, locality_misses) -> str:
"""Return a human-readable string for object locality stats."""
if not locality_misses:
return "[all objects local]"
return f"[{locality_hits}/{locality_hits + locality_misses} objects local]"
def yield_block_with_stats(
block: Block,
build_metadata: "Callable[[Optional[float]], BlockMetadataWithSchema]",
) -> Generator[Union[Block, bytes], "StreamingGeneratorStats", None]:
"""Yield a block then its pickled metadata, per the streaming-gen protocol.
Args:
block: The block to emit.
build_metadata: Given the block serialization time in seconds (or ``None``
if Ray didn't report it), returns the block's metadata to pickle.
Yields:
Union[Block, bytes]: The block, followed by its pickled
``BlockMetadataWithSchema``.
"""
gen_stats: "StreamingGeneratorStats" = yield block
block_ser_time_s = gen_stats.object_creation_dur_s if gen_stats else None
yield pickle.dumps(build_metadata(block_ser_time_s))
def make_callable_class_single_threaded(callable_cls: CallableClass) -> CallableClass:
"""Returns a thread-safe CallableClass with the same logic as the provided
`callable_cls`.
This function allows the usage of concurrent actors by safeguarding user logic
behind a separate thread.
This allows batch slicing and formatting to occur concurrently, to overlap with the
user provided UDF.
"""
class _SingleThreadedWrapper(callable_cls):
def __init__(self, *args, **kwargs):
self.thread_pool_executor = ThreadPoolExecutor(max_workers=1)
super().__init__(*args, **kwargs)
def __repr__(self):
return super().__repr__()
def __call__(self, *args, **kwargs):
# ThreadPoolExecutor will reuse the same thread for every submit call.
future = self.thread_pool_executor.submit(super().__call__, *args, **kwargs)
return future.result()
return _SingleThreadedWrapper