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

349 lines
11 KiB
Python

import random
import numpy as np
import pandas as pd
import pyarrow as pa
import pytest
import ray
from ray.data._internal.planner.exchange.sort_task_spec import SortKey, SortTaskSpec
from ray.data.block import BlockAccessor
from ray.data.tests.conftest import * # noqa
from ray.data.tests.util import extract_values
from ray.tests.conftest import * # noqa
@pytest.mark.parametrize(
"descending,boundaries",
[
(True, list(range(100, 1000, 200))),
(False, list(range(100, 1000, 200))),
(True, [1, 998]),
(False, [1, 998]),
# Test float.
(True, [501.5]),
(False, [501.5]),
],
)
def test_sort_with_specified_boundaries(ray_start_regular, descending, boundaries):
num_items = 1000
ds = ray.data.range(num_items)
ds = ds.sort("id", descending, boundaries).materialize()
items = range(num_items)
boundaries = [0] + sorted([round(b) for b in boundaries]) + [num_items]
expected_blocks = [
items[boundaries[i] : boundaries[i + 1]] for i in range(len(boundaries) - 1)
]
if descending:
expected_blocks = [list(reversed(block)) for block in reversed(expected_blocks)]
blocks = list(ds.iter_batches(batch_size=None))
assert len(blocks) == len(expected_blocks)
for block, expected_block in zip(blocks, expected_blocks):
assert np.all(block["id"] == expected_block)
def test_sort_multiple_keys_produces_equally_sized_blocks(ray_start_regular):
# Test for https://github.com/ray-project/ray/issues/45303.
ds = ray.data.from_items(
[{"a": i, "b": j} for i in range(2) for j in range(5)], override_num_blocks=5
)
ds_sorted = ds.sort(["a", "b"], descending=[False, True])
num_rows_per_block = [
bundle.num_rows() for bundle in ds_sorted.iter_internal_ref_bundles()
]
# Number of output blocks should be equal to the number of input blocks.
assert len(num_rows_per_block) == 5, len(num_rows_per_block)
# Ideally we should have 10 rows / 5 blocks = 2 rows per block, but to make this
# test less fragile we allow for a small deviation.
assert all(
1 <= num_rows <= 3 for num_rows in num_rows_per_block
), num_rows_per_block
def test_sort_simple(ray_start_regular, configure_shuffle_method):
num_items = 100
parallelism = 4
xs = list(range(num_items))
random.shuffle(xs)
ds = ray.data.from_items(xs, override_num_blocks=parallelism)
assert extract_values("item", ds.sort("item").take(num_items)) == list(
range(num_items)
)
# Make sure we have rows in each block.
assert len([n for n in ds.sort("item")._block_num_rows() if n > 0]) == parallelism
assert extract_values(
"item", ds.sort("item", descending=True).take(num_items)
) == list(reversed(range(num_items)))
# Test empty dataset.
ds = ray.data.from_items([])
s1 = ds.sort("item")
assert s1.count() == 0
assert s1.take() == ds.take()
ds = ray.data.range(10).filter(lambda r: r["id"] > 10).sort("id")
assert ds.count() == 0
def test_sort_partition_same_key_to_same_block(
ray_start_regular, configure_shuffle_method
):
num_items = 100
xs = [1] * num_items
ds = ray.data.from_items(xs)
sorted_ds = ds.repartition(num_items).sort("item")
# We still have 100 blocks
assert len(sorted_ds._block_num_rows()) == num_items
# Only one of them is non-empty
count = sum(1 for x in sorted_ds._block_num_rows() if x > 0)
assert count == 1
# That non-empty block contains all rows
total = sum(x for x in sorted_ds._block_num_rows() if x > 0)
assert total == num_items
@pytest.mark.parametrize("num_items,parallelism", [(100, 1), (1000, 4)])
def test_sort_arrow(
ray_start_regular,
num_items,
parallelism,
configure_shuffle_method,
use_polars_sort,
):
ctx = ray.data.context.DataContext.get_current()
try:
original_use_polars = ctx.use_polars_sort
ctx.use_polars_sort = use_polars_sort
a = list(reversed(range(num_items)))
b = [f"{x:03}" for x in range(num_items)]
shard = int(np.ceil(num_items / parallelism))
offset = 0
dfs = []
while offset < num_items:
dfs.append(
pd.DataFrame(
{"a": a[offset : offset + shard], "b": b[offset : offset + shard]}
)
)
offset += shard
if offset < num_items:
dfs.append(pd.DataFrame({"a": a[offset:], "b": b[offset:]}))
ds = ray.data.from_blocks(dfs).map_batches(
lambda t: t, batch_format="pyarrow", batch_size=None
)
def assert_sorted(sorted_ds, expected_rows):
assert [tuple(row.values()) for row in sorted_ds.iter_rows()] == list(
expected_rows
)
assert_sorted(ds.sort(key="a"), zip(reversed(a), reversed(b)))
# Make sure we have rows in each block.
assert (
len([n for n in ds.sort(key="a")._block_num_rows() if n > 0]) == parallelism
)
assert_sorted(ds.sort(key="b"), zip(a, b))
assert_sorted(ds.sort(key="a", descending=True), zip(a, b))
finally:
ctx.use_polars_sort = original_use_polars
def test_sort(ray_start_regular, use_polars_sort):
import random
import pyarrow as pa
N = 100
r = random.Random(0xDEED)
ints = [r.randint(0, 10) for _ in range(N)]
floats = [r.normalvariate(0, 5) for _ in range(N)]
t = pa.Table.from_pydict({"ints": ints, "floats": floats})
sorted_block = BlockAccessor.for_block(t).sort(SortKey(["ints", "floats"]))
sorted_tuples = list(zip(*sorted(zip(ints, floats))))
assert sorted_block == pa.Table.from_pydict(
{"ints": sorted_tuples[0], "floats": sorted_tuples[1]}
)
def test_sort_arrow_with_empty_blocks(
ray_start_regular, configure_shuffle_method, use_polars_sort
):
ctx = ray.data.context.DataContext.get_current()
try:
original_use_polars = ctx.use_polars_sort
ctx.use_polars_sort = use_polars_sort
assert (
BlockAccessor.for_block(pa.Table.from_pydict({}))
.sample(10, SortKey("A"))
.num_rows
== 0
)
partitions = BlockAccessor.for_block(
pa.Table.from_pydict({})
).sort_and_partition([1, 5, 10], SortKey("A"))
assert len(partitions) == 4
for partition in partitions:
assert partition.num_rows == 0
assert (
BlockAccessor.for_block(pa.Table.from_pydict({}))
.merge_sorted_blocks([pa.Table.from_pydict({})], SortKey("A"))[1]
.metadata.num_rows
== 0
)
ds = ray.data.from_items(
[{"A": (x % 3), "B": x} for x in range(3)], override_num_blocks=3
)
ds = ds.filter(lambda r: r["A"] == 0)
assert list(ds.sort("A").iter_rows()) == [{"A": 0, "B": 0}]
# Test empty dataset.
ds = ray.data.range(10).filter(lambda r: r["id"] > 10)
assert (
len(
SortTaskSpec.sample_boundaries(
ds._execute().block_refs, SortKey("id"), 3
)
)
== 2
)
assert ds.sort("id").count() == 0
finally:
ctx.use_polars_sort = original_use_polars
@pytest.mark.parametrize("descending", [False, True])
@pytest.mark.parametrize("batch_format", ["pyarrow", "pandas"])
def test_sort_with_multiple_keys(ray_start_regular, descending, batch_format):
num_items = 1000
num_blocks = 100
df = pd.DataFrame(
{
"a": [random.choice("ABCD") for _ in range(num_items)],
"b": [x % 3 for x in range(num_items)],
"c": [bool(random.getrandbits(1)) for _ in range(num_items)],
}
)
ds = ray.data.from_pandas(df).map_batches(
lambda t: t,
batch_format=batch_format,
batch_size=None,
)
df.sort_values(
["a", "b", "c"],
inplace=True,
ascending=[not descending, descending, not descending],
)
sorted_ds = ds.repartition(num_blocks).sort(
["a", "b", "c"], descending=[descending, not descending, descending]
)
# Number of blocks is preserved
assert len(sorted_ds._block_num_rows()) == num_blocks
# Rows are sorted over the dimensions
assert [tuple(row.values()) for row in sorted_ds.iter_rows()] == list(
zip(df["a"], df["b"], df["c"])
)
@pytest.mark.parametrize("num_items,parallelism", [(100, 1), (1000, 4)])
def test_sort_pandas(
ray_start_regular, num_items, parallelism, configure_shuffle_method
):
a = list(reversed(range(num_items)))
b = [f"{x:03}" for x in range(num_items)]
shard = int(np.ceil(num_items / parallelism))
offset = 0
dfs = []
while offset < num_items:
dfs.append(
pd.DataFrame(
{"a": a[offset : offset + shard], "b": b[offset : offset + shard]}
)
)
offset += shard
if offset < num_items:
dfs.append(pd.DataFrame({"a": a[offset:], "b": b[offset:]}))
ds = ray.data.from_blocks(dfs)
def assert_sorted(sorted_ds, expected_rows):
assert [tuple(row.values()) for row in sorted_ds.iter_rows()] == list(
expected_rows
)
assert_sorted(ds.sort(key="a"), zip(reversed(a), reversed(b)))
# Make sure we have rows in each block.
assert len([n for n in ds.sort(key="a")._block_num_rows() if n > 0]) == parallelism
assert_sorted(ds.sort(key="b"), zip(a, b))
assert_sorted(ds.sort(key="a", descending=True), zip(a, b))
def test_sort_pandas_with_empty_blocks(ray_start_regular, configure_shuffle_method):
assert (
BlockAccessor.for_block(pa.Table.from_pydict({}))
.sample(10, SortKey("A"))
.num_rows
== 0
)
partitions = BlockAccessor.for_block(pa.Table.from_pydict({})).sort_and_partition(
[1, 5, 10], SortKey("A")
)
assert len(partitions) == 4
for partition in partitions:
assert partition.num_rows == 0
assert (
BlockAccessor.for_block(pa.Table.from_pydict({}))
.merge_sorted_blocks([pa.Table.from_pydict({})], SortKey("A"))[1]
.metadata.num_rows
== 0
)
ds = ray.data.from_items(
[{"A": (x % 3), "B": x} for x in range(3)], override_num_blocks=3
)
ds = ds.filter(lambda r: r["A"] == 0)
assert list(ds.sort("A").iter_rows()) == [{"A": 0, "B": 0}]
# Test empty dataset.
ds = ray.data.range(10).filter(lambda r: r["id"] > 10)
assert (
len(SortTaskSpec.sample_boundaries(ds._execute().block_refs, SortKey("id"), 3))
== 2
)
assert ds.sort("id").count() == 0
def test_sort_with_one_block(shutdown_only, configure_shuffle_method):
ray.init(num_cpus=8)
ctx = ray.data.DataContext.get_current()
ctx.execution_options.verbose_progress = True
ctx.use_push_based_shuffle = True
# Use a dataset that will produce only one block to sort.
ray.data.range(1024).map_batches(
lambda _: pa.table([pa.array([1])], ["token_counts"])
).sum("token_counts")
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-sv", __file__]))