import multiprocessing import sys import time import warnings import numpy as np import pytest import ray from ray.cluster_utils import Cluster, cluster_not_supported from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy if ( multiprocessing.cpu_count() < 40 or ray._common.utils.get_system_memory() < 50 * 10**9 ): warnings.warn("This test must be run on large machines.") def create_cluster(num_nodes): cluster = Cluster() for i in range(num_nodes): cluster.add_node(resources={str(i): 100}, object_store_memory=10**9) ray.init(address=cluster.address) return cluster @pytest.fixture() def ray_start_cluster_with_resource(): num_nodes = 5 cluster = create_cluster(num_nodes) yield cluster, num_nodes # The code after the yield will run as teardown code. ray.shutdown() cluster.shutdown() @pytest.mark.parametrize( "ray_start_cluster_head", [ { "num_cpus": 0, "object_store_memory": 75 * 1024 * 1024, } ], indirect=True, ) def test_object_transfer_during_oom(ray_start_cluster_head): cluster = ray_start_cluster_head cluster.add_node(object_store_memory=75 * 1024 * 1024) @ray.remote def put(): return np.random.rand(5 * 1024 * 1024) # 40 MB data _ = ray.put(np.random.rand(5 * 1024 * 1024)) remote_ref = put.remote() # Getting the remote ref is possible even though we don't have enough # memory locally to hold both objects once. ray.get(remote_ref) # When submitting an actor method, we try to pre-emptively push its arguments # to the actor's object manager. However, in the past we did not deduplicate # the pushes and so the same object could get shipped to the same object # manager many times. This test checks that that isn't happening. def test_actor_broadcast(ray_start_cluster_with_resource): cluster, num_nodes = ray_start_cluster_with_resource @ray.remote class Actor: def ready(self): pass def set_weights(self, x): pass actors = [ Actor._remote( args=[], kwargs={}, num_cpus=0.01, resources={str(i % num_nodes): 1} ) for i in range(30) ] # Wait for the actors to start up. ray.get([a.ready.remote() for a in actors]) object_refs = [] # Broadcast a large object to all actors. for _ in range(5): x_id = ray.put(np.zeros(1024 * 1024, dtype=np.uint8)) object_refs.append(x_id) # Pass the object into a method for every actor. ray.get([a.set_weights.remote(x_id) for a in actors]) # Wait for profiling information to be pushed to the profile table. time.sleep(1) # TODO(Sang): Re-enable it after event is introduced. # transfer_events = ray._private.state.object_transfer_timeline() # # Make sure that each object was transferred a reasonable number of times. # noqa # for x_id in object_refs: # relevant_events = [ # event for event in transfer_events if # event["cat"] == "transfer_send" and event["args"][0] == x_id.hex() # noqa # ] # # NOTE: Each event currently appears twice because we duplicate the # # send and receive boxes to underline them with a box (black if it is a # noqa # # send and gray if it is a receive). So we need to remove these extra # # boxes here. # deduplicated_relevant_events = [ # event for event in relevant_events if event["cname"] != "black" # ] # assert len(deduplicated_relevant_events) * 2 == len(relevant_events) # relevant_events = deduplicated_relevant_events # # Each object must have been broadcast to each remote machine. # assert len(relevant_events) >= num_nodes - 1 # # If more object transfers than necessary have been done, print a # # warning. # if len(relevant_events) > num_nodes - 1: # warnings.warn("This object was transferred {} times, when only {} " # noqa # "transfers were required.".format( # len(relevant_events), num_nodes - 1)) # # Each object should not have been broadcast more than once from every # noqa # # machine to every other machine. Also, a pair of machines should not # # both have sent the object to each other. # assert len(relevant_events) <= (num_nodes - 1) * num_nodes / 2 # # Make sure that no object was sent multiple times between the same # # pair of object managers. # send_counts = defaultdict(int) # for event in relevant_events: # # The pid identifies the sender and the tid identifies the # # receiver. # send_counts[(event["pid"], event["tid"])] += 1 # assert all(value == 1 for value in send_counts.values()) # The purpose of this test is to make sure we can transfer many objects. In the # past, this has caused failures in which object managers create too many open # files and run out of resources. def test_many_small_transfers(ray_start_cluster_with_resource): cluster, num_nodes = ray_start_cluster_with_resource @ray.remote def f(*args): pass # This function creates 1000 objects on each machine and then transfers # each object to every other machine. def do_transfers(): id_lists = [] for i in range(num_nodes): id_lists.append( [ f._remote(args=[], kwargs={}, resources={str(i): 1}) for _ in range(1000) ] ) ids = [] for i in range(num_nodes): for j in range(num_nodes): if i == j: continue ids.append( f._remote(args=id_lists[j], kwargs={}, resources={str(i): 1}) ) # Wait for all of the transfers to finish. ray.get(ids) do_transfers() do_transfers() do_transfers() do_transfers() # This is a basic test to ensure that the pull request retry timer is # integrated properly. To test it, we create a 2 node cluster then do the # following: # (1) Fill up the driver's object store. # (2) Fill up the remote node's object store. # (3) Try to get the remote object. This should fail due to an OOM error caused # by step 1. # (4) Allow the local object to be evicted. # (5) Try to get the object again. Now the retry timer should kick in and # successfuly pull the remote object. def test_pull_request_retry(ray_start_cluster): cluster = ray_start_cluster cluster.add_node(num_cpus=0, num_gpus=1, object_store_memory=100 * 2**20) cluster.add_node(num_cpus=1, num_gpus=0, object_store_memory=100 * 2**20) cluster.wait_for_nodes() ray.init(address=cluster.address) @ray.remote def put(): return np.zeros(64 * 2**20, dtype=np.int8) @ray.remote(num_cpus=0, num_gpus=1) def driver(): local_ref = ray.put(np.zeros(64 * 2**20, dtype=np.int8)) remote_ref = put.remote() ready, _ = ray.wait([remote_ref], timeout=30) assert len(ready) == 1 del local_ref # This should always complete within 10 seconds. ready, _ = ray.wait([remote_ref], timeout=20) assert len(ready) > 0 # Pretend the GPU node is the driver. We do this to force the placement of # the driver and `put` task on different nodes. ray.get(driver.remote()) @pytest.mark.xfail(cluster_not_supported, reason="cluster not supported") def test_pull_bundles_admission_control(ray_start_cluster): cluster = ray_start_cluster object_size = int(6e6) num_objects = 10 num_tasks = 10 # Head node can fit all of the objects at once. cluster.add_node( num_cpus=0, object_store_memory=2 * num_tasks * num_objects * object_size ) cluster.wait_for_nodes() ray.init(address=cluster.address) # Worker node can only fit 1 task at a time. cluster.add_node(num_cpus=1, object_store_memory=1.5 * num_objects * object_size) cluster.wait_for_nodes() @ray.remote def foo(*args): return args = [] for _ in range(num_tasks): task_args = [ ray.put(np.zeros(object_size, dtype=np.uint8)) for _ in range(num_objects) ] args.append(task_args) tasks = [foo.remote(*task_args) for task_args in args] ray.get(tasks) @pytest.mark.xfail(cluster_not_supported, reason="cluster not supported") def test_pull_bundles_pinning(ray_start_cluster): cluster = ray_start_cluster object_size = int(50e6) num_objects = 10 # Head node can fit all of the objects at once. cluster.add_node(num_cpus=0, object_store_memory=1000e6) cluster.wait_for_nodes() ray.init(address=cluster.address) # Worker node cannot even fit a single task. cluster.add_node(num_cpus=1, object_store_memory=200e6) cluster.wait_for_nodes() @ray.remote(num_cpus=1) def foo(*args): return task_args = [ ray.put(np.zeros(object_size, dtype=np.uint8)) for _ in range(num_objects) ] ray.get(foo.remote(*task_args)) @pytest.mark.xfail(cluster_not_supported, reason="cluster not supported") def test_pull_bundles_admission_control_dynamic( enable_mac_large_object_store, ray_start_cluster ): # This test is the same as test_pull_bundles_admission_control, except that # the object store's capacity starts off higher and is later consumed # dynamically by concurrent workers. cluster = ray_start_cluster object_size = int(6e6) num_objects = 20 num_tasks = 20 # Head node can fit all of the objects at once. cluster.add_node( num_cpus=0, object_store_memory=2 * num_tasks * num_objects * object_size ) cluster.wait_for_nodes() ray.init(address=cluster.address) # Worker node can fit 2 tasks at a time. cluster.add_node(num_cpus=1, object_store_memory=2.5 * num_objects * object_size) cluster.wait_for_nodes() @ray.remote def foo(i, *args): print("foo", i) return @ray.remote def allocate(i): print("allocate", i) return np.zeros(object_size, dtype=np.uint8) args = [] for _ in range(num_tasks): task_args = [ ray.put(np.zeros(object_size, dtype=np.uint8)) for _ in range(num_objects) ] args.append(task_args) allocated = [allocate.remote(i) for i in range(num_objects)] ray.get(allocated) tasks = [foo.remote(i, *task_args) for i, task_args in enumerate(args)] ray.get(tasks) del allocated @pytest.mark.xfail(cluster_not_supported, reason="cluster not supported") def test_max_pinned_args_memory(ray_start_cluster): cluster = ray_start_cluster cluster.add_node( num_cpus=0, object_store_memory=200 * 1024 * 1024, _system_config={ "max_task_args_memory_fraction": 0.7, }, ) ray.init(address=cluster.address) cluster.add_node(num_cpus=3, object_store_memory=100 * 1024 * 1024) @ray.remote def f(arg): time.sleep(1) return np.zeros(30 * 1024 * 1024, dtype=np.uint8) # Each task arg takes about 30% of the remote node's memory. We should # execute at most 2 at a time to make sure we have room for at least 1 task # output. x = np.zeros(30 * 1024 * 1024, dtype=np.uint8) ray.get([f.remote(ray.put(x)) for _ in range(3)]) @ray.remote def large_arg(arg): return # Executing a task whose args are greater than the memory threshold is # okay. ref = np.zeros(80 * 1024 * 1024, dtype=np.uint8) ray.get(large_arg.remote(ref)) @pytest.mark.xfail(cluster_not_supported, reason="cluster not supported") def test_ray_get_task_args_deadlock(ray_start_cluster): cluster = ray_start_cluster object_size = int(6e6) num_objects = 10 # Head node can fit all of the objects at once. cluster.add_node(num_cpus=0, object_store_memory=4 * num_objects * object_size) cluster.wait_for_nodes() ray.init(address=cluster.address) # Worker node can only fit 1 task at a time. cluster.add_node(num_cpus=1, object_store_memory=1.5 * num_objects * object_size) cluster.wait_for_nodes() @ray.remote def foo(*args): return @ray.remote def test_deadlock(get_args, task_args): foo.remote(*task_args) ray.get(get_args) for i in range(5): start = time.time() get_args = [ ray.put(np.zeros(object_size, dtype=np.uint8)) for _ in range(num_objects) ] task_args = [ ray.put(np.zeros(object_size, dtype=np.uint8)) for _ in range(num_objects) ] ray.get(test_deadlock.remote(get_args, task_args)) print(f"round {i} finished in {time.time() - start}") def test_object_directory_basic(ray_start_cluster_with_resource): cluster, num_nodes = ray_start_cluster_with_resource @ray.remote def task(x): pass # Test a single task. x_id = ray.put(np.zeros(1024 * 1024, dtype=np.uint8)) ray.get(task.options(resources={str(3): 1}).remote(x_id), timeout=10) # Test multiple tasks on all nodes can find locations properly. object_refs = [] for _ in range(num_nodes): object_refs.append(ray.put(np.zeros(1024 * 1024, dtype=np.uint8))) ray.get( [ task.options(resources={str(i): 1}).remote(object_refs[i]) for i in range(num_nodes) ] ) del object_refs @ray.remote class ObjectHolder: def __init__(self): self.x = ray.put(np.zeros(1024 * 1024, dtype=np.uint8)) def get_obj(self): return self.x def ready(self): return True # Test if tasks can find object location properly # when there are multiple owners object_holders = [ ObjectHolder.options(num_cpus=0.01, resources={str(i): 1}).remote() for i in range(num_nodes) ] ray.get([o.ready.remote() for o in object_holders]) object_refs = [] for i in range(num_nodes): object_refs.append(object_holders[(i + 1) % num_nodes].get_obj.remote()) ray.get( [ task.options(num_cpus=0.01, resources={str(i): 1}).remote(object_refs[i]) for i in range(num_nodes) ] ) # Test a stressful scenario. object_refs = [] repeat = 10 for _ in range(num_nodes): for _ in range(repeat): object_refs.append(ray.put(np.zeros(1024 * 1024, dtype=np.uint8))) tasks = [] for i in range(num_nodes): for r in range(repeat): tasks.append( task.options(num_cpus=0.01, resources={str(i): 0.1}).remote( object_refs[i * r] ) ) ray.get(tasks) object_refs = [] for i in range(num_nodes): object_refs.append(object_holders[(i + 1) % num_nodes].get_obj.remote()) tasks = [] for i in range(num_nodes): for _ in range(10): tasks.append( task.options(num_cpus=0.01, resources={str(i): 0.1}).remote( object_refs[(i + 1) % num_nodes] ) ) def test_pull_bundle_deadlock(ray_start_cluster): # Test https://github.com/ray-project/ray/issues/13689 cluster = ray_start_cluster cluster.add_node( num_cpus=0, _system_config={ "max_direct_call_object_size": int(1e7), }, ) ray.init(address=cluster.address) worker_node_1 = cluster.add_node( num_cpus=8, resources={"worker_node_1": 1}, ) cluster.add_node( num_cpus=8, resources={"worker_node_2": 1}, object_store_memory=int(1e8 * 2 - 10), ) cluster.wait_for_nodes() @ray.remote(num_cpus=0) def get_node_id(): return ray.get_runtime_context().get_node_id() worker_node_1_id = ray.get( get_node_id.options(resources={"worker_node_1": 0.1}).remote() ) worker_node_2_id = ray.get( get_node_id.options(resources={"worker_node_2": 0.1}).remote() ) object_a = ray.put(np.zeros(int(1e8), dtype=np.uint8)) @ray.remote( scheduling_strategy=NodeAffinitySchedulingStrategy(worker_node_1_id, soft=True) ) def task_a_to_b(a): return np.zeros(int(1e8), dtype=np.uint8) object_b = task_a_to_b.remote(object_a) ray.wait([object_b], fetch_local=False) @ray.remote(label_selector={ray._raylet.RAY_NODE_ID_KEY: worker_node_2_id}) def task_b_to_c(b): return "c" object_c = task_b_to_c.remote(object_b) # task_a_to_b will be re-executed on worker_node_2 so pull manager there will # have object_a pull request after the existing object_b pull request. # Make sure object_b pull request won't block the object_a pull request. cluster.remove_node(worker_node_1, allow_graceful=False) assert ray.get(object_c) == "c" def test_object_directory_failure(ray_start_cluster): cluster = ray_start_cluster config = { "health_check_initial_delay_ms": 0, "health_check_period_ms": 500, "health_check_failure_threshold": 10, "object_timeout_milliseconds": 200, } # Add a head node. cluster.add_node(_system_config=config) ray.init(address=cluster.address) # Add worker nodes. num_nodes = 5 for i in range(num_nodes): cluster.add_node(resources={str(i): 100}) # Add a node to be removed index_killing_node = num_nodes node_to_kill = cluster.add_node( resources={str(index_killing_node): 100}, object_store_memory=10**9 ) @ray.remote class ObjectHolder: def __init__(self): self.x = ray.put(np.zeros(1024 * 1024, dtype=np.uint8)) def get_obj(self): return [self.x] def ready(self): return True oh = ObjectHolder.options( num_cpus=0.01, resources={str(index_killing_node): 1} ).remote() obj = ray.get(oh.get_obj.remote())[0] @ray.remote def task(x): pass cluster.remove_node(node_to_kill, allow_graceful=False) tasks = [] repeat = 3 for i in range(num_nodes): for _ in range(repeat): tasks.append(task.options(resources={str(i): 1}).remote(obj)) for t in tasks: with pytest.raises(ray.exceptions.RayTaskError): ray.get(t, timeout=10) @pytest.mark.parametrize( "ray_start_cluster_head", [ { "num_cpus": 0, "object_store_memory": 75 * 1024 * 1024, "_system_config": { "worker_lease_timeout_milliseconds": 0, "object_manager_pull_timeout_ms": 20000, "object_spilling_threshold": 1.0, }, } ], indirect=True, ) def test_maximize_concurrent_pull_race_condition(ray_start_cluster_head): # Test if https://github.com/ray-project/ray/issues/18062 is mitigated cluster = ray_start_cluster_head cluster.add_node(num_cpus=8, object_store_memory=75 * 1024 * 1024) @ray.remote class RemoteObjectCreator: def put(self, i): return np.random.rand(i * 1024 * 1024) # 8 MB data def idle(self): pass @ray.remote def f(x): print(f"timestamp={time.time()} pulled {len(x)*8} bytes") time.sleep(1) return remote_obj_creator = RemoteObjectCreator.remote() remote_refs = [remote_obj_creator.put.remote(1) for _ in range(7)] print(remote_refs) # Make sure all objects are created. ray.get(remote_obj_creator.idle.remote()) local_refs = [ray.put(np.random.rand(1 * 1024 * 1024)) for _ in range(20)] remote_tasks = [f.remote(x) for x in local_refs] start = time.time() ray.get(remote_tasks) end = time.time() assert ( end - start < 20 ), "Too much time spent in pulling objects, check the amount of time in retries" if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))