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__]))