blob: 0d131fd1f79fd8ee22a43919f079f9d0c58624f1 [file]
################################################################################
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
################################################################################
import types
import unittest
from unittest import mock
import pyarrow as pa
import pytest
pytest.importorskip("ray")
from pypaimon.ray.data_evolution_merge_into import (
_estimate_merge_input_size_bytes,
)
from pypaimon.ray.partitioning import (
_estimate_dataset_num_rows,
_estimate_dataset_size_bytes,
_resolve_num_partitions,
_resolve_row_id_num_partitions,
)
class RayPartitioningTest(unittest.TestCase):
def test_explicit_num_partitions_is_unchanged(self):
with mock.patch(
"ray.cluster_resources",
side_effect=AssertionError("must not inspect the cluster"),
):
self.assertEqual(_resolve_num_partitions(37, 1), 37)
def test_known_size_uses_target_block_size_and_cpu_cap(self):
from ray.data.context import DataContext
context = DataContext.get_current()
previous_target = context.target_max_block_size
context.target_max_block_size = 128 * 1024 * 1024
try:
with mock.patch(
"ray.cluster_resources", return_value={"CPU": 320}
):
self.assertEqual(_resolve_num_partitions(None, 0), 1)
self.assertEqual(
_resolve_num_partitions(None, 5 * 128 * 1024 * 1024),
5,
)
self.assertEqual(
_resolve_num_partitions(None, 641 * 128 * 1024 * 1024),
640,
)
# At the same average row width, a 10x larger input gets
# proportionally more partitions instead of both using 640.
self.assertEqual(
_resolve_num_partitions(None, 5_000_000 * 1024),
39,
)
self.assertEqual(
_resolve_num_partitions(None, 50_000_000 * 1024),
382,
)
self.assertEqual(
_resolve_num_partitions(None, 1, min_partitions=25),
25,
)
self.assertEqual(
_resolve_num_partitions(None, 1, min_partitions=200),
200,
)
finally:
context.target_max_block_size = previous_target
def test_known_size_uses_dataset_context(self):
import ray
from ray.data.context import DataContext
context = DataContext.get_current()
previous_target = context.target_max_block_size
try:
context.target_max_block_size = 128 * 1024 * 1024
data_context = ray.data.from_items([1]).context
context.target_max_block_size = 1024 * 1024 * 1024
with mock.patch(
"ray.cluster_resources", return_value={"CPU": 320}
):
self.assertEqual(
_resolve_num_partitions(
None,
512 * 1024 * 1024,
data_context=data_context,
),
4,
)
finally:
context.target_max_block_size = previous_target
def test_unknown_size_keeps_cpu_default(self):
with mock.patch(
"ray.cluster_resources", return_value={"CPU": 320}
):
self.assertEqual(_resolve_num_partitions(None, None), 640)
self.assertEqual(
_resolve_num_partitions(
None,
None,
unknown_num_partitions=200,
),
200,
)
def test_dataset_estimate_does_not_call_size_bytes(self):
metadata = types.SimpleNamespace(size_bytes=1234)
dataset = types.SimpleNamespace(
_logical_plan=types.SimpleNamespace(
dag=types.SimpleNamespace(
infer_metadata=mock.Mock(return_value=metadata)
)
),
size_bytes=mock.Mock(
side_effect=AssertionError("must not execute Dataset")
),
)
self.assertEqual(_estimate_dataset_size_bytes(dataset), 1234)
dataset.size_bytes.assert_not_called()
def test_unknown_dataset_estimate_falls_back(self):
metadata = types.SimpleNamespace(size_bytes=None)
dataset = types.SimpleNamespace(
_logical_plan=types.SimpleNamespace(
dag=types.SimpleNamespace(
infer_metadata=mock.Mock(return_value=metadata)
)
)
)
self.assertIsNone(_estimate_dataset_size_bytes(dataset))
def test_ray_metadata_respects_transform_cardinality(self):
import inspect
import ray
dataset = ray.data.from_arrow(pa.table({"id": [1, 2]}))
self.assertEqual(_estimate_dataset_size_bytes(dataset), 16)
self.assertEqual(_estimate_dataset_num_rows(dataset), 2)
mapped = dataset.map_batches(lambda batch: batch)
self.assertIsNone(_estimate_dataset_size_bytes(mapped))
cardinality_parameter = inspect.signature(
ray.data.Dataset.map_batches
).parameters.get("udf_modifying_row_count")
modifies_by_default = (
True
if cardinality_parameter is None
else cardinality_parameter.default
)
expected_rows = None if modifies_by_default else 2
self.assertEqual(_estimate_dataset_num_rows(mapped), expected_rows)
if "udf_modifying_row_count" in inspect.signature(
ray.data.Dataset.map_batches
).parameters:
one_to_one = dataset.map_batches(
lambda batch: batch,
udf_modifying_row_count=False,
)
self.assertIsNone(_estimate_dataset_size_bytes(one_to_one))
self.assertEqual(_estimate_dataset_num_rows(one_to_one), 2)
widened = dataset.map_batches(
lambda batch: pa.table({
"payload": ["x" * 1_000_000] * batch.num_rows,
}),
batch_format="pyarrow",
udf_modifying_row_count=False,
)
self.assertIsNone(_estimate_dataset_size_bytes(widened))
self.assertEqual(_estimate_dataset_num_rows(widened), 2)
self.assertGreater(widened.materialize().size_bytes(), 1_000_000)
def test_legacy_map_batches_cardinality_is_unknown(self):
dependency = types.SimpleNamespace(
infer_metadata=mock.Mock(
return_value=types.SimpleNamespace(num_rows=2)
),
can_modify_num_rows=None,
)
class MapBatches:
input_dependencies = (dependency,)
@staticmethod
def infer_metadata():
return types.SimpleNamespace(num_rows=None)
@property
def can_modify_num_rows(self):
return False
dataset = types.SimpleNamespace(
_logical_plan=types.SimpleNamespace(dag=MapBatches())
)
self.assertIsNone(_estimate_dataset_num_rows(dataset))
MapBatches.can_modify_num_rows = staticmethod(lambda: False)
self.assertIsNone(_estimate_dataset_num_rows(dataset))
def test_ray_legacy_map_batches_cardinality_is_unknown(self):
import inspect
import ray
dataset = ray.data.from_arrow(pa.table({"seed": [0]})).map_batches(
lambda _: pa.table({"id": list(range(500))}),
batch_format="pyarrow",
)
operator = dataset._logical_plan.dag
descriptor = inspect.getattr_static(
type(operator), "can_modify_num_rows", None
)
if not isinstance(descriptor, property):
self.skipTest("Ray does not use legacy MapBatches cardinality")
operator._input_dependencies = [
types.SimpleNamespace(
infer_metadata=mock.Mock(
return_value=types.SimpleNamespace(num_rows=1)
),
can_modify_num_rows=None,
)
]
self.assertIsNone(_estimate_dataset_num_rows(dataset))
def test_sparse_row_ids_keep_shuffle_parallelism(self):
with mock.patch(
"ray.cluster_resources", return_value={"CPU": 320}
):
self.assertEqual(
_resolve_row_id_num_partitions(None, 4096, 500, 500),
200,
)
self.assertEqual(
_resolve_row_id_num_partitions(None, None, None, 500),
200,
)
self.assertEqual(
_resolve_row_id_num_partitions(None, 32, 1, 500),
1,
)
self.assertEqual(
_resolve_row_id_num_partitions(37, 1, 500, 500),
37,
)
def test_row_id_sizing_uses_dataset_context(self):
data_context = types.SimpleNamespace(
target_max_block_size=128 * 1024 * 1024,
default_hash_shuffle_parallelism=25,
)
with mock.patch(
"ray.cluster_resources", return_value={"CPU": 320}
), mock.patch(
"ray.data.context.DataContext.get_current",
side_effect=AssertionError("must use the Dataset context"),
):
self.assertEqual(
_resolve_row_id_num_partitions(
None,
512 * 1024 * 1024,
None,
500,
data_context=data_context,
),
25,
)
def test_merge_estimate_uses_only_reliable_source_size(self):
ctx = types.SimpleNamespace(
is_self_merge=False,
)
with mock.patch(
"pypaimon.ray.data_evolution_merge_into."
"_estimate_dataset_size_bytes",
return_value=100,
):
result = _estimate_merge_input_size_bytes(
object(), ctx,
)
self.assertEqual(result, 100)
def test_merge_estimate_falls_back_when_source_size_is_unknown(self):
ctx = types.SimpleNamespace(is_self_merge=False)
with mock.patch(
"pypaimon.ray.data_evolution_merge_into."
"_estimate_dataset_size_bytes",
return_value=None,
):
result = _estimate_merge_input_size_bytes(
object(), ctx,
)
self.assertIsNone(result)
if __name__ == "__main__":
unittest.main()