807 lines
27 KiB
Python
807 lines
27 KiB
Python
import copy
|
|
import json
|
|
import os
|
|
import platform
|
|
import random
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import ray
|
|
import ray.remote_function
|
|
from ray._common.test_utils import wait_for_condition
|
|
from ray._private.external_storage import (
|
|
ExternalStorageSmartOpenImpl,
|
|
FileSystemStorage,
|
|
_get_unique_spill_filename,
|
|
create_url_with_offset,
|
|
parse_url_with_offset,
|
|
)
|
|
from ray._private.internal_api import memory_summary
|
|
from ray.tests.conftest import (
|
|
buffer_object_spilling_config,
|
|
file_system_object_spilling_config,
|
|
mock_distributed_fs_object_spilling_config,
|
|
)
|
|
|
|
import psutil
|
|
|
|
# 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)]
|
|
|
|
|
|
def run_basic_workload():
|
|
"""Run the workload that requires spilling."""
|
|
arr = np.random.rand(5 * 1024 * 1024) # 40 MB
|
|
refs = []
|
|
refs.append([ray.put(arr) for _ in range(2)])
|
|
ray.get(ray.put(arr))
|
|
|
|
|
|
def is_dir_empty(temp_folder, node_id, append_path=True):
|
|
"""Test if directory temp_folder/f"{DEFAULT_OBJECT_PREFIX}_{node_id}" is empty.
|
|
For file based spilling, this is where the objects are spilled.
|
|
For other use cases, specify append_path as False so that temp_folder itself
|
|
is tested for emptiness.
|
|
"""
|
|
# append_path is used because the file based spilling will append
|
|
# new directory path.
|
|
num_files = 0
|
|
if append_path:
|
|
temp_folder = (
|
|
temp_folder
|
|
/ f"{ray._private.ray_constants.DEFAULT_OBJECT_PREFIX}_{node_id}"
|
|
)
|
|
if not temp_folder.exists():
|
|
return True
|
|
for path in temp_folder.iterdir():
|
|
num_files += 1
|
|
return num_files == 0
|
|
|
|
|
|
@pytest.mark.skipif(platform.system() == "Windows", reason="Doesn't support Windows.")
|
|
def test_spill_file_uniqueness(shutdown_only):
|
|
ray_context = ray.init(num_cpus=0, object_store_memory=75 * 1024 * 1024)
|
|
arr = np.random.rand(128 * 1024) # 1 MB
|
|
refs = []
|
|
refs.append([ray.put(arr)])
|
|
|
|
# for the same object_ref, generating spill urls 10 times yields
|
|
# 10 different urls
|
|
spill_url_set = {_get_unique_spill_filename(refs) for _ in range(10)}
|
|
assert len(spill_url_set) == 10
|
|
|
|
for StorageType in [FileSystemStorage, ExternalStorageSmartOpenImpl]:
|
|
with patch.object(
|
|
StorageType, "_get_objects_from_store"
|
|
) as mock_get_objects_from_store:
|
|
mock_get_objects_from_store.return_value = [
|
|
(b"somedata", b"metadata", None)
|
|
]
|
|
storage = StorageType(ray_context["node_id"], "/tmp")
|
|
spilled_url_set = {
|
|
storage.spill_objects(refs, [b"localhost"])[0] for _ in range(10)
|
|
}
|
|
assert len(spilled_url_set) == 10
|
|
|
|
|
|
def test_invalid_config_raises_exception(shutdown_only):
|
|
# Make sure ray.init raises an exception before
|
|
# it starts processes when invalid object spilling
|
|
# config is given.
|
|
with pytest.raises(ValueError):
|
|
ray.init(
|
|
_system_config={
|
|
"object_spilling_config": json.dumps({"type": "abc"}),
|
|
}
|
|
)
|
|
|
|
with pytest.raises(Exception):
|
|
copied_config = copy.deepcopy(file_system_object_spilling_config)
|
|
# Add invalid params to the config.
|
|
copied_config["params"].update({"random_arg": "abc"})
|
|
ray.init(
|
|
_system_config={
|
|
"object_spilling_config": json.dumps(copied_config),
|
|
}
|
|
)
|
|
|
|
with pytest.raises(Exception):
|
|
copied_config = copy.deepcopy(file_system_object_spilling_config)
|
|
# Add invalid value type to the config.
|
|
copied_config["params"].update({"buffer_size": "abc"})
|
|
ray.init(
|
|
_system_config={
|
|
"object_spilling_config": json.dumps(copied_config),
|
|
}
|
|
)
|
|
|
|
|
|
def test_url_generation_and_parse():
|
|
url = "s3://abc/def/ray_good"
|
|
offset = 10
|
|
size = 30
|
|
url_with_offset = create_url_with_offset(url=url, offset=offset, size=size)
|
|
parsed_result = parse_url_with_offset(url_with_offset)
|
|
assert parsed_result.base_url == url
|
|
assert parsed_result.offset == offset
|
|
assert parsed_result.size == size
|
|
|
|
|
|
def test_default_config(shutdown_only):
|
|
ray_context = ray.init(num_cpus=0, object_store_memory=75 * 1024 * 1024)
|
|
# Make sure the object spilling configuration is properly set.
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == "filesystem"
|
|
assert (
|
|
config["params"]["directory_path"]
|
|
== ray._private.worker._global_node._session_dir
|
|
)
|
|
|
|
# Make sure the spill directory is empty before running the workload.
|
|
assert is_dir_empty(
|
|
Path(ray._private.worker._global_node._session_dir), ray_context["node_id"]
|
|
)
|
|
|
|
# Make sure the basic workload can succeed and the spill directory is not empty.
|
|
run_basic_workload()
|
|
assert not is_dir_empty(
|
|
Path(ray._private.worker._global_node._session_dir), ray_context["node_id"]
|
|
)
|
|
ray.shutdown()
|
|
|
|
# Make sure config is not initalized if spilling is not enabled..
|
|
ray.init(
|
|
num_cpus=0,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={
|
|
"automatic_object_spilling_enabled": False,
|
|
"object_store_full_delay_ms": 100,
|
|
},
|
|
)
|
|
assert "object_spilling_config" not in ray._private.worker._global_node._config
|
|
run_basic_workload()
|
|
ray.shutdown()
|
|
|
|
# Make sure when we use a different config, it is reflected.
|
|
ray.init(
|
|
num_cpus=0,
|
|
_system_config={
|
|
"object_spilling_config": (
|
|
json.dumps(mock_distributed_fs_object_spilling_config)
|
|
)
|
|
},
|
|
)
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == "mock_distributed_fs"
|
|
|
|
|
|
def test_default_config_buffering(shutdown_only):
|
|
ray.init(
|
|
num_cpus=0,
|
|
_system_config={
|
|
"object_spilling_config": (json.dumps(buffer_object_spilling_config))
|
|
},
|
|
)
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == buffer_object_spilling_config["type"]
|
|
assert (
|
|
config["params"]["buffer_size"]
|
|
== buffer_object_spilling_config["params"]["buffer_size"]
|
|
)
|
|
|
|
|
|
def test_default_config_cluster(ray_start_cluster_enabled):
|
|
cluster = ray_start_cluster_enabled
|
|
cluster.add_node(num_cpus=0)
|
|
ray.init(cluster.address)
|
|
worker_nodes = []
|
|
worker_nodes.append(
|
|
cluster.add_node(num_cpus=1, object_store_memory=75 * 1024 * 1024)
|
|
)
|
|
cluster.wait_for_nodes()
|
|
|
|
# Run the basic spilling workload on both
|
|
# worker nodes and make sure they are working.
|
|
@ray.remote
|
|
def task():
|
|
arr = np.random.rand(5 * 1024 * 1024) # 40 MB
|
|
refs = []
|
|
refs.append([ray.put(arr) for _ in range(2)])
|
|
ray.get(ray.put(arr))
|
|
|
|
ray.get([task.remote() for _ in range(2)])
|
|
|
|
|
|
def test_default_config_cluster_with_different_temp_dir(ray_start_cluster_enabled):
|
|
cluster = ray_start_cluster_enabled
|
|
nodes = []
|
|
nodes.append(
|
|
cluster.add_node(
|
|
num_cpus=1,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
temp_dir="/tmp/spill_dir_0",
|
|
)
|
|
)
|
|
nodes.append(
|
|
cluster.add_node(
|
|
num_cpus=1,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
temp_dir="/tmp/spill_dir_1",
|
|
)
|
|
)
|
|
ray.init(cluster.address)
|
|
cluster.wait_for_nodes()
|
|
|
|
# Make sure the spill directory is empty before running `the workload.
|
|
for node in nodes:
|
|
assert is_dir_empty(Path(node._session_dir), node.node_id)
|
|
|
|
# Run spilling workload on both nodes
|
|
@ray.remote
|
|
def task():
|
|
arr = np.random.rand(5 * 1024 * 1024) # 40 MB
|
|
refs = []
|
|
refs.append([ray.put(arr) for _ in range(2)])
|
|
return refs
|
|
|
|
tasks = [
|
|
task.options(label_selector={"ray.io/node-id": node.node_id}).remote()
|
|
for node in nodes
|
|
]
|
|
res = ray.get(tasks)
|
|
# Make sure the spill directory is not empty
|
|
for node in nodes:
|
|
wait_for_condition(
|
|
lambda node=node: not is_dir_empty(Path(node._session_dir), node.node_id)
|
|
)
|
|
|
|
# We hold the object refs until the end to prevent them from being deleted
|
|
# due to out of scope.
|
|
del res
|
|
|
|
|
|
def test_custom_spill_dir_env_var(shutdown_only):
|
|
os.environ["RAY_object_spilling_directory"] = "/tmp/custom_spill_dir"
|
|
ray_context = ray.init(num_cpus=0, object_store_memory=75 * 1024 * 1024)
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == "filesystem"
|
|
assert config["params"]["directory_path"] == "/tmp/custom_spill_dir"
|
|
|
|
# Make sure the spill directory is empty before running the workload.
|
|
assert is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
# Make sure the basic workload can succeed and the spill directory is not empty.
|
|
run_basic_workload()
|
|
assert not is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
|
|
def test_custom_spill_dir_system_config(shutdown_only):
|
|
ray_context = ray.init(
|
|
num_cpus=0,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={"object_spilling_directory": "/tmp/custom_spill_dir"},
|
|
)
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == "filesystem"
|
|
assert config["params"]["directory_path"] == "/tmp/custom_spill_dir"
|
|
|
|
# Make sure the spill directory is empty before running the workload.
|
|
assert is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
# Make sure the basic workload can succeed and the spill directory is not empty.
|
|
run_basic_workload()
|
|
assert not is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
|
|
def test_custom_spill_dir(shutdown_only):
|
|
# Make sure the object spilling directory can be set by the user
|
|
ray_context = ray.init(
|
|
object_spilling_directory="/tmp/custom_spill_dir",
|
|
num_cpus=0,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
)
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == "filesystem"
|
|
assert config["params"]["directory_path"] == "/tmp/custom_spill_dir"
|
|
|
|
# Make sure the spill directory is empty before running the workload.
|
|
assert is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
# Make sure the basic workload can succeed and the spill directory is not empty.
|
|
run_basic_workload()
|
|
assert not is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"call_ray_start",
|
|
[
|
|
"ray start --head --object-spilling-directory=/tmp/custom_spill_dir --num-cpus 0 --object-store-memory 78643200"
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_custom_spill_dir_cli(call_ray_start, shutdown_only):
|
|
ray_context = ray.init(address=call_ray_start)
|
|
|
|
# Make sure the spill directory is empty before running the workload.
|
|
assert is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
# Make sure the basic workload can succeed and the spill directory is not empty.
|
|
run_basic_workload()
|
|
assert not is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
|
|
def test_custom_spill_dir_set_ray_params_and_system_config(shutdown_only):
|
|
# Set directory in both ray params and system config.
|
|
# ray params should take precedence.
|
|
ray_context = ray.init(
|
|
object_spilling_directory="/tmp/custom_spill_dir",
|
|
num_cpus=0,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={"object_spilling_directory": "/tmp/custom_spill_dir2"},
|
|
)
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == "filesystem"
|
|
assert config["params"]["directory_path"] == "/tmp/custom_spill_dir"
|
|
|
|
# Make sure the spill directory is empty before running the workload.
|
|
assert is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
run_basic_workload()
|
|
# Make sure the spill directory is not empty after running the workload.
|
|
assert not is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
|
|
def test_custom_spill_dir_set_system_config_and_env_var(shutdown_only):
|
|
# Set directory in both system config and env var.
|
|
# system config should take precedence.
|
|
os.environ["RAY_object_spilling_directory"] = "/tmp/custom_spill_dir2"
|
|
ray_context = ray.init(
|
|
num_cpus=0,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={"object_spilling_directory": "/tmp/custom_spill_dir"},
|
|
)
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == "filesystem"
|
|
assert config["params"]["directory_path"] == "/tmp/custom_spill_dir"
|
|
|
|
# Make sure the spill directory is empty before running the workload.
|
|
assert is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
run_basic_workload()
|
|
# Make sure the spill directory is not empty after running the workload.
|
|
assert not is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
|
|
def test_set_custom_spill_dir_in_env_var_and_spill_config_in_system_config(
|
|
shutdown_only,
|
|
):
|
|
# Set directory in env var and object spilling config in system config.
|
|
# the directory in env var should take precedence.
|
|
os.environ["RAY_object_spilling_directory"] = "/tmp/custom_spill_dir"
|
|
ray_context = ray.init(
|
|
num_cpus=0,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={
|
|
"object_spilling_config": json.dumps(file_system_object_spilling_config)
|
|
},
|
|
)
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == "filesystem"
|
|
assert config["params"]["directory_path"] == "/tmp/custom_spill_dir"
|
|
|
|
# Make sure the spill directory is empty before running the workload.
|
|
assert is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
run_basic_workload()
|
|
# Make sure the spill directory is not empty after running the workload.
|
|
assert not is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
|
|
def test_set_object_spilling_config_in_system_config_and_env_var(shutdown_only):
|
|
# Set object spilling config in both system config and env var.
|
|
# the object spilling config in system config should take precedence.
|
|
custom_object_spilling_config = {
|
|
"type": "filesystem",
|
|
"params": {"directory_path": "/tmp/custom_spill_dir"},
|
|
}
|
|
ray_context = ray.init(
|
|
num_cpus=0,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={
|
|
"object_spilling_config": json.dumps(custom_object_spilling_config)
|
|
},
|
|
)
|
|
os.environ["RAY_object_spilling_config"] = json.dumps(
|
|
file_system_object_spilling_config
|
|
)
|
|
config = json.loads(
|
|
ray._private.worker._global_node._config["object_spilling_config"]
|
|
)
|
|
assert config["type"] == "filesystem"
|
|
assert config["params"]["directory_path"] == "/tmp/custom_spill_dir"
|
|
|
|
# Make sure the spill directory is empty before running the workload.
|
|
assert is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
run_basic_workload()
|
|
# Make sure the spill directory is not empty after running the workload.
|
|
assert not is_dir_empty(Path("/tmp/custom_spill_dir"), ray_context["node_id"])
|
|
|
|
|
|
def test_node_id_in_spill_dir_name():
|
|
node_id = ray.NodeID.from_random().hex()
|
|
session_dir = "test_session_dir"
|
|
storage = ray._private.external_storage.setup_external_storage(
|
|
file_system_object_spilling_config, node_id, session_dir
|
|
)
|
|
|
|
dir_prefix = ray._private.ray_constants.DEFAULT_OBJECT_PREFIX
|
|
expected_dir_name = f"{dir_prefix}_{node_id}"
|
|
for path in storage._directory_paths:
|
|
dir_name = os.path.basename(path)
|
|
assert dir_name == expected_dir_name
|
|
|
|
# Clean up
|
|
storage.destroy_external_storage()
|
|
|
|
|
|
@pytest.mark.skipif(platform.system() == "Windows", reason="Hangs on Windows.")
|
|
def test_spilling_not_done_for_pinned_object(object_spilling_config, shutdown_only):
|
|
# Limit our object store to 75 MiB of memory.
|
|
object_spilling_config, temp_folder = object_spilling_config
|
|
ray_context = ray.init(
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={
|
|
"max_io_workers": 4,
|
|
"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(5 * 1024 * 1024) # 40 MB
|
|
ref = ray.get(ray.put(arr)) # noqa
|
|
ref2 = ray.put(arr) # noqa
|
|
|
|
print(type(temp_folder))
|
|
wait_for_condition(lambda: is_dir_empty(temp_folder, ray_context["node_id"]))
|
|
|
|
|
|
def test_spill_remote_object(
|
|
ray_start_cluster_enabled, multi_node_object_spilling_config
|
|
):
|
|
cluster = ray_start_cluster_enabled
|
|
object_spilling_config, _ = multi_node_object_spilling_config
|
|
cluster.add_node(
|
|
num_cpus=0,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={
|
|
"automatic_object_spilling_enabled": True,
|
|
"object_store_full_delay_ms": 100,
|
|
"max_io_workers": 4,
|
|
"object_spilling_config": object_spilling_config,
|
|
"min_spilling_size": 0,
|
|
},
|
|
)
|
|
ray.init(address=cluster.address)
|
|
cluster.add_node(object_store_memory=75 * 1024 * 1024)
|
|
cluster.wait_for_nodes()
|
|
|
|
@ray.remote
|
|
def put():
|
|
return np.random.rand(5 * 1024 * 1024) # 40 MB data
|
|
|
|
@ray.remote
|
|
def depends(arg):
|
|
return
|
|
|
|
ref = put.remote()
|
|
copy = np.copy(ray.get(ref))
|
|
# Evict local copy.
|
|
ray.put(np.random.rand(5 * 1024 * 1024)) # 40 MB data
|
|
# Remote copy should cause first remote object to get spilled.
|
|
ray.get(put.remote())
|
|
|
|
sample = ray.get(ref)
|
|
assert np.array_equal(sample, copy)
|
|
# Evict the spilled object.
|
|
del sample
|
|
ray.get(put.remote())
|
|
ray.put(np.random.rand(5 * 1024 * 1024)) # 40 MB data
|
|
|
|
# Test passing the spilled object as an arg to another task.
|
|
ray.get(depends.remote(ref))
|
|
|
|
|
|
@pytest.mark.skipif(platform.system() == "Windows", reason="Hangs on Windows.")
|
|
def test_spill_objects_automatically(fs_only_object_spilling_config, shutdown_only):
|
|
# Limit our object store to 75 MiB of memory.
|
|
object_spilling_config, _ = fs_only_object_spilling_config
|
|
ray.init(
|
|
num_cpus=1,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={
|
|
"max_io_workers": 4,
|
|
"automatic_object_spilling_enabled": True,
|
|
"object_store_full_delay_ms": 100,
|
|
"object_spilling_config": object_spilling_config,
|
|
"min_spilling_size": 0,
|
|
},
|
|
)
|
|
replay_buffer = []
|
|
solution_buffer = []
|
|
buffer_length = 100
|
|
|
|
# Create objects of more than 800 MiB.
|
|
for _ in range(buffer_length):
|
|
ref = None
|
|
while ref is None:
|
|
multiplier = random.choice([1, 2, 3])
|
|
arr = np.random.rand(multiplier * 1024 * 1024)
|
|
ref = ray.put(arr)
|
|
replay_buffer.append(ref)
|
|
solution_buffer.append(arr)
|
|
print("spill done.")
|
|
# randomly sample objects
|
|
for _ in range(1000):
|
|
index = random.choice(list(range(buffer_length)))
|
|
ref = replay_buffer[index]
|
|
solution = solution_buffer[index]
|
|
sample = ray.get(ref, timeout=None)
|
|
assert np.array_equal(sample, solution)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
platform.system() in ["Darwin"],
|
|
reason="Very flaky on OSX.",
|
|
)
|
|
def test_unstable_spill_objects_automatically(unstable_spilling_config, shutdown_only):
|
|
# Limit our object store to 75 MiB of memory.
|
|
object_spilling_config, _ = unstable_spilling_config
|
|
ray.init(
|
|
num_cpus=1,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={
|
|
"max_io_workers": 4,
|
|
"automatic_object_spilling_enabled": True,
|
|
"object_store_full_delay_ms": 100,
|
|
"object_spilling_config": object_spilling_config,
|
|
"min_spilling_size": 0,
|
|
},
|
|
)
|
|
replay_buffer = []
|
|
solution_buffer = []
|
|
buffer_length = 20
|
|
|
|
# Each object averages 16MiB => 320MiB total.
|
|
for _ in range(buffer_length):
|
|
multiplier = random.choice([1, 2, 3])
|
|
arr = np.random.rand(multiplier * 1024 * 1024)
|
|
ref = ray.put(arr)
|
|
replay_buffer.append(ref)
|
|
solution_buffer.append(arr)
|
|
print("spill done.")
|
|
# randomly sample objects
|
|
for _ in range(10):
|
|
index = random.choice(list(range(buffer_length)))
|
|
ref = replay_buffer[index]
|
|
solution = solution_buffer[index]
|
|
sample = ray.get(ref, timeout=None)
|
|
assert np.array_equal(sample, solution)
|
|
|
|
|
|
def test_slow_spill_objects_automatically(slow_spilling_config, shutdown_only):
|
|
# Limit our object store to 75 MiB of memory.
|
|
object_spilling_config, _ = slow_spilling_config
|
|
ray.init(
|
|
num_cpus=1,
|
|
object_store_memory=75 * 1024 * 1024,
|
|
_system_config={
|
|
"max_io_workers": 4,
|
|
"automatic_object_spilling_enabled": True,
|
|
"object_store_full_delay_ms": 100,
|
|
"object_spilling_config": object_spilling_config,
|
|
"min_spilling_size": 0,
|
|
},
|
|
)
|
|
replay_buffer = []
|
|
solution_buffer = []
|
|
buffer_length = 10
|
|
|
|
# Create objects of more than 800 MiB.
|
|
for _ in range(buffer_length):
|
|
ref = None
|
|
while ref is None:
|
|
multiplier = random.choice([1, 2, 3])
|
|
arr = np.random.rand(multiplier * 1024 * 1024)
|
|
ref = ray.put(arr)
|
|
replay_buffer.append(ref)
|
|
solution_buffer.append(arr)
|
|
print("spill done.")
|
|
# randomly sample objects
|
|
for _ in range(buffer_length):
|
|
index = random.choice(list(range(buffer_length)))
|
|
ref = replay_buffer[index]
|
|
solution = solution_buffer[index]
|
|
sample = ray.get(ref, timeout=None)
|
|
assert np.array_equal(sample, solution)
|
|
|
|
|
|
def test_spill_stats(object_spilling_config, shutdown_only):
|
|
# Limit our object store to 75 MiB of memory.
|
|
object_spilling_config, _ = object_spilling_config
|
|
address = ray.init(
|
|
num_cpus=1,
|
|
object_store_memory=100 * 1024 * 1024,
|
|
_system_config={
|
|
"automatic_object_spilling_enabled": True,
|
|
"max_io_workers": 100,
|
|
"min_spilling_size": 1,
|
|
"object_spilling_config": object_spilling_config,
|
|
},
|
|
)
|
|
|
|
@ray.remote
|
|
def f():
|
|
return np.zeros(50 * 1024 * 1024, dtype=np.uint8)
|
|
|
|
ids = []
|
|
for _ in range(4):
|
|
x = f.remote()
|
|
ids.append(x)
|
|
|
|
while ids:
|
|
print(ray.get(ids.pop()))
|
|
|
|
x_id = f.remote() # noqa
|
|
ray.get(x_id)
|
|
s = memory_summary(address=address["address"], stats_only=True)
|
|
assert "Plasma memory usage 50 MiB, 1 objects, 50.0% full" in s, s
|
|
assert "Spilled 200 MiB, 4 objects" in s, s
|
|
assert "Restored 150 MiB, 3 objects" in s, s
|
|
|
|
|
|
@pytest.mark.skipif(platform.system() == "Darwin", reason="Failing on macOS.")
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("is_async", [False, True])
|
|
async def test_spill_during_get(object_spilling_config, shutdown_only, is_async):
|
|
object_spilling_config, _ = object_spilling_config
|
|
ray.init(
|
|
num_cpus=1,
|
|
object_store_memory=100 * 1024 * 1024,
|
|
_system_config={
|
|
"automatic_object_spilling_enabled": True,
|
|
"object_store_full_delay_ms": 100,
|
|
"max_io_workers": 1,
|
|
"object_spilling_config": object_spilling_config,
|
|
"min_spilling_size": 0,
|
|
"worker_register_timeout_seconds": 600,
|
|
},
|
|
)
|
|
|
|
if is_async:
|
|
|
|
@ray.remote(num_cpus=0)
|
|
class Actor:
|
|
async def f(self):
|
|
return np.zeros(10 * 1024 * 1024)
|
|
|
|
else:
|
|
|
|
@ray.remote(num_cpus=0)
|
|
def f():
|
|
return np.zeros(10 * 1024 * 1024)
|
|
|
|
if is_async:
|
|
a = Actor.remote()
|
|
ids = []
|
|
for i in range(10):
|
|
if is_async:
|
|
x = a.f.remote()
|
|
else:
|
|
x = f.remote()
|
|
print(i, x)
|
|
ids.append(x)
|
|
|
|
start = datetime.now()
|
|
# Concurrent gets, which require restoring from external storage, while
|
|
# objects are being created.
|
|
for x in ids:
|
|
if is_async:
|
|
obj = await x
|
|
else:
|
|
obj = ray.get(x)
|
|
print(obj.shape)
|
|
del obj
|
|
|
|
timeout_seconds = 30
|
|
duration = datetime.now() - start
|
|
assert duration <= timedelta(
|
|
seconds=timeout_seconds
|
|
), "Concurrent gets took too long. Maybe IO workers are not started properly." # noqa: E501
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_regular",
|
|
[
|
|
{
|
|
"object_store_memory": 75 * 1024 * 1024,
|
|
"_system_config": {"max_io_workers": 1},
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_recover_from_spill_worker_failure(ray_start_regular):
|
|
@ray.remote
|
|
def f():
|
|
return np.zeros(50 * 1024 * 1024, dtype=np.uint8)
|
|
|
|
def _run_spilling_workload():
|
|
for obj_ref in [f.remote() for _ in range(5)]:
|
|
ray.get(obj_ref)
|
|
|
|
def get_spill_worker():
|
|
for proc in psutil.process_iter():
|
|
try:
|
|
name = ray._private.ray_constants.WORKER_PROCESS_TYPE_SPILL_WORKER_IDLE
|
|
if name in proc.name():
|
|
return proc
|
|
# for macOS
|
|
if proc.cmdline() and name in proc.cmdline()[0]:
|
|
return proc
|
|
# for Windows
|
|
if proc.cmdline() and "--worker-type=SPILL_WORKER" in proc.cmdline():
|
|
return proc
|
|
except psutil.AccessDenied:
|
|
pass
|
|
except psutil.NoSuchProcess:
|
|
pass
|
|
|
|
# Run a workload that forces spilling to occur.
|
|
_run_spilling_workload()
|
|
|
|
# Get the PID of the spill worker that was created and kill it.
|
|
spill_worker_proc = get_spill_worker()
|
|
assert spill_worker_proc
|
|
spill_worker_proc.kill()
|
|
spill_worker_proc.wait()
|
|
|
|
# Run the workload again and ensure that it succeeds.
|
|
_run_spilling_workload()
|
|
|
|
# Check that the spilled files are cleaned up after the workload finishes.
|
|
wait_for_condition(
|
|
lambda: is_dir_empty(
|
|
Path(ray._private.worker._global_node._session_dir),
|
|
ray.get_runtime_context().get_node_id(),
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|