ZhengyangZhang commited on
Commit
f9c25e8
·
verified ·
1 Parent(s): 192b59e

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/ray/experimental/__pycache__/__init__.cpython-312.pyc +0 -0
  2. lib/python3.12/site-packages/ray/experimental/__pycache__/compiled_dag_ref.cpython-312.pyc +0 -0
  3. lib/python3.12/site-packages/ray/experimental/__pycache__/dynamic_resources.cpython-312.pyc +0 -0
  4. lib/python3.12/site-packages/ray/experimental/__pycache__/gradio_utils.cpython-312.pyc +0 -0
  5. lib/python3.12/site-packages/ray/experimental/__pycache__/internal_kv.cpython-312.pyc +0 -0
  6. lib/python3.12/site-packages/ray/experimental/__pycache__/locations.cpython-312.pyc +0 -0
  7. lib/python3.12/site-packages/ray/experimental/__pycache__/queue.cpython-312.pyc +0 -0
  8. lib/python3.12/site-packages/ray/experimental/__pycache__/shuffle.cpython-312.pyc +0 -0
  9. lib/python3.12/site-packages/ray/experimental/__pycache__/tf_utils.cpython-312.pyc +0 -0
  10. lib/python3.12/site-packages/ray/experimental/__pycache__/tqdm_ray.cpython-312.pyc +0 -0
  11. lib/python3.12/site-packages/ray/experimental/channel/__init__.py +43 -0
  12. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/__init__.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/accelerator_context.cpython-312.pyc +0 -0
  14. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/auto_transport_type.cpython-312.pyc +0 -0
  15. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/cached_channel.cpython-312.pyc +0 -0
  16. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/common.cpython-312.pyc +0 -0
  17. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/communicator.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/communicator_handle.cpython-312.pyc +0 -0
  19. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/conftest.cpython-312.pyc +0 -0
  20. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/cpu_communicator.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/intra_process_channel.cpython-312.pyc +0 -0
  22. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/nccl_group.cpython-312.pyc +0 -0
  23. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/serialization_context.cpython-312.pyc +0 -0
  24. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/shared_memory_channel.cpython-312.pyc +0 -0
  25. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/torch_tensor_accelerator_channel.cpython-312.pyc +0 -0
  26. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/torch_tensor_type.cpython-312.pyc +0 -0
  27. lib/python3.12/site-packages/ray/experimental/channel/__pycache__/utils.cpython-312.pyc +0 -0
  28. lib/python3.12/site-packages/ray/experimental/channel/accelerator_context.py +246 -0
  29. lib/python3.12/site-packages/ray/experimental/channel/auto_transport_type.py +182 -0
  30. lib/python3.12/site-packages/ray/experimental/channel/cached_channel.py +111 -0
  31. lib/python3.12/site-packages/ray/experimental/channel/common.py +681 -0
  32. lib/python3.12/site-packages/ray/experimental/channel/communicator.py +199 -0
  33. lib/python3.12/site-packages/ray/experimental/channel/communicator_handle.py +28 -0
  34. lib/python3.12/site-packages/ray/experimental/channel/conftest.py +185 -0
  35. lib/python3.12/site-packages/ray/experimental/channel/cpu_communicator.py +207 -0
  36. lib/python3.12/site-packages/ray/experimental/channel/intra_process_channel.py +70 -0
  37. lib/python3.12/site-packages/ray/experimental/channel/nccl_group.py +374 -0
  38. lib/python3.12/site-packages/ray/experimental/channel/serialization_context.py +231 -0
  39. lib/python3.12/site-packages/ray/experimental/channel/shared_memory_channel.py +796 -0
  40. lib/python3.12/site-packages/ray/experimental/channel/torch_tensor_accelerator_channel.py +872 -0
  41. lib/python3.12/site-packages/ray/experimental/channel/torch_tensor_type.py +193 -0
  42. lib/python3.12/site-packages/ray/experimental/channel/utils.py +92 -0
  43. lib/python3.12/site-packages/ray/experimental/job/__init__.py +0 -0
  44. lib/python3.12/site-packages/ray/experimental/job/__pycache__/__init__.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/ray/experimental/job/example_job/__init__.py +0 -0
  46. lib/python3.12/site-packages/ray/experimental/job/example_job/__pycache__/__init__.cpython-312.pyc +0 -0
  47. lib/python3.12/site-packages/ray/experimental/job/example_job/__pycache__/demo_script.cpython-312.pyc +0 -0
  48. lib/python3.12/site-packages/ray/experimental/job/example_job/demo_script.py +77 -0
  49. lib/python3.12/site-packages/ray/experimental/multiprocessing/__init__.py +5 -0
  50. 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