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,32 @@
from .logical_operator import (
LogicalOperator,
LogicalOperatorPreservesSchema,
LogicalOperatorSupportsPredicatePassThrough,
LogicalOperatorSupportsPredicatePushdown,
LogicalOperatorSupportsProjectionPushdown,
LogicalOperatorUnifiesInputSchemas,
PredicatePassThroughBehavior,
)
from .logical_plan import LogicalPlan
from .operator import Operator
from .optimizer import Optimizer, Rule
from .physical_plan import PhysicalPlan
from .plan import Plan
from .source_operator import SourceOperator
__all__ = [
"LogicalOperator",
"LogicalPlan",
"Operator",
"Optimizer",
"PhysicalPlan",
"Plan",
"Rule",
"SourceOperator",
"LogicalOperatorPreservesSchema",
"LogicalOperatorSupportsProjectionPushdown",
"LogicalOperatorSupportsPredicatePushdown",
"LogicalOperatorSupportsPredicatePassThrough",
"LogicalOperatorUnifiesInputSchemas",
"PredicatePassThroughBehavior",
]
@@ -0,0 +1,247 @@
import copy
from abc import ABC, abstractmethod
from dataclasses import dataclass, field, fields, replace
from enum import Enum
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional
from .operator import Operator
from ray.data.block import BlockMetadata
from ray.data.expressions import Expr
if TYPE_CHECKING:
from ray.data.block import Schema
@dataclass(frozen=True, repr=False, eq=False)
class LogicalOperator(Operator, ABC):
"""Abstract class for logical operators.
A logical operator describes transformation, and later is converted into
physical operator.
"""
_name: Optional[str] = field(init=False, default=None, repr=False)
_input_dependencies: List["LogicalOperator"] = field(
init=False, default_factory=list, repr=False
)
@property
def name(self) -> str:
return self._name or self.__class__.__name__
@property
def num_outputs(self) -> Optional[int]:
"""Expected number of output blocks, if known."""
return None
def estimated_num_outputs(self) -> Optional[int]:
"""Returns the estimated number of blocks that
would be outputted by this logical operator.
This method does not execute the plan, so it does not take into consideration
block splitting. This method only considers high-level block constraints like
`Dataset.repartition(num_blocks=X)`. A more accurate estimation can be given by
`PhysicalOperator.num_outputs_total()` during execution.
"""
if self.num_outputs is not None:
return self.num_outputs
elif len(self.input_dependencies) == 1:
return self.input_dependencies[0].estimated_num_outputs()
return None
# Override the following 3 methods to correct type hints.
@property
def input_dependencies(self) -> List["LogicalOperator"]:
value = self._input_dependencies
for x in value:
assert isinstance(x, LogicalOperator), x
return value
@input_dependencies.setter
def input_dependencies(self, value: List["LogicalOperator"]) -> None:
for x in value:
assert isinstance(x, LogicalOperator), x
object.__setattr__(self, "_input_dependencies", value)
def post_order_iter(self) -> Iterator["LogicalOperator"]:
return super().post_order_iter() # type: ignore
def _apply_transform(
self, transform: Callable[["LogicalOperator"], "LogicalOperator"]
) -> "LogicalOperator":
input_dependencies = self.input_dependencies
transformed_inputs = [
input_op._apply_transform(transform) for input_op in input_dependencies
]
if all(
transformed_input is input_op
for transformed_input, input_op in zip(
transformed_inputs, input_dependencies
)
):
target = self
else:
target = self._with_new_input_dependencies(transformed_inputs)
return transform(target)
def _with_new_input_dependencies(
self, input_dependencies: List["LogicalOperator"]
) -> "LogicalOperator":
if "input_dependencies" in {field.name for field in fields(self)}:
return replace(self, input_dependencies=input_dependencies)
target = copy.copy(self)
object.__setattr__(target, "_input_dependencies", input_dependencies)
return target
def _get_args(self) -> Dict[str, Any]:
"""This Dict must be serializable"""
args: Dict[str, Any] = {}
for dataclass_field in fields(self):
key = dataclass_field.name
value = getattr(self, key)
# Keep underscore-prefixed keys to preserve legacy export schema.
args[key if key.startswith("_") else f"_{key}"] = value
args["_name"] = self.name
# Preserve legacy export shape even though output deps are no longer tracked.
args["_output_dependencies"] = []
# Do not include input dependencies, since we only want to export this
# operator-specific args. Adding input_dependencies isn't wrong, but can
# lead to slow recursive calls with `sanitize_for_struct`, since logical
# operators are dataclasses.
args["_input_dependencies"] = []
return args
def infer_schema(self) -> Optional["Schema"]:
"""Returns the inferred schema of the output blocks."""
return None
def infer_metadata(self) -> "BlockMetadata":
"""A ``BlockMetadata`` that represents the aggregate metadata of the outputs.
This method is used by methods like :meth:`~ray.data.Dataset.schema` to
efficiently return metadata.
"""
return BlockMetadata(None, None, None, None)
def is_lineage_serializable(self) -> bool:
"""Returns whether the lineage of this operator can be serialized.
An operator is lineage serializable if you can serialize it on one machine and
deserialize it on another without losing information. Operators that store
object references (e.g., ``InputData``) aren't lineage serializable because the
objects aren't available on the deserialized machine.
"""
return True
class LogicalOperatorSupportsProjectionPushdown(LogicalOperator):
"""Mixin for reading operators supporting projection pushdown"""
def supports_projection_pushdown(self) -> bool:
return False
def get_projection_map(self) -> Optional[Dict[str, str]]:
return None
def apply_projection(
self,
projection_map: Optional[Dict[str, str]],
) -> LogicalOperator:
return self
class LogicalOperatorPreservesSchema(LogicalOperator):
"""Mixin for operators whose output column layout is identical to their
single input's. Provides a default ``infer_schema()`` that delegates to
the input. Use for ops like ``Filter``, ``Sort``, ``Limit``, etc., that
only re-order or filter rows.
List this mixin last in the bases of subclasses so the concrete operator
base (e.g., ``AbstractMap``, ``AbstractAllToAll``) drives ``__init__`` /
``super()`` chains.
"""
def infer_schema(self) -> Optional["Schema"]:
assert len(self.input_dependencies) == 1, len(self.input_dependencies)
return self.input_dependencies[0].infer_schema()
class LogicalOperatorUnifiesInputSchemas(LogicalOperator):
"""Mixin for n-ary operators whose output schema is the unification of
all inputs' schemas (e.g., ``Union``, ``Mix``). Provides a default
``infer_schema()`` that returns the result of
``unify_schemas_with_validation`` over each input's schema, or
``None`` if any input's schema is unresolvable.
List this mixin last in the bases of subclasses so the concrete operator
base (e.g., ``NAry``) drives ``__init__`` / ``super()`` chains.
"""
def infer_schema(self) -> Optional["Schema"]:
import pyarrow as pa
from ray.data._internal.util import unify_schemas_with_validation
input_schemas = [op.infer_schema() for op in self.input_dependencies]
if not all(isinstance(s, pa.Schema) for s in input_schemas):
return None
try:
return unify_schemas_with_validation(input_schemas)
except (pa.ArrowTypeError, pa.ArrowInvalid):
return None
class LogicalOperatorSupportsPredicatePushdown(LogicalOperator):
"""Mixin for reading operators supporting predicate pushdown"""
def supports_predicate_pushdown(self) -> bool:
return False
def get_current_predicate(self) -> Optional[Expr]:
return None
def apply_predicate(
self,
predicate_expr: Expr,
) -> LogicalOperator:
return self
class PredicatePassThroughBehavior(Enum):
"""Defines how predicates can be passed through an operator."""
# Predicate can be pushed through as-is (e.g., Sort, Repartition, RandomShuffle, Limit)
PASSTHROUGH = "passthrough"
# Predicate can be pushed through but needs column rebinding (e.g., Project)
PASSTHROUGH_WITH_SUBSTITUTION = "passthrough_with_substitution"
# Predicate can be pushed into each branch (e.g., Union)
PUSH_INTO_BRANCHES = "push_into_branches"
# Predicate can be conditionally pushed based on columns (e.g., Join)
CONDITIONAL = "conditional"
class LogicalOperatorSupportsPredicatePassThrough(ABC):
"""Mixin for operators that allow predicates to be pushed through them.
This is distinct from LogicalOperatorSupportsPredicatePushdown, which is for
operators that can *accept* predicates (like Read). This trait is for operators
that allow predicates to *pass through* them.
"""
@abstractmethod
def predicate_passthrough_behavior(self) -> PredicatePassThroughBehavior:
"""Returns the predicate passthrough behavior for this operator."""
pass
def get_column_substitutions(self) -> Optional[Dict[str, str]]:
"""Returns column renames needed when pushing through (for PASSTHROUGH_WITH_SUBSTITUTION).
Returns:
Dict mapping from old_name -> new_name, or None if no rebinding needed
"""
return None
@@ -0,0 +1,56 @@
from typing import TYPE_CHECKING, List, Optional
from .logical_operator import LogicalOperator
from .plan import Plan
if TYPE_CHECKING:
from ray.data.context import DataContext
class LogicalPlan(Plan):
"""The plan with a DAG of logical operators."""
def __init__(self, dag: LogicalOperator, context: "DataContext"):
super().__init__(context)
self._dag = dag
@property
def dag(self) -> LogicalOperator:
"""Get the DAG of logical operators."""
return self._dag
def sources(self) -> List[LogicalOperator]:
"""List of operators that are sources for this plan's DAG."""
# If an operator has no input dependencies, it's a source.
if not any(self._dag.input_dependencies):
return [self._dag]
sources = []
for op in self._dag.input_dependencies:
sources.extend(LogicalPlan(op, self.context).sources())
return sources
def has_lazy_input(self) -> bool:
"""Return whether this plan has lazy input blocks."""
from ray.data._internal.logical.operators import Read
return all(isinstance(op, Read) for op in self.sources())
def require_preserve_order(self) -> bool:
"""Whether this plan requires to preserve order."""
from ray.data._internal.logical.operators import Zip
return any(isinstance(op, Zip) for op in self.dag.post_order_iter())
def input_files(self) -> Optional[List[str]]:
"""Get the input files of the dataset, if available."""
input_files = self.dag.infer_metadata().input_files
if input_files is None:
return None
return list(set(input_files))
def initial_num_blocks(self) -> Optional[int]:
"""Get the estimated number of blocks from the logical plan
after applying execution plan optimizations, but prior to
fully executing the dataset."""
return self.dag.estimated_num_outputs()
@@ -0,0 +1,56 @@
from abc import ABC, abstractmethod
from typing import Callable, Iterator, List
class Operator(ABC):
"""Abstract class for operators.
Operators live on the driver side of the Dataset only.
"""
@property
@abstractmethod
def name(self) -> str:
"""Name for this operator."""
...
@property
def dag_str(self) -> str:
"""String representation of the whole DAG."""
if self.input_dependencies:
out_str = ", ".join([x.dag_str for x in self.input_dependencies])
out_str += " -> "
else:
out_str = ""
out_str += f"{self.__class__.__name__}[{self.name}]"
return out_str
@property
@abstractmethod
def input_dependencies(self) -> List["Operator"]:
"""List of operators that provide inputs for this operator."""
...
def post_order_iter(self) -> Iterator["Operator"]:
"""Depth-first traversal of this operator and its input dependencies."""
for op in self.input_dependencies:
yield from op.post_order_iter()
yield self
@abstractmethod
def _apply_transform(
self, transform: Callable[["Operator"], "Operator"]
) -> "Operator":
"""Recursively applies transformation (in post-order) to the operators DAG
NOTE: This operation should be opting in to avoid in-place modifications,
instead creating new operations whenever any operator needs to be
updated.
"""
...
def __repr__(self) -> str:
return f"{self.__class__.__name__}[{self.name}]"
def __str__(self) -> str:
return repr(self)
@@ -0,0 +1,52 @@
from typing import List, Type
from .plan import Plan
class Rule:
"""Abstract class for optimization rule."""
def apply(self, plan: Plan) -> Plan:
"""Apply the optimization rule to the execution plan."""
raise NotImplementedError
@classmethod
def dependencies(cls) -> List[Type["Rule"]]:
"""List of rules that must be applied before this rule."""
return []
@classmethod
def dependents(cls) -> List[Type["Rule"]]:
"""List of rules that must be applied after this rule."""
return []
class Optimizer:
"""Abstract class for optimizers.
An optimizers transforms a DAG of operators with a list of predefined rules.
"""
@property
def rules(self) -> List[Rule]:
"""List of predefined rules for this optimizer."""
raise NotImplementedError
def optimize(self, plan: Plan) -> Plan:
"""Optimize operators with a list of rules."""
# Apply rules until the plan is not changed
previous_plan = plan
while True:
for rule in self.rules:
plan = rule.apply(plan)
# TODO: Eventually we should implement proper equality.
# Using str to check equality seems brittle
if plan.dag.dag_str == previous_plan.dag.dag_str:
break
previous_plan = plan
return self._post_optimize(plan)
def _post_optimize(self, plan: Plan) -> Plan:
"""Post optimize is used for rules or other post-processing
that needs to be executed only once after the `optimize` loop."""
return plan
@@ -0,0 +1,34 @@
from typing import TYPE_CHECKING, Dict
from .logical_operator import LogicalOperator
from .plan import Plan
if TYPE_CHECKING:
from ray.data._internal.execution.interfaces import PhysicalOperator
from ray.data.context import DataContext
class PhysicalPlan(Plan):
"""The plan with a DAG of physical operators."""
def __init__(
self,
dag: "PhysicalOperator",
op_map: Dict["PhysicalOperator", LogicalOperator],
context: "DataContext",
):
super().__init__(context)
self._dag = dag
self._op_map = op_map
@property
def dag(self) -> "PhysicalOperator":
"""Get the DAG of physical operators."""
return self._dag
@property
def op_map(self) -> Dict["PhysicalOperator", LogicalOperator]:
"""
Get a mapping from physical operators to their corresponding logical operator.
"""
return self._op_map
@@ -0,0 +1,29 @@
from typing import TYPE_CHECKING
from .operator import Operator
if TYPE_CHECKING:
from ray.data.context import DataContext
class Plan:
"""Abstract class for logical/physical execution plans.
This plan should hold an operator representing the plan DAG and any auxiliary data
that's useful for plan optimization or execution.
"""
def __init__(self, context: "DataContext"):
self._context = context
@property
def dag(self) -> Operator:
raise NotImplementedError
@property
def context(self) -> "DataContext":
return self._context
@context.setter
def context(self, context: "DataContext") -> None:
self._context = context
@@ -0,0 +1,17 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, List, Optional
if TYPE_CHECKING:
from ray.data.dataset import RefBundle
class SourceOperator(ABC):
"""Mixin for Logical operators that can be logical source nodes.
Subclasses: Read, InputData, FromAbstract.
"""
@abstractmethod
def output_data(self) -> Optional[List["RefBundle"]]:
"""The output data of this operator if already known, or ``None``."""
pass