Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/ray/experimental/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/__pycache__/compiled_dag_ref.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/__pycache__/dynamic_resources.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/__pycache__/gradio_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/__pycache__/internal_kv.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/__pycache__/locations.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/__pycache__/queue.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/__pycache__/shuffle.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/__pycache__/tf_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/__pycache__/tqdm_ray.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__init__.py +43 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/accelerator_context.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/auto_transport_type.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/cached_channel.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/common.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/communicator.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/communicator_handle.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/conftest.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/cpu_communicator.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/intra_process_channel.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/nccl_group.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/serialization_context.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/shared_memory_channel.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/torch_tensor_accelerator_channel.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/torch_tensor_type.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/__pycache__/utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/channel/accelerator_context.py +246 -0
- lib/python3.12/site-packages/ray/experimental/channel/auto_transport_type.py +182 -0
- lib/python3.12/site-packages/ray/experimental/channel/cached_channel.py +111 -0
- lib/python3.12/site-packages/ray/experimental/channel/common.py +681 -0
- lib/python3.12/site-packages/ray/experimental/channel/communicator.py +199 -0
- lib/python3.12/site-packages/ray/experimental/channel/communicator_handle.py +28 -0
- lib/python3.12/site-packages/ray/experimental/channel/conftest.py +185 -0
- lib/python3.12/site-packages/ray/experimental/channel/cpu_communicator.py +207 -0
- lib/python3.12/site-packages/ray/experimental/channel/intra_process_channel.py +70 -0
- lib/python3.12/site-packages/ray/experimental/channel/nccl_group.py +374 -0
- lib/python3.12/site-packages/ray/experimental/channel/serialization_context.py +231 -0
- lib/python3.12/site-packages/ray/experimental/channel/shared_memory_channel.py +796 -0
- lib/python3.12/site-packages/ray/experimental/channel/torch_tensor_accelerator_channel.py +872 -0
- lib/python3.12/site-packages/ray/experimental/channel/torch_tensor_type.py +193 -0
- lib/python3.12/site-packages/ray/experimental/channel/utils.py +92 -0
- lib/python3.12/site-packages/ray/experimental/job/__init__.py +0 -0
- lib/python3.12/site-packages/ray/experimental/job/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/job/example_job/__init__.py +0 -0
- lib/python3.12/site-packages/ray/experimental/job/example_job/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/job/example_job/__pycache__/demo_script.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/experimental/job/example_job/demo_script.py +77 -0
- lib/python3.12/site-packages/ray/experimental/multiprocessing/__init__.py +5 -0
- lib/python3.12/site-packages/ray/experimental/multiprocessing/__pycache__/__init__.cpython-312.pyc +0 -0
lib/python3.12/site-packages/ray/experimental/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (534 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/__pycache__/compiled_dag_ref.cpython-312.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/__pycache__/dynamic_resources.cpython-312.pyc
ADDED
|
Binary file (630 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/__pycache__/gradio_utils.cpython-312.pyc
ADDED
|
Binary file (842 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/__pycache__/internal_kv.cpython-312.pyc
ADDED
|
Binary file (5.03 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/__pycache__/locations.cpython-312.pyc
ADDED
|
Binary file (3.55 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/__pycache__/queue.cpython-312.pyc
ADDED
|
Binary file (537 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/__pycache__/shuffle.cpython-312.pyc
ADDED
|
Binary file (16.5 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/__pycache__/tf_utils.cpython-312.pyc
ADDED
|
Binary file (324 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/__pycache__/tqdm_ray.cpython-312.pyc
ADDED
|
Binary file (20.6 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ray.experimental.channel.cached_channel import CachedChannel
|
| 2 |
+
from ray.experimental.channel.common import ( # noqa: F401
|
| 3 |
+
AwaitableBackgroundReader,
|
| 4 |
+
AwaitableBackgroundWriter,
|
| 5 |
+
ChannelContext,
|
| 6 |
+
ChannelInterface,
|
| 7 |
+
ChannelOutputType,
|
| 8 |
+
CompiledDAGArgs,
|
| 9 |
+
ReaderInterface,
|
| 10 |
+
SynchronousReader,
|
| 11 |
+
SynchronousWriter,
|
| 12 |
+
WriterInterface,
|
| 13 |
+
)
|
| 14 |
+
from ray.experimental.channel.communicator import Communicator
|
| 15 |
+
from ray.experimental.channel.cpu_communicator import CPUCommunicator
|
| 16 |
+
from ray.experimental.channel.intra_process_channel import IntraProcessChannel
|
| 17 |
+
from ray.experimental.channel.shared_memory_channel import (
|
| 18 |
+
BufferedSharedMemoryChannel,
|
| 19 |
+
Channel,
|
| 20 |
+
CompositeChannel,
|
| 21 |
+
)
|
| 22 |
+
from ray.experimental.channel.torch_tensor_accelerator_channel import (
|
| 23 |
+
TorchTensorAcceleratorChannel,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
__all__ = [
|
| 27 |
+
"AwaitableBackgroundReader",
|
| 28 |
+
"AwaitableBackgroundWriter",
|
| 29 |
+
"CachedChannel",
|
| 30 |
+
"Channel",
|
| 31 |
+
"Communicator",
|
| 32 |
+
"CPUCommunicator",
|
| 33 |
+
"ReaderInterface",
|
| 34 |
+
"SynchronousReader",
|
| 35 |
+
"SynchronousWriter",
|
| 36 |
+
"WriterInterface",
|
| 37 |
+
"ChannelContext",
|
| 38 |
+
"TorchTensorAcceleratorChannel",
|
| 39 |
+
"IntraProcessChannel",
|
| 40 |
+
"CompositeChannel",
|
| 41 |
+
"BufferedSharedMemoryChannel",
|
| 42 |
+
"CompiledDAGArgs",
|
| 43 |
+
]
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.25 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/accelerator_context.cpython-312.pyc
ADDED
|
Binary file (9.88 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/auto_transport_type.cpython-312.pyc
ADDED
|
Binary file (7.91 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/cached_channel.cpython-312.pyc
ADDED
|
Binary file (5.09 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/common.cpython-312.pyc
ADDED
|
Binary file (31.3 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/communicator.cpython-312.pyc
ADDED
|
Binary file (7.42 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/communicator_handle.cpython-312.pyc
ADDED
|
Binary file (1.27 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/conftest.cpython-312.pyc
ADDED
|
Binary file (9.92 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/cpu_communicator.cpython-312.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/intra_process_channel.cpython-312.pyc
ADDED
|
Binary file (3.93 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/nccl_group.cpython-312.pyc
ADDED
|
Binary file (14.8 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/serialization_context.cpython-312.pyc
ADDED
|
Binary file (9 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/shared_memory_channel.cpython-312.pyc
ADDED
|
Binary file (33.8 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/torch_tensor_accelerator_channel.cpython-312.pyc
ADDED
|
Binary file (35 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/torch_tensor_type.cpython-312.pyc
ADDED
|
Binary file (9.49 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/__pycache__/utils.cpython-312.pyc
ADDED
|
Binary file (3.66 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/channel/accelerator_context.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib
|
| 2 |
+
import threading
|
| 3 |
+
from contextlib import nullcontext
|
| 4 |
+
from typing import TYPE_CHECKING, ContextManager, List, Optional, Type
|
| 5 |
+
|
| 6 |
+
import ray
|
| 7 |
+
from ray._private.accelerators import get_accelerator_manager_for_resource
|
| 8 |
+
from ray.experimental.channel.communicator import Communicator
|
| 9 |
+
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
# The accelerator context singleton on this process.
|
| 14 |
+
_accelerator_context_lock = threading.Lock()
|
| 15 |
+
_default_accelerator_context: Optional["AcceleratorContext"] = None
|
| 16 |
+
_global_custom_context: Optional["AcceleratorContext"] = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class AcceleratorContext:
|
| 20 |
+
"""
|
| 21 |
+
Provides a unified interface for managing different accelerator backends
|
| 22 |
+
This includes stream management, event creation, device context control,
|
| 23 |
+
and communicator support for distributed communication.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, torch_module_name: str, communicator_cls: Type[Communicator]):
|
| 27 |
+
"""
|
| 28 |
+
Initializes an accelerator context with the specified torch device module
|
| 29 |
+
and communicator class.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
torch_module_name: Name of the torch device module (e.g., "cuda", "cpu").
|
| 33 |
+
communicator_cls: Class used to handle communication.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
# The name of the torch module (e.g., 'cuda', 'npu')
|
| 37 |
+
self._torch_module_name: str = torch_module_name
|
| 38 |
+
# The Communicator class used to manage communication
|
| 39 |
+
self._communicator_cls: Type[Communicator] = communicator_cls
|
| 40 |
+
|
| 41 |
+
# Import the torch backend module (e.g., torch.cuda) if the device is not 'cpu'.
|
| 42 |
+
if torch_module_name != "cpu":
|
| 43 |
+
self._torch_mod = importlib.import_module(f"torch.{torch_module_name}")
|
| 44 |
+
|
| 45 |
+
@staticmethod
|
| 46 |
+
def get() -> "AcceleratorContext":
|
| 47 |
+
"""
|
| 48 |
+
Returns the singleton instance of the accelerator context.
|
| 49 |
+
|
| 50 |
+
If a custom accelerator has been registered, initializes the context
|
| 51 |
+
based on the registration. Otherwise, selects an appropriate runtime
|
| 52 |
+
based on the available device (CUDA or CPU) and registers the
|
| 53 |
+
corresponding default communicator.
|
| 54 |
+
|
| 55 |
+
Returns:
|
| 56 |
+
AcceleratorContext: A singleton instance of the appropriate
|
| 57 |
+
runtime context.
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
global _default_accelerator_context, _global_custom_context
|
| 61 |
+
|
| 62 |
+
with _accelerator_context_lock:
|
| 63 |
+
if _global_custom_context is not None:
|
| 64 |
+
return _global_custom_context
|
| 65 |
+
|
| 66 |
+
if _default_accelerator_context is None:
|
| 67 |
+
if len(ray.get_gpu_ids()) > 0:
|
| 68 |
+
from ray.experimental.channel.nccl_group import _NcclGroup
|
| 69 |
+
|
| 70 |
+
_default_accelerator_context = AcceleratorContext(
|
| 71 |
+
"cuda", _NcclGroup
|
| 72 |
+
)
|
| 73 |
+
else:
|
| 74 |
+
from ray.experimental.channel.cpu_communicator import (
|
| 75 |
+
CPUCommunicator,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
_default_accelerator_context = AcceleratorContext(
|
| 79 |
+
"cpu", CPUCommunicator
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
return _default_accelerator_context
|
| 83 |
+
|
| 84 |
+
@staticmethod
|
| 85 |
+
def set(accelerator_context: "AcceleratorContext") -> None:
|
| 86 |
+
"""
|
| 87 |
+
Overwrites the default accelerator context.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
accelerator_context: The context to register.
|
| 91 |
+
"""
|
| 92 |
+
global _global_custom_context
|
| 93 |
+
|
| 94 |
+
# Accelerator context is registered.
|
| 95 |
+
_global_custom_context = accelerator_context
|
| 96 |
+
|
| 97 |
+
def get_accelerator_devices(self) -> List["torch.device"]:
|
| 98 |
+
"""
|
| 99 |
+
Gets the torch device list configured for this process.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
List[torch.device]: The torch device list.
|
| 103 |
+
"""
|
| 104 |
+
import torch
|
| 105 |
+
|
| 106 |
+
if self._torch_module_name == "cpu":
|
| 107 |
+
return [torch.device("cpu")]
|
| 108 |
+
|
| 109 |
+
if self._torch_module_name == "cuda":
|
| 110 |
+
accelerator_ids = [str(id) for id in ray.get_gpu_ids()]
|
| 111 |
+
accelerator_manager = get_accelerator_manager_for_resource("GPU")
|
| 112 |
+
else:
|
| 113 |
+
accelerator_ids = [
|
| 114 |
+
str(id)
|
| 115 |
+
for id in ray.get_runtime_context().get_accelerator_ids()[
|
| 116 |
+
self._torch_module_name.upper()
|
| 117 |
+
]
|
| 118 |
+
]
|
| 119 |
+
accelerator_manager = get_accelerator_manager_for_resource(
|
| 120 |
+
self._torch_module_name.upper()
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
device_ids = []
|
| 124 |
+
|
| 125 |
+
if len(accelerator_ids) > 0:
|
| 126 |
+
accelerator_visible_list = (
|
| 127 |
+
accelerator_manager.get_current_process_visible_accelerator_ids()
|
| 128 |
+
)
|
| 129 |
+
if accelerator_visible_list is None:
|
| 130 |
+
accelerator_visible_list = []
|
| 131 |
+
|
| 132 |
+
# If there are multiple Accelerators, return a list of devices.
|
| 133 |
+
# If using fractional Accelerators, these IDs are not guaranteed
|
| 134 |
+
# to be unique across different processes.
|
| 135 |
+
for accelerator_id in accelerator_ids:
|
| 136 |
+
try:
|
| 137 |
+
device_ids.append(accelerator_visible_list.index(accelerator_id))
|
| 138 |
+
except ValueError:
|
| 139 |
+
raise RuntimeError(
|
| 140 |
+
f"{accelerator_manager.get_visible_accelerator_ids_env_var()} set incorrectly. "
|
| 141 |
+
f"expected to include {accelerator_id}. "
|
| 142 |
+
"Did you override this environment"
|
| 143 |
+
" variable? If not, please help file an issue on Github."
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
else:
|
| 147 |
+
# If called on the driver or outside of Ray Train, return the
|
| 148 |
+
# 0th device.
|
| 149 |
+
device_ids.append(0)
|
| 150 |
+
|
| 151 |
+
return [
|
| 152 |
+
torch.device(f"{self._torch_module_name}:{device_id}")
|
| 153 |
+
for device_id in device_ids
|
| 154 |
+
]
|
| 155 |
+
|
| 156 |
+
def get_device_context(self, device: "torch.device") -> ContextManager:
|
| 157 |
+
"""
|
| 158 |
+
Retrieves the context manager for the specified accelerator device.
|
| 159 |
+
There is no device context for CPU, returning a nullcontext.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
device: The target device for which the context manager is required.
|
| 163 |
+
|
| 164 |
+
Returns:
|
| 165 |
+
ContextManager: A context manager specific to the device type.
|
| 166 |
+
"""
|
| 167 |
+
if device.type == "cpu":
|
| 168 |
+
return nullcontext()
|
| 169 |
+
|
| 170 |
+
return self._torch_mod.device(device)
|
| 171 |
+
|
| 172 |
+
def current_stream(self):
|
| 173 |
+
"""
|
| 174 |
+
Retrieves the current execution stream for the accelerator device.
|
| 175 |
+
"""
|
| 176 |
+
return self._torch_mod.current_stream()
|
| 177 |
+
|
| 178 |
+
def create_event(self):
|
| 179 |
+
"""
|
| 180 |
+
Creates an event object for the accelerator device.
|
| 181 |
+
"""
|
| 182 |
+
return self._torch_mod.Event()
|
| 183 |
+
|
| 184 |
+
def generate_communicator_id(self) -> str:
|
| 185 |
+
"""
|
| 186 |
+
Generates a communication identifier for communication group.
|
| 187 |
+
"""
|
| 188 |
+
return self._communicator_cls.generate_communicator_id()
|
| 189 |
+
|
| 190 |
+
def create_communicator(self, *args, **kwargs) -> Communicator:
|
| 191 |
+
"""
|
| 192 |
+
Creates a communication group for collective operations.
|
| 193 |
+
"""
|
| 194 |
+
return self._communicator_cls(*args, **kwargs)
|
| 195 |
+
|
| 196 |
+
@property
|
| 197 |
+
def module_name(self) -> str:
|
| 198 |
+
"""
|
| 199 |
+
Gets the name of the torch module backing the accelerator.
|
| 200 |
+
"""
|
| 201 |
+
return self._torch_module_name
|
| 202 |
+
|
| 203 |
+
@property
|
| 204 |
+
def communicator_cls(self) -> Optional[Type[Communicator]]:
|
| 205 |
+
"""
|
| 206 |
+
Returns the communicator class.
|
| 207 |
+
"""
|
| 208 |
+
return self._communicator_cls
|
| 209 |
+
|
| 210 |
+
@property
|
| 211 |
+
def accelerator_count(self) -> int:
|
| 212 |
+
"""
|
| 213 |
+
Returns the number of accelerators assigned by ray.
|
| 214 |
+
"""
|
| 215 |
+
if self._torch_module_name == "cuda":
|
| 216 |
+
return len(ray.get_gpu_ids())
|
| 217 |
+
else:
|
| 218 |
+
accelerator_ids = ray.get_runtime_context().get_accelerator_ids()
|
| 219 |
+
return len(accelerator_ids.get(self._torch_module_name.upper(), []))
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def register_accelerator_context(
|
| 223 |
+
torch_module_name: str, communicator_cls: Type[Communicator]
|
| 224 |
+
):
|
| 225 |
+
"""
|
| 226 |
+
Registers the accelerator context with the specified device type and communicator.
|
| 227 |
+
|
| 228 |
+
Args:
|
| 229 |
+
torch_module_name: The name of the device module under torch.
|
| 230 |
+
communicator_cls: The communicator class associated with the device.
|
| 231 |
+
"""
|
| 232 |
+
accelerator_context = AcceleratorContext(torch_module_name, communicator_cls)
|
| 233 |
+
AcceleratorContext.set(accelerator_context)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def is_accelerator_context_registered():
|
| 237 |
+
"""
|
| 238 |
+
Checks whether a custom accelerator context has been registered.
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
bool: True if a custom accelerator context is registered
|
| 242 |
+
(_global_custom_context is not None), False otherwise.
|
| 243 |
+
"""
|
| 244 |
+
if _global_custom_context is not None:
|
| 245 |
+
return True
|
| 246 |
+
return False
|
lib/python3.12/site-packages/ray/experimental/channel/auto_transport_type.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List, Optional, Tuple, Union
|
| 2 |
+
|
| 3 |
+
import ray
|
| 4 |
+
from ray.experimental.channel import ChannelOutputType
|
| 5 |
+
from ray.experimental.channel.torch_tensor_type import TorchTensorType
|
| 6 |
+
from ray.experimental.util.types import Device
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class AutoTransportType(ChannelOutputType):
|
| 10 |
+
"""
|
| 11 |
+
Type hint for automatic transport selection for tensors.
|
| 12 |
+
|
| 13 |
+
With this type hint Compiled Graphs automatically decide the best transport
|
| 14 |
+
to use (e.g., accellerator or shared memory) based on the node locations and
|
| 15 |
+
GPU IDs of the readers and writers.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
def __init__(
|
| 19 |
+
self,
|
| 20 |
+
device: Device = Device.DEFAULT,
|
| 21 |
+
_static_shape: bool = False,
|
| 22 |
+
_direct_return: bool = False,
|
| 23 |
+
):
|
| 24 |
+
self._device = device
|
| 25 |
+
self._static_shape = _static_shape
|
| 26 |
+
self._direct_return = _direct_return
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def device(self) -> Device:
|
| 30 |
+
return self._device
|
| 31 |
+
|
| 32 |
+
def create_channel(
|
| 33 |
+
self,
|
| 34 |
+
writer: Optional["ray.actor.ActorHandle"],
|
| 35 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 36 |
+
driver_actor_id: Optional[str] = None,
|
| 37 |
+
) -> "ChannelOutputType":
|
| 38 |
+
"""
|
| 39 |
+
Directly calling create_channel() on AutoTransportType should not happen,
|
| 40 |
+
just raise an exception with informative message.
|
| 41 |
+
"""
|
| 42 |
+
raise ValueError(
|
| 43 |
+
"This should not happen: AutoTransportType should "
|
| 44 |
+
"have been resolved before creating a channel. "
|
| 45 |
+
"Please file a Ray GitHub issue for bug report."
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class TypeHintResolver:
|
| 50 |
+
"""
|
| 51 |
+
This class is used to resolve `AutoChannelType` into an actual channel type
|
| 52 |
+
(e.g., `TorchTensorType` with proper transport) based on node locations and
|
| 53 |
+
GPU IDs of the readers and writers.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self, actor_to_gpu_ids: Dict["ray.actor.ActorHandle", List[str]]):
|
| 57 |
+
"""
|
| 58 |
+
Args:
|
| 59 |
+
actor_to_gpu_ids: Mapping from actor handle to its GPU IDs.
|
| 60 |
+
"""
|
| 61 |
+
self._actor_to_gpu_ids = actor_to_gpu_ids
|
| 62 |
+
|
| 63 |
+
def _get_gpu_ids(self, actor: "ray.actor.ActorHandle") -> List[str]:
|
| 64 |
+
"""
|
| 65 |
+
Get the GPU IDs of the actor.
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
The GPU IDs of the actor. If the actor is not found,
|
| 69 |
+
return an empty list.
|
| 70 |
+
"""
|
| 71 |
+
gpu_ids = self._actor_to_gpu_ids.get(actor, [])
|
| 72 |
+
assert len(gpu_ids) <= 1, (
|
| 73 |
+
"Compiled Graphs currently don't support allocating multiple GPUs "
|
| 74 |
+
"to a single actor"
|
| 75 |
+
)
|
| 76 |
+
return gpu_ids
|
| 77 |
+
|
| 78 |
+
def _use_same_gpu(
|
| 79 |
+
self,
|
| 80 |
+
writer_and_node: Tuple["ray.actor.ActorHandle", str],
|
| 81 |
+
reader_and_node: Union[
|
| 82 |
+
Tuple["ray.actor.ActorHandle", str],
|
| 83 |
+
List[Tuple["ray.actor.ActorHandle", str]],
|
| 84 |
+
],
|
| 85 |
+
) -> bool:
|
| 86 |
+
"""
|
| 87 |
+
Check if the writer and readers use the same GPU.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
writer_and_node: A tuple of writer actor handle and its node ID.
|
| 91 |
+
reader_and_node: A tuple of reader actor handle and its node ID, or
|
| 92 |
+
a list of such tuples.
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
True if the writer and all the readers use the same GPU, False otherwise.
|
| 96 |
+
"""
|
| 97 |
+
if isinstance(reader_and_node, list):
|
| 98 |
+
return all(
|
| 99 |
+
self._use_same_gpu(writer_and_node, entry) for entry in reader_and_node
|
| 100 |
+
)
|
| 101 |
+
if writer_and_node[1] != reader_and_node[1]:
|
| 102 |
+
return False
|
| 103 |
+
writer_gpu_ids = self._get_gpu_ids(writer_and_node[0])
|
| 104 |
+
reader_gpu_ids = self._get_gpu_ids(reader_and_node[0])
|
| 105 |
+
return writer_gpu_ids == reader_gpu_ids
|
| 106 |
+
|
| 107 |
+
def _use_gpu(
|
| 108 |
+
self, actors: Union["ray.actor.ActorHandle", List["ray.actor.ActorHandle"]]
|
| 109 |
+
) -> bool:
|
| 110 |
+
"""
|
| 111 |
+
Check if the actors use GPUs.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
actors: An actor handle or a list of actor handles.
|
| 115 |
+
|
| 116 |
+
Returns:
|
| 117 |
+
True if the actors use GPUs, False otherwise.
|
| 118 |
+
"""
|
| 119 |
+
if isinstance(actors, list):
|
| 120 |
+
return all(self._use_gpu(actor) for actor in actors)
|
| 121 |
+
gpu_ids = self._get_gpu_ids(actors)
|
| 122 |
+
return len(gpu_ids) > 0
|
| 123 |
+
|
| 124 |
+
def resolve(
|
| 125 |
+
self,
|
| 126 |
+
auto_transport_type: AutoTransportType,
|
| 127 |
+
writer_and_node: Tuple[Optional["ray.actor.ActorHandle"], str],
|
| 128 |
+
reader_and_node_list: List[Tuple[Optional["ray.actor.ActorHandle"], str]],
|
| 129 |
+
) -> "ChannelOutputType":
|
| 130 |
+
"""
|
| 131 |
+
Resolve auto_transport_type to the actual channel output type
|
| 132 |
+
based on the node locations and GPU IDs.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
auto_transport_type: The type to resolve
|
| 136 |
+
writer_and_node: A tuple of writer actor handle and its node ID.
|
| 137 |
+
A None writer actor handle means the writer is the driver.
|
| 138 |
+
reader_and_node_list: A list of tuples of reader actor handle and its
|
| 139 |
+
node ID. A None reader actor handle means the reader is the driver.
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
The actual channel type.
|
| 143 |
+
"""
|
| 144 |
+
writer = writer_and_node[0]
|
| 145 |
+
readers = [reader for reader, _ in reader_and_node_list]
|
| 146 |
+
|
| 147 |
+
if writer is None or any(reader is None for reader in readers):
|
| 148 |
+
# None means actor is the driver, currently driver on GPU
|
| 149 |
+
# is not supported, so we always use shared memory to transfer
|
| 150 |
+
# tensors.
|
| 151 |
+
return TorchTensorType(
|
| 152 |
+
device=auto_transport_type.device,
|
| 153 |
+
_static_shape=auto_transport_type._static_shape,
|
| 154 |
+
_direct_return=auto_transport_type._direct_return,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# Case 1: writer and readers don't both use GPU, use shared memory
|
| 158 |
+
# to transport the tensors
|
| 159 |
+
if not (self._use_gpu(writer) and self._use_gpu(readers)):
|
| 160 |
+
return TorchTensorType(
|
| 161 |
+
device=auto_transport_type.device,
|
| 162 |
+
_static_shape=auto_transport_type._static_shape,
|
| 163 |
+
_direct_return=auto_transport_type._direct_return,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
# Case 2: writer and readers use the same GPU are are on the same node,
|
| 167 |
+
# use shared memory to transport the tensors
|
| 168 |
+
if self._use_same_gpu(writer_and_node, reader_and_node_list):
|
| 169 |
+
return TorchTensorType(
|
| 170 |
+
device=auto_transport_type.device,
|
| 171 |
+
_static_shape=auto_transport_type._static_shape,
|
| 172 |
+
_direct_return=auto_transport_type._direct_return,
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
# Case 3: writer and readers use different GPUs, use accelerator to transport
|
| 176 |
+
# the tensors
|
| 177 |
+
return TorchTensorType(
|
| 178 |
+
transport="accelerator",
|
| 179 |
+
device=auto_transport_type.device,
|
| 180 |
+
_static_shape=auto_transport_type._static_shape,
|
| 181 |
+
_direct_return=auto_transport_type._direct_return,
|
| 182 |
+
)
|
lib/python3.12/site-packages/ray/experimental/channel/cached_channel.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from typing import Any, Optional
|
| 3 |
+
|
| 4 |
+
from ray.experimental.channel.common import ChannelInterface
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class CachedChannel(ChannelInterface):
|
| 8 |
+
"""
|
| 9 |
+
CachedChannel wraps an inner channel and caches the data read from it until
|
| 10 |
+
`num_reads` reads have completed. If inner channel is None, the data
|
| 11 |
+
is written to serialization context and retrieved from there. This is useful
|
| 12 |
+
when passing data within the same actor and a shared memory channel can be
|
| 13 |
+
avoided.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
num_reads: The number of reads from this channel that must happen before
|
| 17 |
+
writing again. Readers must be methods of the same actor.
|
| 18 |
+
inner_channel: The inner channel to cache data from. If None, the data is
|
| 19 |
+
read from the serialization context.
|
| 20 |
+
_channel_id: The unique ID for the channel. If None, a new ID is generated.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
num_reads: int,
|
| 26 |
+
inner_channel: Optional[ChannelInterface] = None,
|
| 27 |
+
_channel_id: Optional[str] = None,
|
| 28 |
+
):
|
| 29 |
+
assert num_reads > 0, "num_reads must be greater than 0."
|
| 30 |
+
self._num_reads = num_reads
|
| 31 |
+
self._inner_channel = inner_channel
|
| 32 |
+
# Generate a unique ID for the channel. The writer and reader will use
|
| 33 |
+
# this ID to store and retrieve data from the _SerializationContext.
|
| 34 |
+
self._channel_id = _channel_id
|
| 35 |
+
if self._channel_id is None:
|
| 36 |
+
self._channel_id = str(uuid.uuid4())
|
| 37 |
+
|
| 38 |
+
def ensure_registered_as_writer(self) -> None:
|
| 39 |
+
if self._inner_channel is not None:
|
| 40 |
+
self._inner_channel.ensure_registered_as_writer()
|
| 41 |
+
|
| 42 |
+
def ensure_registered_as_reader(self) -> None:
|
| 43 |
+
if self._inner_channel is not None:
|
| 44 |
+
self._inner_channel.ensure_registered_as_reader()
|
| 45 |
+
|
| 46 |
+
def __reduce__(self):
|
| 47 |
+
return CachedChannel, (
|
| 48 |
+
self._num_reads,
|
| 49 |
+
self._inner_channel,
|
| 50 |
+
self._channel_id,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
def __str__(self) -> str:
|
| 54 |
+
return (
|
| 55 |
+
f"CachedChannel(channel_id={self._channel_id}, "
|
| 56 |
+
f"num_reads={self._num_reads}), "
|
| 57 |
+
f"inner_channel={self._inner_channel})"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
def write(self, value: Any, timeout: Optional[float] = None):
|
| 61 |
+
self.ensure_registered_as_writer()
|
| 62 |
+
# TODO: better organize the imports
|
| 63 |
+
from ray.experimental.channel import ChannelContext
|
| 64 |
+
|
| 65 |
+
if self._inner_channel is not None:
|
| 66 |
+
self._inner_channel.write(value, timeout)
|
| 67 |
+
return
|
| 68 |
+
|
| 69 |
+
# Otherwise no need to check timeout as the operation is non-blocking.
|
| 70 |
+
|
| 71 |
+
# Because both the reader and writer are in the same worker process,
|
| 72 |
+
# we can directly store the data in the context instead of storing
|
| 73 |
+
# it in the channel object. This removes the serialization overhead of `value`.
|
| 74 |
+
ctx = ChannelContext.get_current().serialization_context
|
| 75 |
+
ctx.set_data(self._channel_id, value, self._num_reads)
|
| 76 |
+
|
| 77 |
+
def read(self, timeout: Optional[float] = None) -> Any:
|
| 78 |
+
self.ensure_registered_as_reader()
|
| 79 |
+
# TODO: better organize the imports
|
| 80 |
+
from ray.experimental.channel import ChannelContext
|
| 81 |
+
|
| 82 |
+
ctx = ChannelContext.get_current().serialization_context
|
| 83 |
+
if ctx.has_data(self._channel_id):
|
| 84 |
+
# No need to check timeout as the operation is non-blocking.
|
| 85 |
+
return ctx.get_data(self._channel_id)
|
| 86 |
+
|
| 87 |
+
assert (
|
| 88 |
+
self._inner_channel is not None
|
| 89 |
+
), "Cannot read from the serialization context while inner channel is None."
|
| 90 |
+
value = self._inner_channel.read(timeout)
|
| 91 |
+
ctx.set_data(self._channel_id, value, self._num_reads)
|
| 92 |
+
# NOTE: Currently we make a contract with Compiled Graph users that the
|
| 93 |
+
# channel results should not be mutated by the actor methods.
|
| 94 |
+
# When the user needs to modify the channel results, they should
|
| 95 |
+
# make a copy of the channel results and modify the copy.
|
| 96 |
+
# This is the same contract as used in IntraProcessChannel.
|
| 97 |
+
# This contract is NOT enforced right now in either case.
|
| 98 |
+
# TODO(rui): introduce a flag to control the behavior:
|
| 99 |
+
# for example, by default we make a deep copy of the channel
|
| 100 |
+
# result, but the user can turn off the deep copy for performance
|
| 101 |
+
# improvements.
|
| 102 |
+
# https://github.com/ray-project/ray/issues/47409
|
| 103 |
+
return ctx.get_data(self._channel_id)
|
| 104 |
+
|
| 105 |
+
def close(self) -> None:
|
| 106 |
+
from ray.experimental.channel import ChannelContext
|
| 107 |
+
|
| 108 |
+
if self._inner_channel is not None:
|
| 109 |
+
self._inner_channel.close()
|
| 110 |
+
ctx = ChannelContext.get_current().serialization_context
|
| 111 |
+
ctx.reset_data(self._channel_id)
|
lib/python3.12/site-packages/ray/experimental/channel/common.py
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import concurrent
|
| 3 |
+
import sys
|
| 4 |
+
import threading
|
| 5 |
+
import time
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import (
|
| 8 |
+
TYPE_CHECKING,
|
| 9 |
+
Any,
|
| 10 |
+
Callable,
|
| 11 |
+
Dict,
|
| 12 |
+
List,
|
| 13 |
+
NamedTuple,
|
| 14 |
+
Optional,
|
| 15 |
+
Tuple,
|
| 16 |
+
Union,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
import ray
|
| 20 |
+
import ray.exceptions
|
| 21 |
+
from ray.experimental.channel.accelerator_context import AcceleratorContext
|
| 22 |
+
from ray.experimental.channel.communicator import Communicator
|
| 23 |
+
from ray.experimental.channel.communicator_handle import CommunicatorHandle
|
| 24 |
+
from ray.experimental.channel.serialization_context import _SerializationContext
|
| 25 |
+
from ray.util.annotations import DeveloperAPI, PublicAPI
|
| 26 |
+
|
| 27 |
+
# The context singleton on this process.
|
| 28 |
+
_default_context: "Optional[ChannelContext]" = None
|
| 29 |
+
_context_lock = threading.Lock()
|
| 30 |
+
|
| 31 |
+
if TYPE_CHECKING:
|
| 32 |
+
import torch
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def retry_and_check_interpreter_exit(f: Callable[[], None]) -> bool:
|
| 36 |
+
"""This function is only useful when f contains channel read/write.
|
| 37 |
+
|
| 38 |
+
Keep retrying channel read/write inside `f` and check if interpreter exits.
|
| 39 |
+
It is important in case the read/write happens in a separate thread pool.
|
| 40 |
+
See https://github.com/ray-project/ray/pull/47702
|
| 41 |
+
|
| 42 |
+
f should a function that doesn't receive any input and return nothing.
|
| 43 |
+
"""
|
| 44 |
+
exiting = False
|
| 45 |
+
while True:
|
| 46 |
+
try:
|
| 47 |
+
f()
|
| 48 |
+
break
|
| 49 |
+
except ray.exceptions.RayChannelTimeoutError:
|
| 50 |
+
if sys.is_finalizing():
|
| 51 |
+
# Interpreter exits. We should ignore the error and
|
| 52 |
+
# stop reading so that the thread can join.
|
| 53 |
+
exiting = True
|
| 54 |
+
break
|
| 55 |
+
|
| 56 |
+
return exiting
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# Holds the input arguments for Compiled Graph
|
| 60 |
+
@PublicAPI(stability="alpha")
|
| 61 |
+
class CompiledDAGArgs(NamedTuple):
|
| 62 |
+
args: Tuple[Any, ...]
|
| 63 |
+
kwargs: Dict[str, Any]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@PublicAPI(stability="alpha")
|
| 67 |
+
class ChannelOutputType:
|
| 68 |
+
def register_custom_serializer(self) -> None:
|
| 69 |
+
"""
|
| 70 |
+
Register any custom serializers needed to pass data of this type. This
|
| 71 |
+
method should be run on the reader(s) and writer of a channel, which
|
| 72 |
+
are the driver and/or Ray actors.
|
| 73 |
+
|
| 74 |
+
NOTE: When custom serializers are registered with Ray, the registered
|
| 75 |
+
deserializer is shipped with the serialized value and used on the
|
| 76 |
+
receiving end. Therefore, the deserializer function should *not*
|
| 77 |
+
capture state that is meant to be worker-local, such as the worker's
|
| 78 |
+
default device. Instead, these should be extracted from the
|
| 79 |
+
worker-local _SerializationContext.
|
| 80 |
+
"""
|
| 81 |
+
pass
|
| 82 |
+
|
| 83 |
+
def create_channel(
|
| 84 |
+
self,
|
| 85 |
+
writer: Optional["ray.actor.ActorHandle"],
|
| 86 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 87 |
+
driver_actor_id: Optional[str] = None,
|
| 88 |
+
) -> "ChannelInterface":
|
| 89 |
+
"""
|
| 90 |
+
Instantiate a ChannelInterface class that can be used
|
| 91 |
+
to pass data of this type.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
writer: The actor that may write to the channel. None signifies the driver.
|
| 95 |
+
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
| 96 |
+
actor handle and the node ID where the actor is located.
|
| 97 |
+
driver_actor_id: If this is a CompositeChannel that is read by a driver and
|
| 98 |
+
that driver is an actual actor, this will be the actor ID of that
|
| 99 |
+
driver actor.
|
| 100 |
+
Returns:
|
| 101 |
+
A ChannelInterface that can be used to pass data
|
| 102 |
+
of this type.
|
| 103 |
+
"""
|
| 104 |
+
raise NotImplementedError
|
| 105 |
+
|
| 106 |
+
def requires_accelerator(self) -> bool:
|
| 107 |
+
# By default, channels do not require accelerator.
|
| 108 |
+
return False
|
| 109 |
+
|
| 110 |
+
def get_custom_communicator(self) -> Optional[Communicator]:
|
| 111 |
+
"""
|
| 112 |
+
Return the custom communicator group if one is specified.
|
| 113 |
+
"""
|
| 114 |
+
return None
|
| 115 |
+
|
| 116 |
+
def set_communicator_id(self, group_id: str) -> None:
|
| 117 |
+
raise NotImplementedError
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@DeveloperAPI
|
| 121 |
+
@dataclass
|
| 122 |
+
class ChannelContext:
|
| 123 |
+
serialization_context = _SerializationContext()
|
| 124 |
+
_torch_available: Optional[bool] = None
|
| 125 |
+
_torch_device: Optional["torch.device"] = None
|
| 126 |
+
_current_stream: Optional["torch.cuda.Stream"] = None
|
| 127 |
+
|
| 128 |
+
def __init__(self):
|
| 129 |
+
# Used for the torch.Tensor accelerator transport.
|
| 130 |
+
self.communicators: Dict[str, "Communicator"] = {}
|
| 131 |
+
# Used for driver process to store actors in the communicator.
|
| 132 |
+
self.communicator_handles: Dict[str, "CommunicatorHandle"] = {}
|
| 133 |
+
|
| 134 |
+
@staticmethod
|
| 135 |
+
def get_current() -> "ChannelContext":
|
| 136 |
+
"""Get or create a singleton context.
|
| 137 |
+
|
| 138 |
+
If the context has not yet been created in this process, it will be
|
| 139 |
+
initialized with default settings.
|
| 140 |
+
"""
|
| 141 |
+
|
| 142 |
+
global _default_context
|
| 143 |
+
|
| 144 |
+
with _context_lock:
|
| 145 |
+
if _default_context is None:
|
| 146 |
+
_default_context = ChannelContext()
|
| 147 |
+
|
| 148 |
+
return _default_context
|
| 149 |
+
|
| 150 |
+
@property
|
| 151 |
+
def torch_available(self) -> bool:
|
| 152 |
+
"""
|
| 153 |
+
Check if torch package is available.
|
| 154 |
+
"""
|
| 155 |
+
if self._torch_available is not None:
|
| 156 |
+
return self._torch_available
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
import torch # noqa: F401
|
| 160 |
+
except ImportError:
|
| 161 |
+
self._torch_available = False
|
| 162 |
+
return False
|
| 163 |
+
self._torch_available = True
|
| 164 |
+
return True
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def torch_device(self) -> "torch.device":
|
| 168 |
+
if self._torch_device is None:
|
| 169 |
+
self._torch_device = AcceleratorContext.get().get_accelerator_devices()[0]
|
| 170 |
+
|
| 171 |
+
return self._torch_device
|
| 172 |
+
|
| 173 |
+
def set_torch_device(self, device: "torch.device"):
|
| 174 |
+
self._torch_device = device
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@PublicAPI(stability="alpha")
|
| 178 |
+
class ChannelInterface:
|
| 179 |
+
"""
|
| 180 |
+
Abstraction for a transport between a writer actor and some number of
|
| 181 |
+
reader actors.
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
def __init__(
|
| 185 |
+
self,
|
| 186 |
+
writer: Optional[ray.actor.ActorHandle],
|
| 187 |
+
readers: List[Optional[ray.actor.ActorHandle]],
|
| 188 |
+
typ: Optional["ChannelOutputType"],
|
| 189 |
+
):
|
| 190 |
+
"""
|
| 191 |
+
Create a channel that can be read and written by a Ray driver or actor.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
writer: The actor that may write to the channel. None signifies the driver.
|
| 195 |
+
readers: The actors that may read from the channel. None signifies
|
| 196 |
+
the driver.
|
| 197 |
+
typ: Type information about the values passed through the channel.
|
| 198 |
+
"""
|
| 199 |
+
pass
|
| 200 |
+
|
| 201 |
+
def ensure_registered_as_writer(self):
|
| 202 |
+
"""
|
| 203 |
+
Check whether the process is a valid writer. This method must be idempotent.
|
| 204 |
+
"""
|
| 205 |
+
raise NotImplementedError
|
| 206 |
+
|
| 207 |
+
def ensure_registered_as_reader(self):
|
| 208 |
+
"""
|
| 209 |
+
Check whether the process is a valid reader. This method must be idempotent.
|
| 210 |
+
"""
|
| 211 |
+
raise NotImplementedError
|
| 212 |
+
|
| 213 |
+
def write(self, value: Any, timeout: Optional[float] = None) -> None:
|
| 214 |
+
"""
|
| 215 |
+
Write a value to the channel.
|
| 216 |
+
|
| 217 |
+
Blocks if there are still pending readers for the previous value. The
|
| 218 |
+
writer may not write again until the specified number of readers have
|
| 219 |
+
read the value.
|
| 220 |
+
|
| 221 |
+
Args:
|
| 222 |
+
value: The value to write.
|
| 223 |
+
timeout: The maximum time in seconds to wait to write the value.
|
| 224 |
+
None means using default timeout, 0 means immediate timeout
|
| 225 |
+
(immediate success or timeout without blocking), -1 means
|
| 226 |
+
infinite timeout (block indefinitely).
|
| 227 |
+
"""
|
| 228 |
+
raise NotImplementedError
|
| 229 |
+
|
| 230 |
+
def read(self, timeout: Optional[float] = None) -> Any:
|
| 231 |
+
"""
|
| 232 |
+
Read the latest value from the channel. This call will block until a
|
| 233 |
+
value is available to read.
|
| 234 |
+
|
| 235 |
+
Subsequent calls to read() may *block* if the deserialized object is
|
| 236 |
+
zero-copy (e.g., bytes or a numpy array) *and* the object is still in scope.
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
timeout: The maximum time in seconds to wait to read the value.
|
| 240 |
+
None means using default timeout, 0 means immediate timeout
|
| 241 |
+
(immediate success or timeout without blocking), -1 means
|
| 242 |
+
infinite timeout (block indefinitely).
|
| 243 |
+
|
| 244 |
+
Returns:
|
| 245 |
+
Any: The deserialized value. If the deserialized value is an
|
| 246 |
+
Exception, it will be returned directly instead of being raised.
|
| 247 |
+
"""
|
| 248 |
+
raise NotImplementedError
|
| 249 |
+
|
| 250 |
+
def close(self) -> None:
|
| 251 |
+
"""
|
| 252 |
+
Close this channel. This method must not block and it must be made
|
| 253 |
+
idempotent. Any existing values in the channel may be lost after the
|
| 254 |
+
channel is closed.
|
| 255 |
+
"""
|
| 256 |
+
raise NotImplementedError
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# Interfaces for channel I/O.
|
| 260 |
+
@DeveloperAPI
|
| 261 |
+
class ReaderInterface:
|
| 262 |
+
def __init__(
|
| 263 |
+
self,
|
| 264 |
+
input_channels: List[ChannelInterface],
|
| 265 |
+
):
|
| 266 |
+
assert isinstance(input_channels, list)
|
| 267 |
+
for chan in input_channels:
|
| 268 |
+
assert isinstance(chan, ChannelInterface)
|
| 269 |
+
|
| 270 |
+
self._input_channels = input_channels
|
| 271 |
+
self._closed = False
|
| 272 |
+
self._num_reads = 0
|
| 273 |
+
|
| 274 |
+
# A list of channels that were not read in the last `read` call
|
| 275 |
+
# because the reader returned immediately when a RayTaskError was found.
|
| 276 |
+
# These channels must be consumed before the next read to avoid reading
|
| 277 |
+
# stale data remaining from the last read.
|
| 278 |
+
self._leftover_channels: List[ChannelInterface] = []
|
| 279 |
+
|
| 280 |
+
def get_num_reads(self) -> int:
|
| 281 |
+
return self._num_reads
|
| 282 |
+
|
| 283 |
+
def start(self):
|
| 284 |
+
raise NotImplementedError
|
| 285 |
+
|
| 286 |
+
def _read_list(self, timeout: Optional[float] = None) -> List[Any]:
|
| 287 |
+
"""
|
| 288 |
+
Read a list of values from this reader.
|
| 289 |
+
|
| 290 |
+
Args:
|
| 291 |
+
timeout: The maximum time in seconds to wait for reading.
|
| 292 |
+
None means using default timeout which is infinite, 0 means immediate
|
| 293 |
+
timeout (immediate success or timeout without blocking), -1 means
|
| 294 |
+
infinite timeout (block indefinitely).
|
| 295 |
+
|
| 296 |
+
"""
|
| 297 |
+
raise NotImplementedError
|
| 298 |
+
|
| 299 |
+
def read(self, timeout: Optional[float] = None) -> List[Any]:
|
| 300 |
+
"""
|
| 301 |
+
Read from this reader.
|
| 302 |
+
|
| 303 |
+
Args:
|
| 304 |
+
timeout: The maximum time in seconds to wait for reading.
|
| 305 |
+
None means using default timeout, 0 means immediate timeout
|
| 306 |
+
(immediate success or timeout without blocking), -1 means
|
| 307 |
+
infinite timeout (block indefinitely).
|
| 308 |
+
"""
|
| 309 |
+
assert (
|
| 310 |
+
timeout is None or timeout >= 0 or timeout == -1
|
| 311 |
+
), "Timeout must be non-negative or -1."
|
| 312 |
+
outputs = self._read_list(timeout)
|
| 313 |
+
self._num_reads += 1
|
| 314 |
+
return outputs
|
| 315 |
+
|
| 316 |
+
def close(self) -> None:
|
| 317 |
+
self._closed = True
|
| 318 |
+
for channel in self._input_channels:
|
| 319 |
+
channel.close()
|
| 320 |
+
|
| 321 |
+
def _consume_leftover_channels_if_needed(
|
| 322 |
+
self, timeout: Optional[float] = None
|
| 323 |
+
) -> None:
|
| 324 |
+
# Consume the channels that were not read in the last `read` call because a
|
| 325 |
+
# RayTaskError was returned from another channel. If we don't do this, the
|
| 326 |
+
# read operation will read stale versions of the object refs.
|
| 327 |
+
#
|
| 328 |
+
# If a RayTaskError is returned from a leftover channel, it will be ignored.
|
| 329 |
+
# If a read operation times out, a RayChannelTimeoutError exception will be
|
| 330 |
+
# raised.
|
| 331 |
+
#
|
| 332 |
+
# TODO(kevin85421): Currently, a DAG with NCCL channels and fast fail enabled
|
| 333 |
+
# may not be reusable. Revisit this in the future.
|
| 334 |
+
for c in self._leftover_channels:
|
| 335 |
+
start_time = time.monotonic()
|
| 336 |
+
c.read(timeout)
|
| 337 |
+
if timeout is not None:
|
| 338 |
+
timeout -= time.monotonic() - start_time
|
| 339 |
+
timeout = max(timeout, 0)
|
| 340 |
+
self._leftover_channels = []
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
@DeveloperAPI
|
| 344 |
+
class SynchronousReader(ReaderInterface):
|
| 345 |
+
def __init__(
|
| 346 |
+
self,
|
| 347 |
+
input_channels: List[ChannelInterface],
|
| 348 |
+
):
|
| 349 |
+
super().__init__(input_channels)
|
| 350 |
+
|
| 351 |
+
def start(self):
|
| 352 |
+
pass
|
| 353 |
+
|
| 354 |
+
def _read_list(self, timeout: Optional[float] = None) -> List[Any]:
|
| 355 |
+
self._consume_leftover_channels_if_needed(timeout)
|
| 356 |
+
# We don't update `remaining_timeout` here because in the worst case,
|
| 357 |
+
# consuming leftover channels requires reading all `_input_channels`,
|
| 358 |
+
# which users expect to complete within the original `timeout`. Updating
|
| 359 |
+
# `remaining_timeout` could cause unexpected timeouts in subsequent read
|
| 360 |
+
# operations.
|
| 361 |
+
|
| 362 |
+
# It is a special case that `timeout` is set to 0, which means
|
| 363 |
+
# read once for each channel.
|
| 364 |
+
is_zero_timeout = timeout == 0
|
| 365 |
+
|
| 366 |
+
results = [None for _ in range(len(self._input_channels))]
|
| 367 |
+
if timeout is None or timeout == -1:
|
| 368 |
+
timeout = float("inf")
|
| 369 |
+
timeout_point = time.monotonic() + timeout
|
| 370 |
+
remaining_timeout = timeout
|
| 371 |
+
|
| 372 |
+
from ray.dag import DAGContext
|
| 373 |
+
|
| 374 |
+
ctx = DAGContext.get_current()
|
| 375 |
+
iteration_timeout = ctx.read_iteration_timeout
|
| 376 |
+
|
| 377 |
+
# Iterate over the input channels with a shorter timeout for each iteration
|
| 378 |
+
# to detect RayTaskError early and fail fast.
|
| 379 |
+
done_channels = set()
|
| 380 |
+
while len(done_channels) < len(self._input_channels):
|
| 381 |
+
for i, c in enumerate(self._input_channels):
|
| 382 |
+
if c in done_channels:
|
| 383 |
+
continue
|
| 384 |
+
try:
|
| 385 |
+
result = c.read(min(remaining_timeout, iteration_timeout))
|
| 386 |
+
results[i] = result
|
| 387 |
+
done_channels.add(c)
|
| 388 |
+
if isinstance(result, ray.exceptions.RayTaskError):
|
| 389 |
+
# If we raise an exception immediately, it will be considered
|
| 390 |
+
# as a system error which will cause the execution loop to
|
| 391 |
+
# exit. Hence, return immediately and let `_process_return_vals`
|
| 392 |
+
# handle the exception.
|
| 393 |
+
#
|
| 394 |
+
# Return a list of RayTaskError so that the caller will not
|
| 395 |
+
# get an undefined partial result.
|
| 396 |
+
self._leftover_channels = [
|
| 397 |
+
c for c in self._input_channels if c not in done_channels
|
| 398 |
+
]
|
| 399 |
+
return [result for _ in range(len(self._input_channels))]
|
| 400 |
+
except ray.exceptions.RayChannelTimeoutError as e:
|
| 401 |
+
remaining_timeout = max(timeout_point - time.monotonic(), 0)
|
| 402 |
+
if remaining_timeout == 0:
|
| 403 |
+
raise e
|
| 404 |
+
continue
|
| 405 |
+
|
| 406 |
+
remaining_timeout = max(timeout_point - time.monotonic(), 0)
|
| 407 |
+
if remaining_timeout == 0 and not is_zero_timeout:
|
| 408 |
+
raise ray.exceptions.RayChannelTimeoutError(
|
| 409 |
+
f"Cannot read all channels within {timeout} seconds"
|
| 410 |
+
)
|
| 411 |
+
return results
|
| 412 |
+
|
| 413 |
+
def release_channel_buffers(self, timeout: Optional[float] = None) -> None:
|
| 414 |
+
for c in self._input_channels:
|
| 415 |
+
start_time = time.monotonic()
|
| 416 |
+
assert hasattr(
|
| 417 |
+
c, "release_buffer"
|
| 418 |
+
), "release_buffer() is only supported for shared memory channel "
|
| 419 |
+
"(e.g., Channel, BufferedSharedMemoryChannel, CompositeChannel) "
|
| 420 |
+
"and used between the last actor and the driver, but got a channel"
|
| 421 |
+
f" of type {type(c)}."
|
| 422 |
+
c.release_buffer(timeout)
|
| 423 |
+
if timeout is not None:
|
| 424 |
+
timeout -= time.monotonic() - start_time
|
| 425 |
+
timeout = max(timeout, 0)
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
@DeveloperAPI
|
| 429 |
+
class AwaitableBackgroundReader(ReaderInterface):
|
| 430 |
+
"""
|
| 431 |
+
Asyncio-compatible channel reader.
|
| 432 |
+
|
| 433 |
+
The reader is constructed with an async queue of futures whose values it
|
| 434 |
+
will fulfill. It uses a threadpool to execute the blocking calls to read
|
| 435 |
+
from the input channel(s).
|
| 436 |
+
"""
|
| 437 |
+
|
| 438 |
+
def __init__(
|
| 439 |
+
self,
|
| 440 |
+
input_channels: List[ChannelInterface],
|
| 441 |
+
fut_queue: asyncio.Queue,
|
| 442 |
+
):
|
| 443 |
+
super().__init__(input_channels)
|
| 444 |
+
self._fut_queue = fut_queue
|
| 445 |
+
self._background_task = None
|
| 446 |
+
self._background_task_executor = concurrent.futures.ThreadPoolExecutor(
|
| 447 |
+
max_workers=1, thread_name_prefix="channel.AwaitableBackgroundReader"
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
def start(self):
|
| 451 |
+
self._background_task = asyncio.ensure_future(self.run())
|
| 452 |
+
|
| 453 |
+
def _run(self):
|
| 454 |
+
# Give it a default timeout 60 seconds to release the buffers
|
| 455 |
+
# of the channels that were not read in the last `read` call.
|
| 456 |
+
self._consume_leftover_channels_if_needed(60)
|
| 457 |
+
|
| 458 |
+
results = [None for _ in range(len(self._input_channels))]
|
| 459 |
+
|
| 460 |
+
from ray.dag import DAGContext
|
| 461 |
+
|
| 462 |
+
ctx = DAGContext.get_current()
|
| 463 |
+
iteration_timeout = ctx.read_iteration_timeout
|
| 464 |
+
|
| 465 |
+
done_channels = set()
|
| 466 |
+
while len(done_channels) < len(self._input_channels):
|
| 467 |
+
for i, c in enumerate(self._input_channels):
|
| 468 |
+
if c in done_channels:
|
| 469 |
+
continue
|
| 470 |
+
try:
|
| 471 |
+
result = c.read(iteration_timeout)
|
| 472 |
+
results[i] = result
|
| 473 |
+
done_channels.add(c)
|
| 474 |
+
if isinstance(result, ray.exceptions.RayTaskError):
|
| 475 |
+
self._leftover_channels = [
|
| 476 |
+
c for c in self._input_channels if c not in done_channels
|
| 477 |
+
]
|
| 478 |
+
return [result for _ in range(len(self._input_channels))]
|
| 479 |
+
except ray.exceptions.RayChannelTimeoutError:
|
| 480 |
+
pass
|
| 481 |
+
if sys.is_finalizing():
|
| 482 |
+
return results
|
| 483 |
+
return results
|
| 484 |
+
|
| 485 |
+
async def run(self):
|
| 486 |
+
loop = asyncio.get_running_loop()
|
| 487 |
+
while not self._closed:
|
| 488 |
+
res, fut = await asyncio.gather(
|
| 489 |
+
loop.run_in_executor(self._background_task_executor, self._run),
|
| 490 |
+
self._fut_queue.get(),
|
| 491 |
+
return_exceptions=True,
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
# Set the result on the main thread.
|
| 495 |
+
fut.set_result(res)
|
| 496 |
+
# NOTE(swang): If the object is zero-copy deserialized, then it
|
| 497 |
+
# will stay in scope as long as ret and the future are in scope.
|
| 498 |
+
# Therefore, we must delete both here after fulfilling the future.
|
| 499 |
+
del res
|
| 500 |
+
del fut
|
| 501 |
+
|
| 502 |
+
def close(self):
|
| 503 |
+
super().close()
|
| 504 |
+
self._background_task_executor.shutdown(cancel_futures=True)
|
| 505 |
+
self._background_task.cancel()
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
@DeveloperAPI
|
| 509 |
+
class WriterInterface:
|
| 510 |
+
def __init__(
|
| 511 |
+
self,
|
| 512 |
+
output_channels: List[ChannelInterface],
|
| 513 |
+
output_idxs: List[Optional[Union[int, str]]],
|
| 514 |
+
is_input=False,
|
| 515 |
+
):
|
| 516 |
+
"""
|
| 517 |
+
Initialize the writer.
|
| 518 |
+
|
| 519 |
+
Args:
|
| 520 |
+
output_channels: The output channels to write to.
|
| 521 |
+
output_idxs: The indices of the values to write to each channel.
|
| 522 |
+
This has the same length as `output_channels`. If `is_input` is True,
|
| 523 |
+
the index can be an integer or a string to retrieve the corresponding
|
| 524 |
+
value from `args` or `kwargs` in the DAG's input. If `is_input`
|
| 525 |
+
is False, the entire value is written if the index is None. Otherwise,
|
| 526 |
+
the value at the specified index in the tuple is written.
|
| 527 |
+
is_input: Whether the writer is DAG input writer or not.
|
| 528 |
+
"""
|
| 529 |
+
|
| 530 |
+
assert len(output_channels) == len(output_idxs)
|
| 531 |
+
self._output_channels = output_channels
|
| 532 |
+
self._output_idxs = output_idxs
|
| 533 |
+
self._closed = False
|
| 534 |
+
self._num_writes = 0
|
| 535 |
+
self._is_input = is_input
|
| 536 |
+
|
| 537 |
+
def get_num_writes(self) -> int:
|
| 538 |
+
return self._num_writes
|
| 539 |
+
|
| 540 |
+
def start(self):
|
| 541 |
+
raise NotImplementedError()
|
| 542 |
+
|
| 543 |
+
def write(self, val: Any, timeout: Optional[float] = None) -> None:
|
| 544 |
+
"""
|
| 545 |
+
Write the value.
|
| 546 |
+
|
| 547 |
+
Args:
|
| 548 |
+
timeout: The maximum time in seconds to wait for writing. 0 means
|
| 549 |
+
immediate timeout (immediate success or timeout without blocking).
|
| 550 |
+
-1 and None mean infinite timeout (blocks indefinitely).
|
| 551 |
+
"""
|
| 552 |
+
raise NotImplementedError()
|
| 553 |
+
|
| 554 |
+
def close(self) -> None:
|
| 555 |
+
self._closed = True
|
| 556 |
+
for channel in self._output_channels:
|
| 557 |
+
channel.close()
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
def _adapt(raw_args: Any, key: Optional[Union[int, str]], is_input: bool):
|
| 561 |
+
"""
|
| 562 |
+
Adapt the raw arguments to the key. If `is_input` is True, this method will
|
| 563 |
+
retrieve the value from the input data for an InputAttributeNode. Otherwise, it
|
| 564 |
+
will retrieve either a partial value or the entire value from the output of
|
| 565 |
+
a ClassMethodNode.
|
| 566 |
+
|
| 567 |
+
Args:
|
| 568 |
+
raw_args: The raw arguments to adapt.
|
| 569 |
+
key: The key to adapt.
|
| 570 |
+
is_input: Whether the writer is DAG input writer or not.
|
| 571 |
+
"""
|
| 572 |
+
if is_input:
|
| 573 |
+
if not isinstance(raw_args, CompiledDAGArgs):
|
| 574 |
+
# Fast path for a single input.
|
| 575 |
+
return raw_args
|
| 576 |
+
else:
|
| 577 |
+
args = raw_args.args
|
| 578 |
+
kwargs = raw_args.kwargs
|
| 579 |
+
|
| 580 |
+
if isinstance(key, int):
|
| 581 |
+
return args[key]
|
| 582 |
+
else:
|
| 583 |
+
return kwargs[key]
|
| 584 |
+
else:
|
| 585 |
+
if key is not None:
|
| 586 |
+
return raw_args[key]
|
| 587 |
+
else:
|
| 588 |
+
return raw_args
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
@DeveloperAPI
|
| 592 |
+
class SynchronousWriter(WriterInterface):
|
| 593 |
+
def start(self):
|
| 594 |
+
for channel in self._output_channels:
|
| 595 |
+
channel.ensure_registered_as_writer()
|
| 596 |
+
|
| 597 |
+
def write(self, val: Any, timeout: Optional[float] = None) -> None:
|
| 598 |
+
# If it is an exception, there's only 1 return value.
|
| 599 |
+
# We have to send the same data to all channels.
|
| 600 |
+
if isinstance(val, Exception):
|
| 601 |
+
if len(self._output_channels) > 1:
|
| 602 |
+
val = tuple(val for _ in range(len(self._output_channels)))
|
| 603 |
+
|
| 604 |
+
if not self._is_input:
|
| 605 |
+
if len(self._output_channels) > 1:
|
| 606 |
+
if not isinstance(val, tuple):
|
| 607 |
+
raise ValueError(
|
| 608 |
+
f"Expected a tuple of {len(self._output_channels)} outputs, "
|
| 609 |
+
f"but got {type(val)}"
|
| 610 |
+
)
|
| 611 |
+
if len(val) != len(self._output_channels):
|
| 612 |
+
raise ValueError(
|
| 613 |
+
f"Expected {len(self._output_channels)} outputs, but got "
|
| 614 |
+
f"{len(val)} outputs"
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
for i, channel in enumerate(self._output_channels):
|
| 618 |
+
idx = self._output_idxs[i]
|
| 619 |
+
val_i = _adapt(val, idx, self._is_input)
|
| 620 |
+
channel.write(val_i, timeout)
|
| 621 |
+
self._num_writes += 1
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
@DeveloperAPI
|
| 625 |
+
class AwaitableBackgroundWriter(WriterInterface):
|
| 626 |
+
def __init__(
|
| 627 |
+
self,
|
| 628 |
+
output_channels: List[ChannelInterface],
|
| 629 |
+
output_idxs: List[Optional[Union[int, str]]],
|
| 630 |
+
is_input=False,
|
| 631 |
+
):
|
| 632 |
+
super().__init__(output_channels, output_idxs, is_input=is_input)
|
| 633 |
+
self._queue = asyncio.Queue()
|
| 634 |
+
self._background_task = None
|
| 635 |
+
self._background_task_executor = concurrent.futures.ThreadPoolExecutor(
|
| 636 |
+
max_workers=1, thread_name_prefix="channel.AwaitableBackgroundWriter"
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
def start(self):
|
| 640 |
+
for channel in self._output_channels:
|
| 641 |
+
channel.ensure_registered_as_writer()
|
| 642 |
+
self._background_task = asyncio.ensure_future(self.run())
|
| 643 |
+
|
| 644 |
+
def _run(self, res):
|
| 645 |
+
if not self._is_input:
|
| 646 |
+
if len(self._output_channels) > 1:
|
| 647 |
+
if not isinstance(res, tuple):
|
| 648 |
+
raise ValueError(
|
| 649 |
+
f"Expected a tuple of {len(self._output_channels)} outputs, "
|
| 650 |
+
f"but got {type(res)}"
|
| 651 |
+
)
|
| 652 |
+
if len(res) != len(self._output_channels):
|
| 653 |
+
raise ValueError(
|
| 654 |
+
f"Expected {len(self._output_channels)} outputs, but got "
|
| 655 |
+
f"{len(res)} outputs"
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
for i, channel in enumerate(self._output_channels):
|
| 659 |
+
idx = self._output_idxs[i]
|
| 660 |
+
res_i = _adapt(res, idx, self._is_input)
|
| 661 |
+
exiting = retry_and_check_interpreter_exit(
|
| 662 |
+
lambda: channel.write(res_i, timeout=1)
|
| 663 |
+
)
|
| 664 |
+
if exiting:
|
| 665 |
+
break
|
| 666 |
+
|
| 667 |
+
async def run(self):
|
| 668 |
+
loop = asyncio.get_event_loop()
|
| 669 |
+
while True:
|
| 670 |
+
res = await self._queue.get()
|
| 671 |
+
await loop.run_in_executor(self._background_task_executor, self._run, res)
|
| 672 |
+
|
| 673 |
+
async def write(self, val: Any) -> None:
|
| 674 |
+
if self._closed:
|
| 675 |
+
raise RuntimeError("DAG execution cancelled")
|
| 676 |
+
await self._queue.put(val)
|
| 677 |
+
self._num_writes += 1
|
| 678 |
+
|
| 679 |
+
def close(self):
|
| 680 |
+
self._background_task.cancel()
|
| 681 |
+
super().close()
|
lib/python3.12/site-packages/ray/experimental/channel/communicator.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple
|
| 3 |
+
|
| 4 |
+
import ray
|
| 5 |
+
from ray.experimental.util.types import ReduceOp
|
| 6 |
+
from ray.util.annotations import DeveloperAPI
|
| 7 |
+
|
| 8 |
+
if TYPE_CHECKING:
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Signature for a torch.Tensor allocator is:
|
| 13 |
+
# (shape: Tuple[int], dtype: torch.dtype) -> torch.Tensor.
|
| 14 |
+
TorchTensorAllocator = Callable[[Tuple[int], "torch.dtype"], "torch.Tensor"]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@DeveloperAPI
|
| 18 |
+
class Communicator(ABC):
|
| 19 |
+
"""
|
| 20 |
+
Communicator for a group of Compiled Graph actors on NVIDIA GPU.
|
| 21 |
+
|
| 22 |
+
The Compiled Graph execution leverages this internally to support communication
|
| 23 |
+
between actors in the group.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
@abstractmethod
|
| 27 |
+
def initialize(self, rank: int) -> None:
|
| 28 |
+
"""
|
| 29 |
+
Initialize the communicator from the actor.
|
| 30 |
+
|
| 31 |
+
This is called once by Compiled Graph on each actor to initialize the
|
| 32 |
+
communicator,before any other methods.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
rank: The rank of this actor in the group.
|
| 36 |
+
"""
|
| 37 |
+
raise NotImplementedError
|
| 38 |
+
|
| 39 |
+
@abstractmethod
|
| 40 |
+
def get_actor_handles(self) -> List["ray.actor.ActorHandle"]:
|
| 41 |
+
"""
|
| 42 |
+
Get handles of all actors for this communicator group.
|
| 43 |
+
"""
|
| 44 |
+
raise NotImplementedError
|
| 45 |
+
|
| 46 |
+
@abstractmethod
|
| 47 |
+
def get_rank(self, actor: ray.actor.ActorHandle) -> int:
|
| 48 |
+
"""
|
| 49 |
+
Return the given actor's rank in the group.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
actor: The actor handle to look up.
|
| 53 |
+
"""
|
| 54 |
+
raise NotImplementedError
|
| 55 |
+
|
| 56 |
+
@abstractmethod
|
| 57 |
+
def get_self_rank(self) -> Optional[int]:
|
| 58 |
+
"""
|
| 59 |
+
Return this actor's rank.
|
| 60 |
+
"""
|
| 61 |
+
raise NotImplementedError
|
| 62 |
+
|
| 63 |
+
def get_world_size(self) -> int:
|
| 64 |
+
"""
|
| 65 |
+
Return the number of ranks in the group.
|
| 66 |
+
"""
|
| 67 |
+
raise NotImplementedError
|
| 68 |
+
|
| 69 |
+
@abstractmethod
|
| 70 |
+
def send(self, value: "torch.Tensor", peer_rank: int) -> None:
|
| 71 |
+
"""
|
| 72 |
+
Send a torch.Tensor to a peer.
|
| 73 |
+
|
| 74 |
+
This returns when the send kernel has been queued, but the kernel may
|
| 75 |
+
not have completed. Therefore, the caller should ensure that there are
|
| 76 |
+
no concurrent writes to the sent `value` until the send has finished.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
value: The torch.Tensor to send. It should already be on this
|
| 80 |
+
actor's default device.
|
| 81 |
+
peer_rank: The rank of the actor to send to.
|
| 82 |
+
"""
|
| 83 |
+
raise NotImplementedError
|
| 84 |
+
|
| 85 |
+
@abstractmethod
|
| 86 |
+
def recv(
|
| 87 |
+
self,
|
| 88 |
+
shape: Tuple[int],
|
| 89 |
+
dtype: "torch.dtype",
|
| 90 |
+
peer_rank: int,
|
| 91 |
+
allocator: Optional[TorchTensorAllocator] = None,
|
| 92 |
+
) -> "torch.Tensor":
|
| 93 |
+
"""
|
| 94 |
+
Receive a torch.Tensor from a peer and synchronize.
|
| 95 |
+
|
| 96 |
+
After this call returns, the receive buffer is safe to read from from
|
| 97 |
+
any stream. An RayChannelError will be raised if an error occurred (e.g.,
|
| 98 |
+
remote actor died), and the buffer is not safe to read.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
shape: The shape of the tensor to receive.
|
| 102 |
+
dtype: The dtype of the tensor to receive.
|
| 103 |
+
peer_rank: The rank of the actor to receive from.
|
| 104 |
+
allocator: A function to allocate the tensor to receive into.
|
| 105 |
+
"""
|
| 106 |
+
raise NotImplementedError
|
| 107 |
+
|
| 108 |
+
@property
|
| 109 |
+
@abstractmethod
|
| 110 |
+
def recv_stream(self):
|
| 111 |
+
"""
|
| 112 |
+
Return the torch stream context used for receiving tensors.
|
| 113 |
+
"""
|
| 114 |
+
raise NotImplementedError
|
| 115 |
+
|
| 116 |
+
@property
|
| 117 |
+
@abstractmethod
|
| 118 |
+
def send_stream(self):
|
| 119 |
+
"""
|
| 120 |
+
Return the torch stream context used for sending tensors.
|
| 121 |
+
"""
|
| 122 |
+
raise NotImplementedError
|
| 123 |
+
|
| 124 |
+
@abstractmethod
|
| 125 |
+
def allgather(
|
| 126 |
+
self,
|
| 127 |
+
send_buf: "torch.Tensor",
|
| 128 |
+
recv_buf: "torch.Tensor",
|
| 129 |
+
) -> None:
|
| 130 |
+
"""
|
| 131 |
+
Collectively allgather the tensor across the group.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
send_buf: The input torch.tensor to allgather. It should already be
|
| 135 |
+
on this actor's default device.
|
| 136 |
+
recv_buf: The output torch.tensor to store the allgather result.
|
| 137 |
+
"""
|
| 138 |
+
raise NotImplementedError
|
| 139 |
+
|
| 140 |
+
@abstractmethod
|
| 141 |
+
def allreduce(
|
| 142 |
+
self,
|
| 143 |
+
send_buf: "torch.Tensor",
|
| 144 |
+
recv_buf: "torch.Tensor",
|
| 145 |
+
op: ReduceOp,
|
| 146 |
+
) -> None:
|
| 147 |
+
"""
|
| 148 |
+
Collectively allreduce the tensor across the group.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
send_buf: The input torch.tensor to allreduce. It should already be
|
| 152 |
+
on this actor's default device.
|
| 153 |
+
recv_buf: The output torch.tensor to store the allreduce result.
|
| 154 |
+
op: The reduce operation.
|
| 155 |
+
"""
|
| 156 |
+
raise NotImplementedError
|
| 157 |
+
|
| 158 |
+
@abstractmethod
|
| 159 |
+
def reducescatter(
|
| 160 |
+
self,
|
| 161 |
+
send_buf: "torch.Tensor",
|
| 162 |
+
recv_buf: "torch.Tensor",
|
| 163 |
+
op: ReduceOp,
|
| 164 |
+
) -> None:
|
| 165 |
+
"""
|
| 166 |
+
Collectively reducescatter the tensor across the group.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
send_buf: The input torch.tensor to reducescatter. It should already be
|
| 170 |
+
on this actor's default device.
|
| 171 |
+
recv_buf: The output torch.tensor to store the reducescatter result.
|
| 172 |
+
op: The reduce operation.
|
| 173 |
+
"""
|
| 174 |
+
raise NotImplementedError
|
| 175 |
+
|
| 176 |
+
@abstractmethod
|
| 177 |
+
def destroy(self) -> None:
|
| 178 |
+
"""
|
| 179 |
+
Destroy the GPU communicator.
|
| 180 |
+
|
| 181 |
+
Any destruction and cleanup for the GPU communicator should be
|
| 182 |
+
done here. Implement as a noop is nothing is needed.
|
| 183 |
+
"""
|
| 184 |
+
raise NotImplementedError
|
| 185 |
+
|
| 186 |
+
@abstractmethod
|
| 187 |
+
def get_transport_name(self) -> str:
|
| 188 |
+
"""
|
| 189 |
+
Return the type of the communicator (gpu or cpu).
|
| 190 |
+
"""
|
| 191 |
+
raise NotImplementedError
|
| 192 |
+
|
| 193 |
+
@classmethod
|
| 194 |
+
@abstractmethod
|
| 195 |
+
def generate_communicator_id(cls) -> str:
|
| 196 |
+
"""
|
| 197 |
+
Return the unique id of the communicator.
|
| 198 |
+
"""
|
| 199 |
+
raise NotImplementedError
|
lib/python3.12/site-packages/ray/experimental/channel/communicator_handle.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
|
| 3 |
+
import ray
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class CommunicatorHandle:
|
| 7 |
+
"""
|
| 8 |
+
A lightweight communicator handle used by the driver to store handles to
|
| 9 |
+
the actors in the communicator.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
def __init__(
|
| 13 |
+
self,
|
| 14 |
+
actor_handles: List["ray.actor.ActorHandle"],
|
| 15 |
+
):
|
| 16 |
+
"""
|
| 17 |
+
Initializes the CommunicatorHandle with the given actor handles.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
actor_handles: A list of actor handles to be stored.
|
| 21 |
+
"""
|
| 22 |
+
self._actor_handles = actor_handles
|
| 23 |
+
|
| 24 |
+
def get_actor_handles(self) -> List["ray.actor.ActorHandle"]:
|
| 25 |
+
"""
|
| 26 |
+
Retuan all actor handles in this communicator.
|
| 27 |
+
"""
|
| 28 |
+
return self._actor_handles
|
lib/python3.12/site-packages/ray/experimental/channel/conftest.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from collections import defaultdict
|
| 3 |
+
from typing import Optional, Tuple
|
| 4 |
+
from unittest import mock
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
import ray
|
| 9 |
+
import ray.dag
|
| 10 |
+
import ray.experimental.channel as ray_channel
|
| 11 |
+
from ray.experimental.channel import nccl_group
|
| 12 |
+
from ray.experimental.channel.communicator import TorchTensorAllocator
|
| 13 |
+
from ray.experimental.util.types import Device
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@ray.remote(num_cpus=0)
|
| 17 |
+
class Barrier:
|
| 18 |
+
"""
|
| 19 |
+
Barrier that blocks the given number of actors until all actors have
|
| 20 |
+
reached the barrier. This is used to mock out blocking NCCL ops.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, num_actors=2):
|
| 24 |
+
self.num_actors = num_actors
|
| 25 |
+
self.condition = asyncio.Condition()
|
| 26 |
+
# Buffer for the data that is "sent" between the actors, each entry is
|
| 27 |
+
# one p2p op.
|
| 28 |
+
self.data = {}
|
| 29 |
+
# Buffer for the number of actors seen, each entry is one p2p op.
|
| 30 |
+
self.num_actors_seen = defaultdict(int)
|
| 31 |
+
|
| 32 |
+
# Add a new mock for the TorchTensorType.device property
|
| 33 |
+
device_property_patcher = mock.patch(
|
| 34 |
+
"ray.experimental.channel.torch_tensor_type.TorchTensorType.device",
|
| 35 |
+
new_callable=mock.PropertyMock,
|
| 36 |
+
return_value=Device.CPU,
|
| 37 |
+
)
|
| 38 |
+
device_property_patcher.start()
|
| 39 |
+
|
| 40 |
+
async def wait(self, idx: int, data=None):
|
| 41 |
+
"""
|
| 42 |
+
Wait at barrier until all actors have sent `idx`. One actor should
|
| 43 |
+
provide `data`, and this value will be returned by this method for all
|
| 44 |
+
other actors.
|
| 45 |
+
"""
|
| 46 |
+
async with self.condition:
|
| 47 |
+
if data is not None:
|
| 48 |
+
assert idx not in self.data, (self.data, self.num_actors_seen)
|
| 49 |
+
self.data[idx] = data
|
| 50 |
+
self.num_actors_seen[idx] += 1
|
| 51 |
+
|
| 52 |
+
if self.num_actors_seen[idx] == self.num_actors:
|
| 53 |
+
# Wake up all tasks waiting on this condition.
|
| 54 |
+
self.condition.notify_all()
|
| 55 |
+
else:
|
| 56 |
+
await self.condition.wait_for(
|
| 57 |
+
lambda: self.num_actors_seen[idx] == self.num_actors
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
if data is None:
|
| 61 |
+
data = self.data[idx]
|
| 62 |
+
|
| 63 |
+
return data
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class MockCudaStream:
|
| 67 |
+
def __init__(self):
|
| 68 |
+
self.cuda_stream = 0
|
| 69 |
+
|
| 70 |
+
def synchronize(self):
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class MockNcclGroup(nccl_group._NcclGroup):
|
| 75 |
+
"""
|
| 76 |
+
Mock the internal _NcclGroup to use a barrier actor instead of a NCCL group
|
| 77 |
+
for communication.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
def __init__(self, *args, **kwargs):
|
| 81 |
+
super().__init__(*args, **kwargs)
|
| 82 |
+
# We use the op index to synchronize the sender and receiver at the
|
| 83 |
+
# barrier.
|
| 84 |
+
self.num_ops = defaultdict(int)
|
| 85 |
+
self.barriers = set()
|
| 86 |
+
|
| 87 |
+
def send(self, tensor: torch.Tensor, peer_rank: int):
|
| 88 |
+
# "Send" the tensor to the barrier actor.
|
| 89 |
+
barrier_key = sorted([self.get_self_rank(), peer_rank])
|
| 90 |
+
barrier_key = f"barrier-{barrier_key[0]}-{barrier_key[1]}"
|
| 91 |
+
barrier = ray.get_actor(name=barrier_key)
|
| 92 |
+
self.barriers.add(barrier)
|
| 93 |
+
ray.get(barrier.wait.remote(self.num_ops[barrier_key], tensor))
|
| 94 |
+
self.num_ops[barrier_key] += 1
|
| 95 |
+
|
| 96 |
+
def recv(
|
| 97 |
+
self,
|
| 98 |
+
shape: Tuple[int],
|
| 99 |
+
dtype: torch.dtype,
|
| 100 |
+
peer_rank: int,
|
| 101 |
+
allocator: Optional[TorchTensorAllocator] = None,
|
| 102 |
+
):
|
| 103 |
+
# "Receive" the tensor from the barrier actor.
|
| 104 |
+
barrier_key = sorted([self.get_self_rank(), peer_rank])
|
| 105 |
+
barrier_key = f"barrier-{barrier_key[0]}-{barrier_key[1]}"
|
| 106 |
+
barrier = ray.get_actor(name=barrier_key)
|
| 107 |
+
self.barriers.add(barrier)
|
| 108 |
+
received_tensor = ray.get(barrier.wait.remote(self.num_ops[barrier_key]))
|
| 109 |
+
assert (
|
| 110 |
+
allocator is not None
|
| 111 |
+
), "torch tensor allocator is required for MockNcclGroup"
|
| 112 |
+
buf = allocator(shape, dtype)
|
| 113 |
+
buf[:] = received_tensor[:]
|
| 114 |
+
self.num_ops[barrier_key] += 1
|
| 115 |
+
return buf
|
| 116 |
+
|
| 117 |
+
def destroy(self) -> None:
|
| 118 |
+
for barrier in self.barriers:
|
| 119 |
+
ray.kill(barrier)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def start_nccl_mock():
|
| 123 |
+
"""
|
| 124 |
+
Patch methods that require CUDA.
|
| 125 |
+
"""
|
| 126 |
+
# Mock cupy dependencies.
|
| 127 |
+
nccl_mock = mock.MagicMock()
|
| 128 |
+
nccl_mock.nccl.get_unique_id.return_value = 0
|
| 129 |
+
cp_patcher = mock.patch.dict(
|
| 130 |
+
"sys.modules",
|
| 131 |
+
{
|
| 132 |
+
"cupy.cuda": nccl_mock,
|
| 133 |
+
"cupy": mock.MagicMock(),
|
| 134 |
+
"ray.util.collective.collective_group": mock.MagicMock(),
|
| 135 |
+
},
|
| 136 |
+
)
|
| 137 |
+
cp_patcher.start()
|
| 138 |
+
|
| 139 |
+
# Mock send/recv ops to use an actor instead of NCCL.
|
| 140 |
+
ray.experimental.channel.nccl_group._NcclGroup = MockNcclGroup
|
| 141 |
+
|
| 142 |
+
# PyTorch mocks.
|
| 143 |
+
stream_patcher = mock.patch(
|
| 144 |
+
"torch.cuda.current_stream", new_callable=lambda: MockCudaStream
|
| 145 |
+
)
|
| 146 |
+
stream_patcher.start()
|
| 147 |
+
new_stream_patcher = mock.patch(
|
| 148 |
+
"torch.cuda.Stream", new_callable=lambda: MockCudaStream
|
| 149 |
+
)
|
| 150 |
+
new_stream_patcher.start()
|
| 151 |
+
tensor_patcher = mock.patch("torch.Tensor.device", torch.device("cuda"))
|
| 152 |
+
tensor_patcher.start()
|
| 153 |
+
tensor_patcher = mock.patch("torch.Tensor.is_cuda", True)
|
| 154 |
+
tensor_patcher.start()
|
| 155 |
+
tensor_allocator_patcher = mock.patch(
|
| 156 |
+
"ray.experimental.channel.torch_tensor_accelerator_channel._torch_tensor_allocator",
|
| 157 |
+
lambda shape, dtype: torch.empty(shape, dtype=dtype),
|
| 158 |
+
)
|
| 159 |
+
tensor_allocator_patcher.start()
|
| 160 |
+
|
| 161 |
+
# Add a new mock for the TorchTensorType.device property
|
| 162 |
+
device_property_patcher = mock.patch(
|
| 163 |
+
"ray.experimental.channel.torch_tensor_type.TorchTensorType.device",
|
| 164 |
+
new_callable=mock.PropertyMock,
|
| 165 |
+
return_value=Device.CPU,
|
| 166 |
+
)
|
| 167 |
+
device_property_patcher.start()
|
| 168 |
+
|
| 169 |
+
ctx = ray_channel.ChannelContext.get_current()
|
| 170 |
+
ctx.set_torch_device(torch.device("cuda"))
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class TracedChannel(ray_channel.shared_memory_channel.Channel):
|
| 174 |
+
"""
|
| 175 |
+
Patched Channel that records all write ops for testing.
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
def __init__(self, *args, **kwargs):
|
| 179 |
+
super().__init__(*args, **kwargs)
|
| 180 |
+
|
| 181 |
+
self.ops = []
|
| 182 |
+
|
| 183 |
+
def write(self, *args, **kwargs):
|
| 184 |
+
self.ops.append((args, kwargs))
|
| 185 |
+
return super().write(*args, **kwargs)
|
lib/python3.12/site-packages/ray/experimental/channel/cpu_communicator.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from collections import defaultdict
|
| 3 |
+
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
| 4 |
+
|
| 5 |
+
import ray
|
| 6 |
+
from ray.experimental.channel.communicator import (
|
| 7 |
+
Communicator,
|
| 8 |
+
ReduceOp,
|
| 9 |
+
TorchTensorAllocator,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
if TYPE_CHECKING:
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@ray.remote(num_cpus=0)
|
| 17 |
+
class CPUCommBarrier:
|
| 18 |
+
"""
|
| 19 |
+
Barrier actor that blocks the given number of actors until all actors have
|
| 20 |
+
reached the Barrier.
|
| 21 |
+
|
| 22 |
+
p2p operations are not done here (completed via shared memory channel).
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, num_actors: int):
|
| 26 |
+
self.num_actors = num_actors
|
| 27 |
+
self.condition = asyncio.Condition()
|
| 28 |
+
# Stores the data for each collective operation
|
| 29 |
+
self.collective_data: Dict[int, List["torch.Tensor"]] = defaultdict(list)
|
| 30 |
+
# Stores the shape of data for each collective operation
|
| 31 |
+
self.collective_data_shape: Dict[int, "torch.Tensor.type"] = {}
|
| 32 |
+
# Buffer for the number of actors seen
|
| 33 |
+
self.num_actors_seen = defaultdict(int)
|
| 34 |
+
# Number of actors who have read the result, and are about to exit the function.
|
| 35 |
+
# State is kept so we only garbage collect after the last actor has read the
|
| 36 |
+
# relevant data.
|
| 37 |
+
self.num_actors_read = defaultdict(int)
|
| 38 |
+
|
| 39 |
+
async def wait_collective(self, op_id: int, data: "torch.Tensor", op: ReduceOp):
|
| 40 |
+
"""
|
| 41 |
+
Wait at the communicator until all actors have sent `op_id` and `data`.
|
| 42 |
+
Once data from all actors is received, execute the collective `op`
|
| 43 |
+
on the communicator actor and return the result.
|
| 44 |
+
"""
|
| 45 |
+
async with self.condition:
|
| 46 |
+
self.collective_data[op_id].append(data)
|
| 47 |
+
self.num_actors_seen[op_id] += 1
|
| 48 |
+
|
| 49 |
+
if self.num_actors_seen[op_id] == self.num_actors:
|
| 50 |
+
# Apply the collective operation across all gathered tensors
|
| 51 |
+
data = self._apply_op(op, self.collective_data[op_id])
|
| 52 |
+
self.collective_data[op_id] = data
|
| 53 |
+
self.condition.notify_all()
|
| 54 |
+
else:
|
| 55 |
+
await self.condition.wait_for(
|
| 56 |
+
lambda: self.num_actors_seen[op_id] == self.num_actors
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
data = self.collective_data[op_id]
|
| 60 |
+
self.num_actors_read[op_id] += 1
|
| 61 |
+
|
| 62 |
+
if self.num_actors_read[op_id] == self.num_actors:
|
| 63 |
+
del self.collective_data[op_id]
|
| 64 |
+
del self.num_actors_seen[op_id]
|
| 65 |
+
del self.num_actors_read[op_id]
|
| 66 |
+
|
| 67 |
+
return data
|
| 68 |
+
|
| 69 |
+
def _apply_op(self, op: ReduceOp, tensors: List["torch.Tensor"]) -> "torch.Tensor":
|
| 70 |
+
"""Apply the specified reduction operation across a list of tensors."""
|
| 71 |
+
|
| 72 |
+
result = tensors[0].clone()
|
| 73 |
+
if op == ReduceOp.SUM:
|
| 74 |
+
for tensor in tensors[1:]:
|
| 75 |
+
result += tensor
|
| 76 |
+
elif op == ReduceOp.PRODUCT:
|
| 77 |
+
for tensor in tensors[1:]:
|
| 78 |
+
result *= tensor
|
| 79 |
+
elif op == ReduceOp.MAX:
|
| 80 |
+
for tensor in tensors[1:]:
|
| 81 |
+
result = torch.max(result, tensor)
|
| 82 |
+
elif op == ReduceOp.MIN:
|
| 83 |
+
for tensor in tensors[1:]:
|
| 84 |
+
result = torch.min(result, tensor)
|
| 85 |
+
elif op == ReduceOp.AVG:
|
| 86 |
+
result = sum(tensors) / len(tensors)
|
| 87 |
+
else:
|
| 88 |
+
raise ValueError(f"Operation {op} not supported")
|
| 89 |
+
return result
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class CPUCommunicator(Communicator):
|
| 93 |
+
"""
|
| 94 |
+
Uses a CPU-based communicator actor instead of an accelerator group like NCCL.
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
def __init__(self, world_size: int, actor_handles: List["ray.actor.ActorHandle"]):
|
| 98 |
+
"""We use the op index to synchronize the sender and receiver at the
|
| 99 |
+
communicator actor."""
|
| 100 |
+
self._world_size = world_size
|
| 101 |
+
self._actor_handles = actor_handles
|
| 102 |
+
self.num_ops = defaultdict(int)
|
| 103 |
+
|
| 104 |
+
# For collective communication, one barrier will be created for
|
| 105 |
+
# each unique group of participants.
|
| 106 |
+
self.barriers = set()
|
| 107 |
+
self._rank = None
|
| 108 |
+
|
| 109 |
+
def send(self, tensor: "torch.Tensor", peer_rank: int):
|
| 110 |
+
# p2p operations are done via a shared memory channel, initialized in
|
| 111 |
+
# `create_channel` of `TorchTensorType`
|
| 112 |
+
pass
|
| 113 |
+
|
| 114 |
+
def recv(
|
| 115 |
+
self,
|
| 116 |
+
shape: Tuple[int],
|
| 117 |
+
dtype: "torch.dtype",
|
| 118 |
+
peer_rank: int,
|
| 119 |
+
allocator: Optional[TorchTensorAllocator] = None,
|
| 120 |
+
):
|
| 121 |
+
# See the comment on `send`
|
| 122 |
+
pass
|
| 123 |
+
|
| 124 |
+
def allgather(
|
| 125 |
+
self,
|
| 126 |
+
send_buf: "torch.Tensor",
|
| 127 |
+
recv_buf: "torch.Tensor",
|
| 128 |
+
):
|
| 129 |
+
raise NotImplementedError
|
| 130 |
+
|
| 131 |
+
def allreduce(
|
| 132 |
+
self,
|
| 133 |
+
send_buf: "torch.Tensor",
|
| 134 |
+
recv_buf: "torch.Tensor",
|
| 135 |
+
op: ReduceOp = ReduceOp.SUM,
|
| 136 |
+
):
|
| 137 |
+
all_ranks = [
|
| 138 |
+
self.get_rank(actor_handle) for actor_handle in self.get_actor_handles()
|
| 139 |
+
]
|
| 140 |
+
barrier_key = "barrier-collective-" + "-".join(map(str, sorted(all_ranks)))
|
| 141 |
+
barrier = CPUCommBarrier.options(name=barrier_key, get_if_exists=True).remote(
|
| 142 |
+
self._world_size
|
| 143 |
+
)
|
| 144 |
+
self.barriers.add(barrier)
|
| 145 |
+
|
| 146 |
+
result = ray.get(
|
| 147 |
+
barrier.wait_collective.remote(self.num_ops[barrier_key], send_buf, op)
|
| 148 |
+
)
|
| 149 |
+
assert recv_buf is not None, "Receiving buffer required for CPUCommunicator"
|
| 150 |
+
recv_buf[:] = result[:]
|
| 151 |
+
self.num_ops[barrier_key] += 1
|
| 152 |
+
|
| 153 |
+
def reducescatter(
|
| 154 |
+
self,
|
| 155 |
+
send_buf: "torch.Tensor",
|
| 156 |
+
recv_buf: "torch.Tensor",
|
| 157 |
+
op: ReduceOp = ReduceOp.SUM,
|
| 158 |
+
):
|
| 159 |
+
raise NotImplementedError
|
| 160 |
+
|
| 161 |
+
def destroy(self) -> None:
|
| 162 |
+
for barrier in self.barriers:
|
| 163 |
+
ray.kill(barrier)
|
| 164 |
+
|
| 165 |
+
def initialize(self, rank: int) -> None:
|
| 166 |
+
self._rank = rank
|
| 167 |
+
|
| 168 |
+
def get_actor_handles(self) -> List["ray.actor.ActorHandle"]:
|
| 169 |
+
return self._actor_handles
|
| 170 |
+
|
| 171 |
+
def get_rank(self, actor: ray.actor.ActorHandle) -> int:
|
| 172 |
+
"""
|
| 173 |
+
Return the given actor's rank in the CPU communicator.
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
actor: The actor handle to look up.
|
| 177 |
+
"""
|
| 178 |
+
actor_ids = [a._ray_actor_id for a in self._actor_handles]
|
| 179 |
+
try:
|
| 180 |
+
rank = actor_ids.index(actor._ray_actor_id)
|
| 181 |
+
except ValueError:
|
| 182 |
+
raise ValueError("Actor is not in the CPUCommunicator group.")
|
| 183 |
+
return rank
|
| 184 |
+
|
| 185 |
+
def get_self_rank(self) -> Optional[int]:
|
| 186 |
+
return self._rank
|
| 187 |
+
|
| 188 |
+
def get_world_size(self) -> int:
|
| 189 |
+
"""
|
| 190 |
+
Return the number of ranks in the CPU communicator.
|
| 191 |
+
"""
|
| 192 |
+
return self._world_size
|
| 193 |
+
|
| 194 |
+
def get_transport_name(self) -> str:
|
| 195 |
+
return "cpu"
|
| 196 |
+
|
| 197 |
+
def recv_stream(self):
|
| 198 |
+
raise NotImplementedError
|
| 199 |
+
|
| 200 |
+
def send_stream(self):
|
| 201 |
+
raise NotImplementedError
|
| 202 |
+
|
| 203 |
+
@classmethod
|
| 204 |
+
def generate_communicator_id(cls) -> str:
|
| 205 |
+
import uuid
|
| 206 |
+
|
| 207 |
+
return str(uuid.uuid4())
|
lib/python3.12/site-packages/ray/experimental/channel/intra_process_channel.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from typing import Any, Optional
|
| 3 |
+
|
| 4 |
+
from ray.experimental.channel import ChannelContext
|
| 5 |
+
from ray.experimental.channel.common import ChannelInterface
|
| 6 |
+
from ray.util.annotations import PublicAPI
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@PublicAPI(stability="alpha")
|
| 10 |
+
class IntraProcessChannel(ChannelInterface):
|
| 11 |
+
"""
|
| 12 |
+
IntraProcessChannel is a channel for communication between two tasks in the same
|
| 13 |
+
worker process. It writes data directly to the worker's _SerializationContext
|
| 14 |
+
and reads data from the _SerializationContext to avoid the serialization
|
| 15 |
+
overhead and the need for reading/writing from shared memory. Note that if the
|
| 16 |
+
readers may mutate the data, users should deep copy the data themselves to avoid
|
| 17 |
+
side effects.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
num_readers: The number of readers that will read from this channel. Readers
|
| 21 |
+
can be the same method of the same actor.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
num_readers,
|
| 27 |
+
_channel_id: Optional[str] = None,
|
| 28 |
+
):
|
| 29 |
+
# Generate a unique ID for the channel. The writer and reader will use
|
| 30 |
+
# this ID to store and retrieve data from the _SerializationContext.
|
| 31 |
+
self._channel_id = _channel_id
|
| 32 |
+
self._num_readers = num_readers
|
| 33 |
+
if self._channel_id is None:
|
| 34 |
+
self._channel_id = str(uuid.uuid4())
|
| 35 |
+
|
| 36 |
+
def ensure_registered_as_writer(self) -> None:
|
| 37 |
+
pass
|
| 38 |
+
|
| 39 |
+
def ensure_registered_as_reader(self) -> None:
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
def __reduce__(self):
|
| 43 |
+
return IntraProcessChannel, (
|
| 44 |
+
self._num_readers,
|
| 45 |
+
self._channel_id,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
def __str__(self) -> str:
|
| 49 |
+
return f"IntraProcessChannel(channel_id={self._channel_id})"
|
| 50 |
+
|
| 51 |
+
def write(self, value: Any, timeout: Optional[float] = None):
|
| 52 |
+
self.ensure_registered_as_writer()
|
| 53 |
+
# No need to check timeout as the operation is non-blocking.
|
| 54 |
+
|
| 55 |
+
# Because both the reader and writer are in the same worker process,
|
| 56 |
+
# we can directly store the data in the context instead of storing
|
| 57 |
+
# it in the channel object. This removes the serialization overhead of `value`.
|
| 58 |
+
ctx = ChannelContext.get_current().serialization_context
|
| 59 |
+
ctx.set_data(self._channel_id, value, self._num_readers)
|
| 60 |
+
|
| 61 |
+
def read(self, timeout: Optional[float] = None, deserialize: bool = True) -> Any:
|
| 62 |
+
self.ensure_registered_as_reader()
|
| 63 |
+
assert deserialize, "Data passed from the actor to itself is never serialized"
|
| 64 |
+
# No need to check timeout as the operation is non-blocking.
|
| 65 |
+
ctx = ChannelContext.get_current().serialization_context
|
| 66 |
+
return ctx.get_data(self._channel_id)
|
| 67 |
+
|
| 68 |
+
def close(self) -> None:
|
| 69 |
+
ctx = ChannelContext.get_current().serialization_context
|
| 70 |
+
ctx.reset_data(self._channel_id)
|
lib/python3.12/site-packages/ray/experimental/channel/nccl_group.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from types import ModuleType
|
| 3 |
+
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple
|
| 4 |
+
|
| 5 |
+
import ray
|
| 6 |
+
from ray.exceptions import RayChannelError
|
| 7 |
+
from ray.experimental.channel.accelerator_context import AcceleratorContext
|
| 8 |
+
from ray.experimental.channel.communicator import Communicator, TorchTensorAllocator
|
| 9 |
+
from ray.experimental.util.types import ReduceOp
|
| 10 |
+
|
| 11 |
+
if TYPE_CHECKING:
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Logger for this module. It should be configured at the entry point
|
| 16 |
+
# into the program using Ray. Ray provides a default configuration at
|
| 17 |
+
# entry/init points.
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class _NcclGroup(Communicator):
|
| 22 |
+
"""
|
| 23 |
+
Represents an actor's NCCL communicator. This is the default NCCL communicator
|
| 24 |
+
to be used in Compiled Graph if a custom communicator is not provided.
|
| 25 |
+
|
| 26 |
+
This class is not thread-safe.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
world_size: int,
|
| 32 |
+
comm_id: tuple,
|
| 33 |
+
rank: Optional[int],
|
| 34 |
+
actor_handles: List["ray.actor.ActorHandle"],
|
| 35 |
+
cuda_stream: Optional["torch.cuda.Stream"],
|
| 36 |
+
use_communication_streams: bool = False,
|
| 37 |
+
):
|
| 38 |
+
"""
|
| 39 |
+
Initialize a NCCL communicator that can be used to communicate p2p with
|
| 40 |
+
other GPU actors.
|
| 41 |
+
|
| 42 |
+
This method blocks until the same call has been made on all other
|
| 43 |
+
actors in the group, with the same arguments for world_size and
|
| 44 |
+
comm_id.
|
| 45 |
+
|
| 46 |
+
NOTE: A concurrent NCCL group can coexist with this one but using the
|
| 47 |
+
two groups concurrently on different CUDA streams may cause deadlock.
|
| 48 |
+
See
|
| 49 |
+
https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/communicators.html
|
| 50 |
+
#using-multiple-nccl-communicators-concurrently.
|
| 51 |
+
|
| 52 |
+
If the user can guarantee that all involved actors execute the same ops
|
| 53 |
+
in the same order, then the other NCCL group should use the given
|
| 54 |
+
`cuda_stream`, and there will not be a concurrency issue. Otherwise,
|
| 55 |
+
the other stream needs to synchronize with the given `cuda_stream`
|
| 56 |
+
before and after it launches NCCL ops, e.g., at the beginning and end
|
| 57 |
+
of a DAG task.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
world_size: The number of participating actors/devices.
|
| 61 |
+
comm_id: A unique communicator ID returned by
|
| 62 |
+
cupy.cuda.nccl.get_unique_id().
|
| 63 |
+
rank: The rank of this actor. If None, then the caller is not a
|
| 64 |
+
participant of the NCCL group.
|
| 65 |
+
actor_handles: A list of actor handles, in rank order.
|
| 66 |
+
cuda_stream: A raw CUDA stream to dispatch NCCL ops to. If rank is
|
| 67 |
+
specified, then this must be specified too.
|
| 68 |
+
use_communication_streams: Whether to use dedicated send and recv
|
| 69 |
+
streams for communication. If True, communication and computation
|
| 70 |
+
can be overlapped to improve performance.
|
| 71 |
+
"""
|
| 72 |
+
self._world_size = world_size
|
| 73 |
+
self._rank: Optional[int] = rank
|
| 74 |
+
self.nccl_util: Optional[ModuleType] = None
|
| 75 |
+
self._actor_handles = actor_handles
|
| 76 |
+
self._use_communication_streams = use_communication_streams
|
| 77 |
+
|
| 78 |
+
if rank is not None:
|
| 79 |
+
assert ray.get_gpu_ids(), "NCCL actor has no GPUs assigned"
|
| 80 |
+
assert cuda_stream is not None, "NCCL actor must specify cuda_stream"
|
| 81 |
+
|
| 82 |
+
expected_rank = self.get_rank(ray.get_runtime_context().current_actor)
|
| 83 |
+
assert (
|
| 84 |
+
rank == expected_rank
|
| 85 |
+
), f"NCCL actor's rank {rank} does not match expected rank {expected_rank}"
|
| 86 |
+
|
| 87 |
+
from ray.util.collective.collective_group import nccl_util
|
| 88 |
+
|
| 89 |
+
self.nccl_util = nccl_util
|
| 90 |
+
self._comm = self.nccl_util.NcclCommunicator(world_size, comm_id, rank)
|
| 91 |
+
else:
|
| 92 |
+
# Driver does not have a rank.
|
| 93 |
+
self._comm = None
|
| 94 |
+
|
| 95 |
+
self._cuda_stream: Optional["torch.cuda.Stream"] = None
|
| 96 |
+
self._send_stream: Optional["torch.cuda.Stream"] = None
|
| 97 |
+
self._recv_stream: Optional["torch.cuda.Stream"] = None
|
| 98 |
+
if cuda_stream is not None:
|
| 99 |
+
assert rank is not None, "NCCL actor has no rank assigned"
|
| 100 |
+
self._cuda_stream = cuda_stream
|
| 101 |
+
|
| 102 |
+
if use_communication_streams:
|
| 103 |
+
import torch
|
| 104 |
+
|
| 105 |
+
# TODO(swang): Allow default device to be overridden.
|
| 106 |
+
device = AcceleratorContext.get().get_accelerator_devices()[0]
|
| 107 |
+
|
| 108 |
+
self._send_stream = torch.cuda.Stream(device=device)
|
| 109 |
+
self._recv_stream = torch.cuda.Stream(device=device)
|
| 110 |
+
else:
|
| 111 |
+
self._send_stream = self._cuda_stream
|
| 112 |
+
self._recv_stream = self._cuda_stream
|
| 113 |
+
|
| 114 |
+
self._closed = False
|
| 115 |
+
|
| 116 |
+
def initialize(self, rank: int) -> None:
|
| 117 |
+
# No additional initialization is needed.
|
| 118 |
+
pass
|
| 119 |
+
|
| 120 |
+
def get_actor_handles(self) -> List["ray.actor.ActorHandle"]:
|
| 121 |
+
return self._actor_handles
|
| 122 |
+
|
| 123 |
+
def get_rank(self, actor: ray.actor.ActorHandle) -> int:
|
| 124 |
+
"""
|
| 125 |
+
Return the given actor's rank in the NCCL communicator.
|
| 126 |
+
|
| 127 |
+
Args:
|
| 128 |
+
actor: The actor handle to look up.
|
| 129 |
+
"""
|
| 130 |
+
actor_ids = [a._ray_actor_id for a in self._actor_handles]
|
| 131 |
+
try:
|
| 132 |
+
rank = actor_ids.index(actor._ray_actor_id)
|
| 133 |
+
except ValueError:
|
| 134 |
+
raise ValueError("Actor is not in the NCCL group.")
|
| 135 |
+
return rank
|
| 136 |
+
|
| 137 |
+
def get_self_rank(self) -> Optional[int]:
|
| 138 |
+
"""
|
| 139 |
+
Return this actor's rank.
|
| 140 |
+
"""
|
| 141 |
+
return self._rank
|
| 142 |
+
|
| 143 |
+
def get_world_size(self) -> int:
|
| 144 |
+
"""
|
| 145 |
+
Return the number of ranks in the NCCL communicator.
|
| 146 |
+
"""
|
| 147 |
+
return self._world_size
|
| 148 |
+
|
| 149 |
+
def send(self, buf: "torch.Tensor", peer_rank: int) -> None:
|
| 150 |
+
"""
|
| 151 |
+
Send a torch.Tensor to a peer.
|
| 152 |
+
|
| 153 |
+
This returns when the send kernel has been queued, but the kernel may
|
| 154 |
+
not have completed. Therefore, the caller should ensure that there are
|
| 155 |
+
no concurrent writes to the sent `buf` until the send has finished.
|
| 156 |
+
That is, either all writes should be submitted on the current stream
|
| 157 |
+
(self._cuda_stream) or, if on a different stream, that stream should
|
| 158 |
+
synchronize with the current stream.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
buf: The torch.Tensor to send. It should already be on this
|
| 162 |
+
actor's default device.
|
| 163 |
+
peer_rank: The rank of the actor to send to.
|
| 164 |
+
"""
|
| 165 |
+
if self._closed:
|
| 166 |
+
raise RayChannelError("NCCL group has been destroyed.")
|
| 167 |
+
|
| 168 |
+
if self._use_communication_streams:
|
| 169 |
+
# We observed that if all recv/compute/send operations run on GPU,
|
| 170 |
+
# since there is no synchronization, the CPU execution loop may be
|
| 171 |
+
# far ahead of the GPU operations and lead to runtime failures.
|
| 172 |
+
# To avoid that, we synchronize on the send stream.
|
| 173 |
+
# TODO(rui): find a better approach
|
| 174 |
+
self._send_stream.synchronize()
|
| 175 |
+
|
| 176 |
+
# TODO(swang): Handle send/recv async NCCL errors such as network
|
| 177 |
+
# failures.
|
| 178 |
+
self._comm.send(
|
| 179 |
+
self.nccl_util.get_tensor_ptr(buf),
|
| 180 |
+
buf.numel(),
|
| 181 |
+
self.nccl_util.get_nccl_tensor_dtype(buf),
|
| 182 |
+
peer_rank,
|
| 183 |
+
self._send_stream.cuda_stream,
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
def recv(
|
| 187 |
+
self,
|
| 188 |
+
shape: Tuple[int],
|
| 189 |
+
dtype: "torch.dtype",
|
| 190 |
+
peer_rank: int,
|
| 191 |
+
allocator=Optional[TorchTensorAllocator],
|
| 192 |
+
) -> "torch.Tensor":
|
| 193 |
+
"""
|
| 194 |
+
Receive a torch.Tensor from a peer and synchronize the current stream.
|
| 195 |
+
|
| 196 |
+
After this call returns, the receive buffer is safe to read from from
|
| 197 |
+
any stream. An RayChannelError will be raised if an error occurred (e.g.,
|
| 198 |
+
remote actor died), and the buffer is not safe to read.
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
buf: The torch.Tensor to receive into. This buffer is safe to read
|
| 202 |
+
peer_rank: The rank of the actor to receive from.
|
| 203 |
+
"""
|
| 204 |
+
if self._closed:
|
| 205 |
+
raise RayChannelError("NCCL group has been destroyed.")
|
| 206 |
+
assert allocator is not None, "NCCL group requires a tensor allocator"
|
| 207 |
+
buf = allocator(shape, dtype)
|
| 208 |
+
|
| 209 |
+
if self._use_communication_streams:
|
| 210 |
+
# We observed that if all recv/compute/send operations run on GPU,
|
| 211 |
+
# since there is no synchronization, the CPU execution loop may be
|
| 212 |
+
# far ahead of the GPU operations and lead to runtime failures.
|
| 213 |
+
# To avoid that, we synchronize on the recv stream.
|
| 214 |
+
# TODO(rui): find a better approach
|
| 215 |
+
self._recv_stream.synchronize()
|
| 216 |
+
|
| 217 |
+
self._comm.recv(
|
| 218 |
+
self.nccl_util.get_tensor_ptr(buf),
|
| 219 |
+
buf.numel(),
|
| 220 |
+
self.nccl_util.get_nccl_tensor_dtype(buf),
|
| 221 |
+
peer_rank,
|
| 222 |
+
self._recv_stream.cuda_stream,
|
| 223 |
+
)
|
| 224 |
+
else:
|
| 225 |
+
self._comm.recv(
|
| 226 |
+
self.nccl_util.get_tensor_ptr(buf),
|
| 227 |
+
buf.numel(),
|
| 228 |
+
self.nccl_util.get_nccl_tensor_dtype(buf),
|
| 229 |
+
peer_rank,
|
| 230 |
+
self._recv_stream.cuda_stream,
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# Buffer values are undefined if NCCL ops are aborted. Therefore, we
|
| 234 |
+
# need to synchronize here and check that the channel is still open to
|
| 235 |
+
# ensure that the receive buffer is valid.
|
| 236 |
+
# TODO(swang): Avoid CUDA synchronization.
|
| 237 |
+
self._cuda_stream.synchronize()
|
| 238 |
+
|
| 239 |
+
if self._closed:
|
| 240 |
+
raise RayChannelError("NCCL group has been destroyed.")
|
| 241 |
+
return buf
|
| 242 |
+
|
| 243 |
+
def _exec_collective(
|
| 244 |
+
self,
|
| 245 |
+
send_buf: "torch.Tensor",
|
| 246 |
+
recv_buf: "torch.Tensor",
|
| 247 |
+
operation: "Callable[..., None]",
|
| 248 |
+
*operation_args,
|
| 249 |
+
):
|
| 250 |
+
if self._closed:
|
| 251 |
+
raise RayChannelError("NCCL group has been destroyed.")
|
| 252 |
+
|
| 253 |
+
assert send_buf.dtype == recv_buf.dtype, (
|
| 254 |
+
"Ray Compiled Graph derived the dtype of recv_buf from send_buf, "
|
| 255 |
+
"so send_buf and recv_buf must have the same dtype. "
|
| 256 |
+
"If you see this error, please file an issue at Ray repository."
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
operation(*operation_args)
|
| 260 |
+
|
| 261 |
+
# Buffer values are undefined if NCCL ops are aborted. Therefore, we
|
| 262 |
+
# need to synchronize here and check that the channel is still open to
|
| 263 |
+
# ensure that the receive buffer is valid.
|
| 264 |
+
# TODO(swang): Avoid CUDA synchronization.
|
| 265 |
+
# TODO(wxdeng): This synchronize will be optional after merging the unify PR.
|
| 266 |
+
self._cuda_stream.synchronize()
|
| 267 |
+
if self._closed:
|
| 268 |
+
raise RayChannelError(
|
| 269 |
+
"NCCL group has been destroyed during allreduce operation. "
|
| 270 |
+
"There may be a dtype mismatch between input tensors from "
|
| 271 |
+
"different ranks."
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
def allgather(
|
| 275 |
+
self,
|
| 276 |
+
send_buf: "torch.Tensor",
|
| 277 |
+
recv_buf: "torch.Tensor",
|
| 278 |
+
):
|
| 279 |
+
operation_args = [
|
| 280 |
+
self.nccl_util.get_tensor_ptr(send_buf),
|
| 281 |
+
self.nccl_util.get_tensor_ptr(recv_buf),
|
| 282 |
+
send_buf.numel(),
|
| 283 |
+
self.nccl_util.get_nccl_tensor_dtype(send_buf),
|
| 284 |
+
self._cuda_stream.cuda_stream,
|
| 285 |
+
]
|
| 286 |
+
self._exec_collective(
|
| 287 |
+
send_buf,
|
| 288 |
+
recv_buf,
|
| 289 |
+
self._comm.allGather,
|
| 290 |
+
*operation_args,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
def allreduce(
|
| 294 |
+
self,
|
| 295 |
+
send_buf: "torch.Tensor",
|
| 296 |
+
recv_buf: "torch.Tensor",
|
| 297 |
+
op: ReduceOp = ReduceOp.SUM,
|
| 298 |
+
):
|
| 299 |
+
operation_args = [
|
| 300 |
+
self.nccl_util.get_tensor_ptr(send_buf),
|
| 301 |
+
self.nccl_util.get_tensor_ptr(recv_buf),
|
| 302 |
+
send_buf.numel(),
|
| 303 |
+
self.nccl_util.get_nccl_tensor_dtype(send_buf),
|
| 304 |
+
op.value,
|
| 305 |
+
self._cuda_stream.cuda_stream,
|
| 306 |
+
]
|
| 307 |
+
self._exec_collective(
|
| 308 |
+
send_buf,
|
| 309 |
+
recv_buf,
|
| 310 |
+
self._comm.allReduce,
|
| 311 |
+
*operation_args,
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
def reducescatter(
|
| 315 |
+
self,
|
| 316 |
+
send_buf: "torch.Tensor",
|
| 317 |
+
recv_buf: "torch.Tensor",
|
| 318 |
+
op: ReduceOp = ReduceOp.SUM,
|
| 319 |
+
):
|
| 320 |
+
operation_args = [
|
| 321 |
+
self.nccl_util.get_tensor_ptr(send_buf),
|
| 322 |
+
self.nccl_util.get_tensor_ptr(recv_buf),
|
| 323 |
+
recv_buf.numel(),
|
| 324 |
+
self.nccl_util.get_nccl_tensor_dtype(send_buf),
|
| 325 |
+
op.value,
|
| 326 |
+
self._cuda_stream.cuda_stream,
|
| 327 |
+
]
|
| 328 |
+
self._exec_collective(
|
| 329 |
+
send_buf,
|
| 330 |
+
recv_buf,
|
| 331 |
+
self._comm.reduceScatter,
|
| 332 |
+
*operation_args,
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
@property
|
| 336 |
+
def recv_stream(self):
|
| 337 |
+
import torch
|
| 338 |
+
|
| 339 |
+
return torch.cuda.StreamContext(self._recv_stream)
|
| 340 |
+
|
| 341 |
+
@property
|
| 342 |
+
def send_stream(self):
|
| 343 |
+
import torch
|
| 344 |
+
|
| 345 |
+
return torch.cuda.StreamContext(self._send_stream)
|
| 346 |
+
|
| 347 |
+
def destroy(self) -> None:
|
| 348 |
+
"""
|
| 349 |
+
Destroy the NCCL group.
|
| 350 |
+
"""
|
| 351 |
+
if self._closed:
|
| 352 |
+
return
|
| 353 |
+
|
| 354 |
+
self._closed = True
|
| 355 |
+
|
| 356 |
+
if self._comm is not None:
|
| 357 |
+
logger.info(
|
| 358 |
+
"Destructing NCCL group on actor: "
|
| 359 |
+
f"{ray.get_runtime_context().current_actor}"
|
| 360 |
+
)
|
| 361 |
+
# Abort *after* setting the _closed flag. This ensures that NCCL
|
| 362 |
+
# ops that were blocked on a remote peer will see that the _closed
|
| 363 |
+
# flag is True when they exit from the abort.
|
| 364 |
+
self._comm.abort()
|
| 365 |
+
self._comm.destroy()
|
| 366 |
+
|
| 367 |
+
def get_transport_name(self) -> str:
|
| 368 |
+
return "accelerator"
|
| 369 |
+
|
| 370 |
+
@classmethod
|
| 371 |
+
def generate_communicator_id(cls) -> str:
|
| 372 |
+
from cupy.cuda import nccl
|
| 373 |
+
|
| 374 |
+
return nccl.get_unique_id()
|
lib/python3.12/site-packages/ray/experimental/channel/serialization_context.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import warnings
|
| 2 |
+
from typing import TYPE_CHECKING, Any, Dict, List, Set, Tuple, Union
|
| 3 |
+
|
| 4 |
+
from ray.experimental.util.types import Device
|
| 5 |
+
|
| 6 |
+
if TYPE_CHECKING:
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
_TORCH_WARNING_FILTER_ACTIVATE = True
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class _SerializationContext:
|
| 15 |
+
def __init__(self):
|
| 16 |
+
# If true, then tensors found in the data to serialize are extracted
|
| 17 |
+
# and the caller should send them through an external transport.
|
| 18 |
+
self._use_external_transport: bool = False
|
| 19 |
+
# If _use_external_transport is True, then these are
|
| 20 |
+
# the tensors that should be sent or received
|
| 21 |
+
# out-of-band, through the external transport.
|
| 22 |
+
self._out_of_band_tensors: List["torch.Tensor"] = []
|
| 23 |
+
# During serialization, tensors sent out-of-band are replaced with
|
| 24 |
+
# integer placeholders. This tracks the set of placeholders seen.
|
| 25 |
+
self._deserialized_tensor_placeholders: Set[int] = set()
|
| 26 |
+
|
| 27 |
+
# Buffer for transferring data between tasks in the same worker process.
|
| 28 |
+
# The key is the channel ID, and the value is the data. We don't use a
|
| 29 |
+
# lock when reading/writing the buffer because a DAG node actor will only
|
| 30 |
+
# execute one task at a time in `do_exec_tasks`. It will not execute multiple
|
| 31 |
+
# Ray tasks on a single actor simultaneously.
|
| 32 |
+
self.intra_process_channel_buffers: Dict[str, Any] = {}
|
| 33 |
+
# The number of readers for each channel. When the number of readers
|
| 34 |
+
# reaches 0, remove the data from the buffer.
|
| 35 |
+
self.channel_id_to_num_readers: Dict[str, int] = {}
|
| 36 |
+
|
| 37 |
+
def set_target_device(self, device: Device) -> None:
|
| 38 |
+
self._target_device = device
|
| 39 |
+
|
| 40 |
+
def set_data(self, channel_id: str, value: Any, num_readers: int) -> None:
|
| 41 |
+
assert num_readers > 0, "num_readers must be greater than 0."
|
| 42 |
+
assert (
|
| 43 |
+
channel_id not in self.intra_process_channel_buffers
|
| 44 |
+
), f"Channel {channel_id} already exists in the buffer."
|
| 45 |
+
assert (
|
| 46 |
+
channel_id not in self.channel_id_to_num_readers
|
| 47 |
+
), f"Channel {channel_id} already exists in the channel_id_to_num_readers."
|
| 48 |
+
|
| 49 |
+
self.intra_process_channel_buffers[channel_id] = value
|
| 50 |
+
self.channel_id_to_num_readers[channel_id] = num_readers
|
| 51 |
+
|
| 52 |
+
def has_data(self, channel_id: str) -> bool:
|
| 53 |
+
return channel_id in self.intra_process_channel_buffers
|
| 54 |
+
|
| 55 |
+
def get_data(self, channel_id: str) -> Any:
|
| 56 |
+
assert (
|
| 57 |
+
channel_id in self.intra_process_channel_buffers
|
| 58 |
+
), f"Channel {channel_id} does not exist in the buffer."
|
| 59 |
+
assert (
|
| 60 |
+
channel_id in self.channel_id_to_num_readers
|
| 61 |
+
), f"Channel {channel_id} does not exist in the channel_id_to_num_readers."
|
| 62 |
+
|
| 63 |
+
self.channel_id_to_num_readers[channel_id] -= 1
|
| 64 |
+
if self.channel_id_to_num_readers[channel_id] == 0:
|
| 65 |
+
# All readers have read the data, so we can remove it.
|
| 66 |
+
self.channel_id_to_num_readers.pop(channel_id)
|
| 67 |
+
return self.intra_process_channel_buffers.pop(channel_id)
|
| 68 |
+
return self.intra_process_channel_buffers[channel_id]
|
| 69 |
+
|
| 70 |
+
def reset_data(self, channel_id: str) -> None:
|
| 71 |
+
self.intra_process_channel_buffers.pop(channel_id, None)
|
| 72 |
+
self.channel_id_to_num_readers.pop(channel_id, None)
|
| 73 |
+
|
| 74 |
+
def set_use_external_transport(self, use_external_transport: bool) -> None:
|
| 75 |
+
self._use_external_transport = use_external_transport
|
| 76 |
+
|
| 77 |
+
@property
|
| 78 |
+
def use_external_transport(self) -> bool:
|
| 79 |
+
return self._use_external_transport
|
| 80 |
+
|
| 81 |
+
def reset_out_of_band_tensors(
|
| 82 |
+
self, tensors: List["torch.Tensor"]
|
| 83 |
+
) -> Tuple[List["torch.Tensor"], Set[int]]:
|
| 84 |
+
"""
|
| 85 |
+
Return and reset the out-of-band tensors and all tensor placeholders
|
| 86 |
+
that were deserialized since the last call to reset.
|
| 87 |
+
"""
|
| 88 |
+
prev_tensors = self._out_of_band_tensors
|
| 89 |
+
deserialized_tensor_placeholders = self._deserialized_tensor_placeholders
|
| 90 |
+
self._out_of_band_tensors = tensors
|
| 91 |
+
self._deserialized_tensor_placeholders = set()
|
| 92 |
+
return prev_tensors, deserialized_tensor_placeholders
|
| 93 |
+
|
| 94 |
+
def serialize_tensor(
|
| 95 |
+
self, tensor: "torch.Tensor"
|
| 96 |
+
) -> Union[int, Tuple["np.ndarray", "torch.dtype", str]]:
|
| 97 |
+
from ray.experimental.channel import ChannelContext
|
| 98 |
+
|
| 99 |
+
ctx = ChannelContext.get_current()
|
| 100 |
+
if self._use_external_transport and (
|
| 101 |
+
ctx._torch_device is None or ctx._torch_device == tensor.device
|
| 102 |
+
):
|
| 103 |
+
# External transport is enabled and we found a tensor that matches
|
| 104 |
+
# our device. Add the actual tensor to a buffer. The buffer of
|
| 105 |
+
# tensors should later be popped by the caller and sent via
|
| 106 |
+
# external transport.
|
| 107 |
+
self._out_of_band_tensors.append(tensor)
|
| 108 |
+
# Return a placeholder.
|
| 109 |
+
return len(self._out_of_band_tensors) - 1
|
| 110 |
+
|
| 111 |
+
return self.serialize_to_numpy_or_scalar(tensor)
|
| 112 |
+
|
| 113 |
+
def serialize_to_numpy_or_scalar(
|
| 114 |
+
self, tensor: "torch.Tensor"
|
| 115 |
+
) -> Tuple[Union["np.ndarray", Any], "torch.dtype", str]:
|
| 116 |
+
"""
|
| 117 |
+
Serialize a tensor to a numpy array,
|
| 118 |
+
or a scalar when the tensor is 0-dim.
|
| 119 |
+
"""
|
| 120 |
+
import torch
|
| 121 |
+
|
| 122 |
+
tensor_device_type = tensor.device.type
|
| 123 |
+
|
| 124 |
+
# Transfer through Ray's shared memory store for now.
|
| 125 |
+
# TODO(swang): This requires two copies, one to transfer from GPU to
|
| 126 |
+
# CPU and another from CPU to shared memory. Ideally we should elide
|
| 127 |
+
# the first copy and memcpy directly from GPU to the shared memory
|
| 128 |
+
# buffer.
|
| 129 |
+
if tensor_device_type != "cpu":
|
| 130 |
+
tensor = tensor.to("cpu")
|
| 131 |
+
|
| 132 |
+
# Numpy does not have an equivalent dtype for all torch dtypes, so
|
| 133 |
+
# instead of casting directly to numpy:
|
| 134 |
+
# 1) for non-scalar tensors, we first use a view with a common dtype (uint8)
|
| 135 |
+
# and then view as numpy array.
|
| 136 |
+
# 2) for scalar tensors, we cannot use a uint8 view when the size differs,
|
| 137 |
+
# so we save the original item and type information.
|
| 138 |
+
if tensor.dim() > 0:
|
| 139 |
+
return (tensor.view(torch.uint8).numpy(), tensor.dtype, tensor_device_type)
|
| 140 |
+
else:
|
| 141 |
+
return (tensor.item(), tensor.dtype, tensor_device_type)
|
| 142 |
+
|
| 143 |
+
def deserialize_tensor(
|
| 144 |
+
self,
|
| 145 |
+
val: Union[Tuple["np.ndarray", "torch.dtype", str], int],
|
| 146 |
+
target_device: Device,
|
| 147 |
+
):
|
| 148 |
+
|
| 149 |
+
# Found a placeholder for a tensor that was serialized via accelerator.
|
| 150 |
+
# Replace it with the corresponding deserialized tensor.
|
| 151 |
+
if isinstance(val, int):
|
| 152 |
+
placeholder = val
|
| 153 |
+
self._deserialized_tensor_placeholders.add(placeholder)
|
| 154 |
+
assert placeholder < len(self._out_of_band_tensors), (
|
| 155 |
+
"placeholder",
|
| 156 |
+
placeholder,
|
| 157 |
+
"out_of_band_tensors",
|
| 158 |
+
self._out_of_band_tensors,
|
| 159 |
+
)
|
| 160 |
+
tensor = self._out_of_band_tensors[placeholder]
|
| 161 |
+
if target_device == Device.CPU:
|
| 162 |
+
tensor = tensor.to("cpu")
|
| 163 |
+
return tensor
|
| 164 |
+
|
| 165 |
+
np_array, dtype, tensor_device_type = val
|
| 166 |
+
return self.deserialize_from_numpy_or_scalar(
|
| 167 |
+
np_array, dtype, tensor_device_type, target_device
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
def deserialize_from_numpy_or_scalar(
|
| 171 |
+
self,
|
| 172 |
+
np_array: Union["np.ndarray", Any],
|
| 173 |
+
dtype: "torch.dtype",
|
| 174 |
+
tensor_device_type: str,
|
| 175 |
+
target_device: Device,
|
| 176 |
+
):
|
| 177 |
+
import numpy as np
|
| 178 |
+
import torch
|
| 179 |
+
|
| 180 |
+
if target_device == Device.DEFAULT:
|
| 181 |
+
target_device_type = tensor_device_type
|
| 182 |
+
elif target_device in [Device.GPU, Device.CUDA]:
|
| 183 |
+
target_device_type = "cuda"
|
| 184 |
+
else:
|
| 185 |
+
target_device_type = target_device.value
|
| 186 |
+
|
| 187 |
+
# TODO(swang): Support local P2P transfers if available.
|
| 188 |
+
if target_device_type != "cpu":
|
| 189 |
+
|
| 190 |
+
def convert_numpy_to_tensor(np_array):
|
| 191 |
+
if not isinstance(np_array, np.ndarray):
|
| 192 |
+
# For scalar tensors, create the 0-dim tensor.
|
| 193 |
+
return torch.tensor(
|
| 194 |
+
np_array, device=target_device_type, dtype=dtype
|
| 195 |
+
)
|
| 196 |
+
else:
|
| 197 |
+
# For non-scalar tensors, view as the original dtype.
|
| 198 |
+
# It does zero-copy convert np_array inside shared memory to
|
| 199 |
+
# a tensor. Since we move data to GPU immediately, it is safe.
|
| 200 |
+
cpu_tensor = torch.from_numpy(np_array).view(dtype)
|
| 201 |
+
return cpu_tensor.to(device=target_device_type)
|
| 202 |
+
|
| 203 |
+
global _TORCH_WARNING_FILTER_ACTIVATE
|
| 204 |
+
# filtering warning messages would be the bottleneck for
|
| 205 |
+
# deserializing torch tensors. Since the warning only prompts once,
|
| 206 |
+
# we would only deal with it for the first time.
|
| 207 |
+
if _TORCH_WARNING_FILTER_ACTIVATE:
|
| 208 |
+
with warnings.catch_warnings():
|
| 209 |
+
# Since np_array.is_writable is False (it is set by Ray),
|
| 210 |
+
# this raises a warning. Suppress it.
|
| 211 |
+
warnings.filterwarnings(
|
| 212 |
+
"ignore",
|
| 213 |
+
category=UserWarning,
|
| 214 |
+
message="The given NumPy array is not writable",
|
| 215 |
+
)
|
| 216 |
+
gpu_tensor = convert_numpy_to_tensor(np_array)
|
| 217 |
+
_TORCH_WARNING_FILTER_ACTIVATE = False
|
| 218 |
+
else:
|
| 219 |
+
gpu_tensor = convert_numpy_to_tensor(np_array)
|
| 220 |
+
|
| 221 |
+
return gpu_tensor
|
| 222 |
+
|
| 223 |
+
# TODO(swang): Use zero-copy from_numpy() if np_array.flags.writeable
|
| 224 |
+
# is True. This is safe to set when deserializing np_array if the
|
| 225 |
+
# upstream task has num_readers=1.
|
| 226 |
+
if not isinstance(np_array, np.ndarray):
|
| 227 |
+
# For scalar tensors, create the 0-dim tensor.
|
| 228 |
+
return torch.tensor(np_array, device=target_device_type, dtype=dtype)
|
| 229 |
+
else:
|
| 230 |
+
# For non-scalar tensors, view as the original dtype.
|
| 231 |
+
return torch.tensor(np_array, device=target_device_type).view(dtype)
|
lib/python3.12/site-packages/ray/experimental/channel/shared_memory_channel.py
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import logging
|
| 3 |
+
import time
|
| 4 |
+
from collections import defaultdict, namedtuple
|
| 5 |
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
| 6 |
+
|
| 7 |
+
import ray
|
| 8 |
+
import ray.exceptions
|
| 9 |
+
from ray._raylet import SerializedObject
|
| 10 |
+
from ray.experimental.channel import utils
|
| 11 |
+
from ray.experimental.channel.common import ChannelInterface, ChannelOutputType
|
| 12 |
+
from ray.experimental.channel.intra_process_channel import IntraProcessChannel
|
| 13 |
+
from ray.experimental.channel.utils import get_self_actor
|
| 14 |
+
from ray.util.annotations import DeveloperAPI, PublicAPI
|
| 15 |
+
|
| 16 |
+
# Logger for this module. It should be configured at the entry point
|
| 17 |
+
# into the program using Ray. Ray provides a default configuration at
|
| 18 |
+
# entry/init points.
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _create_channel_ref(
|
| 23 |
+
self,
|
| 24 |
+
buffer_size_bytes: int,
|
| 25 |
+
) -> "ray.ObjectRef":
|
| 26 |
+
"""
|
| 27 |
+
Create a channel that can be read and written through Ray's shared-memory
|
| 28 |
+
object store.
|
| 29 |
+
|
| 30 |
+
The channel has no buffer, so the writer will block until reader(s) have
|
| 31 |
+
read the previous value.
|
| 32 |
+
|
| 33 |
+
A writer and colocated readers can communicate via a shared memory buffer.
|
| 34 |
+
If the readers are remote, then RPC is used to synchronize the writer and
|
| 35 |
+
readers' buffers.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
buffer_size_bytes: The initial buffer size in bytes for messages
|
| 39 |
+
that can be passed between tasks in the DAG. The buffers will
|
| 40 |
+
be automatically resized if larger messages are written to the
|
| 41 |
+
channel.
|
| 42 |
+
Returns:
|
| 43 |
+
Channel: A wrapper around ray.ObjectRef.
|
| 44 |
+
"""
|
| 45 |
+
worker = ray._private.worker.global_worker
|
| 46 |
+
worker.check_connected()
|
| 47 |
+
|
| 48 |
+
value = b"0" * buffer_size_bytes
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
object_ref = worker.put_object(
|
| 52 |
+
value, owner_address=None, _is_experimental_channel=True
|
| 53 |
+
)
|
| 54 |
+
except ray.exceptions.ObjectStoreFullError:
|
| 55 |
+
logger.info(
|
| 56 |
+
"Put failed since the value was either too large or the "
|
| 57 |
+
"store was full of pinned objects."
|
| 58 |
+
)
|
| 59 |
+
raise
|
| 60 |
+
return object_ref
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# Compiled Graph maintains 1 reader object reference (also called buffer) per node.
|
| 64 |
+
# reader_ref: The object reference.
|
| 65 |
+
# ref_owner_actor_id: The actor who created the object reference.
|
| 66 |
+
# num_readers: The number of reader actors who reads this object reference.
|
| 67 |
+
ReaderRefInfo = namedtuple(
|
| 68 |
+
"ReaderRefInfo", ["reader_ref", "ref_owner_actor_id", "num_reader_actors"]
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class _ResizeChannel:
|
| 73 |
+
"""
|
| 74 |
+
When a channel must be resized, the channel backing store must be resized on both
|
| 75 |
+
the writer and the reader nodes. The writer first resizes its own backing store. The
|
| 76 |
+
writer then uses an instance of this class as a sentinel value to tell the reader to
|
| 77 |
+
resize its own backing store. The class instance is sent through the channel.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
def __init__(
|
| 81 |
+
self,
|
| 82 |
+
_node_id_to_reader_ref_info: Dict[str, ReaderRefInfo],
|
| 83 |
+
):
|
| 84 |
+
"""
|
| 85 |
+
Args:
|
| 86 |
+
_node_id_to_reader_ref_info: A node id to ReaderRefInfo.
|
| 87 |
+
"""
|
| 88 |
+
self._node_id_to_reader_ref_info = _node_id_to_reader_ref_info
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class SharedMemoryType(ChannelOutputType):
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
*,
|
| 95 |
+
buffer_size_bytes: Optional[int] = None,
|
| 96 |
+
num_shm_buffers: Optional[int] = None,
|
| 97 |
+
):
|
| 98 |
+
"""
|
| 99 |
+
Args:
|
| 100 |
+
buffer_size_bytes: The initial buffer size in bytes for messages
|
| 101 |
+
that can be passed between tasks in the DAG. The buffers will
|
| 102 |
+
be automatically resized if larger messages are written to the
|
| 103 |
+
channel.
|
| 104 |
+
num_shm_buffers: The number of shared memory buffers per channel.
|
| 105 |
+
Note: In the case of multiple nodes, we only support 1 shared
|
| 106 |
+
memory buffer.
|
| 107 |
+
"""
|
| 108 |
+
super().__init__()
|
| 109 |
+
|
| 110 |
+
from ray.dag import DAGContext
|
| 111 |
+
|
| 112 |
+
ctx = DAGContext.get_current()
|
| 113 |
+
|
| 114 |
+
if buffer_size_bytes is None:
|
| 115 |
+
buffer_size_bytes = ctx.buffer_size_bytes
|
| 116 |
+
self.buffer_size_bytes = buffer_size_bytes
|
| 117 |
+
if num_shm_buffers is None:
|
| 118 |
+
num_shm_buffers = 1
|
| 119 |
+
self._num_shm_buffers = num_shm_buffers
|
| 120 |
+
|
| 121 |
+
def create_channel(
|
| 122 |
+
self,
|
| 123 |
+
writer: Optional["ray.actor.ActorHandle"],
|
| 124 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 125 |
+
driver_actor_id: Optional[str] = None,
|
| 126 |
+
) -> "Channel":
|
| 127 |
+
"""
|
| 128 |
+
Instantiate a ChannelInterface class that can be used
|
| 129 |
+
to pass data of this type.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
writer: The actor that may write to the channel. None signifies the driver.
|
| 133 |
+
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
| 134 |
+
actor handle and the node ID where the actor is located.
|
| 135 |
+
driver_actor_id: If this channel is read by a driver and that driver is an
|
| 136 |
+
actual actor, this will be the actor ID of that driver actor.
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
A ChannelInterface that can be used to pass data
|
| 140 |
+
of this type.
|
| 141 |
+
"""
|
| 142 |
+
return CompositeChannel(
|
| 143 |
+
writer,
|
| 144 |
+
reader_and_node_list,
|
| 145 |
+
self._num_shm_buffers,
|
| 146 |
+
driver_actor_id,
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@PublicAPI(stability="alpha")
|
| 151 |
+
class Channel(ChannelInterface):
|
| 152 |
+
"""
|
| 153 |
+
A wrapper type for ray.ObjectRef. Currently supports ray.get but not
|
| 154 |
+
ray.wait.
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
def __init__(
|
| 158 |
+
self,
|
| 159 |
+
writer: Optional[ray.actor.ActorHandle],
|
| 160 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 161 |
+
typ: Optional[Union[int, SharedMemoryType]] = None,
|
| 162 |
+
_writer_node_id: Optional["ray.NodeID"] = None,
|
| 163 |
+
_writer_ref: Optional["ray.ObjectRef"] = None,
|
| 164 |
+
_node_id_to_reader_ref_info: Optional[Dict[str, ReaderRefInfo]] = None,
|
| 165 |
+
_writer_registered: bool = False,
|
| 166 |
+
_reader_registered: bool = False,
|
| 167 |
+
):
|
| 168 |
+
"""
|
| 169 |
+
Create a channel that can be read and written by co-located Ray processes.
|
| 170 |
+
|
| 171 |
+
Anyone may write to or read from the channel. The channel has no
|
| 172 |
+
buffer, so the writer will block until reader(s) have read the previous
|
| 173 |
+
value.
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
writer: The actor that may write to the channel. None signifies the driver.
|
| 177 |
+
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
| 178 |
+
actor handle and the node ID where the actor is located.
|
| 179 |
+
typ: Type information about the values passed through the channel.
|
| 180 |
+
Either an integer representing the max buffer size in bytes
|
| 181 |
+
allowed, or a SharedMemoryType.
|
| 182 |
+
Returns:
|
| 183 |
+
Channel: A wrapper around ray.ObjectRef.
|
| 184 |
+
"""
|
| 185 |
+
assert len(reader_and_node_list) > 0
|
| 186 |
+
for reader, _ in reader_and_node_list:
|
| 187 |
+
assert isinstance(reader, ray.actor.ActorHandle)
|
| 188 |
+
|
| 189 |
+
if typ is None:
|
| 190 |
+
typ = SharedMemoryType()
|
| 191 |
+
elif isinstance(typ, int):
|
| 192 |
+
typ = SharedMemoryType(buffer_size_bytes=typ)
|
| 193 |
+
|
| 194 |
+
# The min buffer size must be large enough to at least fit an instance of the
|
| 195 |
+
# _ResizeChannel class along with any metadata.
|
| 196 |
+
MIN_BUFFER_SIZE = int(1000) # 1000 bytes
|
| 197 |
+
if typ.buffer_size_bytes < MIN_BUFFER_SIZE:
|
| 198 |
+
raise ValueError(
|
| 199 |
+
"typ.buffer_size_bytes must be at least MIN_BUFFER_SIZE "
|
| 200 |
+
f"({MIN_BUFFER_SIZE} bytes)"
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
self._writer = writer
|
| 204 |
+
self._reader_and_node_list = reader_and_node_list
|
| 205 |
+
self._typ = typ
|
| 206 |
+
|
| 207 |
+
self._worker = ray._private.worker.global_worker
|
| 208 |
+
self._worker.check_connected()
|
| 209 |
+
|
| 210 |
+
self._writer_registered = _writer_registered
|
| 211 |
+
self._reader_registered = _reader_registered
|
| 212 |
+
# NodeID -> ReaderRefInfo on that node. Note that there's only 1
|
| 213 |
+
# reader ref per node.
|
| 214 |
+
self._node_id_to_reader_ref_info: Dict[str, ReaderRefInfo] = (
|
| 215 |
+
_node_id_to_reader_ref_info or {}
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
# Node ID -> a list of reader actors.
|
| 219 |
+
self._node_id_to_readers: Dict[str, "ray.actor.ActorHandle"] = defaultdict(list)
|
| 220 |
+
for reader, node_id in self._reader_and_node_list:
|
| 221 |
+
self._node_id_to_readers[node_id].append(reader)
|
| 222 |
+
|
| 223 |
+
# Number of readers in a local node.
|
| 224 |
+
self._num_local_readers = 0
|
| 225 |
+
|
| 226 |
+
if _writer_ref is None:
|
| 227 |
+
# We are the writer. Check that the passed handle matches the
|
| 228 |
+
# current actor (or it is the driver).
|
| 229 |
+
# TODO(swang): Channels must be initially constructed by the writer
|
| 230 |
+
# actor, so we shouldn't need to include `writer` in the
|
| 231 |
+
# constructor args. Either support Channels being constructed by
|
| 232 |
+
# someone other than the writer or remove it from the args.
|
| 233 |
+
self_actor = get_self_actor()
|
| 234 |
+
assert writer == self_actor
|
| 235 |
+
|
| 236 |
+
self._writer_node_id = (
|
| 237 |
+
ray.runtime_context.get_runtime_context().get_node_id()
|
| 238 |
+
)
|
| 239 |
+
self._writer_ref = _create_channel_ref(self, typ.buffer_size_bytes)
|
| 240 |
+
|
| 241 |
+
self._create_reader_refs(typ.buffer_size_bytes)
|
| 242 |
+
else:
|
| 243 |
+
assert (
|
| 244 |
+
_writer_node_id is not None
|
| 245 |
+
), "_writer_node_id must also be passed to the constructor when "
|
| 246 |
+
"_writer_ref is."
|
| 247 |
+
assert _node_id_to_reader_ref_info is not None, (
|
| 248 |
+
"_node_id_to_reader_ref_info must also be passed to the constructor "
|
| 249 |
+
"when _writer_ref is."
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
self._writer_ref = _writer_ref
|
| 253 |
+
self._writer_node_id = _writer_node_id
|
| 254 |
+
self._node_id_to_reader_ref_info = _node_id_to_reader_ref_info
|
| 255 |
+
|
| 256 |
+
assert self._num_local_readers == 0
|
| 257 |
+
remote_node_exists = False
|
| 258 |
+
for node_id, readers in self._node_id_to_readers.items():
|
| 259 |
+
if self.is_local_node(node_id):
|
| 260 |
+
self._num_local_readers += len(readers)
|
| 261 |
+
else:
|
| 262 |
+
remote_node_exists = True
|
| 263 |
+
# If remote node exists, we have 1 additional reader that listens
|
| 264 |
+
# to object changes and push them to remote nodes.
|
| 265 |
+
if remote_node_exists:
|
| 266 |
+
self._num_local_readers += 1
|
| 267 |
+
# There must be at least 1 local reader
|
| 268 |
+
assert self._num_local_readers > 0
|
| 269 |
+
|
| 270 |
+
self._local_reader_ref: Optional["ray.ObjectRef"] = self._get_local_reader_ref(
|
| 271 |
+
self._node_id_to_reader_ref_info
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
def _get_local_reader_ref(
|
| 275 |
+
self, _node_id_to_reader_ref_info: Dict[str, ReaderRefInfo]
|
| 276 |
+
) -> Optional["ray.ObjectRef"]:
|
| 277 |
+
for node_id, reader_ref_info in _node_id_to_reader_ref_info.items():
|
| 278 |
+
if self.is_local_node(node_id):
|
| 279 |
+
return reader_ref_info.reader_ref
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
def _create_reader_refs(
|
| 283 |
+
self,
|
| 284 |
+
buffer_size_bytes: int,
|
| 285 |
+
):
|
| 286 |
+
# TODO(jhumphri): Free the current reader ref once the reference to it is
|
| 287 |
+
# destroyed below.
|
| 288 |
+
|
| 289 |
+
for node_id, readers in self._node_id_to_readers.items():
|
| 290 |
+
if not self.is_local_node(node_id):
|
| 291 |
+
# Find 1 reader in a remote node to create a reference that's
|
| 292 |
+
# shared by all readers. When a new value is written to a reference,
|
| 293 |
+
# it is sent to this reference.
|
| 294 |
+
reader = readers[0]
|
| 295 |
+
fn = reader.__ray_call__
|
| 296 |
+
self._node_id_to_reader_ref_info[node_id] = ReaderRefInfo(
|
| 297 |
+
reader_ref=ray.get(
|
| 298 |
+
fn.remote(_create_channel_ref, buffer_size_bytes)
|
| 299 |
+
),
|
| 300 |
+
ref_owner_actor_id=reader._actor_id,
|
| 301 |
+
num_reader_actors=len(readers),
|
| 302 |
+
)
|
| 303 |
+
else:
|
| 304 |
+
writer_id = ray.ActorID.nil()
|
| 305 |
+
if self._writer is not None:
|
| 306 |
+
writer_id = self._writer._actor_id
|
| 307 |
+
self._node_id_to_reader_ref_info[node_id] = ReaderRefInfo(
|
| 308 |
+
reader_ref=self._writer_ref,
|
| 309 |
+
ref_owner_actor_id=writer_id,
|
| 310 |
+
num_reader_actors=len(readers),
|
| 311 |
+
)
|
| 312 |
+
# There must be only 1 node reader reference per node.
|
| 313 |
+
assert len(self._node_id_to_reader_ref_info) == len(self._node_id_to_readers)
|
| 314 |
+
|
| 315 |
+
# We need to register the new writer_ref.
|
| 316 |
+
self._writer_registered = False
|
| 317 |
+
self.ensure_registered_as_writer()
|
| 318 |
+
|
| 319 |
+
@staticmethod
|
| 320 |
+
def is_local_node(node_id):
|
| 321 |
+
return ray.runtime_context.get_runtime_context().get_node_id() == node_id
|
| 322 |
+
|
| 323 |
+
def ensure_registered_as_writer(self) -> None:
|
| 324 |
+
if self._writer_registered:
|
| 325 |
+
return
|
| 326 |
+
|
| 327 |
+
if not self.is_local_node(self._writer_node_id):
|
| 328 |
+
raise ValueError(
|
| 329 |
+
"`ensure_registered_as_writer()` must only be called on the node that "
|
| 330 |
+
"the writer is on."
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
remote_reader_ref_info: Dict[str, ReaderRefInfo] = {}
|
| 334 |
+
for node_id, reader_ref_info in self._node_id_to_reader_ref_info.items():
|
| 335 |
+
if self.is_local_node(node_id):
|
| 336 |
+
continue
|
| 337 |
+
remote_reader_ref_info[node_id] = reader_ref_info
|
| 338 |
+
|
| 339 |
+
self._worker.core_worker.experimental_channel_register_writer(
|
| 340 |
+
self._writer_ref,
|
| 341 |
+
remote_reader_ref_info,
|
| 342 |
+
)
|
| 343 |
+
self._writer_registered = True
|
| 344 |
+
|
| 345 |
+
def ensure_registered_as_reader(self) -> None:
|
| 346 |
+
if self._reader_registered:
|
| 347 |
+
return
|
| 348 |
+
|
| 349 |
+
for node_id, reader_ref_info in self._node_id_to_reader_ref_info.items():
|
| 350 |
+
if self.is_local_node(node_id):
|
| 351 |
+
self._worker.core_worker.experimental_channel_register_reader(
|
| 352 |
+
reader_ref_info.reader_ref,
|
| 353 |
+
)
|
| 354 |
+
self._reader_registered = True
|
| 355 |
+
|
| 356 |
+
@staticmethod
|
| 357 |
+
def _deserialize_reader_channel(
|
| 358 |
+
writer: ray.actor.ActorHandle,
|
| 359 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 360 |
+
typ: int,
|
| 361 |
+
writer_node_id,
|
| 362 |
+
writer_ref: "ray.ObjectRef",
|
| 363 |
+
node_id_to_reader_ref_info: Dict[str, ReaderRefInfo],
|
| 364 |
+
writer_registered: bool,
|
| 365 |
+
reader_registered: bool,
|
| 366 |
+
) -> "Channel":
|
| 367 |
+
chan = Channel(
|
| 368 |
+
writer,
|
| 369 |
+
reader_and_node_list,
|
| 370 |
+
typ,
|
| 371 |
+
_writer_node_id=writer_node_id,
|
| 372 |
+
_writer_ref=writer_ref,
|
| 373 |
+
_node_id_to_reader_ref_info=node_id_to_reader_ref_info,
|
| 374 |
+
_writer_registered=writer_registered,
|
| 375 |
+
_reader_registered=reader_registered,
|
| 376 |
+
)
|
| 377 |
+
return chan
|
| 378 |
+
|
| 379 |
+
def __reduce__(self):
|
| 380 |
+
assert self._node_id_to_reader_ref_info is not None
|
| 381 |
+
return self._deserialize_reader_channel, (
|
| 382 |
+
self._writer,
|
| 383 |
+
self._reader_and_node_list,
|
| 384 |
+
self._typ,
|
| 385 |
+
self._writer_node_id,
|
| 386 |
+
self._writer_ref,
|
| 387 |
+
self._node_id_to_reader_ref_info,
|
| 388 |
+
self._writer_registered,
|
| 389 |
+
self._reader_registered,
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
def __str__(self) -> str:
|
| 393 |
+
return (
|
| 394 |
+
f"Channel(_node_id_to_reader_ref_info={self._node_id_to_reader_ref_info}, "
|
| 395 |
+
f"_writer_ref={self._writer_ref})"
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
def _resize_channel_if_needed(self, serialized_value: str, timeout_ms: int):
|
| 399 |
+
# serialized_value.total_bytes *only* includes the size of the data. It does not
|
| 400 |
+
# include the size of the metadata, so we must account for the size of the
|
| 401 |
+
# metadata explicitly.
|
| 402 |
+
size = serialized_value.total_bytes + len(serialized_value.metadata)
|
| 403 |
+
if size > self._typ.buffer_size_bytes:
|
| 404 |
+
# Now make the channel backing store larger.
|
| 405 |
+
self._typ.buffer_size_bytes = size
|
| 406 |
+
# TODO(jhumphri): Free the current writer ref once the reference to it is
|
| 407 |
+
# destroyed below.
|
| 408 |
+
# TODO(sang): Support different policies such as 2X buffer size.
|
| 409 |
+
prev_writer_ref = self._writer_ref
|
| 410 |
+
self._writer_ref = _create_channel_ref(self, self._typ.buffer_size_bytes)
|
| 411 |
+
self._create_reader_refs(self._typ.buffer_size_bytes)
|
| 412 |
+
self._local_reader_ref = self._get_local_reader_ref(
|
| 413 |
+
self._node_id_to_reader_ref_info
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
# Write a special message to the channel so that the readers know to
|
| 417 |
+
# stop using the current reader_ref.
|
| 418 |
+
special_message = _ResizeChannel(self._node_id_to_reader_ref_info)
|
| 419 |
+
special_message_serialized = (
|
| 420 |
+
self._worker.get_serialization_context().serialize(special_message)
|
| 421 |
+
)
|
| 422 |
+
self._worker.core_worker.experimental_channel_put_serialized(
|
| 423 |
+
special_message_serialized,
|
| 424 |
+
prev_writer_ref,
|
| 425 |
+
self._num_local_readers,
|
| 426 |
+
timeout_ms,
|
| 427 |
+
)
|
| 428 |
+
# TODO(sang): Clean the previous ref that won't be used.
|
| 429 |
+
# Right now, if we just close it here, it will not work because
|
| 430 |
+
# of race conditions.
|
| 431 |
+
# self._worker.core_worker.experimental_channel_set_error(
|
| 432 |
+
# prev_writer_ref
|
| 433 |
+
# )
|
| 434 |
+
|
| 435 |
+
def write(self, value: Any, timeout: Optional[float] = None) -> None:
|
| 436 |
+
self.ensure_registered_as_writer()
|
| 437 |
+
assert (
|
| 438 |
+
timeout is None or timeout >= 0 or timeout == -1
|
| 439 |
+
), "Timeout must be non-negative or -1."
|
| 440 |
+
# -1 means no timeout (block indefinitely)
|
| 441 |
+
timeout_ms = int(timeout * 1000) if timeout is not None else -1
|
| 442 |
+
|
| 443 |
+
if not isinstance(value, SerializedObject):
|
| 444 |
+
try:
|
| 445 |
+
serialized_value = self._worker.get_serialization_context().serialize(
|
| 446 |
+
value
|
| 447 |
+
)
|
| 448 |
+
except TypeError as e:
|
| 449 |
+
sio = io.StringIO()
|
| 450 |
+
ray.util.inspect_serializability(value, print_file=sio)
|
| 451 |
+
msg = (
|
| 452 |
+
"Could not serialize the put value "
|
| 453 |
+
f"{repr(value)}:\n"
|
| 454 |
+
f"{sio.getvalue()}"
|
| 455 |
+
)
|
| 456 |
+
raise TypeError(msg) from e
|
| 457 |
+
else:
|
| 458 |
+
serialized_value = value
|
| 459 |
+
|
| 460 |
+
start_time = time.monotonic()
|
| 461 |
+
self._resize_channel_if_needed(serialized_value, timeout_ms)
|
| 462 |
+
if timeout is not None:
|
| 463 |
+
timeout_ms -= int((time.monotonic() - start_time) * 1000)
|
| 464 |
+
timeout_ms = max(timeout_ms, 0)
|
| 465 |
+
|
| 466 |
+
self._worker.core_worker.experimental_channel_put_serialized(
|
| 467 |
+
serialized_value,
|
| 468 |
+
self._writer_ref,
|
| 469 |
+
self._num_local_readers,
|
| 470 |
+
timeout_ms,
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
def read(self, timeout: Optional[float] = None) -> Any:
|
| 474 |
+
assert (
|
| 475 |
+
timeout is None or timeout >= 0 or timeout == -1
|
| 476 |
+
), "Timeout must be non-negative or -1."
|
| 477 |
+
self.ensure_registered_as_reader()
|
| 478 |
+
|
| 479 |
+
start_time = time.monotonic()
|
| 480 |
+
ret = self._worker.get_objects(
|
| 481 |
+
[self._local_reader_ref], timeout=timeout, return_exceptions=True
|
| 482 |
+
)[0][0]
|
| 483 |
+
|
| 484 |
+
if isinstance(ret, _ResizeChannel):
|
| 485 |
+
self._node_id_to_reader_ref_info = ret._node_id_to_reader_ref_info
|
| 486 |
+
self._local_reader_ref = self._get_local_reader_ref(
|
| 487 |
+
self._node_id_to_reader_ref_info
|
| 488 |
+
)
|
| 489 |
+
# We need to register the new reader_ref.
|
| 490 |
+
self._reader_registered = False
|
| 491 |
+
self.ensure_registered_as_reader()
|
| 492 |
+
if timeout is not None:
|
| 493 |
+
timeout -= time.monotonic() - start_time
|
| 494 |
+
timeout = max(timeout, 0)
|
| 495 |
+
ret = self._worker.get_objects(
|
| 496 |
+
[self._local_reader_ref], timeout=timeout, return_exceptions=True
|
| 497 |
+
)[0][0]
|
| 498 |
+
|
| 499 |
+
return ret
|
| 500 |
+
|
| 501 |
+
def release_buffer(self, timeout: Optional[float] = None) -> None:
|
| 502 |
+
assert (
|
| 503 |
+
timeout is None or timeout >= 0 or timeout == -1
|
| 504 |
+
), "Timeout must be non-negative or -1."
|
| 505 |
+
self.ensure_registered_as_reader()
|
| 506 |
+
self._worker.get_objects(
|
| 507 |
+
[self._local_reader_ref],
|
| 508 |
+
timeout=timeout,
|
| 509 |
+
return_exceptions=True,
|
| 510 |
+
skip_deserialization=True,
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
def close(self) -> None:
|
| 514 |
+
"""
|
| 515 |
+
Close this channel by setting the error bit on both the writer_ref and the
|
| 516 |
+
reader_ref.
|
| 517 |
+
"""
|
| 518 |
+
self._worker.core_worker.experimental_channel_set_error(self._writer_ref)
|
| 519 |
+
is_local_node_reader = False
|
| 520 |
+
|
| 521 |
+
for node_id in self._node_id_to_readers.keys():
|
| 522 |
+
if self.is_local_node(node_id):
|
| 523 |
+
is_local_node_reader = True
|
| 524 |
+
if is_local_node_reader:
|
| 525 |
+
self.ensure_registered_as_reader()
|
| 526 |
+
|
| 527 |
+
for reader_ref_info in self._node_id_to_reader_ref_info.values():
|
| 528 |
+
self._worker.core_worker.experimental_channel_set_error(
|
| 529 |
+
reader_ref_info.reader_ref
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
@DeveloperAPI
|
| 534 |
+
class BufferedSharedMemoryChannel(ChannelInterface):
|
| 535 |
+
"""A channel that can be read and written by Ray processes.
|
| 536 |
+
|
| 537 |
+
It creates `num_shm_buffers` number of buffers and allows buffered read and
|
| 538 |
+
write APIs. I.e., read and write APIs are non-blocking as long as it can write to
|
| 539 |
+
next buffer or read from a next buffer. See `read` and `write` APIs for
|
| 540 |
+
more details.
|
| 541 |
+
|
| 542 |
+
Args:
|
| 543 |
+
writer: The actor that may write to the channel. None signifies the driver.
|
| 544 |
+
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
| 545 |
+
actor handle and the node ID where the actor is located. Note that currently
|
| 546 |
+
we only support this for readers on the same node as the writer.
|
| 547 |
+
num_shm_buffers: Number of shared memory buffers to read/write.
|
| 548 |
+
typ: Type information about the values passed through the channel.
|
| 549 |
+
Either an integer representing the max buffer size in bytes
|
| 550 |
+
allowed, or a SharedMemoryType.
|
| 551 |
+
"""
|
| 552 |
+
|
| 553 |
+
def __init__(
|
| 554 |
+
self,
|
| 555 |
+
writer: Optional[ray.actor.ActorHandle],
|
| 556 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 557 |
+
num_shm_buffers: int,
|
| 558 |
+
typ: Optional[Union[int, SharedMemoryType]] = None,
|
| 559 |
+
):
|
| 560 |
+
self._num_shm_buffers = num_shm_buffers
|
| 561 |
+
self._buffers = [
|
| 562 |
+
# We use Channel directly as a buffer implementation as
|
| 563 |
+
# channel only allows to have 1 shared memory buffer.
|
| 564 |
+
Channel(writer, reader_and_node_list, typ)
|
| 565 |
+
for _ in range(num_shm_buffers)
|
| 566 |
+
]
|
| 567 |
+
# The next index to write from self._buffers.
|
| 568 |
+
self._next_write_index = 0
|
| 569 |
+
# The next index to read from self._buffers.
|
| 570 |
+
self._next_read_index = 0
|
| 571 |
+
|
| 572 |
+
def ensure_registered_as_writer(self):
|
| 573 |
+
"""
|
| 574 |
+
Check whether the process is a valid writer. This method must be idempotent.
|
| 575 |
+
"""
|
| 576 |
+
for buffer in self._buffers:
|
| 577 |
+
buffer.ensure_registered_as_writer()
|
| 578 |
+
|
| 579 |
+
def ensure_registered_as_reader(self):
|
| 580 |
+
"""
|
| 581 |
+
Check whether the process is a valid reader. This method must be idempotent.
|
| 582 |
+
"""
|
| 583 |
+
for buffer in self._buffers:
|
| 584 |
+
buffer.ensure_registered_as_reader()
|
| 585 |
+
|
| 586 |
+
def write(self, value: Any, timeout: Optional[float] = None) -> None:
|
| 587 |
+
"""Write a value to a channel.
|
| 588 |
+
|
| 589 |
+
If the next buffer is available, it returns immediately. If the next
|
| 590 |
+
buffer is not read by downstream consumers, it blocks until a buffer is
|
| 591 |
+
available to write. If a buffer is not available within timeout, it raises
|
| 592 |
+
RayChannelTimeoutError.
|
| 593 |
+
"""
|
| 594 |
+
self.ensure_registered_as_writer()
|
| 595 |
+
# A single channel is not supposed to read and write at the same time.
|
| 596 |
+
assert self._next_read_index == 0
|
| 597 |
+
self._buffers[self._next_write_index].write(value, timeout)
|
| 598 |
+
self._next_write_index += 1
|
| 599 |
+
self._next_write_index %= self._num_shm_buffers
|
| 600 |
+
|
| 601 |
+
def read(self, timeout: Optional[float] = None) -> Any:
|
| 602 |
+
"""Read a value from a channel.
|
| 603 |
+
|
| 604 |
+
If the next buffer is available, it returns immediately. If the next
|
| 605 |
+
buffer is not written by an upstream producer, it blocks until a buffer is
|
| 606 |
+
available to read. If a buffer is not available within timeout, it raises
|
| 607 |
+
RayChannelTimeoutError.
|
| 608 |
+
"""
|
| 609 |
+
self.ensure_registered_as_reader()
|
| 610 |
+
# A single channel is not supposed to read and write at the same time.
|
| 611 |
+
assert self._next_write_index == 0
|
| 612 |
+
output = self._buffers[self._next_read_index].read(timeout)
|
| 613 |
+
self._next_read_index += 1
|
| 614 |
+
self._next_read_index %= self._num_shm_buffers
|
| 615 |
+
return output
|
| 616 |
+
|
| 617 |
+
def release_buffer(self, timeout: Optional[float] = None):
|
| 618 |
+
"""Release the native buffer of the channel to allow the buffer to be reused for
|
| 619 |
+
future data.
|
| 620 |
+
|
| 621 |
+
If the next buffer is available, it returns immediately. If the next
|
| 622 |
+
buffer is not written by an upstream producer, it blocks until a buffer is
|
| 623 |
+
available to be released. If a buffer is not available within timeout, it raises
|
| 624 |
+
RayChannelTimeoutError.
|
| 625 |
+
"""
|
| 626 |
+
# A single channel is not supposed to read and write at the same time.
|
| 627 |
+
assert self._next_write_index == 0
|
| 628 |
+
self._buffers[self._next_read_index].release_buffer(timeout)
|
| 629 |
+
self._next_read_index += 1
|
| 630 |
+
self._next_read_index %= self._num_shm_buffers
|
| 631 |
+
|
| 632 |
+
def close(self) -> None:
|
| 633 |
+
for buffer in self._buffers:
|
| 634 |
+
buffer.close()
|
| 635 |
+
|
| 636 |
+
@property
|
| 637 |
+
def next_write_index(self):
|
| 638 |
+
# Testing only
|
| 639 |
+
return self._next_write_index
|
| 640 |
+
|
| 641 |
+
@property
|
| 642 |
+
def next_read_index(self):
|
| 643 |
+
# Testing only
|
| 644 |
+
return self._next_read_index
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
@PublicAPI(stability="alpha")
|
| 648 |
+
class CompositeChannel(ChannelInterface):
|
| 649 |
+
"""
|
| 650 |
+
Can be used to send data to different readers via different channels.
|
| 651 |
+
For example, if the reader is in the same worker process as the writer,
|
| 652 |
+
the data can be sent via IntraProcessChannel. If the reader is in a different
|
| 653 |
+
worker process, the data can be sent via shared memory channel.
|
| 654 |
+
|
| 655 |
+
Args:
|
| 656 |
+
writer: The actor that may write to the channel. None signifies the driver.
|
| 657 |
+
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
| 658 |
+
actor handle and the node ID where the actor is located.
|
| 659 |
+
num_shm_buffers: The number of shared memory buffers per channel.
|
| 660 |
+
Note: In the case of multiple nodes, we only support 1 shared
|
| 661 |
+
memory buffer.
|
| 662 |
+
driver_actor_id: If this channel is read by a driver and that driver is an
|
| 663 |
+
actual actor, this will be the actor ID of that driver actor.
|
| 664 |
+
"""
|
| 665 |
+
|
| 666 |
+
def __init__(
|
| 667 |
+
self,
|
| 668 |
+
writer: Optional[ray.actor.ActorHandle],
|
| 669 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 670 |
+
num_shm_buffers: int,
|
| 671 |
+
driver_actor_id: Optional[str] = None,
|
| 672 |
+
_channel_dict: Optional[Dict[ray.ActorID, ChannelInterface]] = None,
|
| 673 |
+
_channels: Optional[Set[ChannelInterface]] = None,
|
| 674 |
+
_writer_registered: bool = False,
|
| 675 |
+
_reader_registered: bool = False,
|
| 676 |
+
):
|
| 677 |
+
self._writer = writer
|
| 678 |
+
self._reader_and_node_list = reader_and_node_list
|
| 679 |
+
self._num_shm_buffers = num_shm_buffers
|
| 680 |
+
self._driver_actor_id = driver_actor_id
|
| 681 |
+
self._writer_registered = _writer_registered
|
| 682 |
+
self._reader_registered = _reader_registered
|
| 683 |
+
# A dictionary that maps the actor ID to the channel object.
|
| 684 |
+
self._channel_dict = _channel_dict or {}
|
| 685 |
+
# The set of channels is a deduplicated version of the _channel_dict values.
|
| 686 |
+
self._channels = _channels or set()
|
| 687 |
+
if self._channels:
|
| 688 |
+
# This CompositeChannel object is created by deserialization.
|
| 689 |
+
# We don't need to create channels again.
|
| 690 |
+
return
|
| 691 |
+
|
| 692 |
+
(
|
| 693 |
+
remote_reader_and_node_list,
|
| 694 |
+
local_reader_and_node_list,
|
| 695 |
+
) = utils.split_readers_by_locality(self._writer, self._reader_and_node_list)
|
| 696 |
+
# There are some local readers which are the same worker process as the writer.
|
| 697 |
+
# Create a local channel for the writer and the local readers.
|
| 698 |
+
num_local_readers = len(local_reader_and_node_list)
|
| 699 |
+
if num_local_readers > 0:
|
| 700 |
+
# Use num_readers = 1 when creating the local channel,
|
| 701 |
+
# because we have channel cache to support reading
|
| 702 |
+
# from the same channel multiple times.
|
| 703 |
+
local_channel = IntraProcessChannel(num_readers=1)
|
| 704 |
+
self._channels.add(local_channel)
|
| 705 |
+
actor_id = self._get_actor_id(self._writer)
|
| 706 |
+
self._channel_dict[actor_id] = local_channel
|
| 707 |
+
# There are some remote readers which are not the same Ray actor as the writer.
|
| 708 |
+
# We create a BufferedSharedMemoryChannel for readers on the same node, and
|
| 709 |
+
# a single Channel for readers on different nodes due to
|
| 710 |
+
# https://github.com/ray-project/ray/issues/49044
|
| 711 |
+
(
|
| 712 |
+
readers_same_node,
|
| 713 |
+
readers_different_node,
|
| 714 |
+
) = utils.split_actors_by_node_locality(
|
| 715 |
+
utils.get_actor_node(self._writer), remote_reader_and_node_list
|
| 716 |
+
)
|
| 717 |
+
|
| 718 |
+
if len(readers_same_node) != 0:
|
| 719 |
+
remote_channel = BufferedSharedMemoryChannel(
|
| 720 |
+
self._writer, readers_same_node, num_shm_buffers
|
| 721 |
+
)
|
| 722 |
+
self._channels.add(remote_channel)
|
| 723 |
+
for reader, _ in readers_same_node:
|
| 724 |
+
actor_id = self._get_actor_id(reader)
|
| 725 |
+
self._channel_dict[actor_id] = remote_channel
|
| 726 |
+
|
| 727 |
+
if len(readers_different_node) != 0:
|
| 728 |
+
remote_channel = Channel(self._writer, readers_different_node)
|
| 729 |
+
self._channels.add(remote_channel)
|
| 730 |
+
for reader, _ in readers_different_node:
|
| 731 |
+
actor_id = self._get_actor_id(reader)
|
| 732 |
+
self._channel_dict[actor_id] = remote_channel
|
| 733 |
+
|
| 734 |
+
def _get_actor_id(self, reader: ray.actor.ActorHandle) -> str:
|
| 735 |
+
return reader._actor_id.hex()
|
| 736 |
+
|
| 737 |
+
def ensure_registered_as_writer(self) -> None:
|
| 738 |
+
if self._writer_registered:
|
| 739 |
+
return
|
| 740 |
+
for channel in self._channels:
|
| 741 |
+
channel.ensure_registered_as_writer()
|
| 742 |
+
self._writer_registered = True
|
| 743 |
+
|
| 744 |
+
def ensure_registered_as_reader(self) -> None:
|
| 745 |
+
if self._reader_registered:
|
| 746 |
+
return
|
| 747 |
+
for channel in self._channels:
|
| 748 |
+
channel.ensure_registered_as_reader()
|
| 749 |
+
self._reader_registered = True
|
| 750 |
+
|
| 751 |
+
def __reduce__(self):
|
| 752 |
+
return CompositeChannel, (
|
| 753 |
+
self._writer,
|
| 754 |
+
self._reader_and_node_list,
|
| 755 |
+
self._num_shm_buffers,
|
| 756 |
+
self._driver_actor_id,
|
| 757 |
+
self._channel_dict,
|
| 758 |
+
self._channels,
|
| 759 |
+
self._writer_registered,
|
| 760 |
+
self._reader_registered,
|
| 761 |
+
)
|
| 762 |
+
|
| 763 |
+
def __str__(self) -> str:
|
| 764 |
+
return (
|
| 765 |
+
"CompositeChannel(_channels="
|
| 766 |
+
f"{[str(channel) for channel in self._channels]})"
|
| 767 |
+
)
|
| 768 |
+
|
| 769 |
+
def write(self, value: Any, timeout: Optional[float] = None) -> None:
|
| 770 |
+
self.ensure_registered_as_writer()
|
| 771 |
+
for channel in self._channels:
|
| 772 |
+
channel.write(value, timeout)
|
| 773 |
+
|
| 774 |
+
def read(self, timeout: Optional[float] = None) -> Any:
|
| 775 |
+
self.ensure_registered_as_reader()
|
| 776 |
+
return self._channel_dict[self._resolve_actor_id()].read(timeout)
|
| 777 |
+
|
| 778 |
+
def release_buffer(self, timeout: Optional[float] = None):
|
| 779 |
+
self.ensure_registered_as_reader()
|
| 780 |
+
self._channel_dict[self._resolve_actor_id()].release_buffer(timeout)
|
| 781 |
+
|
| 782 |
+
def _resolve_actor_id(self) -> str:
|
| 783 |
+
actor_id = ray.get_runtime_context().get_actor_id()
|
| 784 |
+
# If actor_id is None, read was called by the driver
|
| 785 |
+
# If the driver is an actor, driver_actor_id will be set to that actor id
|
| 786 |
+
if actor_id is None or actor_id == self._driver_actor_id:
|
| 787 |
+
# Use the actor ID of the DAGDriverProxyActor.
|
| 788 |
+
# The proxy actor is always the first actor in the reader_and_node_list.
|
| 789 |
+
assert len(self._reader_and_node_list) >= 1
|
| 790 |
+
driver_proxy_actor = self._reader_and_node_list[0][0]
|
| 791 |
+
actor_id = self._get_actor_id(driver_proxy_actor)
|
| 792 |
+
return actor_id
|
| 793 |
+
|
| 794 |
+
def close(self) -> None:
|
| 795 |
+
for channel in self._channels:
|
| 796 |
+
channel.close()
|
lib/python3.12/site-packages/ray/experimental/channel/torch_tensor_accelerator_channel.py
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import logging
|
| 3 |
+
import uuid
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Type, Union
|
| 7 |
+
|
| 8 |
+
import ray
|
| 9 |
+
import ray.util.serialization
|
| 10 |
+
from ray.experimental.channel import ChannelContext, utils
|
| 11 |
+
from ray.experimental.channel.accelerator_context import (
|
| 12 |
+
AcceleratorContext,
|
| 13 |
+
is_accelerator_context_registered,
|
| 14 |
+
register_accelerator_context,
|
| 15 |
+
)
|
| 16 |
+
from ray.experimental.channel.common import ChannelInterface
|
| 17 |
+
from ray.experimental.channel.communicator import Communicator
|
| 18 |
+
from ray.experimental.channel.communicator_handle import CommunicatorHandle
|
| 19 |
+
from ray.experimental.channel.cpu_communicator import CPUCommunicator
|
| 20 |
+
from ray.experimental.channel.intra_process_channel import IntraProcessChannel
|
| 21 |
+
from ray.experimental.channel.shared_memory_channel import SharedMemoryType
|
| 22 |
+
from ray.experimental.channel.torch_tensor_type import TorchTensorType
|
| 23 |
+
from ray.util.annotations import DeveloperAPI
|
| 24 |
+
|
| 25 |
+
if TYPE_CHECKING:
|
| 26 |
+
import torch
|
| 27 |
+
|
| 28 |
+
from ray.experimental.channel.shared_memory_channel import Channel
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Logger for this module. It should be configured at the entry point
|
| 32 |
+
# into the program using Ray. Ray provides a default configuration at
|
| 33 |
+
# entry/init points.
|
| 34 |
+
logger = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class _TorchTensorMetadata:
|
| 39 |
+
"""
|
| 40 |
+
Metadata for torch.Tensors that can be sent between processes to determine
|
| 41 |
+
how large of a buffer to allocate on the receiver(s).
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
shape: Union[int, Tuple[int]]
|
| 45 |
+
dtype: "torch.dtype"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@DeveloperAPI
|
| 49 |
+
class TorchTensorAcceleratorChannel(ChannelInterface):
|
| 50 |
+
def __init__(
|
| 51 |
+
self,
|
| 52 |
+
writer: ray.actor.ActorHandle,
|
| 53 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 54 |
+
typ: "TorchTensorType",
|
| 55 |
+
driver_actor_id: str,
|
| 56 |
+
tensor_metadata_channel: Optional["Channel"] = None,
|
| 57 |
+
_cpu_data_channel: Optional["Channel"] = None,
|
| 58 |
+
_gpu_data_channel: Optional["_TorchTensorAcceleratorChannel"] = None,
|
| 59 |
+
_local_channel: Optional["IntraProcessChannel"] = None,
|
| 60 |
+
):
|
| 61 |
+
"""
|
| 62 |
+
Can be used to send accelerator tensors nested inside other data. The data is
|
| 63 |
+
sent via shared memory while the accelerator tensors are sent through a P2P
|
| 64 |
+
transport (e.g., NCCL for GPU).
|
| 65 |
+
|
| 66 |
+
NOTE: This class is currently not thread-safe because it reads and
|
| 67 |
+
writes the worker-local
|
| 68 |
+
ray.experimental.channel.serialization_context._SerializationContext
|
| 69 |
+
when serializing data.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
writer: The actor that may write to the channel. None signifies the
|
| 73 |
+
driver.
|
| 74 |
+
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
| 75 |
+
actor handle and the node ID where the actor is located.
|
| 76 |
+
typ: Type information about the values passed through the channel.
|
| 77 |
+
driver_actor_id: The actor ID of the DAGDriverProxyActor.
|
| 78 |
+
tensor_metadata_channel: A shared-memory channel for sending tensor
|
| 79 |
+
metadata.
|
| 80 |
+
_cpu_data_channel: A shared-memory channel for sending
|
| 81 |
+
non-tensor data. Its writer and readers should match the given
|
| 82 |
+
writer and readers. If None is provided, then we assume that
|
| 83 |
+
there is no CPU-specific data, i.e. the task directly returned
|
| 84 |
+
a CUDA torch.Tensor.
|
| 85 |
+
_gpu_data_channel: A channel for sending torch.Tensors via accelerator.
|
| 86 |
+
_local_channel: A channel for sending data between the writer and
|
| 87 |
+
local readers.
|
| 88 |
+
|
| 89 |
+
NOTE: `tensor_metadata_channel` will be set only for testing purposes.
|
| 90 |
+
`_cpu_data_channel` is set for testing purposes and for deserialization.
|
| 91 |
+
`_gpu_data_channel` and `_local_channel` are set only during deserialization.
|
| 92 |
+
"""
|
| 93 |
+
self._writer = writer
|
| 94 |
+
self._reader_and_node_list = reader_and_node_list
|
| 95 |
+
self._typ = typ
|
| 96 |
+
|
| 97 |
+
(
|
| 98 |
+
remote_reader_and_node_list,
|
| 99 |
+
local_reader_and_node_list,
|
| 100 |
+
) = utils.split_readers_by_locality(self._writer, self._reader_and_node_list)
|
| 101 |
+
|
| 102 |
+
num_local_readers = len(local_reader_and_node_list)
|
| 103 |
+
self._local_channel = _local_channel
|
| 104 |
+
if self._local_channel is None and num_local_readers > 0:
|
| 105 |
+
# There are some local readers which are the same worker process as
|
| 106 |
+
# the writer. Create a local channel for the writer and the local readers.
|
| 107 |
+
#
|
| 108 |
+
# Use num_readers = 1 when creating the local channel,
|
| 109 |
+
# because we have channel cache to support reading
|
| 110 |
+
# from the same channel multiple times.
|
| 111 |
+
self._local_channel = IntraProcessChannel(num_readers=1)
|
| 112 |
+
|
| 113 |
+
assert len(remote_reader_and_node_list) > 0, (
|
| 114 |
+
"All readers are from the same actor. "
|
| 115 |
+
"The TorchTensorType type hint is not needed. "
|
| 116 |
+
"No accelerator channel will be created."
|
| 117 |
+
)
|
| 118 |
+
self._gpu_data_channel = _gpu_data_channel
|
| 119 |
+
if self._gpu_data_channel is None:
|
| 120 |
+
self._gpu_data_channel: _TorchTensorAcceleratorChannel = (
|
| 121 |
+
_TorchTensorAcceleratorChannel(
|
| 122 |
+
writer,
|
| 123 |
+
remote_reader_and_node_list,
|
| 124 |
+
typ,
|
| 125 |
+
_meta_channel=tensor_metadata_channel,
|
| 126 |
+
)
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
self._cpu_data_channel: Optional["Channel"] = _cpu_data_channel
|
| 130 |
+
if self._cpu_data_channel is not None:
|
| 131 |
+
assert (
|
| 132 |
+
not self._typ.direct_return
|
| 133 |
+
), "CPU channel should be None if direct return is enabled"
|
| 134 |
+
|
| 135 |
+
if self._cpu_data_channel is None and not self._typ.direct_return:
|
| 136 |
+
# Create a CPU channel to send non-tensor data.
|
| 137 |
+
self._cpu_data_channel = SharedMemoryType().create_channel(
|
| 138 |
+
writer, remote_reader_and_node_list, driver_actor_id
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# Used for serialization.
|
| 142 |
+
self._worker = ray._private.worker.global_worker
|
| 143 |
+
self._worker.check_connected()
|
| 144 |
+
|
| 145 |
+
ctx = ChannelContext.get_current()
|
| 146 |
+
self.serialization_ctx = ctx.serialization_context
|
| 147 |
+
assert self.serialization_ctx is not None
|
| 148 |
+
|
| 149 |
+
def __reduce__(self):
|
| 150 |
+
return (
|
| 151 |
+
TorchTensorAcceleratorChannel,
|
| 152 |
+
(
|
| 153 |
+
self._writer,
|
| 154 |
+
self._reader_and_node_list,
|
| 155 |
+
self._typ,
|
| 156 |
+
# driver_actor_id and tensor_metadata_channel are used to initialize
|
| 157 |
+
# the _cpu_data_channel and _gpu_data_channel, so we don't need to
|
| 158 |
+
# pass them in here.
|
| 159 |
+
None,
|
| 160 |
+
None,
|
| 161 |
+
self._cpu_data_channel,
|
| 162 |
+
self._gpu_data_channel,
|
| 163 |
+
self._local_channel,
|
| 164 |
+
),
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
def ensure_registered_as_writer(self):
|
| 168 |
+
if self._local_channel is not None:
|
| 169 |
+
self._local_channel.ensure_registered_as_writer()
|
| 170 |
+
self._gpu_data_channel.ensure_registered_as_writer()
|
| 171 |
+
if self._cpu_data_channel is not None:
|
| 172 |
+
self._cpu_data_channel.ensure_registered_as_writer()
|
| 173 |
+
|
| 174 |
+
def ensure_registered_as_reader(self):
|
| 175 |
+
reader = utils.get_self_actor()
|
| 176 |
+
if reader == self._writer:
|
| 177 |
+
self._local_channel.ensure_registered_as_reader()
|
| 178 |
+
return
|
| 179 |
+
self._gpu_data_channel.ensure_registered_as_reader()
|
| 180 |
+
if self._cpu_data_channel is not None:
|
| 181 |
+
self._cpu_data_channel.ensure_registered_as_reader()
|
| 182 |
+
|
| 183 |
+
def _send_cpu_and_gpu_data(self, value: Any, timeout: Optional[float]):
|
| 184 |
+
self.serialization_ctx.reset_out_of_band_tensors([])
|
| 185 |
+
# All tensors found in `value` will be transferred via accelerator.
|
| 186 |
+
self.serialization_ctx.set_use_external_transport(True)
|
| 187 |
+
|
| 188 |
+
try:
|
| 189 |
+
# Serialize the data. All tensors that match our current device
|
| 190 |
+
# will be extracted into the serialization context and replaced
|
| 191 |
+
# with a placeholder.
|
| 192 |
+
cpu_data = self._worker.get_serialization_context().serialize(value)
|
| 193 |
+
except TypeError as e:
|
| 194 |
+
sio = io.StringIO()
|
| 195 |
+
ray.util.inspect_serializability(value, print_file=sio)
|
| 196 |
+
msg = (
|
| 197 |
+
"Could not serialize the put value "
|
| 198 |
+
f"{repr(value)}:\n"
|
| 199 |
+
f"{sio.getvalue()}"
|
| 200 |
+
)
|
| 201 |
+
raise TypeError(msg) from e
|
| 202 |
+
finally:
|
| 203 |
+
# Pop the tensors that were found during serialization of `value`.
|
| 204 |
+
gpu_tensors, _ = self.serialization_ctx.reset_out_of_band_tensors([])
|
| 205 |
+
# Reset the serialization method to now serialize torch.Tensors
|
| 206 |
+
# normally.
|
| 207 |
+
self.serialization_ctx.set_use_external_transport(False)
|
| 208 |
+
|
| 209 |
+
# First send the extracted tensors through a GPU-specific channel.
|
| 210 |
+
self._gpu_data_channel.write(gpu_tensors)
|
| 211 |
+
# Next send the non-tensor data through a CPU-specific channel. The
|
| 212 |
+
# data contains placeholders for the extracted tensors.
|
| 213 |
+
self._cpu_data_channel.write(cpu_data)
|
| 214 |
+
|
| 215 |
+
def write(self, value: Any, timeout: Optional[float] = None) -> None:
|
| 216 |
+
"""
|
| 217 |
+
Send a value that may contain torch.Tensors that should be sent via
|
| 218 |
+
external transport.
|
| 219 |
+
|
| 220 |
+
Case 1: Use `_local_channel` to send the data to local readers.
|
| 221 |
+
|
| 222 |
+
Case 2: Otherwise, use the following method to send the data to remote readers.
|
| 223 |
+
|
| 224 |
+
1) Serializes `value`. During serialization, all torch.Tensors that are
|
| 225 |
+
on the default device are extracted and replaced with a unique
|
| 226 |
+
placeholder. Thus, the serialized value will contain all non-tensor
|
| 227 |
+
data, and any tensors that were not on the default device (e.g., CPU
|
| 228 |
+
tensor returned by a GPU actor).
|
| 229 |
+
2) Sends extracted torch.Tensors via the tensor data channel (e.g.,
|
| 230 |
+
NCCL).
|
| 231 |
+
3) Sends the non-tensor data via the non-tensor data channel.
|
| 232 |
+
|
| 233 |
+
If static_non_tensor_data=True was specified, then we only perform step
|
| 234 |
+
(3) on the first `write` call. The reader is expected to reuse the sent
|
| 235 |
+
data for subsequent messages.
|
| 236 |
+
"""
|
| 237 |
+
self.ensure_registered_as_writer()
|
| 238 |
+
|
| 239 |
+
if self._local_channel is not None:
|
| 240 |
+
self._local_channel.write(value)
|
| 241 |
+
|
| 242 |
+
if isinstance(value, ray.exceptions.RayTaskError):
|
| 243 |
+
if self._typ.static_shape or self._typ.direct_return:
|
| 244 |
+
# Raise a fatal error to teardown the DAG.
|
| 245 |
+
# This error will also be caught from `CompiledDAGRef.get()`
|
| 246 |
+
# and raised to the user
|
| 247 |
+
# TODO(swang): Write exceptions to the tensor metadata or
|
| 248 |
+
# non-tensor data channel if it is available to make these
|
| 249 |
+
# exceptions recoverable.
|
| 250 |
+
raise value
|
| 251 |
+
|
| 252 |
+
if self._cpu_data_channel is None:
|
| 253 |
+
# Handle the case where _direct_return=True. In this case, we check
|
| 254 |
+
# that the task returned a CUDA torch.Tensor and just send it
|
| 255 |
+
# directly without trying to serialize it first.
|
| 256 |
+
import torch
|
| 257 |
+
|
| 258 |
+
# These ValueErrors will also be caught from `CompiledDAGRef.get()`
|
| 259 |
+
# and raised to the user
|
| 260 |
+
if not isinstance(value, torch.Tensor):
|
| 261 |
+
# TODO(swang): These errors are currently fatal for the DAG.
|
| 262 |
+
# This could be improved by sending the exception through the
|
| 263 |
+
# gpu_data_channel's CPU-based metadata channel, if one exists.
|
| 264 |
+
raise ValueError(
|
| 265 |
+
"Task annotated with _direct_return=True must "
|
| 266 |
+
"return a CUDA torch.Tensor, instead found value "
|
| 267 |
+
f"`{value}`. DAG will shut down."
|
| 268 |
+
)
|
| 269 |
+
elif not value.is_cuda:
|
| 270 |
+
raise ValueError(
|
| 271 |
+
"Task annotated with _direct_return=True must "
|
| 272 |
+
"return a CUDA torch.Tensor, instead found CPU tensor. "
|
| 273 |
+
"DAG will shut down."
|
| 274 |
+
)
|
| 275 |
+
self._gpu_data_channel.write([value], timeout=timeout)
|
| 276 |
+
else:
|
| 277 |
+
self._send_cpu_and_gpu_data(value, timeout)
|
| 278 |
+
|
| 279 |
+
def _recv_cpu_and_gpu_data(
|
| 280 |
+
self, tensors: List["torch.Tensor"], timeout: Optional[float] = None
|
| 281 |
+
) -> Any:
|
| 282 |
+
"""
|
| 283 |
+
Helper method to receive data that contains a mix of CPU and GPU data.
|
| 284 |
+
|
| 285 |
+
Args:
|
| 286 |
+
tensors: The GPU data. This is a list of the torch.Tensors that
|
| 287 |
+
were found in the sent data.
|
| 288 |
+
timeout: Timeout for channel receive.
|
| 289 |
+
"""
|
| 290 |
+
self.serialization_ctx.reset_out_of_band_tensors(tensors)
|
| 291 |
+
|
| 292 |
+
# Next, read and deserialize the non-tensor data. The registered custom
|
| 293 |
+
# deserializer will replace the found tensor placeholders with
|
| 294 |
+
# `tensors`.
|
| 295 |
+
data = self._cpu_data_channel.read(
|
| 296 |
+
timeout=timeout,
|
| 297 |
+
)
|
| 298 |
+
# Check that all placeholders had a corresponding tensor.
|
| 299 |
+
(
|
| 300 |
+
_,
|
| 301 |
+
deserialized_tensor_placeholders,
|
| 302 |
+
) = self.serialization_ctx.reset_out_of_band_tensors([])
|
| 303 |
+
assert deserialized_tensor_placeholders == set(range(len(tensors)))
|
| 304 |
+
|
| 305 |
+
return data
|
| 306 |
+
|
| 307 |
+
def read(self, timeout: Optional[float] = None) -> Any:
|
| 308 |
+
"""
|
| 309 |
+
Read a value that may contain torch.Tensors sent via external
|
| 310 |
+
transport.
|
| 311 |
+
|
| 312 |
+
Case 1: If the reader is a local reader and is the same actor as the writer,
|
| 313 |
+
then use the `_local_channel` to read the data.
|
| 314 |
+
|
| 315 |
+
Case 2: Otherwise, use the following method to read data from remote readers.
|
| 316 |
+
|
| 317 |
+
1) Receives torch.Tensors via the tensor data channel (e.g., NCCL).
|
| 318 |
+
2) Reads the serialized non-tensor data.
|
| 319 |
+
3) Deserializes the non-tensor data. During deserialization, replaces
|
| 320 |
+
all found placeholders with the received torch.Tensors.
|
| 321 |
+
|
| 322 |
+
If _direct_return=True was specified, then we skip step (2) and (3) and
|
| 323 |
+
directly return the data received in (1).
|
| 324 |
+
"""
|
| 325 |
+
self.ensure_registered_as_reader()
|
| 326 |
+
|
| 327 |
+
# If the reader is the same actor as the writer, then we can use the
|
| 328 |
+
# local channel to read the data.
|
| 329 |
+
reader = utils.get_self_actor()
|
| 330 |
+
if reader == self._writer:
|
| 331 |
+
assert self._local_channel is not None
|
| 332 |
+
return self._local_channel.read()
|
| 333 |
+
|
| 334 |
+
# First, read the tensor data.
|
| 335 |
+
tensors = self._gpu_data_channel.read(timeout)
|
| 336 |
+
|
| 337 |
+
if self._cpu_data_channel is None:
|
| 338 |
+
# Handle _direct_return=True. In this case, we expect to receive
|
| 339 |
+
# only one tensor, and we return it directly.
|
| 340 |
+
assert len(tensors) == 1
|
| 341 |
+
data = tensors[0]
|
| 342 |
+
else:
|
| 343 |
+
data = self._recv_cpu_and_gpu_data(tensors, timeout)
|
| 344 |
+
|
| 345 |
+
return data
|
| 346 |
+
|
| 347 |
+
def close(self) -> None:
|
| 348 |
+
self._gpu_data_channel.close()
|
| 349 |
+
if self._cpu_data_channel is not None:
|
| 350 |
+
self._cpu_data_channel.close()
|
| 351 |
+
if self._local_channel is not None:
|
| 352 |
+
self._local_channel.close()
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def _torch_tensor_allocator(
|
| 356 |
+
shape: Union[int, Tuple[int]],
|
| 357 |
+
dtype: "torch.dtype",
|
| 358 |
+
):
|
| 359 |
+
"""
|
| 360 |
+
Allocate a tensor buffer matching the given metadata.
|
| 361 |
+
"""
|
| 362 |
+
import torch
|
| 363 |
+
|
| 364 |
+
ctx = ChannelContext.get_current()
|
| 365 |
+
return torch.empty(shape, dtype=dtype, device=ctx.torch_device)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
class _TorchTensorAcceleratorChannel(ChannelInterface):
|
| 369 |
+
def __init__(
|
| 370 |
+
self,
|
| 371 |
+
writer: ray.actor.ActorHandle,
|
| 372 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 373 |
+
typ: "TorchTensorType",
|
| 374 |
+
_meta_channel: Optional["Channel"] = None,
|
| 375 |
+
):
|
| 376 |
+
"""
|
| 377 |
+
A helper channel for TorchTensorAcceleratorChannel that is used to transfer
|
| 378 |
+
lists of torch.Tensors via accelerator. This class can only transfer
|
| 379 |
+
torch.Tensors and cannot transfer other CPU data, such as Exception
|
| 380 |
+
objects or tensors nested inside of a dictionary.
|
| 381 |
+
|
| 382 |
+
Args:
|
| 383 |
+
writer: The actor that may write to the channel. None signifies the driver.
|
| 384 |
+
reader_and_node_list: A list of tuples, where each tuple contains a reader
|
| 385 |
+
actor handle and the node ID where the actor is located.
|
| 386 |
+
typ: Type information about the values passed through the channel.
|
| 387 |
+
_meta_channel: A channel used to send metadata for the tensors,
|
| 388 |
+
i.e. shape and dtype. If not provided, and if the typ does not
|
| 389 |
+
specify a static shape and dtype, then a metadata channel based
|
| 390 |
+
on shared memory will be created.
|
| 391 |
+
"""
|
| 392 |
+
import torch
|
| 393 |
+
|
| 394 |
+
self.torch: ModuleType = torch
|
| 395 |
+
|
| 396 |
+
self._writer = writer
|
| 397 |
+
self._writer_rank: Optional[int] = None
|
| 398 |
+
self._reader_and_node_list = reader_and_node_list
|
| 399 |
+
self._reader_ranks: Optional[List[int]] = None
|
| 400 |
+
self._writer_registered: bool = False
|
| 401 |
+
self._reader_registered: bool = False
|
| 402 |
+
|
| 403 |
+
ctx = ChannelContext.get_current()
|
| 404 |
+
assert isinstance(
|
| 405 |
+
typ.communicator_id, str
|
| 406 |
+
), f"accelerator group ID ({typ.communicator_id}) must be a str."
|
| 407 |
+
self._typ = typ
|
| 408 |
+
|
| 409 |
+
self._static_shape = typ.static_shape
|
| 410 |
+
|
| 411 |
+
assert self._typ.communicator_id is not None, "No accelerator group specified."
|
| 412 |
+
self._accelerator_group_id: str = self._typ.communicator_id
|
| 413 |
+
|
| 414 |
+
# If the communicators does not contain the group_id, it means the current
|
| 415 |
+
# process is the driver, and there’s no need to fetch the comm_group.
|
| 416 |
+
if self._typ.communicator_id in ctx.communicators:
|
| 417 |
+
self._accelerator_group: "Communicator" = ctx.communicators[
|
| 418 |
+
self._typ.communicator_id
|
| 419 |
+
]
|
| 420 |
+
assert (
|
| 421 |
+
self._accelerator_group is not None
|
| 422 |
+
), "ChannelContext.accelerator_group is not initialized."
|
| 423 |
+
|
| 424 |
+
self._writer_rank = self._accelerator_group.get_rank(self._writer)
|
| 425 |
+
self._reader_ranks = [
|
| 426 |
+
self._accelerator_group.get_rank(reader)
|
| 427 |
+
for reader, _ in self._reader_and_node_list
|
| 428 |
+
]
|
| 429 |
+
|
| 430 |
+
if (
|
| 431 |
+
self._writer_rank is not None
|
| 432 |
+
and self._writer_rank == self._accelerator_group.get_self_rank()
|
| 433 |
+
):
|
| 434 |
+
self._writer_registered = True
|
| 435 |
+
|
| 436 |
+
if (
|
| 437 |
+
self._reader_ranks
|
| 438 |
+
and self._accelerator_group.get_self_rank() in self._reader_ranks
|
| 439 |
+
):
|
| 440 |
+
self._reader_registered = True
|
| 441 |
+
|
| 442 |
+
# If the channel type specifies that the tensor shape is static, then the
|
| 443 |
+
# receiver can allocate buffers without needing to coordinate with the
|
| 444 |
+
# sender. We set the metadata on the first send-recv op. Thereafter,
|
| 445 |
+
# the sender must ensure that sent tensors match this metadata, and the
|
| 446 |
+
# receiver will allocate tensors with this shape.
|
| 447 |
+
self._static_tensor_metadata: Optional[List[_TorchTensorMetadata]] = None
|
| 448 |
+
self._meta_channel: Optional[Channel] = _meta_channel
|
| 449 |
+
if self._meta_channel is None and self._writer_registered:
|
| 450 |
+
# We are the writer. Therefore, we also need to allocate a metadata
|
| 451 |
+
# channel that will be used to send the shape and dtype of the
|
| 452 |
+
# tensor to the receiver(s).
|
| 453 |
+
metadata_type = SharedMemoryType()
|
| 454 |
+
self._meta_channel = metadata_type.create_channel(
|
| 455 |
+
self._writer,
|
| 456 |
+
self._reader_and_node_list,
|
| 457 |
+
None,
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
def ensure_registered_as_writer(self):
|
| 461 |
+
assert (
|
| 462 |
+
self._accelerator_group is not None
|
| 463 |
+
), "Actor is not part of an accelerator group"
|
| 464 |
+
assert self._writer_registered
|
| 465 |
+
ctx = ChannelContext.get_current()
|
| 466 |
+
assert ctx.torch_device.type != "cpu"
|
| 467 |
+
|
| 468 |
+
def ensure_registered_as_reader(self) -> bool:
|
| 469 |
+
assert (
|
| 470 |
+
self._accelerator_group is not None
|
| 471 |
+
), "Actor is not part of an accelerator group"
|
| 472 |
+
assert self._reader_registered
|
| 473 |
+
ctx = ChannelContext.get_current()
|
| 474 |
+
assert ctx.torch_device.type != "cpu"
|
| 475 |
+
|
| 476 |
+
def __reduce__(self):
|
| 477 |
+
return (
|
| 478 |
+
self.__class__,
|
| 479 |
+
(
|
| 480 |
+
self._writer,
|
| 481 |
+
self._reader_and_node_list,
|
| 482 |
+
self._typ,
|
| 483 |
+
self._meta_channel,
|
| 484 |
+
),
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
def _get_send_tensors_metadata(
|
| 488 |
+
self, tensors: List["torch.Tensor"]
|
| 489 |
+
) -> Optional[List[_TorchTensorMetadata]]:
|
| 490 |
+
"""
|
| 491 |
+
Helper method to get the metadata that should be sent to the reader so
|
| 492 |
+
that they can allocate the proper-sized buffer(s). Throws error if
|
| 493 |
+
static_shape=True was set and the given tensors do not match the
|
| 494 |
+
inferred shapes.
|
| 495 |
+
|
| 496 |
+
Returns: The metadata to send to the reader. None means that we should
|
| 497 |
+
not send any metadata message to the reader.
|
| 498 |
+
"""
|
| 499 |
+
ctx = ChannelContext.get_current()
|
| 500 |
+
|
| 501 |
+
# TODO(swang): Currently any exceptions thrown during this method are
|
| 502 |
+
# fatal for the DAG because there is no way for the receiver to receive
|
| 503 |
+
# the exception. This can be improved by sending the exception through
|
| 504 |
+
# the CPU-based non-tensor-data channel, if one exists. The tensor
|
| 505 |
+
# channel can send empty data alongside the exception to avoid hanging.
|
| 506 |
+
|
| 507 |
+
# Get the shape and dtype of each tensor to send.
|
| 508 |
+
metadata_list = []
|
| 509 |
+
for tensor in tensors:
|
| 510 |
+
# Basic type checking.
|
| 511 |
+
if not isinstance(tensor, self.torch.Tensor):
|
| 512 |
+
raise ValueError("Task must return torch.Tensors")
|
| 513 |
+
|
| 514 |
+
if tensor.device != ctx.torch_device:
|
| 515 |
+
raise ValueError(
|
| 516 |
+
f"torch.Tensor must be on the default device: {ctx.torch_device}"
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
metadata = _TorchTensorMetadata(tensor.shape, tensor.dtype)
|
| 520 |
+
metadata_list.append(metadata)
|
| 521 |
+
|
| 522 |
+
if self._static_tensor_metadata is not None:
|
| 523 |
+
if metadata_list != self._static_tensor_metadata:
|
| 524 |
+
metadata_str = [
|
| 525 |
+
f"(shape={m.shape}, dtype={m.dtype})" for m in metadata_list
|
| 526 |
+
]
|
| 527 |
+
expected_str = [
|
| 528 |
+
f"(shape={m.shape}, dtype={m.dtype})"
|
| 529 |
+
for m in self._static_tensor_metadata
|
| 530 |
+
]
|
| 531 |
+
raise ValueError(
|
| 532 |
+
"Expected torch.Tensors with shapes and dtypes: "
|
| 533 |
+
"[" + ", ".join(expected_str) + "], "
|
| 534 |
+
"found: [" + ", ".join(metadata_str) + "]. "
|
| 535 |
+
"DAG will shut down."
|
| 536 |
+
)
|
| 537 |
+
# The receiver has already determined the shape and dtype of the
|
| 538 |
+
# tensors from a previous send, so no need to send the metadata
|
| 539 |
+
# again.
|
| 540 |
+
return None
|
| 541 |
+
|
| 542 |
+
if self._static_shape:
|
| 543 |
+
# The shape and dtype is static. This is the first send op and
|
| 544 |
+
# afterwards, a ValueError will be thrown if the sent tensors do
|
| 545 |
+
# not match this metadata.
|
| 546 |
+
self._static_tensor_metadata = metadata_list
|
| 547 |
+
return metadata_list
|
| 548 |
+
|
| 549 |
+
def write(
|
| 550 |
+
self,
|
| 551 |
+
tensors: List["torch.Tensor"],
|
| 552 |
+
timeout: Optional[float] = None,
|
| 553 |
+
):
|
| 554 |
+
"""
|
| 555 |
+
Write a list of tensors via accelerator:
|
| 556 |
+
|
| 557 |
+
1) Send the tensor metadata, i.e. the shape and dtypes of all tensors
|
| 558 |
+
via the shared-memory metadata channel.
|
| 559 |
+
2) Send the tensor data via accelerator.
|
| 560 |
+
|
| 561 |
+
If static_shape=True was set, then we only perform step (1) on the
|
| 562 |
+
first message. The reader is expected to reuse the sent metadata for
|
| 563 |
+
subsequent messages.
|
| 564 |
+
"""
|
| 565 |
+
self.ensure_registered_as_writer()
|
| 566 |
+
|
| 567 |
+
import torch
|
| 568 |
+
|
| 569 |
+
for tensor in tensors:
|
| 570 |
+
assert isinstance(
|
| 571 |
+
tensor, torch.Tensor
|
| 572 |
+
), f"{tensor} must be instance of torch.Tensor"
|
| 573 |
+
|
| 574 |
+
# Send the tensors metadata so that the receiver knows what buffers to
|
| 575 |
+
# allocate.
|
| 576 |
+
metadata = self._get_send_tensors_metadata(tensors)
|
| 577 |
+
if metadata is not None:
|
| 578 |
+
self._meta_channel.write(metadata)
|
| 579 |
+
|
| 580 |
+
# NOTE(swang): We must send the metadata *before* launching the accelerator
|
| 581 |
+
# send. We are using blocking accelerator ops, so the following calls will
|
| 582 |
+
# block until the kernel has been enqueued. Also, peers must launch the
|
| 583 |
+
# kernel together before either can proceed. Therefore, we send the
|
| 584 |
+
# metadata first so that the receiver can read the metadata and then
|
| 585 |
+
# launch the same accelerator op.
|
| 586 |
+
for tensor in tensors:
|
| 587 |
+
# TODO: If there are multiple readers, can replace with a
|
| 588 |
+
# broadcast.
|
| 589 |
+
for rank in self._reader_ranks:
|
| 590 |
+
self._accelerator_group.send(tensor, rank)
|
| 591 |
+
|
| 592 |
+
def _get_recv_tensors_metadata(
|
| 593 |
+
self, timeout: Optional[float] = None
|
| 594 |
+
) -> List[_TorchTensorMetadata]:
|
| 595 |
+
"""
|
| 596 |
+
Get the shape(s) and dtype(s) of the tensors to receive from the
|
| 597 |
+
metadata channel. If static_shape=True was set, then we reuse the first
|
| 598 |
+
metadata received.
|
| 599 |
+
"""
|
| 600 |
+
if self._static_tensor_metadata is not None:
|
| 601 |
+
return self._static_tensor_metadata
|
| 602 |
+
|
| 603 |
+
meta = self._meta_channel.read(timeout)
|
| 604 |
+
|
| 605 |
+
if self._static_shape:
|
| 606 |
+
self._static_tensor_metadata = meta
|
| 607 |
+
|
| 608 |
+
return meta
|
| 609 |
+
|
| 610 |
+
def read(
|
| 611 |
+
self,
|
| 612 |
+
timeout: Optional[float] = None,
|
| 613 |
+
) -> Union["torch.Tensor", List["torch.Tensor"]]:
|
| 614 |
+
"""
|
| 615 |
+
Receive a list of tensors.
|
| 616 |
+
|
| 617 |
+
(1) Receive the tensor metadata via the shared-memory metadata channel.
|
| 618 |
+
(2) Allocate buffers on our default device according to the received
|
| 619 |
+
tensor metadata.
|
| 620 |
+
(3) Receive the tensor data via accelerator.
|
| 621 |
+
|
| 622 |
+
If static_data=True was set, then we only perform step (1) on the first
|
| 623 |
+
message. Subsequent messages reuse the same metadata.
|
| 624 |
+
|
| 625 |
+
NOTE: Currently `timeout` only applies to receiving the CPU-based
|
| 626 |
+
tensor metadata. The GPU recv may exceed the timeout without throwing
|
| 627 |
+
an error.
|
| 628 |
+
"""
|
| 629 |
+
self.ensure_registered_as_reader()
|
| 630 |
+
|
| 631 |
+
meta_list: List[_TorchTensorMetadata] = self._get_recv_tensors_metadata(timeout)
|
| 632 |
+
|
| 633 |
+
bufs: List["torch.Tensor"] = []
|
| 634 |
+
for meta in meta_list:
|
| 635 |
+
buf = self._accelerator_group.recv(
|
| 636 |
+
meta.shape, meta.dtype, self._writer_rank, _torch_tensor_allocator
|
| 637 |
+
)
|
| 638 |
+
bufs.append(buf)
|
| 639 |
+
# TODO: Sync CUDA stream after receiving all tensors, instead of after
|
| 640 |
+
# each tensor.
|
| 641 |
+
return bufs
|
| 642 |
+
|
| 643 |
+
def close(self) -> None:
|
| 644 |
+
self._meta_channel.close()
|
| 645 |
+
|
| 646 |
+
self._accelerator_group.destroy()
|
| 647 |
+
ctx = ChannelContext.get_current()
|
| 648 |
+
if self._accelerator_group_id in ctx.communicators:
|
| 649 |
+
del ctx.communicators[self._accelerator_group_id]
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
def _do_init_communicator(
|
| 653 |
+
self,
|
| 654 |
+
group_id,
|
| 655 |
+
world_size,
|
| 656 |
+
comm_id,
|
| 657 |
+
rank,
|
| 658 |
+
actor_handles,
|
| 659 |
+
use_communication_streams,
|
| 660 |
+
custom_communicator: Optional[Communicator] = None,
|
| 661 |
+
):
|
| 662 |
+
if not custom_communicator:
|
| 663 |
+
assert (
|
| 664 |
+
AcceleratorContext.get().accelerator_count > 0
|
| 665 |
+
), "Actors participating in Communication group must have at least one Accelerator assigned"
|
| 666 |
+
|
| 667 |
+
ctx = ChannelContext.get_current()
|
| 668 |
+
if custom_communicator is not None:
|
| 669 |
+
custom_communicator.initialize(rank)
|
| 670 |
+
ctx.communicators[group_id] = custom_communicator
|
| 671 |
+
else:
|
| 672 |
+
# default to CommGroup
|
| 673 |
+
ctx.communicators[group_id] = AcceleratorContext.get().create_communicator(
|
| 674 |
+
world_size,
|
| 675 |
+
comm_id,
|
| 676 |
+
rank,
|
| 677 |
+
actor_handles,
|
| 678 |
+
AcceleratorContext.get().current_stream(),
|
| 679 |
+
use_communication_streams,
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
def _do_destroy_communicator(self, group_id):
|
| 684 |
+
ctx = ChannelContext.get_current()
|
| 685 |
+
if group_id not in ctx.communicators:
|
| 686 |
+
return
|
| 687 |
+
ctx.communicators[group_id].destroy()
|
| 688 |
+
|
| 689 |
+
# Keep the communicator group in the map after destruction in case there is
|
| 690 |
+
# still a task loop running.
|
| 691 |
+
|
| 692 |
+
|
| 693 |
+
def _do_check_has_accelerators(self) -> str:
|
| 694 |
+
return AcceleratorContext.get().accelerator_count > 0
|
| 695 |
+
|
| 696 |
+
|
| 697 |
+
def do_register_accelerator_context(self, name: str, communicator: Type[Communicator]):
|
| 698 |
+
register_accelerator_context(name, communicator)
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
def _do_get_unique_communication_id(self) -> bool:
|
| 702 |
+
return AcceleratorContext.get().generate_communicator_id()
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
def _get_ranks(
|
| 706 |
+
actors: List[ray.actor.ActorHandle], custom_comm_group: Optional[Communicator]
|
| 707 |
+
) -> List[int]:
|
| 708 |
+
"""
|
| 709 |
+
Get ranks for the communicator group to use. If custom_comm_group is specified,
|
| 710 |
+
return the ranks of the actors in the custom communicator group, in the same
|
| 711 |
+
order of the actors; otherwise, return list(range(len(actors))).
|
| 712 |
+
|
| 713 |
+
Args:
|
| 714 |
+
actors: A list of actors that participate in the communicator group.
|
| 715 |
+
custom_comm_group: The custom communicator group to use.
|
| 716 |
+
"""
|
| 717 |
+
if custom_comm_group is None:
|
| 718 |
+
return list(range(len(actors)))
|
| 719 |
+
|
| 720 |
+
assert len(actors) == custom_comm_group.get_world_size(), (
|
| 721 |
+
"The world size of the custom communicator group does not match the "
|
| 722 |
+
"number of actors."
|
| 723 |
+
)
|
| 724 |
+
ranks = []
|
| 725 |
+
for actor in actors:
|
| 726 |
+
rank = custom_comm_group.get_rank(actor)
|
| 727 |
+
assert rank not in ranks, "Duplicate rank in custom communicator group"
|
| 728 |
+
ranks.append(rank)
|
| 729 |
+
assert custom_comm_group.get_world_size() == len(actors), (
|
| 730 |
+
"The world size of the custom communicator group "
|
| 731 |
+
f"({custom_comm_group.get_world_size()}) "
|
| 732 |
+
"does not match the number of actors "
|
| 733 |
+
f"({len(actors)})."
|
| 734 |
+
)
|
| 735 |
+
return ranks
|
| 736 |
+
|
| 737 |
+
|
| 738 |
+
def _init_communicator(
|
| 739 |
+
actors: List[ray.actor.ActorHandle],
|
| 740 |
+
custom_communicator: Optional[Communicator] = None,
|
| 741 |
+
use_communication_streams: bool = False,
|
| 742 |
+
accelerator_module_name: Optional[str] = None,
|
| 743 |
+
accelerator_communicator_cls: Optional[Type[Communicator]] = None,
|
| 744 |
+
) -> str:
|
| 745 |
+
"""
|
| 746 |
+
Initialize a communicator group with the given actors. If a custom communicator
|
| 747 |
+
group is provided, then it will be used, otherwise a new communicator group
|
| 748 |
+
will be created.
|
| 749 |
+
|
| 750 |
+
Args:
|
| 751 |
+
actors: A list of actors that participate in the communicator group.
|
| 752 |
+
custom_communicator: A custom communicator group to initialize.
|
| 753 |
+
use_communication_streams: Whether to use dedicated send and recv
|
| 754 |
+
streams for communication. If True, communication and computation
|
| 755 |
+
can be overlapped to improve performance.
|
| 756 |
+
accelerator_module_name: Optional name of the accelerator module to use.
|
| 757 |
+
accelerator_communicator_cls: Optional communicator class for the accelerator.
|
| 758 |
+
"""
|
| 759 |
+
ctx = ChannelContext.get_current()
|
| 760 |
+
|
| 761 |
+
is_cpu_communicator = custom_communicator and isinstance(
|
| 762 |
+
custom_communicator, CPUCommunicator
|
| 763 |
+
)
|
| 764 |
+
|
| 765 |
+
# Register accelerator context for all actors if accelerator is not default
|
| 766 |
+
if accelerator_module_name and accelerator_communicator_cls:
|
| 767 |
+
if is_accelerator_context_registered():
|
| 768 |
+
ray.get(
|
| 769 |
+
[
|
| 770 |
+
actor.__ray_call__.remote(
|
| 771 |
+
do_register_accelerator_context,
|
| 772 |
+
accelerator_module_name,
|
| 773 |
+
accelerator_communicator_cls,
|
| 774 |
+
)
|
| 775 |
+
for actor in actors
|
| 776 |
+
]
|
| 777 |
+
)
|
| 778 |
+
|
| 779 |
+
has_accelerators = ray.get(
|
| 780 |
+
[actor.__ray_call__.remote(_do_check_has_accelerators) for actor in actors]
|
| 781 |
+
)
|
| 782 |
+
for has_accelerator, actor in zip(has_accelerators, actors):
|
| 783 |
+
if not has_accelerator and not is_cpu_communicator:
|
| 784 |
+
raise ValueError(
|
| 785 |
+
f"Actor {actor} returns a tensor with type hint "
|
| 786 |
+
'TorchTensor(transport="accelerator") or '
|
| 787 |
+
"TorchTensor(transport=accelerator_group_handle) "
|
| 788 |
+
"but actor does not have an accelerator assigned by Ray."
|
| 789 |
+
)
|
| 790 |
+
|
| 791 |
+
actor_ids = {actor._ray_actor_id for actor in actors}
|
| 792 |
+
assert len(actor_ids) == len(actors), "Actors must be unique"
|
| 793 |
+
|
| 794 |
+
# Allocate a communicator ID on one of the actors that will participate in
|
| 795 |
+
# the group. This is in case the driver is not on the same node as one of
|
| 796 |
+
# the communicator actors.
|
| 797 |
+
comm_id = ray.get(actors[0].__ray_call__.remote(_do_get_unique_communication_id))
|
| 798 |
+
|
| 799 |
+
# Used to uniquely identify this communicator group.
|
| 800 |
+
group_id = str(uuid.uuid4())
|
| 801 |
+
|
| 802 |
+
if custom_communicator is not None:
|
| 803 |
+
logger.info(
|
| 804 |
+
f"Initializing custom communicator group {group_id} on actors: {actors}"
|
| 805 |
+
)
|
| 806 |
+
else:
|
| 807 |
+
logger.info(f"Creating communicator group {group_id} on actors: {actors}")
|
| 808 |
+
|
| 809 |
+
world_size = len(actors)
|
| 810 |
+
ranks = _get_ranks(actors, custom_communicator)
|
| 811 |
+
init_tasks = [
|
| 812 |
+
actor.__ray_call__.remote(
|
| 813 |
+
_do_init_communicator,
|
| 814 |
+
group_id,
|
| 815 |
+
world_size,
|
| 816 |
+
comm_id,
|
| 817 |
+
rank,
|
| 818 |
+
actors,
|
| 819 |
+
use_communication_streams,
|
| 820 |
+
custom_communicator,
|
| 821 |
+
)
|
| 822 |
+
for rank, actor in zip(ranks, actors)
|
| 823 |
+
]
|
| 824 |
+
try:
|
| 825 |
+
ray.get(init_tasks, timeout=30)
|
| 826 |
+
except ray.exceptions.GetTimeoutError:
|
| 827 |
+
logger.warning(
|
| 828 |
+
"Communicator group creation not done after 30s. communicator group"
|
| 829 |
+
"creation may be hung."
|
| 830 |
+
)
|
| 831 |
+
ray.get(init_tasks)
|
| 832 |
+
|
| 833 |
+
logger.info("Communicator group initialized.")
|
| 834 |
+
|
| 835 |
+
if custom_communicator is not None:
|
| 836 |
+
ctx.communicator_handles[group_id] = CommunicatorHandle(
|
| 837 |
+
actor_handles=custom_communicator.get_actor_handles(),
|
| 838 |
+
)
|
| 839 |
+
else:
|
| 840 |
+
ctx.communicator_handles[group_id] = CommunicatorHandle(
|
| 841 |
+
actor_handles=actors,
|
| 842 |
+
)
|
| 843 |
+
|
| 844 |
+
return group_id
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
def _destroy_communicator(group_id: str) -> None:
|
| 848 |
+
"""
|
| 849 |
+
Destroy the communicator group with the given ID.
|
| 850 |
+
"""
|
| 851 |
+
ctx = ChannelContext.get_current()
|
| 852 |
+
if group_id not in ctx.communicator_handles:
|
| 853 |
+
return
|
| 854 |
+
|
| 855 |
+
group = ctx.communicator_handles[group_id]
|
| 856 |
+
actors = group.get_actor_handles()
|
| 857 |
+
destroy_tasks = [
|
| 858 |
+
actor.__ray_call__.remote(
|
| 859 |
+
_do_destroy_communicator,
|
| 860 |
+
group_id,
|
| 861 |
+
)
|
| 862 |
+
for actor in actors
|
| 863 |
+
]
|
| 864 |
+
|
| 865 |
+
_, unready = ray.wait(destroy_tasks, timeout=30, num_returns=len(destroy_tasks))
|
| 866 |
+
if unready:
|
| 867 |
+
logger.warning(
|
| 868 |
+
"Communicator group destruction not done after 30s. Communicator"
|
| 869 |
+
"group destruction may be hung."
|
| 870 |
+
)
|
| 871 |
+
|
| 872 |
+
del ctx.communicator_handles[group_id]
|
lib/python3.12/site-packages/ray/experimental/channel/torch_tensor_type.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
| 3 |
+
|
| 4 |
+
import ray
|
| 5 |
+
from ray.experimental.channel import ChannelContext, ChannelOutputType
|
| 6 |
+
from ray.experimental.channel.communicator import Communicator
|
| 7 |
+
from ray.experimental.channel.shared_memory_channel import SharedMemoryType
|
| 8 |
+
from ray.experimental.util.types import Device
|
| 9 |
+
from ray.util.annotations import PublicAPI
|
| 10 |
+
|
| 11 |
+
if TYPE_CHECKING:
|
| 12 |
+
from ray.experimental.channel.shared_memory_channel import Channel
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@PublicAPI(stability="alpha")
|
| 18 |
+
class TorchTensorType(ChannelOutputType):
|
| 19 |
+
AUTO = "auto"
|
| 20 |
+
CPU = "cpu"
|
| 21 |
+
ACCELERATOR = "accelerator"
|
| 22 |
+
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
transport: Optional[Union[str, Communicator]] = AUTO,
|
| 26 |
+
device: Device = Device.DEFAULT,
|
| 27 |
+
_static_shape: bool = False,
|
| 28 |
+
_direct_return: Optional[bool] = False,
|
| 29 |
+
):
|
| 30 |
+
"""
|
| 31 |
+
A type hint that can be used to annotate DAG nodes that return a
|
| 32 |
+
torch.Tensor.
|
| 33 |
+
|
| 34 |
+
NOTE: Use of this type in the DAG will register a custom serializer for
|
| 35 |
+
torch.Tensor that moves the tensor to the correct device on the
|
| 36 |
+
receiver. If you are using ray.cloudpickle to serialize objects and you
|
| 37 |
+
do not want this behavior, deregister the custom serializer using
|
| 38 |
+
ray.util.serialization.deregister_serializer(torch.Tensor).
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
transport: "auto" (default) means that tensors will be passed via
|
| 42 |
+
host memory, using numpy as the serialization format. Pass
|
| 43 |
+
TorchTensorType.ACCELERATOR or "accelerator" to use accelerator
|
| 44 |
+
instead, avoiding the host memory copy.
|
| 45 |
+
device: Target device for tensor transport. Options:
|
| 46 |
+
- "default": Retains the same device type as the sender.
|
| 47 |
+
- "cpu": Moves tensor to CPU on the receiver. Not compatible
|
| 48 |
+
with accelerator transport.
|
| 49 |
+
- "gpu" or "cuda": Moves tensor to GPU on the receiver.
|
| 50 |
+
_static_shape: A hint indicating whether the shape(s) and dtype(s)
|
| 51 |
+
of tensor(s) contained in this value always remain the same
|
| 52 |
+
across different executions of the DAG.
|
| 53 |
+
_direct_return: Whether the tensor is sent directly or inside of
|
| 54 |
+
other data. If a non-default `transport` is used, this allows
|
| 55 |
+
the sender and receiver to eliminate performance overhead from
|
| 56 |
+
an additional data transfer.
|
| 57 |
+
|
| 58 |
+
NOTE: Setting static_shape=True and _direct_return=True can improve
|
| 59 |
+
performance if a non-default transport is used. However, if either flag
|
| 60 |
+
is set, then the user must ensure that the condition is met.
|
| 61 |
+
|
| 62 |
+
If using this type as a Compiled Graph annotation, an exception will
|
| 63 |
+
be thrown in the following cases, and the DAG will be torn down. To
|
| 64 |
+
continue execution, a new DAG must be created:
|
| 65 |
+
1. If _static_shape=True, and the found tensors don't match the
|
| 66 |
+
previous shape or dtype(s).
|
| 67 |
+
2. If _direct_return=True, and the returned value is not a
|
| 68 |
+
torch.Tensor.
|
| 69 |
+
"""
|
| 70 |
+
super().__init__()
|
| 71 |
+
|
| 72 |
+
self._device = device
|
| 73 |
+
self._static_shape = _static_shape
|
| 74 |
+
self._direct_return = _direct_return
|
| 75 |
+
|
| 76 |
+
self._communicator: Optional[Communicator] = None
|
| 77 |
+
if isinstance(transport, Communicator):
|
| 78 |
+
self._communicator = transport
|
| 79 |
+
transport = transport.get_transport_name()
|
| 80 |
+
|
| 81 |
+
if transport not in [self.AUTO, self.CPU, self.ACCELERATOR]:
|
| 82 |
+
raise ValueError(
|
| 83 |
+
"`transport` must be TorchTensorType.AUTO, TorchTensorType.ACCELERATOR "
|
| 84 |
+
"or TorchTensorType.CPU"
|
| 85 |
+
)
|
| 86 |
+
if device == Device.CPU and transport == self.ACCELERATOR:
|
| 87 |
+
raise ValueError(
|
| 88 |
+
"accelerator transport is not supported with CPU target device."
|
| 89 |
+
)
|
| 90 |
+
self.transport = transport
|
| 91 |
+
|
| 92 |
+
self._communicator_id: Optional[str] = None
|
| 93 |
+
|
| 94 |
+
if self._static_shape and self.transport == self.AUTO:
|
| 95 |
+
logger.info(
|
| 96 |
+
"TorchTensorType(_static_shape=True) has no effect when "
|
| 97 |
+
"`transport` is TorchTensorType.AUTO (default)."
|
| 98 |
+
)
|
| 99 |
+
if self._direct_return and self.transport == self.AUTO:
|
| 100 |
+
logger.info(
|
| 101 |
+
"TorchTensorType(_direct_return=True) has no effect when "
|
| 102 |
+
"`transport` is TorchTensorType.AUTO (default)."
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
@property
|
| 106 |
+
def device(self) -> Device:
|
| 107 |
+
return self._device
|
| 108 |
+
|
| 109 |
+
@property
|
| 110 |
+
def static_shape(self):
|
| 111 |
+
return self._static_shape
|
| 112 |
+
|
| 113 |
+
@property
|
| 114 |
+
def direct_return(self):
|
| 115 |
+
return self._direct_return
|
| 116 |
+
|
| 117 |
+
def register_custom_serializer(self) -> None:
|
| 118 |
+
super().register_custom_serializer()
|
| 119 |
+
|
| 120 |
+
import torch
|
| 121 |
+
|
| 122 |
+
def serialize(t):
|
| 123 |
+
ctx = ChannelContext.get_current()
|
| 124 |
+
return ctx.serialization_context.serialize_tensor(t)
|
| 125 |
+
|
| 126 |
+
def deserialize(b):
|
| 127 |
+
ctx = ChannelContext.get_current()
|
| 128 |
+
return ctx.serialization_context.deserialize_tensor(b, self.device)
|
| 129 |
+
|
| 130 |
+
ray.util.serialization.register_serializer(
|
| 131 |
+
torch.Tensor,
|
| 132 |
+
serializer=serialize,
|
| 133 |
+
deserializer=deserialize,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
def create_channel(
|
| 137 |
+
self,
|
| 138 |
+
writer: Optional["ray.actor.ActorHandle"],
|
| 139 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 140 |
+
driver_actor_id: Optional[str] = None,
|
| 141 |
+
_cpu_data_channel: Optional["Channel"] = None,
|
| 142 |
+
_tensor_metadata_channel: Optional["Channel"] = None,
|
| 143 |
+
) -> type:
|
| 144 |
+
if self.requires_accelerator():
|
| 145 |
+
from ray.experimental.channel.torch_tensor_accelerator_channel import (
|
| 146 |
+
TorchTensorAcceleratorChannel,
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
return TorchTensorAcceleratorChannel(
|
| 150 |
+
writer,
|
| 151 |
+
reader_and_node_list,
|
| 152 |
+
self,
|
| 153 |
+
driver_actor_id,
|
| 154 |
+
_tensor_metadata_channel,
|
| 155 |
+
_cpu_data_channel,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
# Data does not require accelerator. Transfer via host memory using a
|
| 159 |
+
# shared-memory channel.
|
| 160 |
+
# TODO(swang): Allow the initial max buffer size to be overridden.
|
| 161 |
+
typ = SharedMemoryType()
|
| 162 |
+
return typ.create_channel(writer, reader_and_node_list, driver_actor_id)
|
| 163 |
+
|
| 164 |
+
def requires_accelerator(self) -> bool:
|
| 165 |
+
return self.transport == self.ACCELERATOR
|
| 166 |
+
|
| 167 |
+
def get_custom_communicator(self) -> Optional[Communicator]:
|
| 168 |
+
"""
|
| 169 |
+
Return the communicator group if one is specified.
|
| 170 |
+
"""
|
| 171 |
+
return self._communicator
|
| 172 |
+
|
| 173 |
+
def set_communicator_id(self, group_id: str) -> None:
|
| 174 |
+
self._communicator_id = group_id
|
| 175 |
+
|
| 176 |
+
@property
|
| 177 |
+
def communicator_id(self) -> Optional[str]:
|
| 178 |
+
return self._communicator_id
|
| 179 |
+
|
| 180 |
+
def __deepcopy__(self, memo):
|
| 181 |
+
"""
|
| 182 |
+
Deep copy all the fields except for the communicator group. The communicator
|
| 183 |
+
group should not be deep copied because it can be shared across `TorchTensorType`
|
| 184 |
+
instances.
|
| 185 |
+
"""
|
| 186 |
+
copy = TorchTensorType(
|
| 187 |
+
transport=self.transport,
|
| 188 |
+
_static_shape=self._static_shape,
|
| 189 |
+
_direct_return=self._direct_return,
|
| 190 |
+
)
|
| 191 |
+
copy._communicator = self._communicator
|
| 192 |
+
copy._communicator_id = self._communicator_id
|
| 193 |
+
return copy
|
lib/python3.12/site-packages/ray/experimental/channel/utils.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional, Tuple
|
| 2 |
+
|
| 3 |
+
import ray
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def get_self_actor() -> Optional["ray.actor.ActorHandle"]:
|
| 7 |
+
"""
|
| 8 |
+
Get the current actor handle in this worker.
|
| 9 |
+
If this is called in a driver process, it will return None.
|
| 10 |
+
"""
|
| 11 |
+
try:
|
| 12 |
+
return ray.get_runtime_context().current_actor
|
| 13 |
+
except RuntimeError:
|
| 14 |
+
return None
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def split_readers_by_locality(
|
| 18 |
+
writer: "ray.actor.ActorHandle",
|
| 19 |
+
reader_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 20 |
+
) -> Tuple[
|
| 21 |
+
List[Tuple["ray.actor.ActorHandle", str]], List[Tuple["ray.actor.ActorHandle", str]]
|
| 22 |
+
]:
|
| 23 |
+
"""Split readers into remote and local readers based on writer.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
writer: The actor handle of the writer
|
| 27 |
+
reader_and_node_list: List of (reader, node) tuples
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Tuple containing:
|
| 31 |
+
- List of (reader, node) tuples for remote readers
|
| 32 |
+
- List of (reader, node) tuples for local readers
|
| 33 |
+
"""
|
| 34 |
+
remote_readers = []
|
| 35 |
+
local_readers = []
|
| 36 |
+
|
| 37 |
+
for reader, node in reader_and_node_list:
|
| 38 |
+
if reader != writer:
|
| 39 |
+
remote_readers.append((reader, node))
|
| 40 |
+
else:
|
| 41 |
+
local_readers.append((reader, node))
|
| 42 |
+
|
| 43 |
+
return remote_readers, local_readers
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def split_actors_by_node_locality(
|
| 47 |
+
node: str,
|
| 48 |
+
actor_and_node_list: List[Tuple["ray.actor.ActorHandle", str]],
|
| 49 |
+
) -> Tuple[
|
| 50 |
+
List[Tuple["ray.actor.ActorHandle", str]], List[Tuple["ray.actor.ActorHandle", str]]
|
| 51 |
+
]:
|
| 52 |
+
"""Split actors into remote and local actors based on node. The local actors will be
|
| 53 |
+
on the same node as the given node. The remote actors will be on a different node.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
writer_node: The node of the writer
|
| 57 |
+
actor_and_node_list: List of (actor, node) tuples
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
Tuple containing:
|
| 61 |
+
- List of (actor, node) tuples for actors on the same node
|
| 62 |
+
- List of (actor, node) tuples for actors on a different node
|
| 63 |
+
"""
|
| 64 |
+
actors_on_same_node = []
|
| 65 |
+
actors_on_different_node = []
|
| 66 |
+
|
| 67 |
+
for actor, actor_node in actor_and_node_list:
|
| 68 |
+
if node == actor_node:
|
| 69 |
+
actors_on_same_node.append((actor, actor_node))
|
| 70 |
+
else:
|
| 71 |
+
actors_on_different_node.append((actor, actor_node))
|
| 72 |
+
|
| 73 |
+
return actors_on_same_node, actors_on_different_node
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def get_actor_node(actor: Optional["ray.actor.ActorHandle"]) -> str:
|
| 77 |
+
"""Get the node of the actor.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
actor: The actor handle of the actor
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
The node of the actor
|
| 84 |
+
"""
|
| 85 |
+
if actor is None or actor == ray.get_runtime_context().current_actor:
|
| 86 |
+
return ray.get_runtime_context().get_node_id()
|
| 87 |
+
else:
|
| 88 |
+
return ray.get(
|
| 89 |
+
actor.__ray_call__.remote(
|
| 90 |
+
lambda self: ray.get_runtime_context().get_node_id()
|
| 91 |
+
)
|
| 92 |
+
)
|
lib/python3.12/site-packages/ray/experimental/job/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/ray/experimental/job/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (199 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/job/example_job/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/ray/experimental/job/example_job/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (211 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/job/example_job/__pycache__/demo_script.cpython-312.pyc
ADDED
|
Binary file (3.79 kB). View file
|
|
|
lib/python3.12/site-packages/ray/experimental/job/example_job/demo_script.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Regular ray application that user wrote and runs on local cluster.
|
| 2 |
+
# intermediate status are dumped to GCS
|
| 3 |
+
import argparse
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
import ray
|
| 7 |
+
import ray.experimental.internal_kv as ray_kv
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@ray.remote
|
| 11 |
+
class StepActor:
|
| 12 |
+
def __init__(self, interval_s=1, total_steps=3):
|
| 13 |
+
self.interval_s = interval_s
|
| 14 |
+
self.stopped = False
|
| 15 |
+
self.current_step = 1
|
| 16 |
+
self.total_steps = total_steps
|
| 17 |
+
|
| 18 |
+
worker = ray._private.worker.global_worker
|
| 19 |
+
worker_id = worker.core_worker.get_actor_id()
|
| 20 |
+
ray_kv._internal_kv_put(f"JOB:{worker_id}", self.current_step, overwrite=True)
|
| 21 |
+
|
| 22 |
+
def run(self):
|
| 23 |
+
worker = ray._private.worker.global_worker
|
| 24 |
+
worker_id = worker.core_worker.get_actor_id()
|
| 25 |
+
|
| 26 |
+
while self.current_step <= self.total_steps:
|
| 27 |
+
if not self.stopped:
|
| 28 |
+
print(
|
| 29 |
+
f"Sleeping {self.interval_s} secs to executing "
|
| 30 |
+
f"step {self.current_step}"
|
| 31 |
+
)
|
| 32 |
+
time.sleep(self.interval_s)
|
| 33 |
+
self.current_step += 1
|
| 34 |
+
ray_kv._internal_kv_put(
|
| 35 |
+
f"JOB:{worker_id}", self.current_step, overwrite=True
|
| 36 |
+
)
|
| 37 |
+
else:
|
| 38 |
+
print("Stop called or reached final step.")
|
| 39 |
+
break
|
| 40 |
+
|
| 41 |
+
self.stopped = True
|
| 42 |
+
ray_kv._internal_kv_put(f"JOB:{worker_id}", "DONE", overwrite=True)
|
| 43 |
+
return "DONE"
|
| 44 |
+
|
| 45 |
+
def get_step(self):
|
| 46 |
+
return self.current_step
|
| 47 |
+
|
| 48 |
+
def stop(self):
|
| 49 |
+
self.stopped = True
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
parser = argparse.ArgumentParser()
|
| 54 |
+
parser.add_argument(
|
| 55 |
+
"--interval-s",
|
| 56 |
+
required=False,
|
| 57 |
+
type=int,
|
| 58 |
+
default=1,
|
| 59 |
+
help="Address to use to connect to Ray",
|
| 60 |
+
)
|
| 61 |
+
parser.add_argument(
|
| 62 |
+
"--total-steps",
|
| 63 |
+
required=False,
|
| 64 |
+
type=int,
|
| 65 |
+
default=3,
|
| 66 |
+
help="Password for connecting to Redis",
|
| 67 |
+
)
|
| 68 |
+
args, _ = parser.parse_known_args()
|
| 69 |
+
|
| 70 |
+
ray.init()
|
| 71 |
+
step_actor = StepActor.remote(
|
| 72 |
+
interval_s=args.interval_s, total_steps=args.total_steps
|
| 73 |
+
)
|
| 74 |
+
ref = step_actor.run.remote()
|
| 75 |
+
print(ray.get([ref]))
|
| 76 |
+
job_key = ray_kv._internal_kv_list("JOB:")[0]
|
| 77 |
+
print(f"{job_key}, {ray_kv._internal_kv_get(job_key)}")
|
lib/python3.12/site-packages/ray/experimental/multiprocessing/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from multiprocessing import TimeoutError
|
| 2 |
+
|
| 3 |
+
from .pool import Pool
|
| 4 |
+
|
| 5 |
+
__all__ = ["Pool", "TimeoutError"]
|
lib/python3.12/site-packages/ray/experimental/multiprocessing/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (342 Bytes). View file
|
|
|