import os import random import sys import time import pytest import ray import ray.remote_function from ray._common.test_utils import wait_for_condition from ray.dag import InputNode, MultiOutputNode from ray.tests.conftest import * # noqa if sys.platform != "linux" and sys.platform != "darwin": pytest.skip("Skipping, requires Linux or Mac.", allow_module_level=True) @ray.remote class Actor: def __init__(self, init_value, fail_after=None, sys_exit=False): self.i = init_value self.fail_after = fail_after self.sys_exit = sys_exit self.count = 0 def _fail_if_needed(self): if self.fail_after and self.count > self.fail_after: # Randomize the failures to better cover multi actor scenarios. if random.random() > 0.5: if self.sys_exit: os._exit(1) else: raise RuntimeError("injected fault") def inc(self, x): self.i += x self.count += 1 self._fail_if_needed() return self.i def double_and_inc(self, x): self.i *= 2 self.i += x return self.i def echo(self, x): print("ECHO!") self.count += 1 self._fail_if_needed() return x def append_to(self, lst): lst.append(self.i) return lst def inc_two(self, x, y): self.i += x self.i += y return self.i def sleep(self, x): time.sleep(x) return x @ray.method(num_returns=2) def return_two(self, x): return x, x + 1 def test_readers_on_different_nodes(ray_start_cluster): cluster = ray_start_cluster # This node is for the driver (including the CompiledDAG.DAGDriverProxyActor) and # one of the readers. cluster.add_node(num_cpus=1) ray.init(address=cluster.address) # 2 more nodes for other readers. cluster.add_node(num_cpus=1) cluster.add_node(num_cpus=1) cluster.wait_for_nodes() # Wait until nodes actually start, otherwise the code below will fail. wait_for_condition(lambda: len(ray.nodes()) == 3) a = Actor.options(num_cpus=1).remote(0) b = Actor.options(num_cpus=1).remote(0) c = Actor.options(num_cpus=1).remote(0) actors = [a, b, c] def _get_node_id(self) -> "ray.NodeID": return ray.get_runtime_context().get_node_id() node_ids = ray.get([act.__ray_call__.remote(_get_node_id) for act in actors]) assert len(set(node_ids)) == 3 with InputNode() as inp: x = a.inc.bind(inp) y = b.inc.bind(inp) z = c.inc.bind(inp) dag = MultiOutputNode([x, y, z]) cdag = dag.experimental_compile() for i in range(1, 10): assert ray.get(cdag.execute(1)) == [i, i, i] def test_bunch_readers_on_different_nodes(ray_start_cluster): cluster = ray_start_cluster ACTORS_PER_NODE = 2 NUM_REMOTE_NODES = 2 # driver node cluster.add_node(num_cpus=ACTORS_PER_NODE) ray.init(address=cluster.address) # additional nodes for multi readers in multi nodes for _ in range(NUM_REMOTE_NODES): cluster.add_node(num_cpus=ACTORS_PER_NODE) cluster.wait_for_nodes() wait_for_condition(lambda: len(ray.nodes()) == NUM_REMOTE_NODES + 1) actors = [ Actor.options(num_cpus=1).remote(0) for _ in range(ACTORS_PER_NODE * (NUM_REMOTE_NODES + 1)) ] def _get_node_id(self) -> "ray.NodeID": return ray.get_runtime_context().get_node_id() node_ids = ray.get([act.__ray_call__.remote(_get_node_id) for act in actors]) assert len(set(node_ids)) == NUM_REMOTE_NODES + 1 with InputNode() as inp: outputs = [] for actor in actors: outputs.append(actor.inc.bind(inp)) dag = MultiOutputNode(outputs) cdag = dag.experimental_compile() for i in range(1, 10): assert ray.get(cdag.execute(1)) == [ i for _ in range(ACTORS_PER_NODE * (NUM_REMOTE_NODES + 1)) ] @pytest.mark.parametrize("single_fetch", [True, False]) def test_pp(ray_start_cluster, single_fetch): cluster = ray_start_cluster # This node is for the driver. cluster.add_node(num_cpus=0) ray.init(address=cluster.address) TP = 2 # This node is for the PP stage 1. cluster.add_node(resources={"pp1": TP}) # This node is for the PP stage 2. cluster.add_node(resources={"pp2": TP}) @ray.remote class Worker: def __init__(self): pass def execute_model(self, val): return val pp1_workers = [ Worker.options(num_cpus=0, resources={"pp1": 1}).remote() for _ in range(TP) ] pp2_workers = [ Worker.options(num_cpus=0, resources={"pp2": 1}).remote() for _ in range(TP) ] with InputNode() as inp: outputs = [inp for _ in range(TP)] outputs = [pp1_workers[i].execute_model.bind(outputs[i]) for i in range(TP)] outputs = [pp2_workers[i].execute_model.bind(outputs[i]) for i in range(TP)] dag = MultiOutputNode(outputs) compiled_dag = dag.experimental_compile() refs = compiled_dag.execute(1) if single_fetch: for i in range(TP): assert ray.get(refs[i]) == 1 else: assert ray.get(refs) == [1] * TP # So that raylets' error messages are printed to the driver time.sleep(2) @pytest.mark.parametrize("single_fetch", [True, False]) def test_pp_exception(ray_start_cluster, single_fetch): """ This test is to verify that the exception can be passed properly through pipeline parallel workers on different nodes. """ cluster = ray_start_cluster # This node is for the driver. cluster.add_node(num_cpus=0) ray.init(address=cluster.address) TP = 2 # This node is for the PP stage 1. cluster.add_node(resources={"pp1": TP}) # This node is for the PP stage 2. cluster.add_node(resources={"pp2": TP}) # This node is for the PP stage 3. cluster.add_node(resources={"pp3": TP}) # Simulate a large error message (e.g., those with a long stack trace) large_error_message = "Model execution failed" * 10000 @ray.remote class Worker: def __init__(self): pass def execute_model(self, val): if val == "exception_trigger": # Simulate an exception happened during model execution raise RuntimeError(large_error_message) return val pp1_workers = [ Worker.options(num_cpus=0, resources={"pp1": 1}).remote() for _ in range(TP) ] pp2_workers = [ Worker.options(num_cpus=0, resources={"pp2": 1}).remote() for _ in range(TP) ] pp3_workers = [ Worker.options(num_cpus=0, resources={"pp3": 1}).remote() for _ in range(TP) ] with InputNode() as inp: outputs = [inp for _ in range(TP)] outputs = [pp1_workers[i].execute_model.bind(outputs[i]) for i in range(TP)] outputs = [pp2_workers[i].execute_model.bind(outputs[i]) for i in range(TP)] outputs = [pp3_workers[i].execute_model.bind(outputs[i]) for i in range(TP)] dag = MultiOutputNode(outputs) compiled_dag = dag.experimental_compile() refs = compiled_dag.execute("exception_trigger") # Without the fix in this PR, we will encounter the following exception: # File "/Users/ruiqiao/repos2/ray/python/ray/_private/serialization.py", # line 460, in deserialize_objects # obj = self._deserialize_object(data, metadata, object_ref) # raise Exception( # Exception: Can't deserialize object: # ObjectRef(00a33d534c5b0ce51bdf175790467da3114801680100000002e1f505), metadata: b'\x00' # With this fix, the original exception will be propagated. if single_fetch: for i in range(TP): with pytest.raises(RuntimeError) as exc_info: ray.get(refs[i]) assert "Can't deserialize object" not in str(exc_info.value) assert large_error_message in str(exc_info.value) else: with pytest.raises(RuntimeError) as exc_info: ray.get(refs) assert "Can't deserialize object" not in str(exc_info.value) assert large_error_message in str(exc_info.value) def test_payload_large(ray_start_cluster, monkeypatch): GRPC_MAX_SIZE = 1024 * 1024 * 5 monkeypatch.setenv("RAY_max_grpc_message_size", str(GRPC_MAX_SIZE)) cluster = ray_start_cluster # This node is for the driver (including the CompiledDAG.DAGDriverProxyActor). first_node_handle = cluster.add_node(num_cpus=1) # This node is for the reader. second_node_handle = cluster.add_node(num_cpus=1) ray.init(address=cluster.address) cluster.wait_for_nodes() nodes = [first_node_handle.node_id, second_node_handle.node_id] # We want to check that there are two nodes. Thus, we convert `nodes` to a set and # then back to a list to remove duplicates. Then we check that the length of `nodes` # is 2. nodes = list(set(nodes)) assert len(nodes) == 2 def create_actor(node): return Actor.options(label_selector={ray._raylet.RAY_NODE_ID_KEY: node}).remote( 0 ) def get_node_id(self): return ray.get_runtime_context().get_node_id() driver_node = get_node_id(None) nodes.remove(driver_node) a = create_actor(nodes[0]) a_node = ray.get(a.__ray_call__.remote(get_node_id)) assert a_node == nodes[0] # Check that the driver and actor are on different nodes. assert driver_node != a_node with InputNode() as i: dag = a.echo.bind(i) compiled_dag = dag.experimental_compile() size = GRPC_MAX_SIZE + (1024 * 1024 * 2) val = b"x" * size for i in range(3): ref = compiled_dag.execute(val) result = ray.get(ref) assert result == val @pytest.mark.parametrize("num_actors", [1, 4]) @pytest.mark.parametrize("num_nodes", [1, 4]) def test_multi_node_multi_reader_large_payload( ray_start_cluster, num_actors, num_nodes, monkeypatch ): # Set max grpc size to 5mb. GRPC_MAX_SIZE = 1024 * 1024 * 5 monkeypatch.setenv("RAY_max_grpc_message_size", str(GRPC_MAX_SIZE)) cluster = ray_start_cluster ACTORS_PER_NODE = num_actors NUM_REMOTE_NODES = num_nodes cluster.add_node(num_cpus=ACTORS_PER_NODE) ray.init(address=cluster.address) # This node is for the other two readers. for _ in range(NUM_REMOTE_NODES): cluster.add_node(num_cpus=ACTORS_PER_NODE) cluster.wait_for_nodes() wait_for_condition(lambda: len(ray.nodes()) == NUM_REMOTE_NODES + 1) actors = [ Actor.options(num_cpus=1).remote(0) for _ in range(ACTORS_PER_NODE * (NUM_REMOTE_NODES + 1)) ] def _get_node_id(self) -> "ray.NodeID": return ray.get_runtime_context().get_node_id() node_ids = ray.get([act.__ray_call__.remote(_get_node_id) for act in actors]) assert len(set(node_ids)) == NUM_REMOTE_NODES + 1 with InputNode() as inp: outputs = [] for actor in actors: outputs.append(actor.echo.bind(inp)) dag = MultiOutputNode(outputs) compiled_dag = dag.experimental_compile() # Set the object size a little bigger than the gRPC size so that # it triggers chunking and resizing. size = GRPC_MAX_SIZE + (1024 * 1024 * 2) val = b"x" * size for _ in range(3): ref = compiled_dag.execute(val) # In the CI environment, the object store may use /tmp instead of /dev/shm # due to limited size of /tmp/shm and therefore has degraded performance. # Therefore, we use a longer timeout to avoid flakiness. result = ray.get(ref, timeout=50) assert result == [val for _ in range(ACTORS_PER_NODE * (NUM_REMOTE_NODES + 1))] def test_multi_node_dag_from_actor(ray_start_cluster): cluster = ray_start_cluster cluster.add_node(num_cpus=1) ray.init() cluster.add_node(num_cpus=1) @ray.remote(num_cpus=0) class SameNodeActor: def predict(self, x: str): return x @ray.remote(num_cpus=1) class RemoteNodeActor: def predict(self, x: str, y: str): return y @ray.remote(num_cpus=1) class DriverActor: def __init__(self): self._base_actor = SameNodeActor.options( label_selector={ ray._raylet.RAY_NODE_ID_KEY: ray.get_runtime_context().get_node_id() } ).remote() self._refiner_actor = RemoteNodeActor.remote() with InputNode() as inp: x = self._base_actor.predict.bind(inp) dag = self._refiner_actor.predict.bind( inp, x, ) self._cdag = dag.experimental_compile( _submit_timeout=120, ) def call(self, prompt: str) -> bytes: return ray.get(self._cdag.execute(prompt)) parallel = DriverActor.remote() assert ray.get(parallel.call.remote("abc")) == "abc" if __name__ == "__main__": if os.environ.get("PARALLEL_CI"): sys.exit(pytest.main(["-n", "auto", "--boxed", "-vs", __file__])) else: sys.exit(pytest.main(["-sv", __file__]))