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

449 lines
16 KiB
Python

import warnings
from functools import partial
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Type, TypeVar
if TYPE_CHECKING:
import pyarrow.fs
from ray.data._internal.execution.execution_callback import ExecutionCallback
from ray.data._internal.execution.interfaces import PhysicalOperator
from ray.data._internal.execution.operators.aggregate_num_rows import (
AggregateNumRows,
)
from ray.data._internal.execution.operators.hash_shuffle_v2 import (
_SHUFFLE_MAP_RUNTIME_ENV,
_make_hash_partition_fn,
)
from ray.data._internal.execution.operators.input_data_buffer import (
InputDataBuffer,
)
from ray.data._internal.execution.operators.join import (
JoinOperator,
_make_join_reduce_fn,
)
from ray.data._internal.execution.operators.limit_operator import LimitOperator
from ray.data._internal.execution.operators.mix_operator import MixOperator
from ray.data._internal.execution.operators.output_splitter import OutputSplitter
from ray.data._internal.execution.operators.shuffle_operators.shuffle_map_operator import (
ShuffleMapOp,
)
from ray.data._internal.execution.operators.shuffle_operators.shuffle_reduce_operator import (
ShuffleReduceOp,
)
from ray.data._internal.execution.operators.union_operator import UnionOperator
from ray.data._internal.execution.operators.zip_operator import ZipOperator
from ray.data._internal.logical.interfaces import (
LogicalOperator,
LogicalPlan,
PhysicalPlan,
)
from ray.data._internal.logical.operators import (
AbstractAllToAll,
AbstractFrom,
AbstractUDFMap,
Count,
Download,
Filter,
InputData,
Join,
JoinType,
Limit,
ListFiles,
Mix,
Project,
Read,
ReadFiles,
StreamingRepartition,
StreamingSplit,
Union,
Write,
Zip,
)
from ray.data._internal.planner.checkpoint import (
plan_read_files_op_with_checkpoint_filter,
plan_read_op_with_checkpoint_filter,
plan_write_op_with_checkpoint_writer,
)
from ray.data._internal.planner.plan_all_to_all_op import plan_all_to_all_op
from ray.data._internal.planner.plan_download_op import plan_download_op
from ray.data._internal.planner.plan_list_files_op import plan_list_files_op
from ray.data._internal.planner.plan_read_files_op import plan_read_files_op
from ray.data._internal.planner.plan_read_op import plan_read_op
from ray.data._internal.planner.plan_udf_map_op import (
plan_filter_op,
plan_project_op,
plan_streaming_repartition_op,
plan_udf_map_op,
)
from ray.data._internal.planner.plan_write_op import plan_write_op
from ray.data._internal.usage import create_usage_callback
from ray.data.checkpoint.load_checkpoint_callback import LoadCheckpointCallback
from ray.data.context import DataContext
from ray.data.datasource.file_datasink import _FileDatasink
LogicalOperatorType = TypeVar("LogicalOperatorType", bound=LogicalOperator)
PlanLogicalOpFn = Callable[
[LogicalOperatorType, List[PhysicalOperator], DataContext], PhysicalOperator
]
def plan_input_data_op(
logical_op: InputData,
physical_children: List[PhysicalOperator],
data_context: DataContext,
) -> PhysicalOperator:
"""Get the corresponding DAG of physical operators for InputData."""
assert len(physical_children) == 0
return InputDataBuffer(
data_context,
input_data=logical_op.input_data,
)
def plan_from_op(
op: AbstractFrom,
physical_children: List[PhysicalOperator],
data_context: DataContext,
) -> PhysicalOperator:
assert len(physical_children) == 0
return InputDataBuffer(data_context, op.input_data)
def plan_zip_op(_, physical_children, data_context):
assert len(physical_children) >= 2
return ZipOperator(data_context, *physical_children)
def plan_mix_op(logical_op, physical_children, data_context):
assert len(physical_children) >= 1
return MixOperator(
data_context,
*physical_children,
weights=logical_op.weights,
stopping_condition=logical_op.stopping_condition,
)
def plan_union_op(_, physical_children, data_context):
assert len(physical_children) >= 2
return UnionOperator(data_context, *physical_children)
def plan_limit_op(logical_op, physical_children, data_context):
assert len(physical_children) == 1
return LimitOperator(logical_op.limit, physical_children[0], data_context)
def plan_count_op(logical_op, physical_children, data_context):
assert len(physical_children) == 1
return AggregateNumRows(
[physical_children[0]], data_context, column_name=Count.COLUMN_NAME
)
def _plan_join_shuffle_v2(
logical_op: Join,
physical_children: List[PhysicalOperator],
data_context: DataContext,
) -> PhysicalOperator:
left_keys = list(logical_op.left_key_columns)
right_keys = list(logical_op.right_key_columns)
num_partitions = logical_op.num_partitions
join_type = JoinType(logical_op.join_type)
left_map = ShuffleMapOp(
physical_children[0],
data_context,
num_partitions=num_partitions,
partition_fn=_make_hash_partition_fn(left_keys, num_partitions),
map_runtime_env=_SHUFFLE_MAP_RUNTIME_ENV,
name=f"JoinShuffleMapLeft(keys={tuple(left_keys)}, parts={num_partitions})",
)
right_map = ShuffleMapOp(
physical_children[1],
data_context,
num_partitions=num_partitions,
partition_fn=_make_hash_partition_fn(right_keys, num_partitions),
map_runtime_env=_SHUFFLE_MAP_RUNTIME_ENV,
name=f"JoinShuffleMapRight(keys={tuple(right_keys)}, parts={num_partitions})",
)
reduce_fn = _make_join_reduce_fn(
join_type=join_type,
left_key_col_names=tuple(left_keys),
right_key_col_names=tuple(right_keys),
left_columns_suffix=logical_op.left_columns_suffix,
right_columns_suffix=logical_op.right_columns_suffix,
left_schema=logical_op.input_dependencies[0].infer_schema(),
right_schema=logical_op.input_dependencies[1].infer_schema(),
)
return ShuffleReduceOp(
[left_map, right_map],
data_context,
num_partitions=num_partitions,
reduce_fn=reduce_fn,
disallow_block_splitting=False,
reduce_ray_remote_args=logical_op.aggregator_ray_remote_args,
name=f"JoinShuffleReduce(num_partitions={num_partitions})",
)
def plan_join_op(
logical_op: Join,
physical_children: List[PhysicalOperator],
data_context: DataContext,
) -> PhysicalOperator:
assert len(physical_children) == 2
if data_context.use_hash_shuffle_v2:
return _plan_join_shuffle_v2(logical_op, physical_children, data_context)
return JoinOperator(
data_context=data_context,
left_input_op=physical_children[0],
right_input_op=physical_children[1],
join_type=logical_op.join_type,
left_key_columns=logical_op.left_key_columns,
right_key_columns=logical_op.right_key_columns,
left_columns_suffix=logical_op.left_columns_suffix,
right_columns_suffix=logical_op.right_columns_suffix,
num_partitions=logical_op.num_outputs,
partition_size_hint=logical_op.partition_size_hint,
aggregator_ray_remote_args_override=logical_op.aggregator_ray_remote_args,
)
def plan_streaming_split_op(
logical_op: StreamingSplit,
physical_children: List[PhysicalOperator],
data_context: DataContext,
):
assert len(physical_children) == 1
return OutputSplitter(
physical_children[0],
n=logical_op.num_splits,
equal=logical_op.equal,
data_context=data_context,
locality_hints=logical_op.locality_hints,
)
class Planner:
"""The planner to convert optimized logical to physical operators.
Note that planner is only doing operators conversion. Physical optimization work is
done by physical optimizer.
"""
_DEFAULT_PLAN_FNS = {
Read: plan_read_op,
ReadFiles: plan_read_files_op,
ListFiles: plan_list_files_op,
InputData: plan_input_data_op,
Write: plan_write_op,
AbstractFrom: plan_from_op,
Filter: plan_filter_op,
AbstractUDFMap: plan_udf_map_op,
AbstractAllToAll: plan_all_to_all_op,
Mix: plan_mix_op,
Union: plan_union_op,
Zip: plan_zip_op,
Limit: plan_limit_op,
Count: plan_count_op,
Project: plan_project_op,
StreamingRepartition: plan_streaming_repartition_op,
Join: plan_join_op,
StreamingSplit: plan_streaming_split_op,
Download: plan_download_op,
}
# Operators that support checkpoint filtering. Subclasses can override.
_CHECKPOINT_FILTER_OPS = (Read, ReadFiles)
def __init__(self):
self._supports_checkpointing = False
self._plan_fns_for_checkpointing = {}
def plan(
self, logical_plan: LogicalPlan
) -> Tuple[PhysicalPlan, List["ExecutionCallback"]]:
"""Convert logical to physical operators recursively in post-order."""
checkpoint_config = logical_plan.context.checkpoint_config
callbacks = [cls() for cls in logical_plan.context.execution_callback_classes]
callbacks.append(create_usage_callback(logical_plan))
if checkpoint_config is not None and self._check_supports_checkpointing(
logical_plan
):
self._supports_checkpointing = True
data_file_dir, data_file_fs = self._get_data_file_info(logical_plan)
checkpoint_callback = self._create_checkpoint_callback(
checkpoint_config,
)
callbacks.append(checkpoint_callback)
# Dynamically set the plan functions for checkpointing because they
# need to a reference to the checkpoint ref.
self._plan_fns_for_checkpointing = self._get_plan_fns_for_checkpointing(
data_file_dir, data_file_fs
)
elif checkpoint_config is not None:
assert not self._check_supports_checkpointing(logical_plan)
warnings.warn(
"You've enabled checkpointing, but the logical plan doesn't support "
"checkpointing. Checkpointing will be disabled."
)
physical_dag, op_map = self._plan_recursively(
logical_plan.dag, logical_plan.context
)
physical_plan = PhysicalPlan(physical_dag, op_map, logical_plan.context)
return physical_plan, callbacks
def get_plan_fn(self, logical_op: LogicalOperator) -> PlanLogicalOpFn:
if self._supports_checkpointing:
assert self._plan_fns_for_checkpointing
plan_fn = find_plan_fn(logical_op, self._plan_fns_for_checkpointing)
if plan_fn is not None:
return plan_fn
plan_fn = find_plan_fn(logical_op, self._DEFAULT_PLAN_FNS)
if plan_fn is not None:
return plan_fn
raise ValueError(
f"Found unknown logical operator during planning: {logical_op}"
)
def _plan_recursively(
self, logical_op: LogicalOperator, data_context: DataContext
) -> Tuple[PhysicalOperator, Dict[LogicalOperator, PhysicalOperator]]:
"""Plan a logical operator and its input dependencies recursively.
Args:
logical_op: The logical operator to plan.
data_context: The data context.
Returns:
A tuple of the physical operator corresponding to the logical operator, and
a mapping from physical to logical operators.
"""
op_map: Dict[PhysicalOperator, LogicalOperator] = {}
# Plan the input dependencies first.
physical_children = []
for child in logical_op.input_dependencies:
physical_child, child_op_map = self._plan_recursively(child, data_context)
physical_children.append(physical_child)
op_map.update(child_op_map)
plan_fn = self.get_plan_fn(logical_op)
# We will call `set_logical_operators()` in the following for-loop,
# no need to do it here.
physical_op = plan_fn(logical_op, physical_children, data_context)
# Traverse up the DAG, and set the mapping from physical to logical operators.
# At this point, all physical operators without logical operators set
# must have been created by the current logical operator.
queue = [physical_op]
while queue:
curr_physical_op = queue.pop()
if curr_physical_op._logical_operators:
continue
curr_physical_op.set_logical_operators(logical_op)
# Add this operator to the op_map so optimizer can find it
op_map[curr_physical_op] = logical_op
queue.extend(curr_physical_op.input_dependencies)
# Also add the final operator (in case the loop didn't catch it)
op_map[physical_op] = logical_op
return physical_op, op_map
def _create_checkpoint_callback(
self,
checkpoint_config,
) -> LoadCheckpointCallback:
"""Factory method to create the LoadCheckpointCallback.
Subclasses can override this to use a different callback implementation.
"""
return LoadCheckpointCallback(
checkpoint_config,
)
@staticmethod
def _get_data_file_info(logical_plan: LogicalPlan):
"""Extract the data file directory and filesystem from the Write op's datasink.
Returns (path, filesystem) for file-based datasinks, or (None, None)
for non-file datasinks.
"""
last_op = logical_plan.dag
if isinstance(last_op, Write):
datasink = last_op.datasink_or_legacy_datasource
if isinstance(datasink, _FileDatasink):
return datasink.unresolved_path, datasink.filesystem
return None, None
def _get_plan_fns_for_checkpointing(
self,
data_file_dir: Optional[str] = None,
data_file_filesystem: Optional["pyarrow.fs.FileSystem"] = None,
) -> Dict[Type[LogicalOperator], PlanLogicalOpFn]:
plan_fns = {
Read: partial(
plan_read_op_with_checkpoint_filter, data_file_dir, data_file_filesystem
),
ReadFiles: partial(
plan_read_files_op_with_checkpoint_filter,
data_file_dir,
data_file_filesystem,
),
Write: plan_write_op_with_checkpoint_writer,
}
return plan_fns
def _check_supports_checkpointing(self, logical_plan: LogicalPlan) -> bool:
"""Check if the logical plan supports checkpointing.
Subclasses can override _CHECKPOINT_FILTER_OPS to support more operators.
"""
if not isinstance(logical_plan.dag, (Write, StreamingSplit)):
return False
def _all_paths_contain_checkpoint_filter(op: LogicalOperator) -> bool:
if isinstance(op, self._CHECKPOINT_FILTER_OPS):
return True
return all(
_all_paths_contain_checkpoint_filter(input_dep)
for input_dep in op.input_dependencies
)
return _all_paths_contain_checkpoint_filter(logical_plan.dag)
def find_plan_fn(
logical_op: LogicalOperator, plan_fns: Dict[Type[LogicalOperator], PlanLogicalOpFn]
) -> Optional[PlanLogicalOpFn]:
"""Find the plan function for a logical operator.
This function goes through the plan functions in order and returns the first one
that is an instance of the logical operator type.
Args:
logical_op: The logical operator to find the plan function for.
plan_fns: The dictionary of plan functions.
Returns:
The plan function for the logical operator, or None if no plan function is
found.
"""
# TODO: This implementation doesn't account for type hierarchies conflicts or
# multiple inheritance.
for op_type, plan_fn in plan_fns.items():
if isinstance(logical_op, op_type):
return plan_fn
return None