820 lines
33 KiB
Python
820 lines
33 KiB
Python
import io
|
|
import logging
|
|
import time
|
|
from collections import defaultdict, namedtuple
|
|
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
|
|
import ray
|
|
import ray.exceptions
|
|
from ray._raylet import SerializedObject
|
|
from ray.experimental.channel import utils
|
|
from ray.experimental.channel.common import ChannelInterface, ChannelOutputType
|
|
from ray.experimental.channel.intra_process_channel import IntraProcessChannel
|
|
from ray.experimental.channel.utils import get_self_actor
|
|
from ray.util.annotations import DeveloperAPI, PublicAPI
|
|
|
|
# Logger for this module. It should be configured at the entry point
|
|
# into the program using Ray. Ray provides a default configuration at
|
|
# entry/init points.
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _create_channel_ref(
|
|
self: Any,
|
|
buffer_size_bytes: int,
|
|
) -> "ray.ObjectRef":
|
|
"""Create a channel that can be read and written through Ray's shared-memory
|
|
object store.
|
|
|
|
The channel has no buffer, so the writer will block until reader(s) have
|
|
read the previous value.
|
|
|
|
A writer and colocated readers can communicate via a shared memory buffer.
|
|
If the readers are remote, then RPC is used to synchronize the writer and
|
|
readers' buffers.
|
|
|
|
Args:
|
|
self: The actor on which to allocate the channel buffer. Passed via
|
|
``__ray_call__`` so this function executes on the actor.
|
|
buffer_size_bytes: The initial buffer size in bytes for messages
|
|
that can be passed between tasks in the DAG. The buffers will
|
|
be automatically resized if larger messages are written to the
|
|
channel.
|
|
|
|
Returns:
|
|
A wrapper around ``ray.ObjectRef`` backing the channel.
|
|
"""
|
|
worker = ray._private.worker.global_worker
|
|
worker.check_connected()
|
|
|
|
value = b"0" * buffer_size_bytes
|
|
|
|
try:
|
|
object_ref = worker.put_object(value, _is_experimental_channel=True)
|
|
except ray.exceptions.ObjectStoreFullError:
|
|
logger.info(
|
|
"Put failed since the value was either too large or the "
|
|
"store was full of pinned objects."
|
|
)
|
|
raise
|
|
return object_ref
|
|
|
|
|
|
# Compiled Graph maintains 1 reader object reference (also called buffer) per node.
|
|
# reader_ref: The object reference.
|
|
# ref_owner_actor_id: The actor who created the object reference.
|
|
# num_readers: The number of reader actors who reads this object reference.
|
|
ReaderRefInfo = namedtuple(
|
|
"ReaderRefInfo", ["reader_ref", "ref_owner_actor_id", "num_reader_actors"]
|
|
)
|
|
|
|
|
|
class _ResizeChannel:
|
|
"""Sentinel used to resize a channel's backing store on the readers.
|
|
|
|
When a channel must be resized, the channel backing store must be resized on both
|
|
the writer and the reader nodes. The writer first resizes its own backing store. The
|
|
writer then uses an instance of this class as a sentinel value to tell the reader to
|
|
resize its own backing store. The class instance is sent through the channel.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
_node_id_to_reader_ref_info: Dict[str, ReaderRefInfo],
|
|
):
|
|
"""Initialize the resize sentinel.
|
|
|
|
Args:
|
|
_node_id_to_reader_ref_info: Mapping from node id to ``ReaderRefInfo``
|
|
describing the new reader buffers per node.
|
|
"""
|
|
self._node_id_to_reader_ref_info = _node_id_to_reader_ref_info
|
|
|
|
|
|
class SharedMemoryType(ChannelOutputType):
|
|
def __init__(
|
|
self,
|
|
*,
|
|
buffer_size_bytes: Optional[int] = None,
|
|
num_shm_buffers: Optional[int] = None,
|
|
):
|
|
"""Initialize a ``SharedMemoryType``.
|
|
|
|
Args:
|
|
buffer_size_bytes: The initial buffer size in bytes for messages
|
|
that can be passed between tasks in the DAG. The buffers will
|
|
be automatically resized if larger messages are written to the
|
|
channel.
|
|
num_shm_buffers: The number of shared memory buffers per channel.
|
|
Note: In the case of multiple nodes, we only support 1 shared
|
|
memory buffer.
|
|
"""
|
|
super().__init__()
|
|
|
|
from ray.dag import DAGContext
|
|
|
|
ctx = DAGContext.get_current()
|
|
|
|
if buffer_size_bytes is None:
|
|
buffer_size_bytes = ctx.buffer_size_bytes
|
|
self.buffer_size_bytes = buffer_size_bytes
|
|
if num_shm_buffers is None:
|
|
num_shm_buffers = 1
|
|
self._num_shm_buffers = num_shm_buffers
|
|
|
|
def create_channel(
|
|
self,
|
|
writer: Optional["ray.actor.ActorHandle"],
|
|
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
|
driver_actor_id: Optional[str] = None,
|
|
) -> "Channel":
|
|
"""
|
|
Instantiate a ChannelInterface class that can be used
|
|
to pass data of this type.
|
|
|
|
Args:
|
|
writer: The actor that may write to the channel. None signifies the driver.
|
|
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
|
actor handle and the node ID where the actor is located.
|
|
driver_actor_id: If this channel is read by a driver and that driver is an
|
|
actual actor, this will be the actor ID of that driver actor.
|
|
|
|
Returns:
|
|
A ChannelInterface that can be used to pass data
|
|
of this type.
|
|
"""
|
|
return CompositeChannel(
|
|
writer,
|
|
reader_and_node_list,
|
|
self._num_shm_buffers,
|
|
driver_actor_id,
|
|
)
|
|
|
|
|
|
@PublicAPI(stability="alpha")
|
|
class Channel(ChannelInterface):
|
|
"""
|
|
A wrapper type for ray.ObjectRef. Currently supports ray.get but not
|
|
ray.wait.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
writer: Optional[ray.actor.ActorHandle],
|
|
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
|
typ: Optional[Union[int, SharedMemoryType]] = None,
|
|
_writer_node_id: Optional["ray.NodeID"] = None,
|
|
_writer_ref: Optional["ray.ObjectRef"] = None,
|
|
_node_id_to_reader_ref_info: Optional[Dict[str, ReaderRefInfo]] = None,
|
|
_writer_registered: bool = False,
|
|
_reader_registered: bool = False,
|
|
):
|
|
"""Create a channel that can be read and written by co-located Ray processes.
|
|
|
|
Anyone may write to or read from the channel. The channel has no
|
|
buffer, so the writer will block until reader(s) have read the previous
|
|
value.
|
|
|
|
Args:
|
|
writer: The actor that may write to the channel. None signifies the driver.
|
|
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
|
actor handle and the node ID where the actor is located.
|
|
typ: Type information about the values passed through the channel.
|
|
Either an integer representing the max buffer size in bytes
|
|
allowed, or a SharedMemoryType.
|
|
_writer_node_id: Internal. Node ID hosting the writer. Provided
|
|
when rehydrating a channel that was constructed on another
|
|
process.
|
|
_writer_ref: Internal. Pre-existing writer-side ``ObjectRef``.
|
|
When set, the constructor skips allocating a new writer buffer.
|
|
_node_id_to_reader_ref_info: Internal. Mapping from node id to
|
|
``ReaderRefInfo`` describing existing reader buffers per node.
|
|
_writer_registered: Internal. Whether the writer side has already
|
|
been registered with the core worker.
|
|
_reader_registered: Internal. Whether the reader side has already
|
|
been registered with the core worker.
|
|
"""
|
|
assert len(reader_and_node_list) > 0
|
|
for reader, _ in reader_and_node_list:
|
|
assert isinstance(reader, ray.actor.ActorHandle)
|
|
|
|
if typ is None:
|
|
typ = SharedMemoryType()
|
|
elif isinstance(typ, int):
|
|
typ = SharedMemoryType(buffer_size_bytes=typ)
|
|
|
|
# The min buffer size must be large enough to at least fit an instance of the
|
|
# _ResizeChannel class along with any metadata.
|
|
MIN_BUFFER_SIZE = int(1000) # 1000 bytes
|
|
if typ.buffer_size_bytes < MIN_BUFFER_SIZE:
|
|
raise ValueError(
|
|
"typ.buffer_size_bytes must be at least MIN_BUFFER_SIZE "
|
|
f"({MIN_BUFFER_SIZE} bytes)"
|
|
)
|
|
|
|
self._writer = writer
|
|
self._reader_and_node_list = reader_and_node_list
|
|
self._typ = typ
|
|
|
|
self._worker = ray._private.worker.global_worker
|
|
self._worker.check_connected()
|
|
|
|
self._writer_registered = _writer_registered
|
|
self._reader_registered = _reader_registered
|
|
# NodeID -> ReaderRefInfo on that node. Note that there's only 1
|
|
# reader ref per node.
|
|
self._node_id_to_reader_ref_info: Dict[str, ReaderRefInfo] = (
|
|
_node_id_to_reader_ref_info or {}
|
|
)
|
|
|
|
# Node ID -> a list of reader actors.
|
|
self._node_id_to_readers: Dict[str, "ray.actor.ActorHandle"] = defaultdict(list)
|
|
for reader, node_id in self._reader_and_node_list:
|
|
self._node_id_to_readers[node_id].append(reader)
|
|
|
|
# Number of readers in a local node.
|
|
self._num_local_readers = 0
|
|
|
|
if _writer_ref is None:
|
|
# We are the writer. Check that the passed handle matches the
|
|
# current actor (or it is the driver).
|
|
# TODO(swang): Channels must be initially constructed by the writer
|
|
# actor, so we shouldn't need to include `writer` in the
|
|
# constructor args. Either support Channels being constructed by
|
|
# someone other than the writer or remove it from the args.
|
|
self_actor = get_self_actor()
|
|
assert writer == self_actor
|
|
|
|
self._writer_node_id = (
|
|
ray.runtime_context.get_runtime_context().get_node_id()
|
|
)
|
|
self._writer_ref = _create_channel_ref(self, typ.buffer_size_bytes)
|
|
|
|
self._create_reader_refs(typ.buffer_size_bytes)
|
|
else:
|
|
assert (
|
|
_writer_node_id is not None
|
|
), "_writer_node_id must also be passed to the constructor when "
|
|
"_writer_ref is."
|
|
assert _node_id_to_reader_ref_info is not None, (
|
|
"_node_id_to_reader_ref_info must also be passed to the constructor "
|
|
"when _writer_ref is."
|
|
)
|
|
|
|
self._writer_ref = _writer_ref
|
|
self._writer_node_id = _writer_node_id
|
|
self._node_id_to_reader_ref_info = _node_id_to_reader_ref_info
|
|
|
|
assert self._num_local_readers == 0
|
|
remote_node_exists = False
|
|
for node_id, readers in self._node_id_to_readers.items():
|
|
if self.is_local_node(node_id):
|
|
self._num_local_readers += len(readers)
|
|
else:
|
|
remote_node_exists = True
|
|
# If remote node exists, we have 1 additional reader that listens
|
|
# to object changes and push them to remote nodes.
|
|
if remote_node_exists:
|
|
self._num_local_readers += 1
|
|
# There must be at least 1 local reader
|
|
assert self._num_local_readers > 0
|
|
|
|
self._local_reader_ref: Optional["ray.ObjectRef"] = self._get_local_reader_ref(
|
|
self._node_id_to_reader_ref_info
|
|
)
|
|
|
|
def _get_local_reader_ref(
|
|
self, _node_id_to_reader_ref_info: Dict[str, ReaderRefInfo]
|
|
) -> Optional["ray.ObjectRef"]:
|
|
for node_id, reader_ref_info in _node_id_to_reader_ref_info.items():
|
|
if self.is_local_node(node_id):
|
|
return reader_ref_info.reader_ref
|
|
return None
|
|
|
|
def _create_reader_refs(
|
|
self,
|
|
buffer_size_bytes: int,
|
|
):
|
|
# TODO(jhumphri): Free the current reader ref once the reference to it is
|
|
# destroyed below.
|
|
|
|
for node_id, readers in self._node_id_to_readers.items():
|
|
if not self.is_local_node(node_id):
|
|
# Find 1 reader in a remote node to create a reference that's
|
|
# shared by all readers. When a new value is written to a reference,
|
|
# it is sent to this reference.
|
|
reader = readers[0]
|
|
fn = reader.__ray_call__
|
|
self._node_id_to_reader_ref_info[node_id] = ReaderRefInfo(
|
|
reader_ref=ray.get(
|
|
fn.remote(_create_channel_ref, buffer_size_bytes)
|
|
),
|
|
ref_owner_actor_id=reader._actor_id,
|
|
num_reader_actors=len(readers),
|
|
)
|
|
else:
|
|
writer_id = ray.ActorID.nil()
|
|
if self._writer is not None:
|
|
writer_id = self._writer._actor_id
|
|
self._node_id_to_reader_ref_info[node_id] = ReaderRefInfo(
|
|
reader_ref=self._writer_ref,
|
|
ref_owner_actor_id=writer_id,
|
|
num_reader_actors=len(readers),
|
|
)
|
|
# There must be only 1 node reader reference per node.
|
|
assert len(self._node_id_to_reader_ref_info) == len(self._node_id_to_readers)
|
|
|
|
# We need to register the new writer_ref.
|
|
self._writer_registered = False
|
|
self.ensure_registered_as_writer()
|
|
|
|
@staticmethod
|
|
def is_local_node(node_id):
|
|
return ray.runtime_context.get_runtime_context().get_node_id() == node_id
|
|
|
|
def ensure_registered_as_writer(self) -> None:
|
|
if self._writer_registered:
|
|
return
|
|
|
|
if not self.is_local_node(self._writer_node_id):
|
|
raise ValueError(
|
|
"`ensure_registered_as_writer()` must only be called on the node that "
|
|
"the writer is on."
|
|
)
|
|
|
|
remote_reader_ref_info: Dict[str, ReaderRefInfo] = {}
|
|
for node_id, reader_ref_info in self._node_id_to_reader_ref_info.items():
|
|
if self.is_local_node(node_id):
|
|
continue
|
|
remote_reader_ref_info[node_id] = reader_ref_info
|
|
|
|
self._worker.core_worker.experimental_channel_register_writer(
|
|
self._writer_ref,
|
|
remote_reader_ref_info,
|
|
)
|
|
self._writer_registered = True
|
|
|
|
def ensure_registered_as_reader(self) -> None:
|
|
if self._reader_registered:
|
|
return
|
|
|
|
for node_id, reader_ref_info in self._node_id_to_reader_ref_info.items():
|
|
if self.is_local_node(node_id):
|
|
self._worker.core_worker.experimental_channel_register_reader(
|
|
reader_ref_info.reader_ref,
|
|
)
|
|
self._reader_registered = True
|
|
|
|
@staticmethod
|
|
def _deserialize_reader_channel(
|
|
writer: ray.actor.ActorHandle,
|
|
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
|
typ: int,
|
|
writer_node_id,
|
|
writer_ref: "ray.ObjectRef",
|
|
node_id_to_reader_ref_info: Dict[str, ReaderRefInfo],
|
|
writer_registered: bool,
|
|
reader_registered: bool,
|
|
) -> "Channel":
|
|
chan = Channel(
|
|
writer,
|
|
reader_and_node_list,
|
|
typ,
|
|
_writer_node_id=writer_node_id,
|
|
_writer_ref=writer_ref,
|
|
_node_id_to_reader_ref_info=node_id_to_reader_ref_info,
|
|
_writer_registered=writer_registered,
|
|
_reader_registered=reader_registered,
|
|
)
|
|
return chan
|
|
|
|
def __reduce__(self):
|
|
assert self._node_id_to_reader_ref_info is not None
|
|
return self._deserialize_reader_channel, (
|
|
self._writer,
|
|
self._reader_and_node_list,
|
|
self._typ,
|
|
self._writer_node_id,
|
|
self._writer_ref,
|
|
self._node_id_to_reader_ref_info,
|
|
self._writer_registered,
|
|
self._reader_registered,
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return (
|
|
f"Channel(_node_id_to_reader_ref_info={self._node_id_to_reader_ref_info}, "
|
|
f"_writer_ref={self._writer_ref})"
|
|
)
|
|
|
|
def _resize_channel_if_needed(self, serialized_value: str, timeout_ms: int):
|
|
# serialized_value.total_bytes *only* includes the size of the data. It does not
|
|
# include the size of the metadata, so we must account for the size of the
|
|
# metadata explicitly.
|
|
size = serialized_value.total_bytes + len(serialized_value.metadata)
|
|
if size > self._typ.buffer_size_bytes:
|
|
# Now make the channel backing store larger.
|
|
self._typ.buffer_size_bytes = size
|
|
# TODO(jhumphri): Free the current writer ref once the reference to it is
|
|
# destroyed below.
|
|
# TODO(sang): Support different policies such as 2X buffer size.
|
|
prev_writer_ref = self._writer_ref
|
|
self._writer_ref = _create_channel_ref(self, self._typ.buffer_size_bytes)
|
|
self._create_reader_refs(self._typ.buffer_size_bytes)
|
|
self._local_reader_ref = self._get_local_reader_ref(
|
|
self._node_id_to_reader_ref_info
|
|
)
|
|
|
|
# Write a special message to the channel so that the readers know to
|
|
# stop using the current reader_ref.
|
|
special_message = _ResizeChannel(self._node_id_to_reader_ref_info)
|
|
special_message_serialized = (
|
|
self._worker.get_serialization_context().serialize(special_message)
|
|
)
|
|
self._worker.core_worker.experimental_channel_put_serialized(
|
|
special_message_serialized,
|
|
prev_writer_ref,
|
|
self._num_local_readers,
|
|
timeout_ms,
|
|
)
|
|
# TODO(sang): Clean the previous ref that won't be used.
|
|
# Right now, if we just close it here, it will not work because
|
|
# of race conditions.
|
|
# self._worker.core_worker.experimental_channel_set_error(
|
|
# prev_writer_ref
|
|
# )
|
|
|
|
def write(self, value: Any, timeout: Optional[float] = None) -> None:
|
|
self.ensure_registered_as_writer()
|
|
assert (
|
|
timeout is None or timeout >= 0 or timeout == -1
|
|
), "Timeout must be non-negative or -1."
|
|
# -1 means no timeout (block indefinitely)
|
|
timeout_ms = int(timeout * 1000) if timeout is not None else -1
|
|
|
|
if not isinstance(value, SerializedObject):
|
|
try:
|
|
serialized_value = self._worker.get_serialization_context().serialize(
|
|
value
|
|
)
|
|
except TypeError as e:
|
|
sio = io.StringIO()
|
|
ray.util.inspect_serializability(value, print_file=sio)
|
|
msg = (
|
|
"Could not serialize the put value "
|
|
f"{repr(value)}:\n"
|
|
f"{sio.getvalue()}"
|
|
)
|
|
raise TypeError(msg) from e
|
|
else:
|
|
serialized_value = value
|
|
|
|
start_time = time.monotonic()
|
|
self._resize_channel_if_needed(serialized_value, timeout_ms)
|
|
if timeout is not None:
|
|
timeout_ms -= int((time.monotonic() - start_time) * 1000)
|
|
timeout_ms = max(timeout_ms, 0)
|
|
|
|
self._worker.core_worker.experimental_channel_put_serialized(
|
|
serialized_value,
|
|
self._writer_ref,
|
|
self._num_local_readers,
|
|
timeout_ms,
|
|
)
|
|
|
|
def read(self, timeout: Optional[float] = None) -> Any:
|
|
assert (
|
|
timeout is None or timeout >= 0 or timeout == -1
|
|
), "Timeout must be non-negative or -1."
|
|
self.ensure_registered_as_reader()
|
|
|
|
start_time = time.monotonic()
|
|
ret = self._worker.get_objects(
|
|
[self._local_reader_ref], timeout=timeout, return_exceptions=True
|
|
)[0][0]
|
|
|
|
if isinstance(ret, _ResizeChannel):
|
|
self._node_id_to_reader_ref_info = ret._node_id_to_reader_ref_info
|
|
self._local_reader_ref = self._get_local_reader_ref(
|
|
self._node_id_to_reader_ref_info
|
|
)
|
|
# We need to register the new reader_ref.
|
|
self._reader_registered = False
|
|
self.ensure_registered_as_reader()
|
|
if timeout is not None:
|
|
timeout -= time.monotonic() - start_time
|
|
timeout = max(timeout, 0)
|
|
ret = self._worker.get_objects(
|
|
[self._local_reader_ref], timeout=timeout, return_exceptions=True
|
|
)[0][0]
|
|
|
|
return ret
|
|
|
|
def release_buffer(self, timeout: Optional[float] = None) -> None:
|
|
assert (
|
|
timeout is None or timeout >= 0 or timeout == -1
|
|
), "Timeout must be non-negative or -1."
|
|
self.ensure_registered_as_reader()
|
|
self._worker.get_objects(
|
|
[self._local_reader_ref],
|
|
timeout=timeout,
|
|
return_exceptions=True,
|
|
skip_deserialization=True,
|
|
)
|
|
|
|
def close(self) -> None:
|
|
"""
|
|
Close this channel by setting the error bit on both the writer_ref and the
|
|
reader_ref.
|
|
"""
|
|
self._worker.core_worker.experimental_channel_set_error(self._writer_ref)
|
|
is_local_node_reader = False
|
|
|
|
for node_id in self._node_id_to_readers.keys():
|
|
if self.is_local_node(node_id):
|
|
is_local_node_reader = True
|
|
if is_local_node_reader:
|
|
self.ensure_registered_as_reader()
|
|
|
|
for reader_ref_info in self._node_id_to_reader_ref_info.values():
|
|
self._worker.core_worker.experimental_channel_set_error(
|
|
reader_ref_info.reader_ref
|
|
)
|
|
|
|
|
|
@DeveloperAPI
|
|
class BufferedSharedMemoryChannel(ChannelInterface):
|
|
"""A channel that can be read and written by Ray processes.
|
|
|
|
It creates `num_shm_buffers` number of buffers and allows buffered read and
|
|
write APIs. I.e., read and write APIs are non-blocking as long as it can write to
|
|
next buffer or read from a next buffer. See `read` and `write` APIs for
|
|
more details.
|
|
|
|
Args:
|
|
writer: The actor that may write to the channel. None signifies the driver.
|
|
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
|
actor handle and the node ID where the actor is located. Note that currently
|
|
we only support this for readers on the same node as the writer.
|
|
num_shm_buffers: Number of shared memory buffers to read/write.
|
|
typ: Type information about the values passed through the channel.
|
|
Either an integer representing the max buffer size in bytes
|
|
allowed, or a SharedMemoryType.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
writer: Optional[ray.actor.ActorHandle],
|
|
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
|
num_shm_buffers: int,
|
|
typ: Optional[Union[int, SharedMemoryType]] = None,
|
|
):
|
|
self._num_shm_buffers = num_shm_buffers
|
|
self._buffers = [
|
|
# We use Channel directly as a buffer implementation as
|
|
# channel only allows to have 1 shared memory buffer.
|
|
Channel(writer, reader_and_node_list, typ)
|
|
for _ in range(num_shm_buffers)
|
|
]
|
|
# The next index to write from self._buffers.
|
|
self._next_write_index = 0
|
|
# The next index to read from self._buffers.
|
|
self._next_read_index = 0
|
|
|
|
def ensure_registered_as_writer(self):
|
|
"""
|
|
Check whether the process is a valid writer. This method must be idempotent.
|
|
"""
|
|
for buffer in self._buffers:
|
|
buffer.ensure_registered_as_writer()
|
|
|
|
def ensure_registered_as_reader(self):
|
|
"""
|
|
Check whether the process is a valid reader. This method must be idempotent.
|
|
"""
|
|
for buffer in self._buffers:
|
|
buffer.ensure_registered_as_reader()
|
|
|
|
def write(self, value: Any, timeout: Optional[float] = None) -> None:
|
|
"""Write a value to a channel.
|
|
|
|
If the next buffer is available, it returns immediately. If the next
|
|
buffer is not read by downstream consumers, it blocks until a buffer is
|
|
available to write. If a buffer is not available within timeout, it raises
|
|
RayChannelTimeoutError.
|
|
"""
|
|
self.ensure_registered_as_writer()
|
|
# A single channel is not supposed to read and write at the same time.
|
|
assert self._next_read_index == 0
|
|
self._buffers[self._next_write_index].write(value, timeout)
|
|
self._next_write_index += 1
|
|
self._next_write_index %= self._num_shm_buffers
|
|
|
|
def read(self, timeout: Optional[float] = None) -> Any:
|
|
"""Read a value from a channel.
|
|
|
|
If the next buffer is available, it returns immediately. If the next
|
|
buffer is not written by an upstream producer, it blocks until a buffer is
|
|
available to read. If a buffer is not available within timeout, it raises
|
|
RayChannelTimeoutError.
|
|
"""
|
|
self.ensure_registered_as_reader()
|
|
# A single channel is not supposed to read and write at the same time.
|
|
assert self._next_write_index == 0
|
|
output = self._buffers[self._next_read_index].read(timeout)
|
|
self._next_read_index += 1
|
|
self._next_read_index %= self._num_shm_buffers
|
|
return output
|
|
|
|
def release_buffer(self, timeout: Optional[float] = None):
|
|
"""Release the native buffer of the channel to allow the buffer to be reused for
|
|
future data.
|
|
|
|
If the next buffer is available, it returns immediately. If the next
|
|
buffer is not written by an upstream producer, it blocks until a buffer is
|
|
available to be released. If a buffer is not available within timeout, it raises
|
|
RayChannelTimeoutError.
|
|
"""
|
|
# A single channel is not supposed to read and write at the same time.
|
|
assert self._next_write_index == 0
|
|
self._buffers[self._next_read_index].release_buffer(timeout)
|
|
self._next_read_index += 1
|
|
self._next_read_index %= self._num_shm_buffers
|
|
|
|
def close(self) -> None:
|
|
for buffer in self._buffers:
|
|
buffer.close()
|
|
|
|
@property
|
|
def next_write_index(self):
|
|
# Testing only
|
|
return self._next_write_index
|
|
|
|
@property
|
|
def next_read_index(self):
|
|
# Testing only
|
|
return self._next_read_index
|
|
|
|
|
|
@PublicAPI(stability="alpha")
|
|
class CompositeChannel(ChannelInterface):
|
|
"""Routes data to different readers via per-locality channels.
|
|
|
|
For example, if the reader is in the same worker process as the writer,
|
|
the data can be sent via IntraProcessChannel. If the reader is in a different
|
|
worker process, the data can be sent via shared memory channel.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
writer: Optional[ray.actor.ActorHandle],
|
|
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
|
num_shm_buffers: int,
|
|
driver_actor_id: Optional[str] = None,
|
|
_channel_dict: Optional[Dict[ray.ActorID, ChannelInterface]] = None,
|
|
_channels: Optional[Set[ChannelInterface]] = None,
|
|
_writer_registered: bool = False,
|
|
_reader_registered: bool = False,
|
|
):
|
|
"""Initialize a ``CompositeChannel``.
|
|
|
|
Args:
|
|
writer: The actor that may write to the channel. None signifies the driver.
|
|
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
|
actor handle and the node ID where the actor is located.
|
|
num_shm_buffers: The number of shared memory buffers per channel.
|
|
Note: In the case of multiple nodes, we only support 1 shared
|
|
memory buffer.
|
|
driver_actor_id: If this channel is read by a driver and that driver is an
|
|
actual actor, this will be the actor ID of that driver actor.
|
|
_channel_dict: Internal. Pre-populated mapping from actor id to
|
|
the underlying channel. When provided, channels are not
|
|
re-created (used during deserialization).
|
|
_channels: Internal. Deduplicated set of channels backing
|
|
``_channel_dict``. When provided, channels are not re-created.
|
|
_writer_registered: Internal. Whether the writer side has already
|
|
been registered with the core worker.
|
|
_reader_registered: Internal. Whether the reader side has already
|
|
been registered with the core worker.
|
|
"""
|
|
self._writer = writer
|
|
self._reader_and_node_list = reader_and_node_list
|
|
self._num_shm_buffers = num_shm_buffers
|
|
self._driver_actor_id = driver_actor_id
|
|
self._writer_registered = _writer_registered
|
|
self._reader_registered = _reader_registered
|
|
# A dictionary that maps the actor ID to the channel object.
|
|
self._channel_dict = _channel_dict or {}
|
|
# The set of channels is a deduplicated version of the _channel_dict values.
|
|
self._channels = _channels or set()
|
|
if self._channels:
|
|
# This CompositeChannel object is created by deserialization.
|
|
# We don't need to create channels again.
|
|
return
|
|
|
|
(
|
|
remote_reader_and_node_list,
|
|
local_reader_and_node_list,
|
|
) = utils.split_readers_by_locality(self._writer, self._reader_and_node_list)
|
|
# There are some local readers which are the same worker process as the writer.
|
|
# Create a local channel for the writer and the local readers.
|
|
num_local_readers = len(local_reader_and_node_list)
|
|
if num_local_readers > 0:
|
|
# Use num_readers = 1 when creating the local channel,
|
|
# because we have channel cache to support reading
|
|
# from the same channel multiple times.
|
|
local_channel = IntraProcessChannel(num_readers=1)
|
|
self._channels.add(local_channel)
|
|
actor_id = self._get_actor_id(self._writer)
|
|
self._channel_dict[actor_id] = local_channel
|
|
# There are some remote readers which are not the same Ray actor as the writer.
|
|
# We create a BufferedSharedMemoryChannel for readers on the same node, and
|
|
# a single Channel for readers on different nodes due to
|
|
# https://github.com/ray-project/ray/issues/49044
|
|
(
|
|
readers_same_node,
|
|
readers_different_node,
|
|
) = utils.split_actors_by_node_locality(
|
|
utils.get_actor_node(self._writer), remote_reader_and_node_list
|
|
)
|
|
|
|
if len(readers_same_node) != 0:
|
|
remote_channel = BufferedSharedMemoryChannel(
|
|
self._writer, readers_same_node, num_shm_buffers
|
|
)
|
|
self._channels.add(remote_channel)
|
|
for reader, _ in readers_same_node:
|
|
actor_id = self._get_actor_id(reader)
|
|
self._channel_dict[actor_id] = remote_channel
|
|
|
|
if len(readers_different_node) != 0:
|
|
remote_channel = Channel(self._writer, readers_different_node)
|
|
self._channels.add(remote_channel)
|
|
for reader, _ in readers_different_node:
|
|
actor_id = self._get_actor_id(reader)
|
|
self._channel_dict[actor_id] = remote_channel
|
|
|
|
def _get_actor_id(self, reader: ray.actor.ActorHandle) -> str:
|
|
return reader._actor_id.hex()
|
|
|
|
def ensure_registered_as_writer(self) -> None:
|
|
if self._writer_registered:
|
|
return
|
|
for channel in self._channels:
|
|
channel.ensure_registered_as_writer()
|
|
self._writer_registered = True
|
|
|
|
def ensure_registered_as_reader(self) -> None:
|
|
if self._reader_registered:
|
|
return
|
|
for channel in self._channels:
|
|
channel.ensure_registered_as_reader()
|
|
self._reader_registered = True
|
|
|
|
def __reduce__(self):
|
|
return CompositeChannel, (
|
|
self._writer,
|
|
self._reader_and_node_list,
|
|
self._num_shm_buffers,
|
|
self._driver_actor_id,
|
|
self._channel_dict,
|
|
self._channels,
|
|
self._writer_registered,
|
|
self._reader_registered,
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return (
|
|
"CompositeChannel(_channels="
|
|
f"{[str(channel) for channel in self._channels]})"
|
|
)
|
|
|
|
def write(self, value: Any, timeout: Optional[float] = None) -> None:
|
|
self.ensure_registered_as_writer()
|
|
for channel in self._channels:
|
|
channel.write(value, timeout)
|
|
|
|
def read(self, timeout: Optional[float] = None) -> Any:
|
|
self.ensure_registered_as_reader()
|
|
return self._channel_dict[self._resolve_actor_id()].read(timeout)
|
|
|
|
def release_buffer(self, timeout: Optional[float] = None):
|
|
self.ensure_registered_as_reader()
|
|
self._channel_dict[self._resolve_actor_id()].release_buffer(timeout)
|
|
|
|
def _resolve_actor_id(self) -> str:
|
|
actor_id = ray.get_runtime_context().get_actor_id()
|
|
# If actor_id is None, read was called by the driver
|
|
# If the driver is an actor, driver_actor_id will be set to that actor id
|
|
if actor_id is None or actor_id == self._driver_actor_id:
|
|
# Use the actor ID of the DAGDriverProxyActor.
|
|
# The proxy actor is always the first actor in the reader_and_node_list.
|
|
assert len(self._reader_and_node_list) >= 1
|
|
driver_proxy_actor = self._reader_and_node_list[0][0]
|
|
actor_id = self._get_actor_id(driver_proxy_actor)
|
|
return actor_id
|
|
|
|
def close(self) -> None:
|
|
for channel in self._channels:
|
|
channel.close()
|