565 lines
17 KiB
Python
565 lines
17 KiB
Python
import os
|
|
import sys
|
|
import time
|
|
from dataclasses import astuple, dataclass
|
|
from typing import TYPE_CHECKING, List, Optional
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pyarrow as pa
|
|
import pytest
|
|
|
|
import ray
|
|
from ray.data._internal.arrow_block import ArrowBlockBuilder
|
|
from ray.data._internal.datasource.csv_datasource import CSVDatasource
|
|
from ray.data.block import BlockMetadata
|
|
from ray.data.dataset import Dataset
|
|
from ray.data.datasource import Datasource
|
|
from ray.data.datasource.datasource import ReadTask
|
|
from ray.data.tests.conftest import (
|
|
CoreExecutionMetrics,
|
|
assert_blocks_expected_in_plasma,
|
|
assert_core_execution_metrics_equals,
|
|
get_initial_core_execution_metrics_snapshot,
|
|
)
|
|
from ray.tests.conftest import * # noqa
|
|
|
|
if TYPE_CHECKING:
|
|
from ray.data.context import DataContext
|
|
|
|
|
|
# Data source generates random bytes data
|
|
class RandomBytesDatasource(Datasource):
|
|
def __init__(
|
|
self,
|
|
num_tasks: int,
|
|
num_batches_per_task: int,
|
|
row_size: int,
|
|
num_rows_per_batch=None,
|
|
use_bytes=True,
|
|
use_arrow=False,
|
|
):
|
|
self.num_tasks = num_tasks
|
|
self.num_batches_per_task = num_batches_per_task
|
|
self.row_size = row_size
|
|
if num_rows_per_batch is None:
|
|
num_rows_per_batch = 1
|
|
self.num_rows_per_batch = num_rows_per_batch
|
|
self.use_bytes = use_bytes
|
|
self.use_arrow = use_arrow
|
|
|
|
def estimate_inmemory_data_size(self):
|
|
return None
|
|
|
|
def get_read_tasks(
|
|
self,
|
|
parallelism: int,
|
|
per_task_row_limit: Optional[int] = None,
|
|
data_context: Optional["DataContext"] = None,
|
|
) -> List[ReadTask]:
|
|
def _blocks_generator():
|
|
for _ in range(self.num_batches_per_task):
|
|
if self.use_bytes:
|
|
# NOTE(swang): Each np object has some metadata bytes, so
|
|
# actual size can be much more than num_rows_per_batch * row_size
|
|
# if row_size is small.
|
|
yield pd.DataFrame(
|
|
{
|
|
"one": [
|
|
np.random.bytes(self.row_size)
|
|
for _ in range(self.num_rows_per_batch)
|
|
]
|
|
}
|
|
)
|
|
elif self.use_arrow:
|
|
batch = {
|
|
"one": np.ones(
|
|
(self.num_rows_per_batch, self.row_size), dtype=np.uint8
|
|
)
|
|
}
|
|
block = ArrowBlockBuilder._table_from_pydict(batch)
|
|
yield block
|
|
else:
|
|
yield pd.DataFrame(
|
|
{
|
|
"one": [
|
|
np.array2string(np.ones(self.row_size, dtype=int))
|
|
for _ in range(self.num_rows_per_batch)
|
|
]
|
|
}
|
|
)
|
|
|
|
return self.num_tasks * [
|
|
ReadTask(
|
|
lambda: _blocks_generator(),
|
|
BlockMetadata(
|
|
num_rows=self.num_batches_per_task * self.num_rows_per_batch,
|
|
size_bytes=self.num_batches_per_task
|
|
* self.num_rows_per_batch
|
|
* self.row_size,
|
|
input_files=None,
|
|
exec_stats=None,
|
|
),
|
|
per_task_row_limit=per_task_row_limit,
|
|
)
|
|
]
|
|
|
|
def num_rows(self) -> int:
|
|
return self.num_tasks * self.num_batches_per_task * self.num_rows_per_batch
|
|
|
|
|
|
class SlowCSVDatasource(CSVDatasource):
|
|
def _read_stream(self, f: "pa.NativeFile", path: str):
|
|
for block in super()._read_stream(f, path):
|
|
time.sleep(3)
|
|
yield block
|
|
|
|
|
|
# Tests that we don't block on exponential rampup when doing bulk reads.
|
|
# https://github.com/ray-project/ray/issues/20625
|
|
@pytest.mark.parametrize("block_split", [False, True])
|
|
def test_bulk_lazy_eval_split_mode(shutdown_only, block_split, tmp_path):
|
|
# Defensively shutdown Ray for the first test here to make sure there
|
|
# is no existing Ray cluster.
|
|
ray.shutdown()
|
|
|
|
ray.init(num_cpus=8)
|
|
ctx = ray.data.context.DataContext.get_current()
|
|
|
|
ray.data.range(8, override_num_blocks=8).write_csv(str(tmp_path))
|
|
if not block_split:
|
|
# Setting a huge block size effectively disables block splitting.
|
|
ctx.target_max_block_size = 2**64
|
|
ds = ray.data.read_datasource(
|
|
SlowCSVDatasource(str(tmp_path)), override_num_blocks=8
|
|
)
|
|
|
|
start = time.time()
|
|
ds.map(lambda x: x)
|
|
delta = time.time() - start
|
|
|
|
print("full read time", delta)
|
|
# Should run in ~3 seconds. It takes >9 seconds if bulk read is broken.
|
|
assert delta < 8, delta
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"compute",
|
|
[
|
|
"tasks",
|
|
"actors",
|
|
],
|
|
)
|
|
def test_dataset(
|
|
shutdown_only,
|
|
restore_data_context,
|
|
compute,
|
|
):
|
|
def identity_fn(x):
|
|
return x
|
|
|
|
def empty_fn(x):
|
|
return {}
|
|
|
|
class IdentityClass:
|
|
def __call__(self, x):
|
|
return x
|
|
|
|
class EmptyClass:
|
|
def __call__(self, x):
|
|
return {}
|
|
|
|
ctx = ray.data.DataContext.get_current()
|
|
# 1MiB.
|
|
ctx.target_max_block_size = 1024 * 1024
|
|
|
|
if compute == "tasks":
|
|
compute = ray.data._internal.compute.TaskPoolStrategy()
|
|
identity_func = identity_fn
|
|
empty_func = empty_fn
|
|
func_name = "identity_fn"
|
|
task_name = f"ReadRandomBytes->MapBatches({func_name})"
|
|
else:
|
|
compute = ray.data.ActorPoolStrategy()
|
|
identity_func = IdentityClass
|
|
empty_func = EmptyClass
|
|
func_name = "IdentityClass"
|
|
task_name = f"MapWorker(ReadRandomBytes->MapBatches({func_name})).submit"
|
|
|
|
ray.shutdown()
|
|
# We need at least 2 CPUs to run a actorpool streaming
|
|
ray.init(num_cpus=2, object_store_memory=1e9)
|
|
# Test 10 tasks, each task returning 10 blocks, each block has 1 row and each
|
|
# row has 1024 bytes.
|
|
num_blocks_per_task = 10
|
|
num_tasks = 10
|
|
|
|
@ray.remote
|
|
def warmup():
|
|
return np.zeros(ctx.target_max_block_size, dtype=np.uint8)
|
|
|
|
last_snapshot = get_initial_core_execution_metrics_snapshot()
|
|
ds = ray.data.read_datasource(
|
|
RandomBytesDatasource(
|
|
num_tasks=num_tasks,
|
|
num_batches_per_task=num_blocks_per_task,
|
|
row_size=ctx.target_max_block_size,
|
|
),
|
|
override_num_blocks=num_tasks,
|
|
)
|
|
# Note the following calls to ds will not fully execute it.
|
|
assert ds.schema() is not None
|
|
assert ds.count() == num_blocks_per_task * num_tasks
|
|
assert ds._logical_plan.initial_num_blocks() == num_tasks
|
|
last_snapshot = assert_core_execution_metrics_equals(
|
|
CoreExecutionMetrics(
|
|
task_count={
|
|
"ReadRandomBytes": lambda count: count <= num_tasks,
|
|
},
|
|
object_store_stats={
|
|
"cumulative_created_plasma_bytes": lambda count: True,
|
|
"cumulative_created_plasma_objects": lambda count: True,
|
|
},
|
|
),
|
|
last_snapshot,
|
|
)
|
|
|
|
# Too-large blocks will get split to respect target max block size.
|
|
map_ds = ds.map_batches(identity_func, compute=compute)
|
|
map_ds = map_ds.materialize()
|
|
num_blocks_expected = num_tasks * num_blocks_per_task
|
|
assert map_ds._logical_plan.initial_num_blocks() == num_blocks_expected
|
|
expected_actor_name = f"MapWorker(ReadRandomBytes->MapBatches({func_name}))"
|
|
assert_core_execution_metrics_equals(
|
|
CoreExecutionMetrics(
|
|
task_count={
|
|
f"{expected_actor_name}.__init__": lambda count: True,
|
|
f"{expected_actor_name}.get_location": lambda count: True,
|
|
task_name: num_tasks,
|
|
},
|
|
),
|
|
last_snapshot,
|
|
)
|
|
assert_blocks_expected_in_plasma(
|
|
last_snapshot,
|
|
num_blocks_expected,
|
|
block_size_expected=ctx.target_max_block_size,
|
|
)
|
|
|
|
# Blocks smaller than requested batch size will get coalesced.
|
|
map_ds = ds.map_batches(
|
|
empty_func,
|
|
batch_size=num_blocks_per_task * num_tasks,
|
|
compute=compute,
|
|
)
|
|
map_ds = map_ds.materialize()
|
|
assert map_ds._logical_plan.initial_num_blocks() == 1
|
|
map_ds = ds.map(identity_func, compute=compute)
|
|
map_ds = map_ds.materialize()
|
|
assert map_ds._logical_plan.initial_num_blocks() == num_blocks_per_task * num_tasks
|
|
|
|
ds_list = ds.split(5)
|
|
assert len(ds_list) == 5
|
|
for new_ds in ds_list:
|
|
assert (
|
|
new_ds._logical_plan.initial_num_blocks()
|
|
== num_blocks_per_task * num_tasks / 5
|
|
)
|
|
|
|
train, test = ds.train_test_split(test_size=0.25)
|
|
assert (
|
|
train._logical_plan.initial_num_blocks()
|
|
== num_blocks_per_task * num_tasks * 0.75
|
|
)
|
|
assert (
|
|
test._logical_plan.initial_num_blocks()
|
|
== num_blocks_per_task * num_tasks * 0.25
|
|
)
|
|
|
|
new_ds = ds.union(ds, ds)
|
|
assert new_ds._logical_plan.initial_num_blocks() == num_tasks * 3
|
|
new_ds = new_ds.materialize()
|
|
assert (
|
|
new_ds._logical_plan.initial_num_blocks() == num_blocks_per_task * num_tasks * 3
|
|
)
|
|
|
|
new_ds = ds.random_shuffle()
|
|
assert new_ds._logical_plan.initial_num_blocks() == num_tasks
|
|
new_ds = ds.randomize_block_order()
|
|
assert new_ds._logical_plan.initial_num_blocks() == num_tasks
|
|
assert ds.groupby("one").count().count() == num_blocks_per_task * num_tasks
|
|
|
|
new_ds = ds.zip(ds)
|
|
new_ds = new_ds.materialize()
|
|
assert new_ds._logical_plan.initial_num_blocks() == num_blocks_per_task * num_tasks
|
|
|
|
assert len(ds.take(5)) == 5
|
|
assert len(ds.take_all()) == num_blocks_per_task * num_tasks
|
|
for batch in ds.iter_batches(batch_size=10):
|
|
assert len(batch["one"]) == 10
|
|
|
|
|
|
def test_filter(ray_start_regular_shared, target_max_block_size):
|
|
# Test 10 tasks, each task returning 10 blocks, each block has 1 row and each
|
|
# row has 1024 bytes.
|
|
num_blocks_per_task = 10
|
|
block_size = 1024
|
|
|
|
ds = ray.data.read_datasource(
|
|
RandomBytesDatasource(
|
|
num_tasks=1,
|
|
num_batches_per_task=num_blocks_per_task,
|
|
row_size=block_size,
|
|
),
|
|
override_num_blocks=1,
|
|
)
|
|
|
|
ds = ds.filter(lambda _: True)
|
|
ds = ds.materialize()
|
|
assert ds.count() == num_blocks_per_task
|
|
assert ds._logical_plan.initial_num_blocks() == num_blocks_per_task
|
|
|
|
ds = ds.filter(lambda _: False)
|
|
ds = ds.materialize()
|
|
assert ds.count() == 0
|
|
assert ds._logical_plan.initial_num_blocks() == num_blocks_per_task
|
|
|
|
|
|
@pytest.mark.skip("Needs zero-copy optimization for read->map_batches.")
|
|
def test_read_large_data(ray_start_cluster):
|
|
# Test 20G input with single task
|
|
num_blocks_per_task = 20
|
|
block_size = 1024 * 1024 * 1024
|
|
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=1)
|
|
|
|
ray.init(cluster.address)
|
|
|
|
def foo(batch):
|
|
return pd.DataFrame({"one": [1]})
|
|
|
|
ds = ray.data.read_datasource(
|
|
RandomBytesDatasource(
|
|
num_tasks=1,
|
|
num_batches_per_task=num_blocks_per_task,
|
|
row_size=block_size,
|
|
),
|
|
override_num_blocks=1,
|
|
)
|
|
|
|
ds = ds.map_batches(foo, num_rows_per_batch=None)
|
|
assert ds.count() == num_blocks_per_task
|
|
|
|
|
|
def _test_write_large_data(
|
|
tmp_path, ext, write_fn, read_fn, use_bytes, write_kwargs=None
|
|
):
|
|
# Test 2G input with single task
|
|
num_blocks_per_task = 200
|
|
block_size = 10 * 1024 * 1024
|
|
|
|
ds = ray.data.read_datasource(
|
|
RandomBytesDatasource(
|
|
num_tasks=1,
|
|
num_batches_per_task=num_blocks_per_task,
|
|
row_size=block_size,
|
|
use_bytes=use_bytes,
|
|
),
|
|
override_num_blocks=1,
|
|
)
|
|
|
|
# This should succeed without OOM.
|
|
# https://github.com/ray-project/ray/pull/37966.
|
|
out_dir = os.path.join(tmp_path, ext)
|
|
write_kwargs = {} if write_kwargs is None else write_kwargs
|
|
write_fn(ds, out_dir, **write_kwargs)
|
|
|
|
# Make sure we can read out a record.
|
|
if read_fn is not None:
|
|
assert read_fn(out_dir).count() == num_blocks_per_task
|
|
|
|
|
|
def test_write_large_data_parquet(shutdown_only, tmp_path):
|
|
_test_write_large_data(
|
|
tmp_path,
|
|
"parquet",
|
|
Dataset.write_parquet,
|
|
ray.data.read_parquet,
|
|
use_bytes=True,
|
|
)
|
|
|
|
|
|
def test_write_large_data_json(shutdown_only, tmp_path):
|
|
_test_write_large_data(
|
|
tmp_path, "json", Dataset.write_json, ray.data.read_json, use_bytes=False
|
|
)
|
|
|
|
|
|
def test_write_large_data_numpy(shutdown_only, tmp_path):
|
|
_test_write_large_data(
|
|
tmp_path,
|
|
"numpy",
|
|
Dataset.write_numpy,
|
|
ray.data.read_numpy,
|
|
use_bytes=False,
|
|
write_kwargs={"column": "one"},
|
|
)
|
|
|
|
|
|
def test_write_large_data_csv(shutdown_only, tmp_path):
|
|
_test_write_large_data(
|
|
tmp_path, "csv", Dataset.write_csv, ray.data.read_csv, use_bytes=False
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.version_info >= (3, 12),
|
|
reason="Skip due to incompatibility tensorflow with Python 3.12+",
|
|
)
|
|
def test_write_large_data_tfrecords(shutdown_only, tmp_path):
|
|
_test_write_large_data(
|
|
tmp_path,
|
|
"tfrecords",
|
|
Dataset.write_tfrecords,
|
|
ray.data.read_tfrecords,
|
|
use_bytes=True,
|
|
)
|
|
|
|
|
|
def test_write_large_data_webdataset(shutdown_only, tmp_path):
|
|
_test_write_large_data(
|
|
tmp_path,
|
|
"webdataset",
|
|
Dataset.write_webdataset,
|
|
ray.data.read_webdataset,
|
|
use_bytes=True,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TestCase:
|
|
target_max_block_size: int
|
|
batch_size: int
|
|
num_batches: int
|
|
expected_num_blocks: int
|
|
|
|
|
|
TEST_CASES = [
|
|
# Don't create blocks smaller than 50%.
|
|
TestCase(
|
|
target_max_block_size=1024,
|
|
batch_size=int(1024 * 1.125),
|
|
num_batches=1,
|
|
expected_num_blocks=1,
|
|
),
|
|
# Split blocks larger than 150% the target block size.
|
|
TestCase(
|
|
target_max_block_size=1024,
|
|
batch_size=int(1024 * 1.8),
|
|
num_batches=1,
|
|
expected_num_blocks=2,
|
|
),
|
|
# Huge batch will get split into multiple blocks.
|
|
TestCase(
|
|
target_max_block_size=1024,
|
|
batch_size=int(1024 * 10.125),
|
|
num_batches=1,
|
|
expected_num_blocks=10,
|
|
),
|
|
# Different batch sizes but same total size should produce a similar number
|
|
# of blocks.
|
|
TestCase(
|
|
target_max_block_size=1024,
|
|
batch_size=int(1024 * 1.5),
|
|
num_batches=4,
|
|
expected_num_blocks=6,
|
|
),
|
|
TestCase(
|
|
target_max_block_size=1024,
|
|
batch_size=int(1024 * 0.75),
|
|
num_batches=8,
|
|
expected_num_blocks=6,
|
|
),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"target_max_block_size,batch_size,num_batches,expected_num_blocks",
|
|
[astuple(test) for test in TEST_CASES],
|
|
)
|
|
def test_block_slicing(
|
|
ray_start_regular_shared,
|
|
restore_data_context,
|
|
target_max_block_size,
|
|
batch_size,
|
|
num_batches,
|
|
expected_num_blocks,
|
|
):
|
|
ctx = ray.data.context.DataContext.get_current()
|
|
ctx.target_max_block_size = target_max_block_size
|
|
|
|
# Row sizes smaller than this seem to add significant amounts of per-row
|
|
# metadata overhead.
|
|
row_size = 128
|
|
num_rows_per_batch = int(batch_size / row_size)
|
|
num_tasks = 1
|
|
|
|
ds = ray.data.read_datasource(
|
|
RandomBytesDatasource(
|
|
num_tasks=num_tasks,
|
|
num_batches_per_task=num_batches,
|
|
num_rows_per_batch=num_rows_per_batch,
|
|
row_size=row_size,
|
|
use_bytes=False,
|
|
use_arrow=True,
|
|
),
|
|
override_num_blocks=num_tasks,
|
|
).materialize()
|
|
assert ds._logical_plan.initial_num_blocks() == expected_num_blocks
|
|
|
|
block_sizes = []
|
|
num_rows = 0
|
|
for batch in ds.iter_batches(batch_size=None, batch_format="numpy"):
|
|
block_sizes.append(batch["one"].size)
|
|
num_rows += len(batch["one"])
|
|
assert num_rows == num_rows_per_batch * num_batches
|
|
for size in block_sizes:
|
|
# Blocks are not too big.
|
|
assert (
|
|
size <= target_max_block_size * ray.data.context.MAX_SAFE_BLOCK_SIZE_FACTOR
|
|
)
|
|
# Blocks are not too small.
|
|
assert size >= target_max_block_size / 2
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"target_max_block_size",
|
|
[128, 256, 512],
|
|
)
|
|
def test_dynamic_block_split_deterministic(
|
|
ray_start_regular_shared, target_max_block_size
|
|
):
|
|
# Tests the determinism of block splitting.
|
|
TEST_ITERATIONS = 10
|
|
ctx = ray.data.DataContext.get_current()
|
|
ctx.target_max_block_size = target_max_block_size
|
|
|
|
# ~800 bytes per block
|
|
ds = ray.data.range(1000, override_num_blocks=10).map_batches(lambda x: x)
|
|
data = [ray.get(block) for block in ds.materialize()._cache._bundle.block_refs]
|
|
# Maps: first item of block -> block
|
|
block_map = {block["id"][0]: block for block in data}
|
|
# Iterate over multiple executions of the dataset,
|
|
# and check that blocks were split in the same way
|
|
for _ in range(TEST_ITERATIONS):
|
|
data = [ray.get(block) for block in ds.materialize()._cache._bundle.block_refs]
|
|
for block in data:
|
|
assert block_map[block["id"][0]] == block
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|