Files
2026-07-13 13:17:40 +08:00

397 lines
12 KiB
Python

# coding: utf-8
import gc
import logging
import sys
import time
import numpy as np
import pytest
from ray._common.test_utils import wait_for_condition
from ray._private.test_utils import client_test_enabled
from ray._private.worker import _wait_generators_bulk
from ray.exceptions import ObjectRefStreamEndOfStreamError, RayTaskError
if client_test_enabled():
from ray.util.client import ray
else:
import ray
import ray.util.state
logger = logging.getLogger(__name__)
def test_wait(ray_start_regular):
@ray.remote
def f(delay):
time.sleep(delay)
return
object_refs = [f.remote(0), f.remote(0), f.remote(0), f.remote(0)]
ready_ids, remaining_ids = ray.wait(object_refs)
assert len(ready_ids) == 1
assert len(remaining_ids) == 3
ready_ids, remaining_ids = ray.wait(object_refs, num_returns=4)
assert set(ready_ids) == set(object_refs)
assert remaining_ids == []
object_refs = [f.remote(0), f.remote(5)]
ready_ids, remaining_ids = ray.wait(object_refs, timeout=0.5, num_returns=2)
assert len(ready_ids) == 1
assert len(remaining_ids) == 1
# Verify that calling wait with duplicate object refs throws an
# exception.
x = ray.put(1)
with pytest.raises(Exception):
ray.wait([x, x])
# Make sure it is possible to call wait with an empty list.
ready_ids, remaining_ids = ray.wait([])
assert ready_ids == []
assert remaining_ids == []
# Test semantics of num_returns with no timeout.
obj_refs = [ray.put(i) for i in range(10)]
(found, rest) = ray.wait(obj_refs, num_returns=2)
assert len(found) == 2
assert len(rest) == 8
# Verify that incorrect usage raises a TypeError.
x = ray.put(1)
with pytest.raises(TypeError):
ray.wait(x)
with pytest.raises(TypeError):
ray.wait(1)
with pytest.raises(TypeError):
ray.wait([1])
def test_wait_timing(ray_start_2_cpus):
@ray.remote
def f():
time.sleep(1)
future = f.remote()
start = time.time()
ready, not_ready = ray.wait([future], timeout=0.2)
assert 0.2 < time.time() - start < 0.3
assert len(ready) == 0
assert len(not_ready) == 1
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test_wait_always_fetch_local(monkeypatch, ray_start_cluster):
monkeypatch.setenv("RAY_scheduler_report_pinned_bytes_only", "false")
cluster = ray_start_cluster
head_node = cluster.add_node(num_cpus=0, object_store_memory=300e6)
ray.init(address=cluster.address)
worker_node = cluster.add_node(num_cpus=1, object_store_memory=300e6)
@ray.remote(num_cpus=1)
def return_large_object():
# 100mb so will spill on worker, but not once on head
return np.zeros(100 * 1024 * 1024, dtype=np.uint8)
@ray.remote(num_cpus=0)
def small_local_task():
return 1
put_on_head = {ray._raylet.RAY_NODE_ID_KEY: head_node.node_id}
put_on_worker = {ray._raylet.RAY_NODE_ID_KEY: worker_node.node_id}
x = small_local_task.options(label_selector=put_on_head).remote()
y = return_large_object.options(label_selector=put_on_worker).remote()
z = return_large_object.options(label_selector=put_on_worker).remote()
# will return when tasks are done
ray.wait([x, y, z], num_returns=3, fetch_local=False)
assert (
ray._private.state.available_resources_per_node()[head_node.node_id][
"object_store_memory"
]
> 250e6
)
# x should be immediately available locally, start fetching y and z
ray.wait([x, y, z], num_returns=1, fetch_local=True)
assert (
ray._private.state.available_resources_per_node()[head_node.node_id][
"object_store_memory"
]
> 250e6
)
time.sleep(5)
# y, z should be pulled here
assert (
ray._private.state.available_resources_per_node()[head_node.node_id][
"object_store_memory"
]
< 150e6
)
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test__wait_generators_bulk_fetch_local(monkeypatch, ray_start_cluster):
monkeypatch.setenv("RAY_scheduler_report_pinned_bytes_only", "false")
cluster = ray_start_cluster
cluster.add_node(num_cpus=0, object_store_memory=500e6)
ray.init(address=cluster.address)
worker_node = cluster.add_node(num_cpus=2, object_store_memory=500e6)
@ray.remote(num_cpus=1)
def gen_large_objects():
# 100mb so the object is stored in plasma.
yield np.zeros(100 * 1024 * 1024, dtype=np.uint8)
yield np.ones(100 * 1024 * 1024, dtype=np.uint8)
put_on_worker = {ray._raylet.RAY_NODE_ID_KEY: worker_node.node_id}
gen1 = gen_large_objects.options(label_selector=put_on_worker).remote()
gen2 = gen_large_objects.options(label_selector=put_on_worker).remote()
ready = _wait_generators_bulk(
[(gen1, [True, False]), (gen2, [False, True])],
num_return=2,
timeout=10,
)
assert len(ready) == 2
assert [gen for gen, _ in ready] == [gen1, gen2]
assert all(len(refs) == 2 for _, refs in ready)
assert np.all(ray.get(ready[0][1][0], timeout=0) == 0)
assert np.all(ray.get(ready[1][1][1], timeout=0) == 1)
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test__wait_generators_bulk_wait_for_at_most_num_return(ray_start_regular):
@ray.remote
def gen(base, delays):
for i, delay in enumerate(delays):
time.sleep(delay)
yield base + i
gen1 = gen.remote(10, [0, 0, 0])
gen2 = gen.remote(20, [0, 5])
ready = _wait_generators_bulk(
[(gen1, [True, False]), (gen2, [False, True])],
num_return=1,
timeout=2,
)
assert len(ready) == 1
ready_gen, refs = ready[0]
assert ready_gen is gen1
assert ray.get(refs) == [10, 11]
# The returned refs are consumed from the stream.
assert ray.get(next(gen1)) == 12
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test__wait_generators_bulk_timeout(ray_start_regular):
@ray.remote(num_cpus=0, max_concurrency=2)
class Signal:
def __init__(self):
self.ready = False
def wait(self):
while not self.ready:
time.sleep(0.01)
def send(self):
self.ready = True
@ray.remote
def slow_gen(signal):
ray.get(signal.wait.remote())
yield 1
signal = Signal.remote()
gen = slow_gen.remote(signal)
assert _wait_generators_bulk([(gen, [False])], timeout=0.01) == []
ray.get(signal.send.remote())
ready = _wait_generators_bulk([(gen, [False])], timeout=5)
assert len(ready) == 1
ready_gen, refs = ready[0]
assert ready_gen is gen
assert ray.get(refs) == [1]
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test__wait_generators_bulk_validation(ray_start_regular):
@ray.remote
def gen():
yield 1
gen = gen.remote()
with pytest.raises(TypeError):
_wait_generators_bulk({})
with pytest.raises(TypeError):
_wait_generators_bulk([(ray.put(1), [False])])
with pytest.raises(TypeError):
_wait_generators_bulk([(gen, False)])
with pytest.raises(ValueError):
_wait_generators_bulk([(gen, [])])
with pytest.raises(ValueError):
_wait_generators_bulk([(gen, [False])], num_return=2)
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test__consume_next_ref_n_rejects_unready(ray_start_regular):
"""Consuming before the last requested ref is ready must raise rather than
silently advancing past (and dropping) the not-yet-produced object."""
@ray.remote(num_cpus=0, max_concurrency=2)
class Signal:
def __init__(self):
self.ready = False
def wait(self):
while not self.ready:
time.sleep(0.01)
def send(self):
self.ready = True
@ray.remote
def slow_gen(signal):
ray.get(signal.wait.remote())
yield 1
signal = Signal.remote()
gen = slow_gen.remote(signal)
# Peek without waiting: the ref isn't produced yet, so consuming it is rejected.
gen._get_next_ref_n(1)
with pytest.raises(ValueError):
gen._consume_next_ref_n(1)
# After the value is produced, the same generator consumes normally.
ray.get(signal.send.remote())
ready = _wait_generators_bulk([(gen, [False])], timeout=10)
assert len(ready) == 1
assert ray.get(ready[0][1]) == [1]
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test__wait_generators_bulk_after_eof_raise_EndOfStreamError(ray_start_regular):
@ray.remote
def empty_gen():
if False:
yield 1
empty = empty_gen.remote()
ready = _wait_generators_bulk([(empty, [True, True, True])], timeout=1)
assert len(ready) == 1
ready_gen, refs = ready[0]
assert ready_gen is empty
assert len(set(refs)) == 3
for ref in refs:
with pytest.raises(ObjectRefStreamEndOfStreamError):
ray.get(ref)
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test__wait_generators_bulk_after_partial_eof(ray_start_regular):
@ray.remote
def one_item_gen():
yield 1
one_item = one_item_gen.remote()
ready = _wait_generators_bulk([(one_item, [False, False, False])], timeout=1)
assert len(ready) == 1
ready_gen, refs = ready[0]
assert ready_gen is one_item
assert len(set(refs)) == 3
assert ray.get(refs[0]) == 1
for ref in refs[1:]:
with pytest.raises(ObjectRefStreamEndOfStreamError):
ray.get(ref)
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test__wait_generators_bulk_after_partial_error(ray_start_regular):
@ray.remote
def one_item_then_error_gen():
yield 1
raise ValueError("expected test error")
one_item_then_error = one_item_then_error_gen.remote()
ready = _wait_generators_bulk(
[(one_item_then_error, [False, False, False])], timeout=1
)
assert len(ready) == 1
ready_gen, refs = ready[0]
assert ready_gen is one_item_then_error
assert len(set(refs)) == 3
assert ray.get(refs[0]) == 1
with pytest.raises(RayTaskError) as exc_info:
ray.get(refs[1])
assert isinstance(exc_info.value.as_instanceof_cause(), ValueError)
with pytest.raises(ObjectRefStreamEndOfStreamError):
ray.get(refs[2])
def _assert_no_owned_refs_leak():
"""Wait until the owner holds no live references and the store is empty."""
def check():
gc.collect()
core_worker = ray._private.worker.global_worker.core_worker
ref_counts = core_worker.get_all_reference_counts()
for rc in ref_counts.values():
if rc["local"] != 0 or rc["submitted"] != 0:
return False
return core_worker.get_memory_store_size() == 0
wait_for_condition(check, timeout=30)
@pytest.mark.skipif(client_test_enabled(), reason="util not available with ray client")
def test__wait_generators_bulk_no_ref_leak(ray_start_regular):
"""Draining a generator entirely via _wait_generators_bulk must not leak
owner-side references for the consumed objects.
The bulk peek (peek_object_ref_stream_n) hands back ObjectRefs that add their
own local reference, while the owner-side stream reference taken at peek/report
time is only released for *unconsumed* refs at stream teardown. This test
confirms whether consumed refs leave that owner-side reference dangling.
"""
@ray.remote
def gen():
for i in range(3):
yield i
g = gen.remote()
collected = []
saw_eof = False
while not saw_eof:
ready = _wait_generators_bulk([(g, [True, True, True])], timeout=10)
assert len(ready) == 1
# Avoid binding the generator object to a local (e.g. via tuple unpacking),
# which would keep its stream alive and prevent teardown.
refs = ready[0][1]
for ref in refs:
try:
collected.append(ray.get(ref))
except ObjectRefStreamEndOfStreamError:
saw_eof = True
break
assert collected == [0, 1, 2]
# Drop every handle to the generator and its (consumed) objects.
del g, ready, refs, ref
_assert_no_owned_refs_leak()
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))