Files
2026-07-13 13:17:40 +08:00

371 lines
14 KiB
Python

import threading
from collections import defaultdict, deque
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
from ray.experimental.rdt.tensor_transport_manager import (
CommunicatorMetadata,
TensorTransportMetadata,
)
from ray.experimental.rdt.util import (
device_match_transport,
get_tensor_transport_manager,
)
if TYPE_CHECKING:
import torch
def __ray_send__(
self,
obj_id: str,
tensor_transport_meta: TensorTransportMetadata,
communicator_meta: CommunicatorMetadata,
backend: str,
):
"""Helper function that runs on the src actor to send tensors to the dst actor."""
from ray._private.worker import global_worker
rdt_store = global_worker.rdt_manager._rdt_store
assert rdt_store.has_object(obj_id), f"obj_id={obj_id} not found in RDT store"
tensors = rdt_store.get_object(obj_id)
tensor_transport_manager = get_tensor_transport_manager(backend)
tensor_transport_manager.send_multiple_tensors(
tensors,
tensor_transport_meta,
communicator_meta,
)
def validate_tensor_buffers(
tensor_buffers: List["torch.Tensor"],
tensor_meta: List[Tuple["torch.Size", "torch.dtype"]],
device: str,
):
if len(tensor_buffers) != len(tensor_meta):
raise ValueError(
f"Length of tensor_buffers ({len(tensor_buffers)}) does not match length from object metadata ({len(tensor_meta)})."
)
def tensor_buffer_mismatch_msg(prop, idx, actual, expected):
return f"{prop} of tensor_buffer at index {idx} ({actual}) does not match {prop.lower()} from object metadata ({expected})."
for idx, single_buffer in enumerate(tensor_buffers):
shape, dtype = tensor_meta[idx]
if single_buffer.shape != shape:
raise ValueError(
tensor_buffer_mismatch_msg("Shape", idx, single_buffer.shape, shape)
)
if single_buffer.dtype != dtype:
raise ValueError(
tensor_buffer_mismatch_msg("Dtype", idx, single_buffer.dtype, dtype)
)
if single_buffer.device.type != device:
raise ValueError(
tensor_buffer_mismatch_msg(
"Device", idx, single_buffer.device.type, device
)
)
if not single_buffer.is_contiguous():
raise ValueError(f"Tensor buffer at index {idx} is not contiguous.")
def __ray_recv__(
self,
obj_id: str,
tensor_transport_meta: TensorTransportMetadata,
communicator_meta: CommunicatorMetadata,
backend: str,
target_buffers: Optional[List[Any]] = None,
):
"""Helper function that runs on the dst actor to receive tensors from the src actor."""
from ray._private.worker import global_worker
rdt_store = global_worker.rdt_manager.rdt_store
try:
tensor_transport_manager = get_tensor_transport_manager(backend)
if target_buffers:
# Currently only torch tensors are supported as target buffers. We could make this
# more generic in the future by adding a pluggable buffer validation function.
validate_tensor_buffers(
target_buffers,
tensor_transport_meta.tensor_meta,
tensor_transport_meta.tensor_device,
)
tensors = tensor_transport_manager.recv_multiple_tensors(
obj_id,
tensor_transport_meta,
communicator_meta,
target_buffers,
)
assert len(tensors) == len(tensor_transport_meta.tensor_meta)
rdt_store.add_object(obj_id, tensors)
except Exception as e:
# Store the error as an RDT object if the recv fails, so waiters will raise the error.
rdt_store.add_object(obj_id, e)
def __ray_abort_transport__(
self, obj_id: str, communicator_meta: CommunicatorMetadata, backend: str
):
"""Helper function that can run on an actor doing a send or recv to abort the transport."""
tensor_transport_manager = get_tensor_transport_manager(backend)
tensor_transport_manager.abort_transport(obj_id, communicator_meta)
def __ray_free__(
self,
obj_id: str,
tensor_transport_backend: str,
tensor_transport_meta: TensorTransportMetadata,
):
try:
from ray._private.worker import global_worker
tensor_transport_manager = get_tensor_transport_manager(
tensor_transport_backend
)
rdt_manager = global_worker.rdt_manager
rdt_store = rdt_manager.rdt_store
if not rdt_store.has_object(obj_id):
return
tensors = rdt_store.get_object(obj_id)
tensor_transport_manager.garbage_collect(obj_id, tensor_transport_meta, tensors)
rdt_store.pop_object(obj_id)
except AssertionError:
# This could fail if this is a retry and it's already been freed.
pass
def __ray_fetch_rdt_object__(self, obj_id: str):
"""Helper function that runs on the src actor to fetch tensors from the RDT store via the object store."""
from ray._private.worker import global_worker
rdt_store = global_worker.rdt_manager.rdt_store
rdt_object = rdt_store.wait_and_get_object(obj_id)
return rdt_object
@dataclass
class _RDTObject:
# A list of tensors representing the RDT object.
data: List[Any]
# Whether the RDT object is the primary copy.
is_primary: bool
# If a recv failed, we store the error here.
error: Optional[Exception] = None
class RDTStore:
"""
This class is thread-safe. The GPU object store is meant to be read and
written by the following threads:
1. The main thread, which is executing user code. This thread may get, put,
and pop objects.
2. The background _ray_system thread, which executes data transfers. This
thread may get and put objects.
3. The background CoreWorker server thread, which executes garbage
collection callbacks that pop objects that are no longer in use.
"""
def __init__(self):
# A dictionary that maps from an object ID to a queue of tensor lists.
#
# Note: Currently, `_rdt_store` is only supported for Ray Actors.
self._rdt_store: Dict[str, deque[_RDTObject]] = defaultdict(deque)
# Mapping from tensor data pointer to the IDs of objects that contain it.
self._tensor_to_object_ids: Dict[int, Set[str]] = defaultdict[int, Set[str]](
set
)
# Synchronization for the RDT store.
self._lock = threading.RLock()
# Signal when an object becomes present in the object store.
self._object_present_cv = threading.Condition(self._lock)
# Signal when an object is freed from the object store.
self._object_freed_cv = threading.Condition(self._lock)
def has_object(self, obj_id: str) -> bool:
with self._lock:
existed = obj_id in self._rdt_store
if existed:
return len(self._rdt_store[obj_id]) > 0
return existed
def has_tensor(self, tensor: Any) -> bool:
# Method only used for testing.
with self._lock:
return id(tensor) in self._tensor_to_object_ids
def get_object(self, obj_id: str) -> Optional[List[Any]]:
with self._lock:
if self._rdt_store[obj_id][0].error:
raise self._rdt_store[obj_id][0].error
return self._rdt_store[obj_id][0].data
def add_object(
self,
obj_id: str,
rdt_object: Union[List[Any], Exception],
is_primary: bool = False,
):
"""
Add an RDT object to the RDT store.
Args:
obj_id: The object ID of the RDT object.
rdt_object: A list of tensors representing the RDT object.
is_primary: Whether the RDT object is the primary copy.
"""
with self._object_present_cv:
if isinstance(rdt_object, Exception):
self._rdt_store[obj_id].append(
_RDTObject([], is_primary, error=rdt_object)
)
else:
for tensor in rdt_object:
self._tensor_to_object_ids[id(tensor)].add(obj_id)
# Append to the queue instead of overwriting
self._rdt_store[obj_id].append(
_RDTObject(
rdt_object,
is_primary,
)
)
self._object_present_cv.notify_all()
def add_object_primary(
self, obj_id: str, tensors: List[Any], tensor_transport: str
) -> TensorTransportMetadata:
with self._object_present_cv:
# A primary entry may already exist from a prior attempt of the
# same task (e.g., a task that succeeded and populated the RDT
# store but whose reply was lost, then got retried). Keep the
# existing primary — do not re-store — and return metadata
# derived from it so the metadata matches what `__ray_send__`
# will actually transmit.
queue = self._rdt_store.get(obj_id)
if queue:
tensors_to_describe = queue[0].data
else:
self.add_object(obj_id, tensors, is_primary=True)
tensors_to_describe = tensors
tensor_transport_manager = get_tensor_transport_manager(tensor_transport)
tensor_transport_meta = (
tensor_transport_manager.extract_tensor_transport_metadata(
obj_id, tensors_to_describe
)
)
if tensor_transport_meta.tensor_meta and not device_match_transport(
tensor_transport_meta.tensor_device, tensor_transport
):
raise ValueError(
f"Tensor transport backend {tensor_transport} does not support "
f"tensor transfer on device {tensor_transport_meta.tensor_device}."
)
return tensor_transport_meta
def is_primary_copy(self, obj_id: str) -> bool:
with self._lock:
return self.has_object(obj_id) and self._rdt_store[obj_id][0].is_primary
def wait_and_get_object(
self, obj_id: str, timeout: Optional[float] = None
) -> List[Any]:
"""Atomically waits for the RDT object to be present in the RDT
store, then gets it. If the object is not present after the optional
timeout, raise a TimeoutError.
Args:
obj_id: The object ID to wait for.
timeout: The maximum time in seconds to wait for the object to be
present in the RDT store. If not specified, wait indefinitely.
Returns:
The tensors in the RDT object.
"""
with self._lock:
self._wait_object(obj_id, timeout)
return self.get_object(obj_id)
def wait_and_pop_object(
self, obj_id: str, timeout: Optional[float] = None
) -> List[Any]:
"""Atomically waits for the RDT object to be present in the RDT
store, then pops it. If the object is not present after the optional
timeout, raise a TimeoutError.
Args:
obj_id: The object ID to wait for.
timeout: The maximum time in seconds to wait for the object to be
present in the RDT store. If not specified, wait indefinitely.
Returns:
The RDT object.
"""
with self._lock:
self._wait_object(obj_id, timeout)
return self.pop_object(obj_id)
def _wait_object(self, obj_id: str, timeout: Optional[float] = None) -> None:
"""Helper method to wait for the RDT object to be present in the RDT store.
If the object is not present after the optional timeout, raise a
TimeoutError.
Args:
obj_id: The object ID to wait for.
timeout: The maximum time in seconds to wait for the object to be
present in the RDT store. If not specified, wait indefinitely.
"""
with self._object_present_cv:
if not self._object_present_cv.wait_for(
lambda: self.has_object(obj_id),
timeout=timeout,
):
raise TimeoutError(
f"ObjectRef({obj_id}) not found in RDT object store after {timeout}s, transfer may have failed. Please report this issue on GitHub: https://github.com/ray-project/ray/issues/new/choose"
)
def pop_object(self, obj_id: str) -> List[Any]:
with self._lock:
queue = self._rdt_store.get(obj_id)
assert queue is not None, f"obj_id={obj_id} not found in RDT store"
rdt_object = queue.popleft()
if len(queue) == 0:
del self._rdt_store[obj_id]
if rdt_object.error:
raise rdt_object.error
for tensor in rdt_object.data:
self._tensor_to_object_ids[id(tensor)].remove(obj_id)
if len(self._tensor_to_object_ids[id(tensor)]) == 0:
self._tensor_to_object_ids.pop(id(tensor))
self._object_freed_cv.notify_all()
return rdt_object.data
def wait_tensor_freed(self, tensor: Any, timeout: Optional[float] = None) -> None:
"""
Wait for the object to be freed from the RDT store.
"""
with self._object_freed_cv:
if not self._object_freed_cv.wait_for(
lambda: id(tensor) not in self._tensor_to_object_ids,
timeout=timeout,
):
raise TimeoutError(
f"Tensor {tensor} not freed from RDT object store after {timeout}s. The tensor will not be freed until all ObjectRefs containing the tensor have gone out of scope."
)
def get_num_objects(self) -> int:
"""
Return the number of objects in the RDT store.
"""
with self._lock:
# Count total objects across all queues
return sum(len(queue) for queue in self._rdt_store.values())