# coding: utf-8 import logging import pickle import sys import time import traceback import numpy as np import pytest import torch import ray import ray.cluster_utils import ray.exceptions import ray.experimental.channel as ray_channel from ray._private.test_utils import get_actor_node_id from ray.dag.compiled_dag_node import CompiledDAG from ray.exceptions import RayChannelError, RayChannelTimeoutError from ray.experimental.channel.torch_tensor_type import TorchTensorType logger = logging.getLogger(__name__) def create_driver_actor(): return CompiledDAG.DAGDriverProxyActor.options( label_selector={ ray._raylet.RAY_NODE_ID_KEY: ray.get_runtime_context().get_node_id() } ).remote() @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_put_local_get(ray_start_regular): driver_actor = create_driver_actor() chan = ray_channel.Channel( None, [ (driver_actor, get_actor_node_id(driver_actor)), ], 1000, ) num_writes = 1000 for i in range(num_writes): val = i.to_bytes(8, "little") chan.write(val) assert chan.read() == val @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_read_timeout(ray_start_regular): driver_actor = create_driver_actor() chan = ray_channel.Channel( None, [ (driver_actor, get_actor_node_id(driver_actor)), ], 1000, ) with pytest.raises(RayChannelTimeoutError): chan.read(timeout=1) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_write_timeout(ray_start_regular): driver_actor = create_driver_actor() chan = ray_channel.Channel( None, [ (driver_actor, get_actor_node_id(driver_actor)), ], 1000, ) val = 1 bytes = val.to_bytes(8, "little") chan.write(bytes, timeout=1) with pytest.raises(RayChannelTimeoutError): chan.write(bytes, timeout=1) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) @pytest.mark.parametrize("remote", [True, False]) def test_driver_as_reader(ray_start_cluster, remote): cluster = ray_start_cluster if remote: # This node is for the driver. num_cpus is 1 because the # CompiledDAG.DAGDriverProxyActor needs a place to run. cluster.add_node(num_cpus=1) ray.init(address=cluster.address) # This node is for the writer actor. cluster.add_node(num_cpus=1) else: # This node is for both the driver (including the # CompiledDAG.DAGDriverProxyActor) and the writer actor. cluster.add_node(num_cpus=2) ray.init(address=cluster.address) @ray.remote(num_cpus=1) class Actor: def setup(self, driver_actor): self._channel = ray_channel.Channel( ray.get_runtime_context().current_actor, [(driver_actor, get_actor_node_id(driver_actor))], 1000, ) def get_channel(self): return self._channel def write(self): self._channel.write(b"x") a = Actor.remote() ray.get(a.setup.remote(create_driver_actor())) chan = ray.get(a.get_channel.remote()) ray.get(a.write.remote()) assert chan.read() == b"x" @pytest.mark.parametrize("remote", [True, False]) def test_driver_as_reader_with_resize(ray_start_cluster, remote): cluster = ray_start_cluster if remote: # This node is for the driver. num_cpus is 1 because the # CompiledDAG.DAGDriverProxyActor needs a place to run. cluster.add_node(num_cpus=1) ray.init(address=cluster.address) # This node is for the writer actor. cluster.add_node(num_cpus=1) else: # This node is for both the driver (including the # CompiledDAG.DAGDriverProxyActor) and the writer actor. cluster.add_node(num_cpus=2) ray.init(address=cluster.address) @ray.remote(num_cpus=1) class Actor: def setup(self, driver_actor): self._channel = ray_channel.Channel( ray.get_runtime_context().current_actor, [(driver_actor, get_actor_node_id(driver_actor))], 1000, ) def get_channel(self): return self._channel def write(self): self._channel.write(b"x") def write_large(self): self._channel.write(b"x" * 2000) a = Actor.remote() ray.get(a.setup.remote(create_driver_actor())) chan = ray.get(a.get_channel.remote()) ray.get(a.write.remote()) assert chan.read() == b"x" ray.get(a.write_large.remote()) assert chan.read() == b"x" * 2000 @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_set_error_before_read(ray_start_regular): """ Tests that if a channel is closed after a reader, a subsequent read does not block forever. """ @ray.remote class Actor: def __init__(self): self.arr = None def create_channel(self, writer, reader_and_node_list): self._channel = ray_channel.Channel(writer, reader_and_node_list, 1000) return self._channel def pass_channel(self, channel): self._channel = channel def close(self): self._channel.close() def write(self, arr): self._channel.write(arr) def read(self): self.arr = self._channel.read() # Keep self.arr in scope. While self.arr is in scope, its backing # shared_ptr in C++ will also stay in scope. # Under normal execution, this will block the next read() from # returning, since we are still using the shared buffer. # In this test we are checking that if the channel is closed, then # the next read() will return an error immediately instead of # blocking, even though we still have self.arr in scope. return self.arr for _ in range(10): a = Actor.remote() b = Actor.remote() node_b = get_actor_node_id(b) chan = ray.get(a.create_channel.remote(a, [(b, node_b)])) ray.get(b.pass_channel.remote(chan)) # Use numpy to enable zero-copy deserialization. arr = np.random.rand(100) ray.get(a.write.remote(arr)) assert (arr == ray.get(b.read.remote())).all() # Check that the thread does not block on the second call to read() below. # read() acquires a lock, though if the lock is not released when # read() fails (because the channel has been closed), then an additional # call to read() *could* block. # We wrap both calls to read() in pytest.raises() as both calls could # trigger an RayChannelError exception if the channel has already been closed. with pytest.raises( ray.exceptions.RayTaskError, match=r"Channel closed" ) as exc_info: ray.get([a.close.remote(), b.read.remote()]) assert isinstance(exc_info.value.as_instanceof_cause(), RayChannelError) with pytest.raises(ray.exceptions.RayTaskError) as exc_info: ray.get(b.read.remote()) assert isinstance(exc_info.value.as_instanceof_cause(), RayChannelError) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_errors(ray_start_regular): """ Tests that an exception is thrown when there are more readers than specificed in the channel constructor. """ @ray.remote class Actor: def make_chan(self, readers, do_write=True): self.chan = ray_channel.Channel( ray.get_runtime_context().current_actor, readers, 1000 ) if do_write: self.chan.write(b"hello") return self.chan a = Actor.remote() # Multiple consecutive reads from the same process are fine. driver_actor = create_driver_actor() chan = ray.get( a.make_chan.remote( [(driver_actor, get_actor_node_id(driver_actor))], do_write=True ) ) assert chan.read() == b"hello" @ray.remote class Reader: def __init__(self): pass def read(self, chan): return chan.read() readers = [Reader.remote(), Reader.remote()] # Check that an exception is thrown when there are more readers than specificed in # the channel constructor. chan = ray.get( a.make_chan.remote([(readers[0], get_actor_node_id(readers[0]))], do_write=True) ) # At least 1 reader. with pytest.raises(ray.exceptions.RayTaskError) as exc_info: ray.get([reader.read.remote(chan) for reader in readers]) assert "ray.exceptions.RaySystemError" in str(exc_info.value) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_put_different_meta(ray_start_regular): driver_actor = create_driver_actor() chan = ray_channel.Channel( None, [(driver_actor, get_actor_node_id(driver_actor))], 1000 ) def _test(val): chan.write(val) read_val = chan.read() if isinstance(val, np.ndarray): assert np.array_equal(read_val, val) else: assert read_val == val _test(b"hello") _test("hello") _test(1000) _test(np.random.rand(10)) def test_multiple_channels_different_nodes(ray_start_cluster): """ Tests that multiple channels can be used at the same time between two nodes. """ cluster = ray_start_cluster # This node is for the driver. cluster.add_node(num_cpus=0) ray.init(address=cluster.address) # This node is for the Reader actors. cluster.add_node(num_cpus=1) @ray.remote(num_cpus=1) class Actor: def read(self, channel, val): read_val = channel.read() if isinstance(val, np.ndarray): assert np.array_equal(read_val, val) else: assert read_val == val a = Actor.remote() node_a = get_actor_node_id(a) chan_a = ray_channel.Channel(None, [(a, node_a)], 1000) chan_b = ray_channel.Channel(None, [(a, node_a)], 1000) channels = [chan_a, chan_b] val = np.random.rand(5) for i in range(10): for channel in channels: channel.write(val) for channel in channels: ray.get(a.read.remote(channel, val)) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_resize_channel_on_same_node(ray_start_regular): """ Tests that the channel backing store is automatically increased when a large object is written to it. The writer and reader are on the same node. """ driver_actor = create_driver_actor() chan = ray_channel.Channel( None, [(driver_actor, get_actor_node_id(driver_actor))], 1000 ) def _test(val): chan.write(val) read_val = chan.read() if isinstance(val, np.ndarray): assert np.array_equal(read_val, val) else: assert read_val == val # `np.random.rand(100)` requires more than 1000 bytes of storage. The channel is # allocated above with a backing store size of 1000 bytes. _test(np.random.rand(100)) # Check that another write still works. _test(np.random.rand(5)) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_resize_channel_on_same_node_with_actor(ray_start_regular): """ Tests that the channel backing store is automatically increased when a large object is written to it. The writer and reader are on the same node, and the reader is an actor. """ @ray.remote class Actor: def __init__(self): pass def read(self, channel, val): read_val = channel.read() if isinstance(val, np.ndarray): assert np.array_equal(read_val, val) else: assert read_val == val def _test(channel, actor, val): channel.write(val) ray.get(actor.read.remote(channel, val)) a = Actor.remote() node_a = get_actor_node_id(a) chan = ray_channel.Channel(None, [(a, node_a)], 1000) # `np.random.rand(100)` requires more than 1000 bytes of storage. The channel is # allocated above with a backing store size of 1000 bytes. _test(chan, a, np.random.rand(100)) # Check that another write still works. _test(chan, a, np.random.rand(5)) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_resize_channel_on_different_nodes(ray_start_cluster): """ Tests that the channel backing store is automatically increased when a large object is written to it. The writer and reader are on different nodes, and the reader is an actor. """ cluster = ray_start_cluster # This node is for the driver. cluster.add_node(num_cpus=0) ray.init(address=cluster.address) # This node is for the Reader actors. cluster.add_node(num_cpus=1) @ray.remote(num_cpus=1) class Actor: def __init__(self): pass def read(self, channel, val): read_val = channel.read() if isinstance(val, np.ndarray): assert np.array_equal(read_val, val) else: assert read_val == val def _test(channel, actor, val): channel.write(val) ray.get(actor.read.remote(channel, val)) a = Actor.remote() node_a = get_actor_node_id(a) chan = ray_channel.Channel(None, [(a, node_a)], 1000) # `np.random.rand(100)` requires more than 1000 bytes of storage. The channel is # allocated above with a backing store size of 1000 bytes. _test(chan, a, np.random.rand(100)) # Check that another write still works. _test(chan, a, np.random.rand(5)) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) @pytest.mark.parametrize("num_readers", [1, 4]) def test_put_remote_get(ray_start_regular, num_readers): """ Tests that an actor can read objects/primitives of various types through a channel when the reader is spawned with @ray.remote. """ @ray.remote(num_cpus=0) class Reader: def __init__(self): pass def read(self, chan, num_writes): for i in range(num_writes): val = i.to_bytes(8, "little") assert chan.read() == val for i in range(num_writes): val = i.to_bytes(100, "little") assert chan.read() == val for val in [ b"hello world", "hello again", 1000, ]: assert chan.read() == val num_writes = 1000 reader_and_node_list = [] for _ in range(num_readers): handle = Reader.remote() node = get_actor_node_id(handle) reader_and_node_list.append((handle, node)) chan = ray_channel.Channel(None, reader_and_node_list, 1000) chan.ensure_registered_as_writer() done = [reader.read.remote(chan, num_writes) for reader, _ in reader_and_node_list] for i in range(num_writes): val = i.to_bytes(8, "little") chan.write(val) # Test different data size. for i in range(num_writes): val = i.to_bytes(100, "little") chan.write(val) # Test different metadata. for val in [ b"hello world", "hello again", 1000, ]: chan.write(val) ray.get(done) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) @pytest.mark.parametrize("remote", [True, False]) def test_remote_reader(ray_start_cluster, remote): """ Tests that an actor can read objects/primitives of various types through a channel when the reader and writer are on the (1) same node (remote=False) along with (2) different nodes (remote=True). """ num_readers = 10 num_writes = 1000 num_iterations = 3 cluster = ray_start_cluster if remote: # This node is for the driver. cluster.add_node(num_cpus=0) ray.init(address=cluster.address) # This node is for the Reader actors. cluster.add_node(num_cpus=num_readers) else: # This node is for both the driver and the Reader actors. cluster.add_node(num_cpus=num_readers) ray.init(address=cluster.address) @ray.remote(num_cpus=1) class Reader: def __init__(self): pass def get_node_id(self) -> str: return ray.get_runtime_context().get_node_id() def pass_channel(self, channel): self._reader_chan = channel def read(self, num_reads): for i in range(num_reads): self._reader_chan.read() reader_and_node_list = [] for _ in range(num_readers): handle = Reader.remote() node = get_actor_node_id(handle) reader_and_node_list.append((handle, node)) channel = ray_channel.Channel(None, reader_and_node_list, 1000) # All readers have received the channel. ray.get([reader.pass_channel.remote(channel) for reader, _ in reader_and_node_list]) for _ in range(num_iterations): work = [reader.read.remote(num_writes) for reader, _ in reader_and_node_list] start = time.perf_counter() for i in range(num_writes): channel.write(b"x") end = time.perf_counter() ray.get(work) print(end - start, 10_000 / (end - start)) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) @pytest.mark.parametrize("remote", [True, False]) def test_remote_reader_close(ray_start_cluster, remote): """ Tests that readers do not block forever on read() when they close the channel. Specifically, the following behavior should happen: 1. Each reader calls read() on one channel. 2. Each reader calls close() on the channel on a different thread. 3. Each reader should unblock and return from read(). Tests (1) the readers and writer on the same node (remote=False) along with different nodes (remote=True). """ num_readers = 10 cluster = ray_start_cluster if remote: # This node is for the driver. cluster.add_node(num_cpus=0) ray.init(address=cluster.address) # This node is for the Reader actors. cluster.add_node(num_cpus=num_readers) else: # This node is for both the driver and the Reader actors. cluster.add_node(num_cpus=num_readers) ray.init(address=cluster.address) @ray.remote(num_cpus=1) class Reader: def __init__(self): pass def get_node_id(self) -> str: return ray.get_runtime_context().get_node_id() def pass_channel(self, channel): self._reader_chan = channel def read(self): try: self._reader_chan.read() except RayChannelError: pass def close(self): self._reader_chan.close() reader_and_node_list = [] for _ in range(num_readers): handle = Reader.remote() node = get_actor_node_id(handle) reader_and_node_list.append((handle, node)) channel = ray_channel.Channel(None, reader_and_node_list, 1000) # All readers have received the channel. ray.get([reader.pass_channel.remote(channel) for reader, _ in reader_and_node_list]) reads = [ reader.read.options(concurrency_group="_ray_system").remote() for reader, _ in reader_and_node_list ] with pytest.raises(ray.exceptions.GetTimeoutError): ray.get(reads, timeout=1.0) ray.get([reader.close.remote() for reader, _ in reader_and_node_list]) ray.get(reads) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_intra_process_channel_single_reader(ray_start_cluster): """ (1) Test whether an actor can read/write from an IntraProcessChannel. (2) Test whether the _SerializationContext cleans up the data after all readers have read it. (3) Test whether the actor can write again after reading 1 time. """ # This node is for both the driver and the Reader actors. cluster = ray_start_cluster cluster.add_node(num_cpus=1) ray.init(address=cluster.address) @ray.remote(num_cpus=1) class Actor: def __init__(self): pass def pass_channel(self, channel): self._chan = channel def read(self): return self._chan.read() def write(self, value): self._chan.write(value) def get_ctx_buffer_size(self): ctx = ray_channel.ChannelContext.get_current().serialization_context return len(ctx.intra_process_channel_buffers) actor = Actor.remote() channel = ray_channel.IntraProcessChannel(num_readers=1) ray.get(actor.pass_channel.remote(channel)) ray.get(actor.write.remote("hello")) assert ray.get(actor.read.remote()) == "hello" # The _SerializationContext should clean up the data after a read. assert ray.get(actor.get_ctx_buffer_size.remote()) == 0 # Write again after reading num_readers times. ray.get(actor.write.remote("world")) assert ray.get(actor.read.remote()) == "world" # The _SerializationContext should clean up the data after a read. assert ray.get(actor.get_ctx_buffer_size.remote()) == 0 @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_intra_process_channel_multi_readers(ray_start_cluster): """ (1) Test whether an actor can read/write from an IntraProcessChannel. (2) Test whether the _SerializationContext cleans up the data after all readers have read it. (3) Test whether the actor can write again after reading num_readers times. (4) Test whether an exception is raised when calling write() before all readers have read the data. """ # This node is for both the driver and the Reader actors. cluster = ray_start_cluster cluster.add_node(num_cpus=1) ray.init(address=cluster.address) @ray.remote(num_cpus=1) class Actor: def __init__(self): pass def pass_channel(self, channel): self._chan = channel def read(self): return self._chan.read() def write(self, value): self._chan.write(value) def get_ctx_buffer_size(self): ctx = ray_channel.ChannelContext.get_current().serialization_context return len(ctx.intra_process_channel_buffers) actor = Actor.remote() channel = ray_channel.IntraProcessChannel(num_readers=2) ray.get(actor.pass_channel.remote(channel)) ray.get(actor.write.remote("hello")) # first read assert ray.get(actor.read.remote()) == "hello" assert ray.get(actor.get_ctx_buffer_size.remote()) == 1 # second read assert ray.get(actor.read.remote()) == "hello" assert ray.get(actor.get_ctx_buffer_size.remote()) == 0 # Write again after reading num_readers times. ray.get(actor.write.remote("world")) # first read assert ray.get(actor.read.remote()) == "world" assert ray.get(actor.get_ctx_buffer_size.remote()) == 1 # second read assert ray.get(actor.read.remote()) == "world" assert ray.get(actor.get_ctx_buffer_size.remote()) == 0 # Write again ray.get(actor.write.remote("hello world")) # first read assert ray.get(actor.read.remote()) == "hello world" assert ray.get(actor.get_ctx_buffer_size.remote()) == 1 with pytest.raises(ray.exceptions.RayTaskError): ray.get(actor.write.remote("world hello")) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_cached_channel_single_reader(): ray.init() @ray.remote class Actor: def __init__(self): pass def pass_channel(self, channel): self._chan = channel def read(self): return self._chan.read() def get_ctx_buffer_size(self): ctx = ray_channel.ChannelContext.get_current().serialization_context return len(ctx.intra_process_channel_buffers) actor = Actor.remote() inner_channel = ray_channel.Channel( None, [ (actor, get_actor_node_id(actor)), ], 1000, ) channel = ray_channel.CachedChannel(num_reads=1, inner_channel=inner_channel) ray.get(actor.pass_channel.remote(channel)) channel.write("hello") assert ray.get(actor.read.remote()) == "hello" # The _SerializationContext should clean up the data after a read. assert ray.get(actor.get_ctx_buffer_size.remote()) == 0 # Write again after reading num_readers times. channel.write("world") assert ray.get(actor.read.remote()) == "world" # The _SerializationContext should clean up the data after a read. assert ray.get(actor.get_ctx_buffer_size.remote()) == 0 @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_cached_channel_multi_readers(ray_start_cluster): @ray.remote class Actor: def __init__(self): pass def pass_channel(self, channel): self._chan = channel def read(self): return self._chan.read() def get_ctx_buffer_size(self): ctx = ray_channel.ChannelContext.get_current().serialization_context return len(ctx.intra_process_channel_buffers) actor = Actor.remote() inner_channel = ray_channel.Channel( None, [ (actor, get_actor_node_id(actor)), ], 1000, ) channel = ray_channel.CachedChannel(num_reads=2, inner_channel=inner_channel) ray.get(actor.pass_channel.remote(channel)) channel.write("hello") # first read assert ray.get(actor.read.remote()) == "hello" assert ray.get(actor.get_ctx_buffer_size.remote()) == 1 # second read assert ray.get(actor.read.remote()) == "hello" assert ray.get(actor.get_ctx_buffer_size.remote()) == 0 # Write again after reading num_readers times. channel.write("world") # first read assert ray.get(actor.read.remote()) == "world" assert ray.get(actor.get_ctx_buffer_size.remote()) == 1 # second read assert ray.get(actor.read.remote()) == "world" assert ray.get(actor.get_ctx_buffer_size.remote()) == 0 @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_composite_channel_single_reader(ray_start_cluster): """ (1) The driver can write data to CompositeChannel and an actor can read it. (2) An actor can write data to CompositeChannel and the actor itself can read it. (3) An actor can write data to CompositeChannel and another actor can read it. (4) An actor can write data to CompositeChannel and the driver can read it. """ # This node is for both the driver and the Reader actors. cluster = ray_start_cluster cluster.add_node(num_cpus=2) ray.init(address=cluster.address) @ray.remote(num_cpus=1) class Actor: def __init__(self): pass def pass_channel(self, channel): self._chan = channel def create_composite_channel( self, writer, reader_and_node_list, driver_actor_id ): self._chan = ray_channel.CompositeChannel( writer, reader_and_node_list, 10, driver_actor_id ) return self._chan def read(self): return self._chan.read() def write(self, value): self._chan.write(value) actor1 = Actor.remote() actor2 = Actor.remote() node1 = get_actor_node_id(actor1) node2 = get_actor_node_id(actor2) # Create a channel to communicate between driver process and actor1. driver_to_actor1_channel = ray_channel.CompositeChannel(None, [(actor1, node1)], 10) ray.get(actor1.pass_channel.remote(driver_to_actor1_channel)) driver_to_actor1_channel.write("hello") assert ray.get(actor1.read.remote()) == "hello" # Create a channel to communicate between two tasks in actor1. ray.get(actor1.create_composite_channel.remote(actor1, [(actor1, node1)], None)) ray.get(actor1.write.remote("world")) assert ray.get(actor1.read.remote()) == "world" # Create a channel to communicate between actor1 and actor2. actor1_to_actor2_channel = ray.get( actor1.create_composite_channel.remote(actor1, [(actor2, node2)], None) ) ray.get(actor2.pass_channel.remote(actor1_to_actor2_channel)) ray.get(actor1.write.remote("hello world")) assert ray.get(actor2.read.remote()) == "hello world" # Create a channel to communicate between actor2 and driver process. driver_actor = create_driver_actor() actor2_to_driver_channel = ray.get( actor2.create_composite_channel.remote( actor2, [(driver_actor, get_actor_node_id(driver_actor))], driver_actor._actor_id.hex(), ) ) ray.get(actor2.write.remote("world hello")) assert actor2_to_driver_channel.read() == "world hello" @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_composite_channel_multiple_readers(ray_start_cluster): """ Test the behavior of CompositeChannel when there are multiple readers. (1) The driver can write data to CompositeChannel and two actors can read it. (2) An actor can write data to CompositeChannel and another actor, as well as itself, can read it. (3) An actor writes data to CompositeChannel and two actor methods on the same actor read it. This is not supported and should raise an exception. """ # This node is for both the driver and the Reader actors. cluster = ray_start_cluster cluster.add_node(num_cpus=2) ray.init(address=cluster.address) @ray.remote(num_cpus=1) class Actor: def __init__(self): pass def pass_channel(self, channel): self._chan = channel def create_composite_channel(self, writer, reader_and_node_list): self._chan = ray_channel.CompositeChannel(writer, reader_and_node_list, 10) return self._chan def read(self): return self._chan.read() def write(self, value): self._chan.write(value) actor1 = Actor.remote() actor2 = Actor.remote() node1 = get_actor_node_id(actor1) node2 = get_actor_node_id(actor2) # The driver writes data to CompositeChannel and actor1 and actor2 read it. driver_output_channel = ray_channel.CompositeChannel( None, [(actor1, node1), (actor2, node2)], 10 ) ray.get(actor1.pass_channel.remote(driver_output_channel)) ray.get(actor2.pass_channel.remote(driver_output_channel)) driver_output_channel.write("hello") assert ray.get([actor1.read.remote(), actor2.read.remote()]) == ["hello"] * 2 # actor1 writes data to CompositeChannel and actor1 and actor2 read it. actor1_output_channel = ray.get( actor1.create_composite_channel.remote( actor1, [(actor1, node1), (actor2, node2)] ) ) ray.get(actor2.pass_channel.remote(actor1_output_channel)) ray.get(actor1.write.remote("world")) assert ray.get([actor1.read.remote(), actor2.read.remote()]) == ["world"] * 2 actor1_output_channel = ray.get( actor1.create_composite_channel.remote( actor1, [(actor1, node1), (actor1, node1)] ) ) ray.get(actor1.write.remote("hello world")) assert ray.get(actor1.read.remote()) == "hello world" with pytest.raises(ray.exceptions.RayTaskError): # actor1_output_channel can be read only once if the readers # are the same actor. Note that reading the channel multiple # times is supported via channel cache mechanism. ray.get(actor1.read.remote()) # actor1 writes data to CompositeChannel and actor2 and the driver reads it. driver_actor = create_driver_actor() actor1_output_channel = ray.get( actor1.create_composite_channel.remote( actor1, [(driver_actor, get_actor_node_id(driver_actor)), (actor2, node2)], ) ) ray.get(actor2.pass_channel.remote(actor1_output_channel)) ray.get(actor1.write.remote("world hello")) assert ray.get(actor2.read.remote()) == "world hello" assert actor1_output_channel.read() == "world hello" @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_put_error(ray_start_cluster): cluster = ray_start_cluster # This node is for both the driver (including the CompiledDAG.DAGDriverProxyActor) # and the writer actor. cluster.add_node(num_cpus=2) ray.init(address=cluster.address) def _wrap_exception(exc): backtrace = ray._private.utils.format_error_message( "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)), task_exception=True, ) wrapped = ray.exceptions.RayTaskError( function_name="do_exec_tasks", traceback_str=backtrace, cause=exc, ) return wrapped @ray.remote(num_cpus=1) class Actor: def setup(self, reader_and_node_list): self._channel = ray_channel.Channel( ray.get_runtime_context().current_actor, reader_and_node_list, 1000, ) def get_channel(self): return self._channel def write(self, write_error): if write_error: try: raise ValueError("") except Exception as exc: self._channel.write(_wrap_exception(exc)) else: self._channel.write(b"x") a = Actor.remote() driver_actor = create_driver_actor() ray.get(a.setup.remote([(driver_actor, get_actor_node_id(driver_actor))])) chan = ray.get(a.get_channel.remote()) # Putting a bytes object multiple times is okay. for _ in range(3): ray.get(a.write.remote(write_error=False)) assert chan.read() == b"x" # Putting an exception multiple times is okay. for _ in range(3): ray.get(a.write.remote(write_error=True)) try: assert chan.read() except Exception as exc: assert isinstance(exc, ValueError) assert isinstance(exc, ray.exceptions.RayTaskError) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_payload_large(ray_start_cluster): cluster = ray_start_cluster # This node is for the driver. 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 @ray.remote(num_cpus=1) class Actor: def get_node_id(self): return ray.get_runtime_context().get_node_id() def read(self, channel, val): assert channel.read() == val def create_actor(node): return Actor.options( label_selector={ray._raylet.RAY_NODE_ID_KEY: node} ).remote() driver_node = ray.get_runtime_context().get_node_id() actor_node = nodes[0] if nodes[0] != driver_node else nodes[1] assert driver_node != actor_node a = create_actor(actor_node) node_a = ray.get(a.get_node_id.remote()) assert driver_node != ray.get(a.get_node_id.remote()) # Ray sets the gRPC payload max size to 512 MiB. We choose a size in this test that # is a bit larger. size = 1024 * 1024 * 600 ch = ray_channel.Channel(None, [(a, node_a)], size) val = b"x" * size ch.write(val) ray.get(a.read.remote(ch, val)) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_payload_resize_large(ray_start_cluster): cluster = ray_start_cluster # This node is for the driver. 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 @ray.remote(num_cpus=1) class Actor: def get_node_id(self): return ray.get_runtime_context().get_node_id() def read(self, channel, val): assert channel.read() == val def create_actor(node): return Actor.options( label_selector={ray._raylet.RAY_NODE_ID_KEY: node} ).remote() driver_node = ray.get_runtime_context().get_node_id() actor_node = nodes[0] if nodes[0] != driver_node else nodes[1] assert driver_node != actor_node a = create_actor(actor_node) assert driver_node != ray.get(a.get_node_id.remote()) ch = ray_channel.Channel(None, [(a, actor_node)], 1000) # Ray sets the gRPC payload max size to 512 MiB. We choose a size in this test that # is a bit larger. size = 1024 * 1024 * 600 val = b"x" * size ch.write(val) ray.get(a.read.remote(ch, val)) @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) 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. first_node_handle = cluster.add_node(num_cpus=2) # This node is for the other 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 @ray.remote(num_cpus=1) class Actor: def get_node_id(self): return ray.get_runtime_context().get_node_id() def read(self, channel, val): assert channel.read() == val return val def create_actor(node): return Actor.options( label_selector={ray._raylet.RAY_NODE_ID_KEY: node} ).remote() a = create_actor(nodes[0]) b = create_actor(nodes[1]) actors = [a, b] nodes_check = ray.get([act.get_node_id.remote() for act in actors]) a_node = nodes_check[0] b_node = nodes_check[1] assert a_node != b_node driver_actor = create_driver_actor() driver_node = get_actor_node_id(driver_actor) ch = ray_channel.Channel( None, [(driver_actor, driver_node), (a, a_node), (b, b_node)], 1000 ) val = 1 ch.write(val) assert ray.get([a.read.remote(ch, val) for a in actors]) == [val, val] @pytest.mark.skipif( sys.platform != "linux" and sys.platform != "darwin", reason="Requires Linux or Mac.", ) def test_bunch_readers_on_different_nodes(ray_start_cluster): cluster = ray_start_cluster # This node is for the driver (including the DriverHelperActor) and two of the # readers. first_node_handle = cluster.add_node(num_cpus=3) # This node is for the other two readers. second_node_handle = cluster.add_node(num_cpus=2) 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 the readers are on different 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 @ray.remote(num_cpus=1) class Actor: def get_node_id(self): return ray.get_runtime_context().get_node_id() def read(self, channel, val): assert channel.read() == val return val def create_actor(node): return Actor.options( label_selector={ray._raylet.RAY_NODE_ID_KEY: node} ).remote() a = create_actor(nodes[0]) b = create_actor(nodes[0]) c = create_actor(nodes[1]) d = create_actor(nodes[1]) actors = [a, b, c, d] nodes_check = ray.get([act.get_node_id.remote() for act in actors]) a_node = nodes_check[0] b_node = nodes_check[1] c_node = nodes_check[2] d_node = nodes_check[3] assert a_node == b_node assert b_node != c_node assert c_node == d_node driver_actor = create_driver_actor() driver_node = get_actor_node_id(driver_actor) ch = ray_channel.Channel( None, [ (driver_actor, driver_node), (a, a_node), (b, b_node), (c, c_node), (d, d_node), ], 1000, ) i = 1 ch.write(i) assert ray.get([a.read.remote(ch, i) for a in actors]) == [ i for _ in range(len(actors)) ] def test_buffered_channel(shutdown_only): """Test buffered shared memory channel.""" BUFFER_SIZE = 5 @ray.remote(num_cpus=1) class Actor: def __init__(self): self.write_index = 0 def setup(self, driver_actor): self._channel = ray_channel.BufferedSharedMemoryChannel( ray.get_runtime_context().current_actor, [(driver_actor, get_actor_node_id(driver_actor))], BUFFER_SIZE, typ=1000, ) def get_channel(self): return self._channel def write(self, i, timeout=None) -> bool: """Write to a channel Return False if channel times out. Return true otherwise. """ self.write_index += 1 try: self._channel.write(i, timeout) except ray.exceptions.RayChannelTimeoutError: return False assert self._channel.next_write_index == self.write_index % BUFFER_SIZE return True a = Actor.remote() ray.get(a.setup.remote(create_driver_actor())) chan = ray.get(a.get_channel.remote()) print("Test basic.") # Iterate more than buffer size to make sure it works over and over again. read_idx = 0 for i in range(BUFFER_SIZE * 3): read_idx += 1 assert ray.get(a.write.remote(i)) assert chan.read() == i assert chan.next_read_index == read_idx % BUFFER_SIZE print("Test Write timeout.") # Test write timeout. for i in range(BUFFER_SIZE): # fill the buffer withtout read. ray.get(a.write.remote(i)) # timeout because all the buffer is exhausted without being consumed. assert ray.get(a.write.remote(1, timeout=1)) is False print("Test Read timeout.") # Test read timeout. for i in range(BUFFER_SIZE): # This reads all previous writes. assert chan.read() == i # This read times out because there's no new write, and the call blocks. with pytest.raises(ray.exceptions.RayChannelTimeoutError): chan.read(timeout=1) print("Test serialization/deserialization works") deserialized = pickle.loads(pickle.dumps(chan)) assert len(chan._buffers) == len(deserialized._buffers) for i in range(len(chan._buffers)): assert ( deserialized._buffers[i]._writer._actor_id == chan._buffers[i]._writer._actor_id ) def test_torch_dtype(): typ = TorchTensorType() typ.register_custom_serializer() t = torch.randn(5, 5, dtype=torch.bfloat16) with pytest.raises(TypeError): t.numpy() ref = ray.put(t) t_out = ray.get(ref) assert (t == t_out).all() assert t_out.dtype == t.dtype if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))