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

287 lines
8.5 KiB
Python

import itertools
import os
import uuid
from typing import Iterable
import pytest
import ray
from ray.data._internal.arrow_block import ArrowBlockBuilder
from ray.data._internal.execution.interfaces.ref_bundle import RefBundle
from ray.tests.conftest import * # noqa
SMALL_VALUE = "a" * 100
LARGE_VALUE = "a" * 10000
ARROW_SMALL_VALUE = {"value": "a" * 100}
ARROW_LARGE_VALUE = {"value": "a" * 10000}
def assert_close(actual, expected, tolerance=0.3):
print("assert_close", actual, expected)
assert abs(actual - expected) / expected < tolerance, (actual, expected)
def test_arrow_size(ray_start_regular_shared):
b = ArrowBlockBuilder()
assert b.get_estimated_memory_usage() == 0
b.add(ARROW_SMALL_VALUE)
assert_close(b.get_estimated_memory_usage(), 118)
b.add(ARROW_SMALL_VALUE)
assert_close(b.get_estimated_memory_usage(), 236)
for _ in range(8):
b.add(ARROW_SMALL_VALUE)
assert_close(b.get_estimated_memory_usage(), 1180)
for _ in range(90):
b.add(ARROW_SMALL_VALUE)
assert_close(b.get_estimated_memory_usage(), 11800)
for _ in range(900):
b.add(ARROW_SMALL_VALUE)
assert_close(b.get_estimated_memory_usage(), 118000)
assert b.build().num_rows == 1000
def test_arrow_size_diff_values(ray_start_regular_shared):
b = ArrowBlockBuilder()
assert b.get_estimated_memory_usage() == 0
b.add(ARROW_LARGE_VALUE)
assert b._num_compactions == 0
assert_close(b.get_estimated_memory_usage(), 10019)
b.add(ARROW_LARGE_VALUE)
assert b._num_compactions == 0
assert_close(b.get_estimated_memory_usage(), 20038)
for _ in range(10):
b.add(ARROW_SMALL_VALUE)
assert_close(b.get_estimated_memory_usage(), 25178)
for _ in range(100):
b.add(ARROW_SMALL_VALUE)
assert b._num_compactions == 0
assert_close(b.get_estimated_memory_usage(), 35394)
for _ in range(13000):
b.add(ARROW_LARGE_VALUE)
assert_close(b.get_estimated_memory_usage(), 130131680)
assert b._num_compactions == 0
for _ in range(4000):
b.add(ARROW_LARGE_VALUE)
assert_close(b.get_estimated_memory_usage(), 170129189)
assert b._num_compactions == 1
assert b.build().num_rows == 17112
def test_arrow_size_add_block(ray_start_regular_shared):
b = ArrowBlockBuilder()
for _ in range(2000):
b.add(ARROW_LARGE_VALUE)
block = b.build()
b2 = ArrowBlockBuilder()
for _ in range(5):
b2.add_block(block)
assert b2._num_compactions == 0
assert_close(b2.get_estimated_memory_usage(), 100040020)
assert b2.build().num_rows == 10000
def test_split_read_csv(ray_start_regular_shared, tmp_path):
ctx = ray.data.context.DataContext.get_current()
def gen(name):
path = os.path.join(tmp_path, name)
ray.data.range(1000, override_num_blocks=1).map(
lambda _: {"out": LARGE_VALUE}
).write_csv(path)
return ray.data.read_csv(path, override_num_blocks=1)
# 20MiB
ctx.target_max_block_size = 20_000_000
ds1 = gen("out1")
assert ds1._block_num_rows() == [1000]
# 3MiB
ctx.target_max_block_size = 3_000_000
ds2 = gen("out2")
nrow = ds2._block_num_rows()
assert 3 < len(nrow) < 5, nrow
for x in nrow[:-1]:
assert 200 < x < 400, (x, nrow)
# 1MiB
ctx.target_max_block_size = 1_000_000
ds3 = gen("out3")
nrow = ds3._block_num_rows()
assert 8 < len(nrow) < 12, nrow
for x in nrow[:-1]:
assert 80 < x < 120, (x, nrow)
# Disabled.
# Setting a huge block size effectively disables block splitting.
ctx.target_max_block_size = 2**64
ds4 = gen("out4")
assert ds4._block_num_rows() == [1000]
def test_split_read_parquet(ray_start_regular_shared, tmp_path):
ctx = ray.data.context.DataContext.get_current()
def gen(name):
path = os.path.join(tmp_path, name)
ds = (
ray.data.range(200000, override_num_blocks=1)
.map(lambda _: {"out": uuid.uuid4().hex})
.materialize()
)
# Fully execute the operations prior to write, because with
# override_num_blocks=1, there is only one task; so the write operator
# will only write to one file, even though there are multiple
# blocks created by block splitting.
ds.write_parquet(path)
return ray.data.read_parquet(path, override_num_blocks=1)
# 20MiB
ctx.target_max_block_size = 20_000_000
ds1 = gen("out1")
assert ds1._block_num_rows() == [200000]
# 3MiB
ctx.target_max_block_size = 3_000_000
ds2 = gen("out2")
nrow = ds2._block_num_rows()
assert 2 < len(nrow) < 5, nrow
for x in nrow[:-1]:
assert 50000 < x < 96000, (x, nrow)
# 1MiB
ctx.target_max_block_size = 1_000_000
ds3 = gen("out3")
nrow = ds3._block_num_rows()
assert 6 < len(nrow) < 12, nrow
for x in nrow[:-1]:
assert 20000 < x < 35000, (x, nrow)
@pytest.mark.parametrize("use_actors", [False, True])
def test_split_map(shutdown_only, use_actors):
ray.shutdown()
ray.init(num_cpus=3)
kwargs = {}
def arrow_udf(x):
return ARROW_LARGE_VALUE
def identity_udf(x):
return x
class ArrowUDFClass:
def __call__(self, x):
return ARROW_LARGE_VALUE
class IdentityUDFClass:
def __call__(self, x):
return x
if use_actors:
kwargs = {"compute": ray.data.ActorPoolStrategy()}
arrow_fn = ArrowUDFClass
identity_fn = IdentityUDFClass
else:
arrow_fn = arrow_udf
identity_fn = identity_udf
# Arrow block
ctx = ray.data.context.DataContext.get_current()
ctx.target_max_block_size = 20_000_000
ds2 = ray.data.range(1000, override_num_blocks=1).map(arrow_fn, **kwargs)
bundles = ds2.map(identity_fn, **kwargs).iter_internal_ref_bundles()
blocks = _fetch_blocks(bundles)
num_rows = _get_total_rows(blocks)
assert len(blocks) == 1
assert num_rows == 1000
ctx.target_max_block_size = 2_000_000
ds3 = ray.data.range(1000, override_num_blocks=1).map(arrow_fn, **kwargs)
bundles = ds3.map(identity_fn, **kwargs).iter_internal_ref_bundles()
blocks = _fetch_blocks(bundles)
num_rows = _get_total_rows(blocks)
assert 4 < len(blocks) < 7
assert num_rows == 1000
# Disabled.
# Setting a huge block size effectively disables block splitting.
ctx.target_max_block_size = 2**64
ds3 = ray.data.range(1000, override_num_blocks=1).map(arrow_fn, **kwargs)
bundles = ds3.map(identity_fn, **kwargs).iter_internal_ref_bundles()
blocks = _fetch_blocks(bundles)
num_rows = _get_total_rows(blocks)
assert len(blocks) == 1
assert num_rows == 1000
def _get_total_rows(blocks):
return sum([b.num_rows for b in blocks])
def _fetch_blocks(bundles: Iterable[RefBundle]):
return ray.get(list(itertools.chain(*[b.block_refs for b in bundles])))
def test_split_flat_map(ray_start_regular_shared):
ctx = ray.data.context.DataContext.get_current()
# Arrow block
ctx.target_max_block_size = 20_000_000
ds2 = ray.data.range(1000, override_num_blocks=1).map(lambda _: ARROW_LARGE_VALUE)
bundles = ds2.flat_map(lambda x: [x]).iter_internal_ref_bundles()
blocks = _fetch_blocks(bundles)
num_rows = _get_total_rows(blocks)
assert len(blocks) == 1
assert num_rows == 1000
ctx.target_max_block_size = 2_000_000
ds3 = ray.data.range(1000, override_num_blocks=1).map(lambda _: ARROW_LARGE_VALUE)
bundles = ds3.flat_map(lambda x: [x]).iter_internal_ref_bundles()
blocks = _fetch_blocks(bundles)
num_rows = _get_total_rows(blocks)
assert 4 < len(blocks) < 7
assert num_rows == 1000
def test_split_map_batches(ray_start_regular_shared):
ctx = ray.data.context.DataContext.get_current()
# Arrow block
ctx.target_max_block_size = 20_000_000
ds2 = ray.data.range(1000, override_num_blocks=1).map(lambda _: ARROW_LARGE_VALUE)
bundles = ds2.map_batches(lambda x: x, batch_size=1).iter_internal_ref_bundles()
blocks = _fetch_blocks(bundles)
num_rows = _get_total_rows(blocks)
assert len(blocks) == 1
assert num_rows == 1000
ctx.target_max_block_size = 2_000_000
ds3 = ray.data.range(1000, override_num_blocks=1).map(lambda _: ARROW_LARGE_VALUE)
bundles = ds3.map_batches(lambda x: x, batch_size=16).iter_internal_ref_bundles()
blocks = _fetch_blocks(bundles)
num_rows = _get_total_rows(blocks)
assert 4 < len(blocks) < 7
assert num_rows == 1000
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))