Files
ray-project--ray/python/ray/data/tests/test_idempotency_optimizer_guards.py
T
2026-07-13 13:17:40 +08:00

159 lines
5.9 KiB
Python

"""Tests that logical optimizer rules do not duplicate, reorder, or merge
non-idempotent expressions (``random``/``uuid``/``monotonically_increasing_id``).
Covers the shared ``is_idempotent`` contract as consumed by CommonSubExprElimination,
ProjectionPushdown, PredicatePushdown, and LimitPushdown.
"""
import pytest
import ray
from ray.data._internal.logical.optimizers import LogicalOptimizer
from ray.data.expressions import (
col,
monotonically_increasing_id,
random,
uuid,
)
def _optimized_plan_str(ds) -> str:
return LogicalOptimizer().optimize(ds._logical_plan).dag.dag_str
# ---------------------------------------------------------------------------
# CommonSubExprElimination: independent non-idempotent draws must not merge.
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("make_expr", [uuid, random])
def test_cse_does_not_merge_independent_nondeterministic_columns(
ray_start_regular_shared, make_expr
):
ds = ray.data.range(8).with_column("a", make_expr()).with_column("b", make_expr())
rows = ds.take_all()
assert all(r["a"] != r["b"] for r in rows)
def test_cse_does_not_merge_reused_seeded_random_object(ray_start_regular_shared):
# The same seeded RandomExpr object reused in two columns must not be merged:
# the runtime advances a per-instance block counter between evaluations, so the
# two columns legitimately differ. (Under the old bug, CSE collapsed them.)
shared = random(seed=42)
ds = ray.data.range(8).with_column("a", shared).with_column("b", shared)
rows = ds.take_all()
assert any(r["a"] != r["b"] for r in rows)
def test_cse_still_merges_deterministic_subexpression(ray_start_regular_shared):
# A repeated deterministic sub-expression must still be eligible for CSE.
ds = ray.data.range(5).with_column("y", (col("id") + 1) * (col("id") + 1))
assert ds.take_all() == [{"id": i, "y": (i + 1) * (i + 1)} for i in range(5)]
# ---------------------------------------------------------------------------
# ProjectionPushdown: a non-idempotent column referenced multiple times must
# not be inlined into multiple independent evaluations.
# ---------------------------------------------------------------------------
def test_projection_does_not_inline_nonidempotent_into_multiple_refs(
ray_start_regular_shared,
):
ds = (
ray.data.range(6)
.with_column("x", random())
.with_column("y", col("x") + col("x"))
)
rows = ds.take_all()
# If ``x`` were inlined as two independent draws, ``y`` would not equal ``2*x``.
assert all(abs(r["y"] - 2 * r["x"]) < 1e-12 for r in rows)
def test_projection_still_fuses_deterministic_chain(ray_start_regular_shared):
ds = (
ray.data.range(6)
.with_column("x", col("id") + 1)
.with_column("y", col("x") + col("x"))
)
assert ds.take_all() == [{"id": i, "x": i + 1, "y": 2 * (i + 1)} for i in range(6)]
# ---------------------------------------------------------------------------
# PredicatePushdown: a filter must not be pushed below a projection that
# produces a non-idempotent column.
# ---------------------------------------------------------------------------
def test_predicate_not_pushed_below_nonidempotent_projection(ray_start_regular_shared):
ds = (
ray.data.range(10)
.with_column("rid", monotonically_increasing_id())
.filter(expr=col("id") > 4)
)
plan = _optimized_plan_str(ds)
# Filter remains downstream of (above) the Project.
assert plan.rfind("Filter") > plan.rfind("Project"), plan
def test_predicate_still_pushed_below_deterministic_projection(
ray_start_regular_shared,
):
ds = ray.data.range(10).with_column("k", col("id") + 1).filter(expr=col("id") > 4)
plan = _optimized_plan_str(ds)
assert plan.rfind("Filter") < plan.rfind("Project"), plan
def test_filter_with_nonidempotent_predicate_not_pushed_into_union_branches(
ray_start_regular_shared,
):
# A filter whose own predicate is non-idempotent must not be duplicated into
# each Union branch (each branch would compute a separate local id).
union = ray.data.range(10).union(ray.data.range(10))
ds = union.filter(expr=monotonically_increasing_id() < 5)
plan = _optimized_plan_str(ds)
assert plan.rfind("Filter") > plan.rfind("Union"), plan
def test_idempotent_filter_still_pushed_into_union_branches(ray_start_regular_shared):
union = ray.data.range(10).union(ray.data.range(10))
ds = union.filter(expr=col("id") > 2)
plan = _optimized_plan_str(ds)
# An idempotent predicate is still pushed below the Union, into the branches.
assert plan.rfind("Union") > plan.rfind("Filter"), plan
def test_filter_with_nonidempotent_predicate_not_fused(ray_start_regular_shared):
# Fusing would move where monotonically_increasing_id() is evaluated.
ds = (
ray.data.range(20)
.filter(expr=monotonically_increasing_id() < 15)
.filter(expr=col("id") > 2)
)
plan = _optimized_plan_str(ds)
assert plan.count("Filter[") == 2, plan
# ---------------------------------------------------------------------------
# LimitPushdown: a limit must not be pushed past a projection that produces a
# non-idempotent column.
# ---------------------------------------------------------------------------
def test_limit_not_pushed_past_nonidempotent_projection(ray_start_regular_shared):
ds = ray.data.range(100).with_column("rid", monotonically_increasing_id()).limit(10)
plan = _optimized_plan_str(ds)
assert plan.rfind("Limit") > plan.rfind("Project"), plan
def test_limit_still_pushed_past_deterministic_projection(ray_start_regular_shared):
ds = ray.data.range(100).with_column("k", col("id") + 1).limit(10)
plan = _optimized_plan_str(ds)
assert plan.rfind("Limit") < plan.rfind("Project"), plan
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))