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

288 lines
9.6 KiB
Python

import random
import time
import numpy as np
import pandas as pd
import pytest
import ray
from ray.data._internal.execution.interfaces.ref_bundle import (
_ref_bundles_iterator_to_block_refs_list,
)
from ray.data.context import DataContext
from ray.data.tests.conftest import * # noqa
from ray.data.tests.util import named_values
from ray.tests.conftest import * # noqa
RANDOM_SEED = 123
def test_empty_shuffle(
ray_start_regular_shared_2_cpus, disable_fallback_to_object_extension
):
ds = ray.data.range(100, override_num_blocks=100)
ds = ds.filter(lambda x: x)
ds = ds.map_batches(lambda x: x)
ds = ds.random_shuffle() # Would prev. crash with AssertionError: pyarrow.Table.
ds.show()
@pytest.mark.parametrize("num_parts", [1, 30])
@pytest.mark.parametrize("ds_format", ["arrow", "pandas"])
def test_global_tabular_sum(
ray_start_regular_shared_2_cpus,
ds_format,
num_parts,
configure_shuffle_method,
disable_fallback_to_object_extension,
):
seed = int(time.time())
print(f"Seeding RNG for test_global_arrow_sum with: {seed}")
random.seed(seed)
xs = list(range(100))
random.shuffle(xs)
def _to_pandas(ds):
return ds.map_batches(lambda x: x, batch_size=None, batch_format="pandas")
# Test built-in global sum aggregation
ds = ray.data.from_items([{"A": x} for x in xs]).repartition(num_parts)
if ds_format == "pandas":
ds = _to_pandas(ds)
assert ds.sum("A") == 4950
# Test empty dataset
ds = ray.data.range(10)
if ds_format == "pandas":
ds = _to_pandas(ds)
assert ds.filter(lambda r: r["id"] > 10).sum("id") is None
# Test built-in global sum aggregation with nans
nan_ds = ray.data.from_items([{"A": x} for x in xs] + [{"A": None}]).repartition(
num_parts
)
if ds_format == "pandas":
nan_ds = _to_pandas(nan_ds)
assert nan_ds.sum("A") == 4950
# Test ignore_nulls=False
assert pd.isnull(nan_ds.sum("A", ignore_nulls=False))
# Test all nans
nan_ds = ray.data.from_items([{"A": None}] * len(xs)).repartition(num_parts)
if ds_format == "pandas":
nan_ds = _to_pandas(nan_ds)
assert nan_ds.sum("A") is None
assert pd.isnull(nan_ds.sum("A", ignore_nulls=False))
def test_random_block_order_schema(
ray_start_regular_shared_2_cpus, disable_fallback_to_object_extension
):
df = pd.DataFrame({"a": np.random.rand(10), "b": np.random.rand(10)})
ds = ray.data.from_pandas(df).randomize_block_order()
ds.schema().names == ["a", "b"]
def test_random_block_order(
ray_start_regular_shared_2_cpus,
restore_data_context,
disable_fallback_to_object_extension,
):
ctx = DataContext.get_current()
ctx.execution_options.preserve_order = True
# Test BlockList.randomize_block_order.
ds = ray.data.range(12).repartition(4)
ds = ds.randomize_block_order(seed=0)
results = ds.take()
expected = named_values("id", [6, 7, 8, 0, 1, 2, 3, 4, 5, 9, 10, 11])
assert results == expected
# Test LazyBlockList.randomize_block_order.
lazy_blocklist_ds = ray.data.range(12, override_num_blocks=4)
lazy_blocklist_ds = lazy_blocklist_ds.randomize_block_order(seed=0)
lazy_blocklist_results = lazy_blocklist_ds.take()
lazy_blocklist_expected = named_values("id", [6, 7, 8, 0, 1, 2, 3, 4, 5, 9, 10, 11])
assert lazy_blocklist_results == lazy_blocklist_expected
# NOTE: All tests above share a Ray cluster, while the tests below do not. These
# tests should only be carefully reordered to retain this invariant!
def test_random_shuffle(
shutdown_only, configure_shuffle_method, disable_fallback_to_object_extension
):
# Assert random 2 distinct random-shuffle pipelines yield different orders
r1 = ray.data.range(100).random_shuffle().take(999)
r2 = ray.data.range(100).random_shuffle().take(999)
assert r1 != r2, (r1, r2)
# Assert same random-shuffle pipeline yielding 2 different orders,
# when executed
ds = ray.data.range(100).random_shuffle()
r1 = ds.take(999)
r2 = ds.take(999)
assert r1 != r2, (r1, r2)
r1 = ray.data.range(100, override_num_blocks=1).random_shuffle().take(999)
r2 = ray.data.range(100, override_num_blocks=1).random_shuffle().take(999)
assert r1 != r2, (r1, r2)
assert (
ray.data.range(100)
.random_shuffle()
.repartition(1)
._logical_plan.initial_num_blocks()
== 1
)
r1 = ray.data.range(100).random_shuffle().repartition(1).take(999)
r2 = ray.data.range(100).random_shuffle().repartition(1).take(999)
assert r1 != r2, (r1, r2)
r0 = ray.data.range(100, override_num_blocks=5).take(999)
r1 = ray.data.range(100, override_num_blocks=5).random_shuffle(seed=0).take(999)
r2 = ray.data.range(100, override_num_blocks=5).random_shuffle(seed=0).take(999)
r3 = ray.data.range(100, override_num_blocks=5).random_shuffle(seed=12345).take(999)
assert r1 == r2, (r1, r2)
assert r1 != r0, (r1, r0)
assert r1 != r3, (r1, r3)
r0 = ray.data.range(100, override_num_blocks=5).take(999)
r1 = ray.data.range(100, override_num_blocks=5).random_shuffle(seed=0).take(999)
r2 = ray.data.range(100, override_num_blocks=5).random_shuffle(seed=0).take(999)
assert r1 == r2, (r1, r2)
assert r1 != r0, (r1, r0)
# Test move.
ds = ray.data.range(100, override_num_blocks=2)
r1 = ds.random_shuffle().take(999)
ds = ds.map(lambda x: x).take(999)
r2 = ray.data.range(100).random_shuffle().take(999)
assert r1 != r2, (r1, r2)
# Test empty dataset.
ds = ray.data.from_items([])
r1 = ds.random_shuffle()
assert r1.count() == 0
assert r1.take() == ds.take()
def test_random_shuffle_check_random(
shutdown_only, disable_fallback_to_object_extension
):
# Rows from the same input should not be contiguous in the final output.
num_files = 10
num_rows = 100
items = [i for i in range(num_files) for _ in range(num_rows)]
ds = ray.data.from_items(items, override_num_blocks=num_files)
out = ds.random_shuffle().take(num_files * num_rows)
for i in range(num_files):
part = out[i * num_rows : (i + 1) * num_rows]
seen = set()
num_contiguous = 1
prev = -1
for x in part:
x = x["item"]
if prev != x:
prev = x
num_contiguous = 1
else:
num_contiguous += 1
assert num_contiguous < (
num_rows / num_files
), f"{part} contains too many contiguous rows from same input block"
seen.add(x)
assert (
set(range(num_files)) == seen
), f"{part} does not contain elements from all input blocks"
# Rows from the same input should appear in a different order in the
# output.
num_files = 10
num_rows = 100
items = [j for i in range(num_files) for j in range(num_rows)]
ds = ray.data.from_items(items, override_num_blocks=num_files)
out = ds.random_shuffle().take(num_files * num_rows)
for i in range(num_files):
part = out[i * num_rows : (i + 1) * num_rows]
num_increasing = 0
prev = -1
for x in part:
x = x["item"]
if x >= prev:
num_increasing += 1
else:
assert num_increasing < (
num_rows / num_files
), f"{part} contains non-shuffled rows from input blocks"
num_increasing = 0
prev = x
def test_random_shuffle_with_custom_resource(
ray_start_cluster, configure_shuffle_method, disable_fallback_to_object_extension
):
cluster = ray_start_cluster
# Create two nodes which have different custom resources.
cluster.add_node(
resources={"foo": 100},
num_cpus=1,
)
cluster.add_node(resources={"bar": 100}, num_cpus=1)
ray.init(cluster.address)
# Run dataset in "bar" nodes.
ds = ray.data.read_parquet(
"example://parquet_images_mini",
override_num_blocks=2,
ray_remote_args={"resources": {"bar": 1}},
)
ds = ds.random_shuffle(resources={"bar": 1}).materialize()
assert "1 nodes used" in ds.stats()
assert "2 nodes used" not in ds.stats()
def test_random_shuffle_spread(
ray_start_cluster, configure_shuffle_method, disable_fallback_to_object_extension
):
cluster = ray_start_cluster
cluster.add_node(
resources={"bar:1": 100},
num_cpus=10,
_system_config={"max_direct_call_object_size": 0},
)
cluster.add_node(resources={"bar:2": 100}, num_cpus=10)
cluster.add_node(resources={"bar:3": 100}, num_cpus=0)
ray.init(cluster.address)
@ray.remote
def get_node_id():
return ray.get_runtime_context().get_node_id()
node1_id = ray.get(get_node_id.options(resources={"bar:1": 1}).remote())
node2_id = ray.get(get_node_id.options(resources={"bar:2": 1}).remote())
ds = ray.data.range(100, override_num_blocks=2).random_shuffle()
bundles = ds.iter_internal_ref_bundles()
blocks = _ref_bundles_iterator_to_block_refs_list(bundles)
ray.wait(blocks, num_returns=len(blocks), fetch_local=False)
location_data = ray.experimental.get_object_locations(blocks)
locations = []
for block in blocks:
locations.extend(location_data[block]["node_ids"])
assert "2 nodes used" in ds.stats()
if not configure_shuffle_method:
# We don't check this for push-based shuffle since it will try to
# colocate reduce tasks to improve locality.
assert set(locations) == {node1_id, node2_id}
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))