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

297 lines
7.7 KiB
Python

import asyncio
import os
import sys
import tempfile
import time
import pytest
import ray
from ray._common.test_utils import Semaphore
def test_nested_tasks(shutdown_only):
ray.init(num_cpus=1)
@ray.remote
class Counter:
def __init__(self):
self.count = 0
def inc(self):
self.count += 1
# Since we relex the cap after a timeout we can have slightly more
# than 1 task. We should never have 20 though since that takes 2^20
# * 10ms time.
assert self.count < 20
def dec(self):
self.count -= 1
counter = Counter.remote()
@ray.remote(num_cpus=1)
def g():
return None
@ray.remote(num_cpus=1)
def f():
ray.get(counter.inc.remote())
res = ray.get(g.remote())
ray.get(counter.dec.remote())
return res
ready, _ = ray.wait(
[f.remote() for _ in range(1000)], timeout=60.0, num_returns=1000
)
assert len(ready) == 1000, len(ready)
# Ensure the assertion in `inc` didn't fail.
ray.get(ready)
def test_recursion(shutdown_only):
ray.init(num_cpus=1)
@ray.remote
def summer(n):
if n == 0:
return 0
return n + ray.get(summer.remote(n - 1))
assert ray.get(summer.remote(10)) == sum(range(11))
def test_out_of_order_scheduling(shutdown_only):
"""Ensure that when a task runs before its dependency, and they're of the same
scheduling class, the dependency is eventually able to run."""
ray.init(num_cpus=1)
@ray.remote
def foo(arg, path):
(ref,) = arg
should_die = not os.path.exists(path)
with open(path, "w") as f:
f.write("")
if should_die:
print("dying!!!")
os._exit(-1)
if ref:
print("hogging the only available slot for a while")
ray.get(ref)
return "done!"
with tempfile.TemporaryDirectory() as tmpdir:
path = f"{tmpdir}/temp.txt"
first = foo.remote((None,), path)
second = foo.remote((first,), path)
print(ray.get(second))
def test_limit_concurrency(shutdown_only):
ray.init(num_cpus=1)
block_task = Semaphore.remote(0)
block_driver = Semaphore.remote(0)
ray.get([block_task.locked.remote(), block_driver.locked.remote()])
@ray.remote(num_cpus=1)
def foo():
ray.get(block_driver.release.remote())
ray.get(block_task.acquire.remote())
refs = [foo.remote() for _ in range(20)]
block_driver_refs = [block_driver.acquire.remote() for _ in range(20)]
# Some of the tasks will run since we relax the cap, but not all because it
# should take exponentially long for the cap to be increased.
ready, not_ready = ray.wait(block_driver_refs, timeout=10, num_returns=20)
assert len(not_ready) >= 1
# Now the first instance of foo finishes, so the second starts to run.
ray.get([block_task.release.remote() for _ in range(19)])
ready, not_ready = ray.wait(block_driver_refs, timeout=10, num_returns=20)
assert len(not_ready) == 0
ready, not_ready = ray.wait(refs, num_returns=20, timeout=15)
assert len(ready) == 19
assert len(not_ready) == 1
def test_zero_cpu_scheduling(shutdown_only):
ray.init(num_cpus=1)
block_task = Semaphore.remote(0)
block_driver = Semaphore.remote(0)
@ray.remote(num_cpus=0)
def foo():
ray.get(block_driver.release.remote())
ray.get(block_task.acquire.remote())
foo.remote()
foo.remote()
ray.get(block_driver.acquire.remote())
block_driver_ref = block_driver.acquire.remote()
# Both tasks should be running, so the driver should be unblocked.
timeout_value = 5 if sys.platform == "win32" else 1
_, not_ready = ray.wait([block_driver_ref], timeout=timeout_value)
assert len(not_ready) == 0
def test_exponential_wait(shutdown_only):
ray.init(num_cpus=2)
num_tasks = 6
@ray.remote(num_cpus=0)
class Barrier:
def __init__(self, limit):
self.i = 0
self.limit = limit
async def join(self):
self.i += 1
while self.i < self.limit:
await asyncio.sleep(1)
b = Barrier.remote(num_tasks)
@ray.remote
def f(i, start):
delta = time.time() - start
print("Launch", i, delta)
ray.get(b.join.remote())
return delta
start = time.time()
results = ray.get([f.remote(i, start) for i in range(num_tasks)])
last_wait = results[-1] - results[-2]
second_last = results[-2] - results[-3]
# Assert that last_wwait / second_last ~= 2, with a healthy buffer since ci
# is noisy.
assert second_last < last_wait < 4 * second_last
assert 7 < last_wait
def test_spillback(ray_start_cluster):
"""Ensure that we can spillback without waiting for the worker cap to be lifed"""
cluster = ray_start_cluster
cluster.add_node(
num_cpus=1,
resources={"head": 1},
_system_config={
"worker_cap_initial_backoff_delay_ms": 36000_000,
"worker_cap_max_backoff_delay_ms": 36000_000,
},
)
cluster.wait_for_nodes()
ray.init(address=cluster.address)
@ray.remote(num_cpus=0)
class Counter:
def __init__(self):
self.i = 0
def inc(self):
self.i = self.i + 1
def get(self):
return self.i
counter = Counter.remote()
@ray.remote
def get_node_id():
return ray.get_runtime_context().get_node_id()
@ray.remote
def func(i, counter):
if i == 0:
counter.inc.remote()
while True:
time.sleep(1)
else:
return ray.get_runtime_context().get_node_id()
refs = [func.remote(i, counter) for i in range(2)]
# Make sure the first task is running
# and the second task is in the dispatch queue hitting the worker cap
while ray.get(counter.get.remote()) != 1:
time.sleep(0.1)
time.sleep(1)
# A new node is added,
# the second task should be spilled back to it
# instead of waiting for the cap to be lifted on the head node after 10h.
cluster.add_node(
num_cpus=1,
resources={"worker": 1},
)
worker_node_id = ray.get(
get_node_id.options(num_cpus=0, resources={"worker": 1}).remote()
)
assert ray.get(refs[1]) == worker_node_id
ray.cancel(refs[0], force=True)
def test_idle_workers(shutdown_only):
ray.init(
num_cpus=2,
_system_config={
"idle_worker_killing_time_threshold_ms": 10,
},
)
@ray.remote(num_cpus=0)
class Actor:
def get(self):
pass
@ray.remote
def getpid():
time.sleep(0.1)
return os.getpid()
# We start exactly as many workers as there are CPUs.
for _ in range(3):
pids = set(ray.get([getpid.remote() for _ in range(4)]))
assert len(pids) <= 2, pids
# Wait for at least the idle worker timeout.
time.sleep(0.1)
# Now test with two actors that uses 1 process each but 0 CPUs.
a1 = Actor.remote()
a2 = Actor.remote()
ray.get([a1.get.remote(), a2.get.remote()])
for _ in range(3):
pids = set(ray.get([getpid.remote() for _ in range(4)]))
assert len(pids) <= 2, pids
# Wait for at least the idle worker timeout.
time.sleep(0.1)
# Kill the actors and test again.
del a1
del a2
for _ in range(3):
pids = set(ray.get([getpid.remote() for _ in range(4)]))
assert len(pids) <= 2, pids
# Wait for at least the idle worker timeout.
time.sleep(0.1)
if __name__ == "__main__":
os.environ["RAY_worker_cap_enabled"] = "true"
sys.exit(pytest.main(["-sv", __file__]))