chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
import json
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import zlib
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray._common.test_utils import wait_for_condition
|
||||
from ray.cluster_utils import Cluster, cluster_not_supported
|
||||
from ray.tests.test_object_spilling import is_dir_empty
|
||||
|
||||
# Note: Disk write speed can be as low as 6 MiB/s in AWS Mac instances, so we have to
|
||||
# increase the timeout.
|
||||
pytestmark = [pytest.mark.timeout(900 if platform.system() == "Darwin" else 180)]
|
||||
|
||||
|
||||
@pytest.mark.skipif(platform.system() in ["Windows"], reason="Failing on Windows.")
|
||||
def test_multiple_directories(tmp_path, shutdown_only):
|
||||
num_dirs = 3
|
||||
temp_dirs = []
|
||||
for i in range(num_dirs):
|
||||
temp_folder = tmp_path / f"spill_{i}"
|
||||
temp_folder.mkdir()
|
||||
temp_dirs.append(temp_folder)
|
||||
|
||||
# Limit our object store to 75 MiB of memory.
|
||||
min_spilling_size = 0
|
||||
object_spilling_config = json.dumps(
|
||||
{
|
||||
"type": "filesystem",
|
||||
"params": {"directory_path": [str(directory) for directory in temp_dirs]},
|
||||
}
|
||||
)
|
||||
ray_context = ray.init(
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={
|
||||
"max_io_workers": 5,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
"min_spilling_size": min_spilling_size,
|
||||
},
|
||||
)
|
||||
|
||||
arr = np.ones(74 * 1024 * 1024, dtype=np.uint8) # 74MB.
|
||||
object_refs = []
|
||||
# Now the storage is full.
|
||||
object_refs.append(ray.put(arr))
|
||||
|
||||
num_object_spilled = 20
|
||||
for _ in range(num_object_spilled):
|
||||
object_refs.append(ray.put(arr))
|
||||
|
||||
num_files = defaultdict(int)
|
||||
for temp_dir in temp_dirs:
|
||||
# Under temp_dir there are spilled_objects_dir(s) with name pattern
|
||||
# "ray_spilled_objects[_<node_id>]", each containing spilled object files.
|
||||
for spilled_objects_dir in temp_folder.iterdir():
|
||||
for path in spilled_objects_dir.iterdir():
|
||||
num_files[str(temp_folder)] += 1
|
||||
|
||||
for ref in object_refs:
|
||||
assert np.array_equal(ray.get(ref), arr)
|
||||
|
||||
print("Check distribution...")
|
||||
min_count = 5
|
||||
is_distributed = [n_files >= min_count for n_files in num_files.values()]
|
||||
assert all(is_distributed)
|
||||
|
||||
print("Check deletion...")
|
||||
# Empty object refs.
|
||||
object_refs = []
|
||||
# Add a new small object so that the last entry is evicted and we don't
|
||||
# exceed the spill threshold.
|
||||
ref = ray.put(np.ones(5 * 1024 * 1024, dtype=np.uint8))
|
||||
for temp_dir in temp_dirs:
|
||||
temp_folder = temp_dir
|
||||
wait_for_condition(lambda: is_dir_empty(temp_folder, ray_context["node_id"]))
|
||||
|
||||
# Now kill ray and see all directories are deleted.
|
||||
print("Check directories are deleted...")
|
||||
ray.shutdown()
|
||||
for temp_dir in temp_dirs:
|
||||
wait_for_condition(lambda: is_dir_empty(temp_dir, ray_context["node_id"]))
|
||||
|
||||
|
||||
def _check_spilled(num_objects_spilled=0):
|
||||
def ok():
|
||||
s = ray._private.internal_api.memory_summary(stats_only=True)
|
||||
if num_objects_spilled == 0:
|
||||
return "Spilled " not in s
|
||||
|
||||
m = re.search(r"Spilled (\d+) MiB, (\d+) objects", s)
|
||||
if m is not None:
|
||||
actual_num_objects = int(m.group(2))
|
||||
return actual_num_objects >= num_objects_spilled
|
||||
|
||||
return False
|
||||
|
||||
wait_for_condition(ok, timeout=90, retry_interval_ms=5000)
|
||||
|
||||
|
||||
def _test_object_spilling_threshold(thres, num_objects, num_objects_spilled):
|
||||
try:
|
||||
ray.init(
|
||||
object_store_memory=2_200_000_000,
|
||||
_system_config={"object_spilling_threshold": thres} if thres else {},
|
||||
)
|
||||
objs = []
|
||||
for _ in range(num_objects):
|
||||
objs.append(ray.put(np.empty(200_000_000, dtype=np.uint8)))
|
||||
if num_objects_spilled == 0:
|
||||
time.sleep(10) # Wait for spilling to happen
|
||||
_check_spilled(num_objects_spilled)
|
||||
finally:
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.skipif(platform.system() != "Linux", reason="Failing on Windows/macOS.")
|
||||
def test_object_spilling_threshold_default():
|
||||
_test_object_spilling_threshold(None, 10, 5)
|
||||
|
||||
|
||||
@pytest.mark.skipif(platform.system() != "Linux", reason="Failing on Windows/macOS.")
|
||||
def test_object_spilling_threshold_1_0():
|
||||
_test_object_spilling_threshold(1.0, 10, 0)
|
||||
|
||||
|
||||
@pytest.mark.skipif(platform.system() != "Linux", reason="Failing on Windows/macOS.")
|
||||
def test_object_spilling_threshold_0_1():
|
||||
_test_object_spilling_threshold(0.1, 10, 5)
|
||||
|
||||
|
||||
def test_partial_retval_allocation(ray_start_cluster_enabled):
|
||||
cluster = ray_start_cluster_enabled
|
||||
cluster.add_node(object_store_memory=100 * 1024 * 1024)
|
||||
ray.init(cluster.address)
|
||||
|
||||
@ray.remote(num_returns=4)
|
||||
def f():
|
||||
return [np.zeros(50 * 1024 * 1024, dtype=np.uint8) for _ in range(4)]
|
||||
|
||||
ret = f.remote()
|
||||
for obj in ret:
|
||||
obj = ray.get(obj)
|
||||
print(obj.size)
|
||||
|
||||
|
||||
def test_pull_spilled_object(
|
||||
ray_start_cluster_enabled, multi_node_object_spilling_config, shutdown_only
|
||||
):
|
||||
cluster = ray_start_cluster_enabled
|
||||
object_spilling_config, _ = multi_node_object_spilling_config
|
||||
|
||||
# Head node.
|
||||
cluster.add_node(
|
||||
num_cpus=1,
|
||||
resources={"custom": 0},
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={
|
||||
"max_io_workers": 2,
|
||||
"min_spilling_size": 1 * 1024 * 1024,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
},
|
||||
)
|
||||
ray.init(cluster.address)
|
||||
|
||||
# add 1 worker node
|
||||
cluster.add_node(
|
||||
num_cpus=1, resources={"custom": 1}, object_store_memory=75 * 1024 * 1024
|
||||
)
|
||||
cluster.wait_for_nodes()
|
||||
|
||||
@ray.remote(num_cpus=1, resources={"custom": 1})
|
||||
def create_objects():
|
||||
results = []
|
||||
for size in range(5):
|
||||
arr = np.random.rand(size * 1024 * 1024)
|
||||
hash_value = zlib.crc32(arr.tobytes())
|
||||
results.append([ray.put(arr), hash_value])
|
||||
# ensure the objects are spilled
|
||||
arr = np.random.rand(5 * 1024 * 1024)
|
||||
ray.get(ray.put(arr))
|
||||
ray.get(ray.put(arr))
|
||||
return results
|
||||
|
||||
@ray.remote(num_cpus=1, resources={"custom": 0})
|
||||
def get_object(arr):
|
||||
return zlib.crc32(arr.tobytes())
|
||||
|
||||
results = ray.get(create_objects.remote())
|
||||
for value_ref, hash_value in results:
|
||||
hash_value1 = ray.get(get_object.remote(value_ref))
|
||||
assert hash_value == hash_value1
|
||||
|
||||
|
||||
# TODO(chenshen): fix error handling when spilled file
|
||||
# missing/corrupted
|
||||
@pytest.mark.skipif(True, reason="Currently hangs.")
|
||||
def test_pull_spilled_object_failure(object_spilling_config, ray_start_cluster):
|
||||
object_spilling_config, temp_folder = object_spilling_config
|
||||
cluster = ray_start_cluster
|
||||
|
||||
# Head node.
|
||||
cluster.add_node(
|
||||
num_cpus=1,
|
||||
resources={"custom": 0},
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={
|
||||
"max_io_workers": 2,
|
||||
"min_spilling_size": 1 * 1024 * 1024,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
},
|
||||
)
|
||||
ray.init(cluster.address)
|
||||
|
||||
# add 1 worker node
|
||||
cluster.add_node(
|
||||
num_cpus=1, resources={"custom": 1}, object_store_memory=75 * 1024 * 1024
|
||||
)
|
||||
cluster.wait_for_nodes()
|
||||
|
||||
@ray.remote(num_cpus=1, resources={"custom": 1})
|
||||
def create_objects():
|
||||
arr = np.random.rand(5 * 1024 * 1024)
|
||||
hash_value = zlib.crc32(arr.tobytes())
|
||||
results = [ray.put(arr), hash_value]
|
||||
# ensure the objects are spilled
|
||||
arr = np.random.rand(5 * 1024 * 1024)
|
||||
ray.get(ray.put(arr))
|
||||
ray.get(ray.put(arr))
|
||||
return results
|
||||
|
||||
@ray.remote(num_cpus=1, resources={"custom": 0})
|
||||
def get_object(arr):
|
||||
return zlib.crc32(arr.tobytes())
|
||||
|
||||
[ref, hash_value] = ray.get(create_objects.remote())
|
||||
|
||||
# remove spilled file
|
||||
shutil.rmtree(temp_folder)
|
||||
|
||||
hash_value1 = ray.get(get_object.remote(ref))
|
||||
assert hash_value == hash_value1
|
||||
|
||||
|
||||
@pytest.mark.xfail(cluster_not_supported, reason="cluster not supported")
|
||||
def test_spill_dir_cleanup_on_node_removal(fs_only_object_spilling_config):
|
||||
object_spilling_config, temp_folder = fs_only_object_spilling_config
|
||||
cluster = Cluster()
|
||||
cluster.add_node(
|
||||
num_cpus=0,
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={"object_spilling_config": object_spilling_config},
|
||||
)
|
||||
ray.init(address=cluster.address)
|
||||
node2 = cluster.add_node(num_cpus=1, object_store_memory=75 * 1024 * 1024)
|
||||
|
||||
# This task will run on node 2 because node 1 has no CPU resource
|
||||
@ray.remote(num_cpus=1)
|
||||
def run_workload():
|
||||
ids = []
|
||||
for _ in range(2):
|
||||
arr = np.random.rand(5 * 1024 * 1024) # 40 MB
|
||||
ids.append(ray.put(arr))
|
||||
return ids
|
||||
|
||||
ids = ray.get(run_workload.remote())
|
||||
node2_id = node2.node_id
|
||||
assert not is_dir_empty(temp_folder, node2_id)
|
||||
|
||||
# Kill node 2
|
||||
cluster.remove_node(node2)
|
||||
|
||||
# Verify that the spill folder is cleaned up upon node removal
|
||||
assert is_dir_empty(temp_folder, node2_id)
|
||||
|
||||
# We hold the object refs to prevent them from being deleted
|
||||
# due to out of scope.
|
||||
del ids
|
||||
ray.shutdown()
|
||||
cluster.shutdown()
|
||||
|
||||
|
||||
def test_spill_deadlock(object_spilling_config, shutdown_only):
|
||||
object_spilling_config, _ = object_spilling_config
|
||||
# Limit our object store to 75 MiB of memory.
|
||||
ray.init(
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={
|
||||
"max_io_workers": 1,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
"min_spilling_size": 0,
|
||||
},
|
||||
)
|
||||
arr = np.random.rand(1024 * 1024) # 8 MB data
|
||||
replay_buffer = []
|
||||
|
||||
# Create objects of more than 400 MiB.
|
||||
for _ in range(50):
|
||||
ref = None
|
||||
while ref is None:
|
||||
ref = ray.put(arr)
|
||||
replay_buffer.append(ref)
|
||||
# This is doing random sampling with 50% prob.
|
||||
if random.randint(0, 9) < 5:
|
||||
for _ in range(5):
|
||||
ref = random.choice(replay_buffer)
|
||||
sample = ray.get(ref, timeout=None)
|
||||
assert np.array_equal(sample, arr)
|
||||
|
||||
|
||||
def test_spill_reconstruction_errors(ray_start_cluster, object_spilling_config):
|
||||
config = {
|
||||
"health_check_failure_threshold": 10,
|
||||
"health_check_period_ms": 100,
|
||||
"health_check_initial_delay_ms": 0,
|
||||
"max_direct_call_object_size": 100,
|
||||
"task_retry_delay_ms": 100,
|
||||
"object_timeout_milliseconds": 200,
|
||||
}
|
||||
cluster = ray_start_cluster
|
||||
# Head node with no resources.
|
||||
cluster.add_node(num_cpus=0, _system_config=config, object_store_memory=10**8)
|
||||
ray.init(address=cluster.address)
|
||||
# Node to place the initial object.
|
||||
node_to_kill = cluster.add_node(num_cpus=1, object_store_memory=10**8)
|
||||
|
||||
@ray.remote
|
||||
def put():
|
||||
return np.zeros(10**5, dtype=np.uint8)
|
||||
|
||||
@ray.remote
|
||||
def check(x):
|
||||
return
|
||||
|
||||
ref = put.remote()
|
||||
for _ in range(4):
|
||||
ray.get(check.remote(ref))
|
||||
cluster.remove_node(node_to_kill, allow_graceful=False)
|
||||
node_to_kill = cluster.add_node(num_cpus=1, object_store_memory=10**8)
|
||||
|
||||
# All reconstruction attempts used up. The object's value should now be an
|
||||
# error in the local store.
|
||||
# Force object spilling and check that it can complete.
|
||||
xs = []
|
||||
for _ in range(20):
|
||||
xs.append(ray.put(np.zeros(10**7, dtype=np.uint8)))
|
||||
for x in xs:
|
||||
ray.get(x, timeout=10)
|
||||
|
||||
with pytest.raises(ray.exceptions.ObjectLostError):
|
||||
ray.get(ref)
|
||||
|
||||
|
||||
def test_evict_secondary_copies_before_spill(ray_start_cluster, object_spilling_config):
|
||||
cluster = ray_start_cluster
|
||||
cluster.add_node(num_cpus=1, object_store_memory=10**8)
|
||||
ray.init(address=cluster.address)
|
||||
for _ in range(3):
|
||||
cluster.add_node(num_cpus=1, object_store_memory=10**8)
|
||||
wait_for_condition(lambda: ray.cluster_resources()["CPU"] >= 4)
|
||||
|
||||
# Spread data onto all nodes.
|
||||
@ray.remote
|
||||
def gen():
|
||||
time.sleep(0.5)
|
||||
return np.ones(10 * 1024 * 1024, dtype=np.uint8)
|
||||
|
||||
refs = [
|
||||
gen.options(scheduling_strategy="SPREAD").remote() for _ in range(16)
|
||||
] # 160MiB
|
||||
|
||||
# Iterate over the data on the worker nodes from the head node.
|
||||
for i in range(10):
|
||||
for j, r in enumerate(refs):
|
||||
print("Iteration", i, j)
|
||||
ray.get(r)
|
||||
|
||||
_check_spilled()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
sys.exit(pytest.main(["-sv", __file__]))
|
||||
Reference in New Issue
Block a user