mirror of
https://github.com/wassname/ray.git
synced 2026-07-09 04:00:44 +08:00
[Object Spilling] Implement level triggered logic to make streaming shuffle work + additional cleanup (#12773)
This commit is contained in:
+8
-14
@@ -638,9 +638,11 @@ cdef c_vector[c_string] spill_objects_handler(
|
||||
return return_urls
|
||||
|
||||
|
||||
cdef void restore_spilled_objects_handler(
|
||||
cdef int64_t restore_spilled_objects_handler(
|
||||
const c_vector[CObjectID]& object_ids_to_restore,
|
||||
const c_vector[c_string]& object_urls) nogil:
|
||||
cdef:
|
||||
int64_t bytes_restored = 0
|
||||
with gil:
|
||||
urls = []
|
||||
size = object_urls.size()
|
||||
@@ -651,7 +653,8 @@ cdef void restore_spilled_objects_handler(
|
||||
with ray.worker._changeproctitle(
|
||||
ray_constants.WORKER_PROCESS_TYPE_RESTORE_WORKER,
|
||||
ray_constants.WORKER_PROCESS_TYPE_RESTORE_WORKER_IDLE):
|
||||
external_storage.restore_spilled_objects(object_refs, urls)
|
||||
bytes_restored = external_storage.restore_spilled_objects(
|
||||
object_refs, urls)
|
||||
except Exception:
|
||||
exception_str = (
|
||||
"An unexpected internal error occurred while the IO worker "
|
||||
@@ -662,6 +665,7 @@ cdef void restore_spilled_objects_handler(
|
||||
"restore_spilled_objects_error",
|
||||
traceback.format_exc() + exception_str,
|
||||
job_id=None)
|
||||
return bytes_restored
|
||||
|
||||
|
||||
cdef void delete_spilled_objects_handler(
|
||||
@@ -873,7 +877,8 @@ cdef class CoreWorker:
|
||||
return self.plasma_event_handler
|
||||
|
||||
def get_objects(self, object_refs, TaskID current_task_id,
|
||||
int64_t timeout_ms=-1, plasma_objects_only=False):
|
||||
int64_t timeout_ms=-1,
|
||||
plasma_objects_only=False):
|
||||
cdef:
|
||||
c_vector[shared_ptr[CRayObject]] results
|
||||
CTaskID c_task_id = current_task_id.native()
|
||||
@@ -1573,17 +1578,6 @@ cdef class CoreWorker:
|
||||
resource_name.encode("ascii"), capacity,
|
||||
CNodeID.FromBinary(client_id.binary()))
|
||||
|
||||
def force_spill_objects(self, object_refs):
|
||||
cdef c_vector[CObjectID] object_ids
|
||||
object_ids = ObjectRefsToVector(object_refs)
|
||||
assert not RayConfig.instance().automatic_object_deletion_enabled(), (
|
||||
"Automatic object deletion is not supported for"
|
||||
"force_spill_objects yet. Please set"
|
||||
"automatic_object_deletion_enabled: False in Ray's system config.")
|
||||
with nogil:
|
||||
check_status(CCoreWorkerProcess.GetCoreWorker()
|
||||
.SpillObjects(object_ids))
|
||||
|
||||
cdef void async_set_result(shared_ptr[CRayObject] obj,
|
||||
CObjectID object_ref,
|
||||
void *future) with gil:
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from .dynamic_resources import set_resource
|
||||
from .object_spilling import force_spill_objects
|
||||
__all__ = [
|
||||
"set_resource",
|
||||
"force_spill_objects",
|
||||
]
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import ray
|
||||
|
||||
|
||||
def force_spill_objects(object_refs):
|
||||
"""Force spilling objects to external storage.
|
||||
|
||||
Args:
|
||||
object_refs: Object refs of the objects to be
|
||||
spilled.
|
||||
"""
|
||||
core_worker = ray.worker.global_worker.core_worker
|
||||
# Make sure that the values are object refs.
|
||||
for object_ref in object_refs:
|
||||
if not isinstance(object_ref, ray.ObjectRef):
|
||||
raise TypeError(
|
||||
f"Attempting to call `force_spill_objects` on the "
|
||||
f"value {object_ref}, which is not an ray.ObjectRef.")
|
||||
return core_worker.force_spill_objects(object_refs)
|
||||
@@ -157,12 +157,15 @@ class ExternalStorage(metaclass=abc.ABCMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def restore_spilled_objects(self, object_refs: List[ObjectRef],
|
||||
url_with_offset_list: List[str]):
|
||||
url_with_offset_list: List[str]) -> int:
|
||||
"""Restore objects from the external storage.
|
||||
|
||||
Args:
|
||||
object_refs: List of object IDs (note that it is not ref).
|
||||
url_with_offset_list: List of url_with_offset.
|
||||
|
||||
Returns:
|
||||
The total number of bytes restored.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -215,6 +218,7 @@ class FileSystemStorage(ExternalStorage):
|
||||
|
||||
def restore_spilled_objects(self, object_refs: List[ObjectRef],
|
||||
url_with_offset_list: List[str]):
|
||||
total = 0
|
||||
for i in range(len(object_refs)):
|
||||
object_ref = object_refs[i]
|
||||
url_with_offset = url_with_offset_list[i].decode()
|
||||
@@ -228,9 +232,11 @@ class FileSystemStorage(ExternalStorage):
|
||||
metadata_len = int.from_bytes(f.read(8), byteorder="little")
|
||||
buf_len = int.from_bytes(f.read(8), byteorder="little")
|
||||
self._size_check(metadata_len, buf_len, parsed_result.size)
|
||||
total += buf_len
|
||||
metadata = f.read(metadata_len)
|
||||
# read remaining data to our buffer
|
||||
self._put_object_to_store(metadata, buf_len, f, object_ref)
|
||||
return total
|
||||
|
||||
def delete_spilled_objects(self, urls: List[str]):
|
||||
for url in urls:
|
||||
@@ -297,6 +303,7 @@ class ExternalStorageSmartOpenImpl(ExternalStorage):
|
||||
def restore_spilled_objects(self, object_refs: List[ObjectRef],
|
||||
url_with_offset_list: List[str]):
|
||||
from smart_open import open
|
||||
total = 0
|
||||
for i in range(len(object_refs)):
|
||||
object_ref = object_refs[i]
|
||||
url_with_offset = url_with_offset_list[i].decode()
|
||||
@@ -315,9 +322,11 @@ class ExternalStorageSmartOpenImpl(ExternalStorage):
|
||||
metadata_len = int.from_bytes(f.read(8), byteorder="little")
|
||||
buf_len = int.from_bytes(f.read(8), byteorder="little")
|
||||
self._size_check(metadata_len, buf_len, parsed_result.size)
|
||||
total += buf_len
|
||||
metadata = f.read(metadata_len)
|
||||
# read remaining data to our buffer
|
||||
self._put_object_to_store(metadata, buf_len, f, object_ref)
|
||||
return total
|
||||
|
||||
def delete_spilled_objects(self, urls: List[str]):
|
||||
pass
|
||||
@@ -367,8 +376,8 @@ def restore_spilled_objects(object_refs: List[ObjectRef],
|
||||
object_refs: List of object IDs (note that it is not ref).
|
||||
url_with_offset_list: List of url_with_offset.
|
||||
"""
|
||||
_external_storage.restore_spilled_objects(object_refs,
|
||||
url_with_offset_list)
|
||||
return _external_storage.restore_spilled_objects(object_refs,
|
||||
url_with_offset_list)
|
||||
|
||||
|
||||
def delete_spilled_objects(urls: List[str]):
|
||||
|
||||
@@ -233,7 +233,7 @@ cdef extern from "ray/core_worker/core_worker.h" nogil:
|
||||
(CRayStatus() nogil) check_signals
|
||||
(void() nogil) gc_collect
|
||||
(c_vector[c_string](const c_vector[CObjectID] &) nogil) spill_objects
|
||||
(void(
|
||||
(int64_t(
|
||||
const c_vector[CObjectID] &,
|
||||
const c_vector[c_string] &) nogil) restore_spilled_objects
|
||||
(void(
|
||||
|
||||
@@ -23,7 +23,7 @@ def get_default_fixure_system_config():
|
||||
"object_timeout_milliseconds": 200,
|
||||
"num_heartbeats_timeout": 10,
|
||||
"object_store_full_max_retries": 3,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
}
|
||||
return system_config
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ import os
|
||||
import random
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import psutil
|
||||
import ray
|
||||
from ray.external_storage import (create_url_with_offset,
|
||||
parse_url_with_offset)
|
||||
@@ -43,57 +41,6 @@ def object_spilling_config(request, tmpdir):
|
||||
yield json.dumps(request.param)
|
||||
|
||||
|
||||
@pytest.mark.skip("This test is for local benchmark.")
|
||||
def test_sample_benchmark(object_spilling_config, shutdown_only):
|
||||
# --Config values--
|
||||
max_io_workers = 10
|
||||
object_store_limit = 500 * 1024 * 1024
|
||||
eight_mb = 1024 * 1024
|
||||
object_size = 12 * eight_mb
|
||||
spill_cnt = 50
|
||||
|
||||
# Limit our object store to 200 MiB of memory.
|
||||
ray.init(
|
||||
object_store_memory=object_store_limit,
|
||||
_system_config={
|
||||
"object_store_full_max_retries": 0,
|
||||
"max_io_workers": max_io_workers,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
"automatic_object_deletion_enabled": False,
|
||||
})
|
||||
arr = np.random.rand(object_size)
|
||||
replay_buffer = []
|
||||
pinned_objects = set()
|
||||
|
||||
# Create objects of more than 200 MiB.
|
||||
spill_start = time.perf_counter()
|
||||
for _ in range(spill_cnt):
|
||||
ref = None
|
||||
while ref is None:
|
||||
try:
|
||||
ref = ray.put(arr)
|
||||
replay_buffer.append(ref)
|
||||
pinned_objects.add(ref)
|
||||
except ray.exceptions.ObjectStoreFullError:
|
||||
ref_to_spill = pinned_objects.pop()
|
||||
ray.experimental.force_spill_objects([ref_to_spill])
|
||||
spill_end = time.perf_counter()
|
||||
|
||||
# Make sure to remove unpinned objects.
|
||||
del pinned_objects
|
||||
restore_start = time.perf_counter()
|
||||
while replay_buffer:
|
||||
ref = replay_buffer.pop()
|
||||
sample = ray.get(ref) # noqa
|
||||
restore_end = time.perf_counter()
|
||||
|
||||
print(f"Object spilling benchmark for the config {object_spilling_config}")
|
||||
print(f"Spilling {spill_cnt} number of objects of size {object_size}B "
|
||||
f"takes {spill_end - spill_start} seconds with {max_io_workers} "
|
||||
"number of io workers.")
|
||||
print(f"Getting all objects takes {restore_end - restore_start} seconds.")
|
||||
|
||||
|
||||
def test_invalid_config_raises_exception(shutdown_only):
|
||||
# Make sure ray.init raises an exception before
|
||||
# it starts processes when invalid object spilling
|
||||
@@ -127,123 +74,38 @@ def test_url_generation_and_parse():
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.system() == "Windows", reason="Failing on Windows.")
|
||||
def test_spill_objects_manually(object_spilling_config, shutdown_only):
|
||||
def test_spilling_not_done_for_pinned_object(tmp_path, shutdown_only):
|
||||
# Limit our object store to 75 MiB of memory.
|
||||
temp_folder = tmp_path / "spill"
|
||||
temp_folder.mkdir()
|
||||
ray.init(
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={
|
||||
"object_store_full_max_retries": 0,
|
||||
"automatic_object_spilling_enabled": False,
|
||||
"max_io_workers": 4,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_max_retries": 4,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": json.dumps({
|
||||
"type": "filesystem",
|
||||
"params": {
|
||||
"directory_path": str(temp_folder)
|
||||
}
|
||||
}),
|
||||
"min_spilling_size": 0,
|
||||
"automatic_object_deletion_enabled": False,
|
||||
})
|
||||
arr = np.random.rand(1024 * 1024) # 8 MB data
|
||||
replay_buffer = []
|
||||
pinned_objects = set()
|
||||
arr = np.random.rand(5 * 1024 * 1024) # 40 MB
|
||||
ref = ray.get(ray.put(arr)) # noqa
|
||||
# Since the ref exists, it should raise OOM.
|
||||
with pytest.raises(ray.exceptions.ObjectStoreFullError):
|
||||
ref2 = ray.put(arr) # noqa
|
||||
|
||||
# Create objects of more than 200 MiB.
|
||||
for _ in range(25):
|
||||
ref = None
|
||||
while ref is None:
|
||||
try:
|
||||
ref = ray.put(arr)
|
||||
replay_buffer.append(ref)
|
||||
pinned_objects.add(ref)
|
||||
except ray.exceptions.ObjectStoreFullError:
|
||||
ref_to_spill = pinned_objects.pop()
|
||||
ray.experimental.force_spill_objects([ref_to_spill])
|
||||
def is_dir_empty():
|
||||
num_files = 0
|
||||
for path in temp_folder.iterdir():
|
||||
num_files += 1
|
||||
return num_files == 0
|
||||
|
||||
def is_worker(cmdline):
|
||||
return cmdline and cmdline[0].startswith("ray::")
|
||||
|
||||
# Make sure io workers are spawned with proper name.
|
||||
processes = [
|
||||
x.cmdline()[0] for x in psutil.process_iter(attrs=["cmdline"])
|
||||
if is_worker(x.info["cmdline"])
|
||||
]
|
||||
assert (
|
||||
ray.ray_constants.WORKER_PROCESS_TYPE_SPILL_WORKER_IDLE in processes)
|
||||
|
||||
# Spill 2 more objects so we will always have enough space for
|
||||
# restoring objects back.
|
||||
refs_to_spill = (pinned_objects.pop(), pinned_objects.pop())
|
||||
ray.experimental.force_spill_objects(refs_to_spill)
|
||||
|
||||
# randomly sample objects
|
||||
for _ in range(100):
|
||||
ref = random.choice(replay_buffer)
|
||||
sample = ray.get(ref)
|
||||
assert np.array_equal(sample, arr)
|
||||
|
||||
# Make sure io workers are spawned with proper name.
|
||||
processes = [
|
||||
x.cmdline()[0] for x in psutil.process_iter(attrs=["cmdline"])
|
||||
if is_worker(x.info["cmdline"])
|
||||
]
|
||||
assert (
|
||||
ray.ray_constants.WORKER_PROCESS_TYPE_RESTORE_WORKER_IDLE in processes)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.system() == "Windows", reason="Failing on Windows.")
|
||||
def test_spill_objects_manually_from_workers(object_spilling_config,
|
||||
shutdown_only):
|
||||
# Limit our object store to 100 MiB of memory.
|
||||
ray.init(
|
||||
object_store_memory=100 * 1024 * 1024,
|
||||
_system_config={
|
||||
"object_store_full_max_retries": 0,
|
||||
"automatic_object_spilling_enabled": False,
|
||||
"max_io_workers": 4,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
"min_spilling_size": 0,
|
||||
"automatic_object_deletion_enabled": False,
|
||||
})
|
||||
|
||||
@ray.remote
|
||||
def _worker():
|
||||
arr = np.random.rand(1024 * 1024) # 8 MB data
|
||||
ref = ray.put(arr)
|
||||
ray.experimental.force_spill_objects([ref])
|
||||
return ref
|
||||
|
||||
# Create objects of more than 200 MiB.
|
||||
replay_buffer = [ray.get(_worker.remote()) for _ in range(25)]
|
||||
values = {ref: np.copy(ray.get(ref)) for ref in replay_buffer}
|
||||
# Randomly sample objects.
|
||||
for _ in range(100):
|
||||
ref = random.choice(replay_buffer)
|
||||
sample = ray.get(ref)
|
||||
assert np.array_equal(sample, values[ref])
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Not implemented yet.")
|
||||
def test_spill_objects_manually_with_workers(object_spilling_config,
|
||||
shutdown_only):
|
||||
# Limit our object store to 75 MiB of memory.
|
||||
ray.init(
|
||||
object_store_memory=100 * 1024 * 1024,
|
||||
_system_config={
|
||||
"object_store_full_max_retries": 0,
|
||||
"automatic_object_spilling_enabled": False,
|
||||
"max_io_workers": 4,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
"min_spilling_size": 0,
|
||||
"automatic_object_deletion_enabled": False,
|
||||
})
|
||||
arrays = [np.random.rand(100 * 1024) for _ in range(50)]
|
||||
objects = [ray.put(arr) for arr in arrays]
|
||||
|
||||
@ray.remote
|
||||
def _worker(object_refs):
|
||||
ray.experimental.force_spill_objects(object_refs)
|
||||
|
||||
ray.get([_worker.remote([o]) for o in objects])
|
||||
|
||||
for restored, arr in zip(ray.get(objects), arrays):
|
||||
assert np.array_equal(restored, arr)
|
||||
wait_for_condition(is_dir_empty)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
@@ -255,7 +117,7 @@ def test_spill_objects_manually_with_workers(object_spilling_config,
|
||||
"_system_config": {
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_max_retries": 4,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"max_io_workers": 4,
|
||||
"object_spilling_config": json.dumps({
|
||||
"type": "filesystem",
|
||||
@@ -308,7 +170,7 @@ def test_spill_objects_automatically(object_spilling_config, shutdown_only):
|
||||
"max_io_workers": 4,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_max_retries": 4,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
"min_spilling_size": 0
|
||||
})
|
||||
@@ -344,7 +206,7 @@ def test_spill_during_get(object_spilling_config, shutdown_only):
|
||||
object_store_memory=100 * 1024 * 1024,
|
||||
_system_config={
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
# NOTE(swang): Use infinite retries because the OOM timer can still
|
||||
# get accidentally triggered when objects are released too slowly
|
||||
# (see github.com/ray-project/ray/issues/12040).
|
||||
@@ -381,7 +243,7 @@ def test_spill_deadlock(object_spilling_config, shutdown_only):
|
||||
"max_io_workers": 1,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_max_retries": 4,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": object_spilling_config,
|
||||
"min_spilling_size": 0,
|
||||
})
|
||||
@@ -411,10 +273,11 @@ def test_delete_objects(tmp_path, shutdown_only):
|
||||
ray.init(
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={
|
||||
"max_io_workers": 4,
|
||||
"max_io_workers": 1,
|
||||
"min_spilling_size": 0,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_max_retries": 4,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": json.dumps({
|
||||
"type": "filesystem",
|
||||
"params": {
|
||||
@@ -454,9 +317,10 @@ def test_delete_objects_delete_while_creating(tmp_path, shutdown_only):
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={
|
||||
"max_io_workers": 4,
|
||||
"min_spilling_size": 0,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_max_retries": 4,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": json.dumps({
|
||||
"type": "filesystem",
|
||||
"params": {
|
||||
@@ -506,7 +370,7 @@ def test_delete_objects_on_worker_failure(tmp_path, shutdown_only):
|
||||
"max_io_workers": 4,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_max_retries": 4,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": json.dumps({
|
||||
"type": "filesystem",
|
||||
"params": {
|
||||
@@ -579,9 +443,10 @@ def test_delete_objects_multi_node(tmp_path, ray_start_cluster):
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={
|
||||
"max_io_workers": 2,
|
||||
"min_spilling_size": 20 * 1024 * 1024,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_max_retries": 4,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": json.dumps({
|
||||
"type": "filesystem",
|
||||
"params": {
|
||||
@@ -648,14 +513,14 @@ def test_fusion_objects(tmp_path, shutdown_only):
|
||||
# Limit our object store to 75 MiB of memory.
|
||||
temp_folder = tmp_path / "spill"
|
||||
temp_folder.mkdir()
|
||||
min_spilling_size = 30 * 1024 * 1024
|
||||
min_spilling_size = 10 * 1024 * 1024
|
||||
ray.init(
|
||||
object_store_memory=75 * 1024 * 1024,
|
||||
_system_config={
|
||||
"max_io_workers": 4,
|
||||
"max_io_workers": 3,
|
||||
"automatic_object_spilling_enabled": True,
|
||||
"object_store_full_max_retries": 4,
|
||||
"object_store_full_initial_delay_ms": 100,
|
||||
"object_store_full_delay_ms": 100,
|
||||
"object_spilling_config": json.dumps({
|
||||
"type": "filesystem",
|
||||
"params": {
|
||||
|
||||
@@ -19,7 +19,6 @@ logger = logging.getLogger(__name__)
|
||||
@pytest.fixture
|
||||
def one_worker_100MiB(request):
|
||||
config = {
|
||||
"object_store_full_max_retries": 2,
|
||||
"task_retry_delay_ms": 0,
|
||||
}
|
||||
yield ray.init(
|
||||
|
||||
Reference in New Issue
Block a user