chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
This file tests the reproducibility of the random API with different seed configurations.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.data import RandomSeedConfig
|
||||
from ray.data.context import DataContext
|
||||
from ray.data.tests.conftest import * # noqa
|
||||
from ray.tests.conftest import * # noqa
|
||||
|
||||
DATASET_SIZE = 40
|
||||
NUM_BLOCKS = 8
|
||||
|
||||
METHODS = ["random_shuffle", "random_sample", "randomize_block_order"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_dataset(ray_start_regular_shared) -> ray.data.Dataset:
|
||||
"""Create a base dataset for testing."""
|
||||
ctx = DataContext.get_current()
|
||||
ctx.enable_progress_bars = False
|
||||
return ray.data.range(DATASET_SIZE, override_num_blocks=NUM_BLOCKS)
|
||||
|
||||
|
||||
@pytest.fixture(params=METHODS)
|
||||
def method(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def create_seed_config(seed_param):
|
||||
"""Create seed config from parametrized values."""
|
||||
if seed_param is None:
|
||||
return None
|
||||
if isinstance(seed_param, int):
|
||||
return seed_param
|
||||
if seed_param == "RandomSeedConfig_false":
|
||||
return RandomSeedConfig(seed=42, reseed_after_execution=False)
|
||||
if seed_param == "RandomSeedConfig_true":
|
||||
return RandomSeedConfig(seed=42, reseed_after_execution=True)
|
||||
raise ValueError(f"Unknown seed_param: {seed_param}")
|
||||
|
||||
|
||||
def collect_batches_from_epochs(ds, num_epochs=3, batch_size=5):
|
||||
"""Collect batches from multiple epochs of iter_batches."""
|
||||
epochs = []
|
||||
for _ in range(num_epochs):
|
||||
epoch_data = []
|
||||
for batch in ds.iter_batches(batch_size=batch_size, batch_format="pandas"):
|
||||
epoch_data.extend(batch["id"].tolist())
|
||||
epochs.append(epoch_data)
|
||||
return epochs
|
||||
|
||||
|
||||
def apply_random_operation(ds, method, seed, fraction=None):
|
||||
"""Apply the specified random operation to the dataset."""
|
||||
if method == "random_shuffle":
|
||||
return ds.random_shuffle(seed=seed)
|
||||
if method == "random_sample":
|
||||
return ds.random_sample(fraction=fraction or 0.5, seed=seed)
|
||||
if method == "randomize_block_order":
|
||||
return ds.randomize_block_order(seed=seed)
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
|
||||
def _normalize_results(method, result):
|
||||
"""Sort random_sample results to ignore parallelism-induced ordering differences."""
|
||||
if method == "random_sample":
|
||||
return sorted(result, key=lambda x: x["id"])
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"seeds,expect_same",
|
||||
[
|
||||
((42, 42), True),
|
||||
((42, 123), False),
|
||||
],
|
||||
)
|
||||
def test_random_api_seed_determinism(base_dataset, method, seeds, expect_same):
|
||||
"""Test that same seeds produce same results and different seeds produce different
|
||||
results."""
|
||||
seed1, seed2 = seeds
|
||||
ds1 = apply_random_operation(base_dataset, method, seed1)
|
||||
ds2 = apply_random_operation(base_dataset, method, seed2)
|
||||
result1 = _normalize_results(method, ds1.take_all())
|
||||
result2 = _normalize_results(method, ds2.take_all())
|
||||
|
||||
if expect_same:
|
||||
assert result1 == result2, f"Same seed should produce same results for {method}"
|
||||
else:
|
||||
assert (
|
||||
result1 != result2
|
||||
), f"Different seeds should produce different results for {method}"
|
||||
|
||||
|
||||
def test_random_api_none_seed(base_dataset, method):
|
||||
"""Test that seed=None produces non-deterministic results.
|
||||
|
||||
For random_sample, results are sorted so we compare which items were
|
||||
sampled rather than task completion order. For shuffle methods, ordering
|
||||
*is* the randomness being tested.
|
||||
"""
|
||||
ds1 = apply_random_operation(base_dataset, method, None)
|
||||
ds2 = apply_random_operation(base_dataset, method, None)
|
||||
result1 = _normalize_results(method, ds1.take_all())
|
||||
result2 = _normalize_results(method, ds2.take_all())
|
||||
|
||||
assert (
|
||||
result1 != result2
|
||||
), f"seed=None should produce non-deterministic results for {method}"
|
||||
|
||||
|
||||
def test_random_api_reseed_behavior(base_dataset):
|
||||
"""Test reseed_after_execution behavior across multiple epochs.
|
||||
|
||||
For shuffle/block_order we compare full row order (no sort); for random_sample
|
||||
we compare sorted items so we only assert same set of sampled rows.
|
||||
"""
|
||||
seed_value = 42
|
||||
seed_false = RandomSeedConfig(seed=seed_value, reseed_after_execution=False)
|
||||
seed_true = RandomSeedConfig(seed=seed_value, reseed_after_execution=True)
|
||||
|
||||
# Test random_shuffle with reseed_after_execution=False
|
||||
ds = base_dataset.random_shuffle(seed=seed_false)
|
||||
epochs = collect_batches_from_epochs(ds, num_epochs=3, batch_size=5)
|
||||
assert (
|
||||
epochs[0] == epochs[1] == epochs[2]
|
||||
), "reseed_after_execution=False should produce same results"
|
||||
|
||||
# Test random_shuffle with reseed_after_execution=True
|
||||
ds = base_dataset.random_shuffle(seed=seed_true)
|
||||
epochs = collect_batches_from_epochs(ds, num_epochs=3, batch_size=5)
|
||||
assert (epochs[0] != epochs[1]) or (
|
||||
epochs[1] != epochs[2]
|
||||
), "reseed_after_execution=True should produce different results per epoch"
|
||||
|
||||
# Test random_sample with reseed_after_execution=False
|
||||
ds = base_dataset.random_sample(fraction=0.5, seed=seed_false)
|
||||
epochs = collect_batches_from_epochs(ds, num_epochs=2, batch_size=5)
|
||||
assert sorted(epochs[0]) == sorted(
|
||||
epochs[1]
|
||||
), "reseed_after_execution=False should produce same sampled items"
|
||||
|
||||
# Test randomize_block_order with reseed_after_execution=False
|
||||
ds = base_dataset.randomize_block_order(seed=seed_false)
|
||||
epochs = collect_batches_from_epochs(ds, num_epochs=2, batch_size=5)
|
||||
assert (
|
||||
epochs[0] == epochs[1]
|
||||
), "reseed_after_execution=False should produce same block order"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"seed_param", [None, 42, "RandomSeedConfig_false", "RandomSeedConfig_true"]
|
||||
)
|
||||
def test_random_api_multiple_epochs(base_dataset, method, seed_param):
|
||||
"""Comprehensive test for all methods and seed configurations across multiple
|
||||
epochs."""
|
||||
seed = create_seed_config(seed_param)
|
||||
ds = apply_random_operation(
|
||||
base_dataset, method, seed, fraction=0.5 if method == "random_sample" else None
|
||||
)
|
||||
epochs = collect_batches_from_epochs(ds, num_epochs=3, batch_size=5)
|
||||
|
||||
assert len(epochs) == 3
|
||||
for i, epoch in enumerate(epochs):
|
||||
if method == "random_sample":
|
||||
assert len(epoch) > 0, f"Epoch {i} should have data"
|
||||
else:
|
||||
assert (
|
||||
len(epoch) == DATASET_SIZE
|
||||
), f"Epoch {i} should have all {DATASET_SIZE} items"
|
||||
|
||||
if seed_param is None:
|
||||
all_same = all(epoch == epochs[0] for epoch in epochs)
|
||||
assert (
|
||||
not all_same
|
||||
), f"seed=None should produce different results across epochs for {method}"
|
||||
|
||||
elif seed_param == 42 or seed_param == "RandomSeedConfig_false":
|
||||
assert (
|
||||
epochs[0] == epochs[1] == epochs[2]
|
||||
), f"Fixed seed should produce same results across epochs for {method}"
|
||||
|
||||
elif seed_param == "RandomSeedConfig_true":
|
||||
# Require at least two consecutive epochs to differ (reseed produces new orderings).
|
||||
assert (epochs[0] != epochs[1]) or (
|
||||
epochs[1] != epochs[2]
|
||||
), f"reseed_after_execution=True should produce different results per epoch for {method}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
Reference in New Issue
Block a user