239 lines
7.3 KiB
Python
239 lines
7.3 KiB
Python
import os
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pyarrow as pa
|
|
import pytest
|
|
|
|
import ray
|
|
from ray.data._internal.arrow_ops.transform_pyarrow import (
|
|
MIN_PYARROW_VERSION_TYPE_PROMOTION,
|
|
)
|
|
from ray.data._internal.tensor_extensions.arrow import (
|
|
ArrowTensorTypeV2,
|
|
FixedShapeTensorFormat,
|
|
)
|
|
from ray.data._internal.utils.arrow_utils import get_pyarrow_version
|
|
from ray.data.context import DataContext
|
|
from ray.data.extensions import (
|
|
ArrowConversionError,
|
|
ArrowPythonObjectType,
|
|
ArrowTensorArray,
|
|
ArrowTensorType,
|
|
)
|
|
|
|
|
|
def test_convert_to_pyarrow(ray_start_regular_shared, tmp_path):
|
|
ds = ray.data.range(100)
|
|
path = os.path.join(tmp_path, "test_parquet_dir")
|
|
os.mkdir(path)
|
|
ds.write_parquet(path)
|
|
assert ray.data.read_parquet(path).count() == 100
|
|
|
|
|
|
def test_pyarrow(ray_start_regular_shared):
|
|
ds = ray.data.range(5)
|
|
assert ds.map(lambda x: {"b": x["id"] + 2}).take() == [
|
|
{"b": 2},
|
|
{"b": 3},
|
|
{"b": 4},
|
|
{"b": 5},
|
|
{"b": 6},
|
|
]
|
|
assert ds.map(lambda x: {"b": x["id"] + 2}).filter(
|
|
lambda x: x["b"] % 2 == 0
|
|
).take() == [{"b": 2}, {"b": 4}, {"b": 6}]
|
|
assert ds.filter(lambda x: x["id"] == 0).flat_map(
|
|
lambda x: [{"b": x["id"] + 2}, {"b": x["id"] + 20}]
|
|
).take() == [{"b": 2}, {"b": 20}]
|
|
|
|
|
|
def _create_dataset(op, data):
|
|
ds = ray.data.range(2, override_num_blocks=2)
|
|
|
|
if op == "map":
|
|
|
|
def map(x):
|
|
return {
|
|
"id": x["id"],
|
|
"my_data": data[x["id"]],
|
|
}
|
|
|
|
ds = ds.map(map)
|
|
else:
|
|
assert op == "map_batches"
|
|
|
|
def map_batches(x):
|
|
row_id = x["id"][0]
|
|
return {
|
|
"id": x["id"],
|
|
"my_data": [data[row_id]],
|
|
}
|
|
|
|
ds = ds.map_batches(map_batches, batch_size=None)
|
|
|
|
# Needed for the map_batches case to trigger the error,
|
|
# because the error happens when merging the blocks.
|
|
ds = ds.map_batches(lambda x: x, batch_size=2)
|
|
return ds
|
|
|
|
|
|
def test_map_batches_fallback_to_pandas_on_incompatible_data(
|
|
ray_start_regular_shared,
|
|
restore_data_context,
|
|
):
|
|
# For map_batches, if the first UDF output is incompatible with Arrow,
|
|
# Ray Data will fall back to using Pandas.
|
|
class UnsupportedType:
|
|
pass
|
|
|
|
data = [UnsupportedType(), 1]
|
|
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
|
|
ds = _create_dataset("map_batches", data)
|
|
ds = ds.materialize()
|
|
bundles = ds.iter_internal_ref_bundles()
|
|
block = ray.get(next(bundles).block_refs[0])
|
|
assert isinstance(block, pd.DataFrame)
|
|
|
|
|
|
def test_map_raises_on_incompatible_data(
|
|
ray_start_regular_shared,
|
|
restore_data_context,
|
|
):
|
|
# For row-based map, the output buffer builds Arrow blocks eagerly, so
|
|
# incompatible data raises ArrowConversionError when object fallback is disabled.
|
|
class UnsupportedType:
|
|
pass
|
|
|
|
data = [UnsupportedType(), 1]
|
|
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
|
|
ds = _create_dataset("map", data)
|
|
with pytest.raises(ArrowConversionError):
|
|
ds.materialize()
|
|
|
|
|
|
_PYARROW_SUPPORTS_TYPE_PROMOTION = (
|
|
get_pyarrow_version() >= MIN_PYARROW_VERSION_TYPE_PROMOTION
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"op, data, should_fail, expected_type",
|
|
[
|
|
# Case A: Upon serializing to Arrow fallback to `ArrowPythonObjectType`
|
|
("map_batches", [1, 2**100], False, ArrowPythonObjectType()),
|
|
("map_batches", [1.0, 2**100], False, ArrowPythonObjectType()),
|
|
("map_batches", ["1.0", 2**100], False, ArrowPythonObjectType()),
|
|
# Case B: No fallback to `ArrowPythonObjectType`, but type promotion allows
|
|
# int to be promoted to a double
|
|
(
|
|
"map_batches",
|
|
[1.0, 2**4],
|
|
not _PYARROW_SUPPORTS_TYPE_PROMOTION,
|
|
pa.float64(),
|
|
),
|
|
# Case C: No fallback to `ArrowPythonObjectType` and no type promotion possible
|
|
("map_batches", ["1.0", 2**4], True, None),
|
|
],
|
|
)
|
|
def test_pyarrow_conversion_error_handling(
|
|
ray_start_regular_shared,
|
|
op,
|
|
data,
|
|
should_fail: bool,
|
|
expected_type: pa.DataType,
|
|
):
|
|
# Ray Data infers the block type (arrow or pandas) and the block schema
|
|
# based on the first *block* produced by UDF.
|
|
#
|
|
# These tests simulate following scenarios
|
|
# 1. (Case A) Type of the value of the first block is deduced as Arrow scalar
|
|
# type, but second block carries value that overflows pa.int64 representation,
|
|
# and column henceforth will be serialized as `ArrowPythonObjectExtensionType`
|
|
# coercing first block to it as well
|
|
# 2. (Case B) Both blocks carry proper Arrow scalars which, however, have
|
|
# diverging types and therefore Arrow fails during merging of these blocks
|
|
# into 1
|
|
ds = _create_dataset(op, data)
|
|
|
|
if should_fail:
|
|
with pytest.raises(Exception) as e:
|
|
ds.materialize()
|
|
|
|
error_msg = str(e.value)
|
|
expected_msg = "ArrowConversionError: Error converting data to Arrow:"
|
|
|
|
assert expected_msg in error_msg
|
|
assert "my_data" in error_msg
|
|
|
|
else:
|
|
ds.materialize()
|
|
|
|
assert ds.schema().base_schema == pa.schema(
|
|
[pa.field("id", pa.int64()), pa.field("my_data", expected_type)]
|
|
)
|
|
|
|
results = sorted(ds.take_all(), key=lambda r: r["id"])
|
|
assert results == [{"id": i, "my_data": data[i]} for i in range(len(data))]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"tensor_format", [FixedShapeTensorFormat.V1, FixedShapeTensorFormat.V2]
|
|
)
|
|
@pytest.mark.skipif(
|
|
get_pyarrow_version() < MIN_PYARROW_VERSION_TYPE_PROMOTION,
|
|
reason="Requires Arrow version of at least 14.0.0",
|
|
)
|
|
def test_concat_with_mixed_tensor_types_and_native_pyarrow_types(tensor_format_context):
|
|
tensor_format = tensor_format_context
|
|
num_rows = 1024
|
|
|
|
# Block A: int is uint64; tensor = Ray tensor extension
|
|
t_uint = pa.table(
|
|
{
|
|
"int": pa.array(np.zeros(num_rows // 2, dtype=np.uint64), type=pa.uint64()),
|
|
"tensor": ArrowTensorArray.from_numpy(
|
|
np.zeros((num_rows // 2, 3, 3), dtype=np.float32)
|
|
),
|
|
}
|
|
)
|
|
|
|
# Block B: int is float64 with NaNs; tensor = same extension type
|
|
f = np.ones(num_rows // 2, dtype=np.float64)
|
|
f[::8] = np.nan
|
|
t_float = pa.table(
|
|
{
|
|
"int": pa.array(f, type=pa.float64()),
|
|
"tensor": ArrowTensorArray.from_numpy(
|
|
np.zeros((num_rows // 2, 3, 3), dtype=np.float32)
|
|
),
|
|
}
|
|
)
|
|
|
|
# Two input blocks with different Arrow dtypes for "int"
|
|
ds = ray.data.from_arrow([t_uint, t_float])
|
|
|
|
# Force a concat across blocks
|
|
ds = ds.repartition(1)
|
|
|
|
# This should not raise: RuntimeError: Types mismatch: double != uint64
|
|
ds.materialize()
|
|
|
|
# Ensure that the result is correct
|
|
# Determine expected tensor type based on current DataContext setting
|
|
if tensor_format == FixedShapeTensorFormat.V2:
|
|
expected_tensor_type = ArrowTensorTypeV2((3, 3), pa.float32())
|
|
else:
|
|
expected_tensor_type = ArrowTensorType((3, 3), pa.float32())
|
|
|
|
assert ds.schema().base_schema == pa.schema(
|
|
[("int", pa.float64()), ("tensor", expected_tensor_type)]
|
|
)
|
|
assert ds.count() == num_rows
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|