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

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/dag/__init__.py +46 -0
  2. lib/python3.12/site-packages/ray/dag/__pycache__/__init__.cpython-312.pyc +0 -0
  3. lib/python3.12/site-packages/ray/dag/__pycache__/base.cpython-312.pyc +0 -0
  4. lib/python3.12/site-packages/ray/dag/__pycache__/class_node.cpython-312.pyc +0 -0
  5. lib/python3.12/site-packages/ray/dag/__pycache__/collective_node.cpython-312.pyc +0 -0
  6. lib/python3.12/site-packages/ray/dag/__pycache__/conftest.cpython-312.pyc +0 -0
  7. lib/python3.12/site-packages/ray/dag/__pycache__/constants.cpython-312.pyc +0 -0
  8. lib/python3.12/site-packages/ray/dag/__pycache__/context.cpython-312.pyc +0 -0
  9. lib/python3.12/site-packages/ray/dag/__pycache__/dag_node.cpython-312.pyc +0 -0
  10. lib/python3.12/site-packages/ray/dag/__pycache__/dag_node_operation.cpython-312.pyc +0 -0
  11. lib/python3.12/site-packages/ray/dag/__pycache__/dag_operation_future.cpython-312.pyc +0 -0
  12. lib/python3.12/site-packages/ray/dag/__pycache__/format_utils.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/ray/dag/__pycache__/function_node.cpython-312.pyc +0 -0
  14. lib/python3.12/site-packages/ray/dag/__pycache__/input_node.cpython-312.pyc +0 -0
  15. lib/python3.12/site-packages/ray/dag/__pycache__/output_node.cpython-312.pyc +0 -0
  16. lib/python3.12/site-packages/ray/dag/__pycache__/py_obj_scanner.cpython-312.pyc +0 -0
  17. lib/python3.12/site-packages/ray/dag/__pycache__/utils.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/ray/dag/__pycache__/vis_utils.cpython-312.pyc +0 -0
  19. lib/python3.12/site-packages/ray/dag/base.py +8 -0
  20. lib/python3.12/site-packages/ray/dag/class_node.py +320 -0
  21. lib/python3.12/site-packages/ray/dag/collective_node.py +307 -0
  22. lib/python3.12/site-packages/ray/dag/compiled_dag_node.py +0 -0
  23. lib/python3.12/site-packages/ray/dag/conftest.py +17 -0
  24. lib/python3.12/site-packages/ray/dag/constants.py +40 -0
  25. lib/python3.12/site-packages/ray/dag/context.py +108 -0
  26. lib/python3.12/site-packages/ray/dag/dag_node.py +705 -0
  27. lib/python3.12/site-packages/ray/dag/dag_node_operation.py +853 -0
  28. lib/python3.12/site-packages/ray/dag/dag_operation_future.py +144 -0
  29. lib/python3.12/site-packages/ray/dag/experimental/__init__.py +0 -0
  30. lib/python3.12/site-packages/ray/dag/experimental/__pycache__/__init__.cpython-312.pyc +0 -0
  31. lib/python3.12/site-packages/ray/dag/format_utils.py +155 -0
  32. lib/python3.12/site-packages/ray/dag/function_node.py +59 -0
  33. lib/python3.12/site-packages/ray/dag/input_node.py +321 -0
  34. lib/python3.12/site-packages/ray/dag/output_node.py +45 -0
  35. lib/python3.12/site-packages/ray/dag/py_obj_scanner.py +103 -0
  36. lib/python3.12/site-packages/ray/dag/utils.py +66 -0
  37. lib/python3.12/site-packages/ray/dag/vis_utils.py +114 -0
  38. lib/python3.12/site-packages/ray/experimental/__init__.py +11 -0
  39. lib/python3.12/site-packages/ray/experimental/collective/__init__.py +23 -0
  40. lib/python3.12/site-packages/ray/experimental/collective/__pycache__/__init__.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/ray/experimental/collective/__pycache__/collective.cpython-312.pyc +0 -0
  42. lib/python3.12/site-packages/ray/experimental/collective/__pycache__/collective_tensor_transport.cpython-312.pyc +0 -0
  43. lib/python3.12/site-packages/ray/experimental/collective/__pycache__/communicator.cpython-312.pyc +0 -0
  44. lib/python3.12/site-packages/ray/experimental/collective/__pycache__/conftest.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/ray/experimental/collective/__pycache__/nixl_tensor_transport.cpython-312.pyc +0 -0
  46. lib/python3.12/site-packages/ray/experimental/collective/__pycache__/operations.cpython-312.pyc +0 -0
  47. lib/python3.12/site-packages/ray/experimental/collective/__pycache__/tensor_transport_manager.cpython-312.pyc +0 -0
  48. lib/python3.12/site-packages/ray/experimental/collective/__pycache__/util.cpython-312.pyc +0 -0
  49. lib/python3.12/site-packages/ray/experimental/collective/collective.py +233 -0
  50. lib/python3.12/site-packages/ray/experimental/collective/collective_tensor_transport.py +199 -0
lib/python3.12/site-packages/ray/dag/__init__.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ray.dag.dag_node import DAGNode
2
+ from ray.dag.function_node import FunctionNode
3
+ from ray.dag.class_node import (
4
+ ClassNode,
5
+ ClassMethodNode,
6
+ )
7
+ from ray.dag.collective_node import CollectiveOutputNode
8
+ from ray.dag.input_node import (
9
+ InputNode,
10
+ InputAttributeNode,
11
+ DAGInputData,
12
+ )
13
+ from ray.dag.output_node import MultiOutputNode
14
+ from ray.dag.dag_operation_future import DAGOperationFuture, GPUFuture
15
+ from ray.dag.constants import (
16
+ PARENT_CLASS_NODE_KEY,
17
+ PREV_CLASS_METHOD_CALL_KEY,
18
+ BIND_INDEX_KEY,
19
+ IS_CLASS_METHOD_OUTPUT_KEY,
20
+ COLLECTIVE_OPERATION_KEY,
21
+ DAGNODE_TYPE_KEY,
22
+ )
23
+ from ray.dag.vis_utils import plot
24
+ from ray.dag.context import DAGContext
25
+
26
+ __all__ = [
27
+ "ClassNode",
28
+ "ClassMethodNode",
29
+ "CollectiveOutputNode",
30
+ "DAGNode",
31
+ "DAGOperationFuture",
32
+ "FunctionNode",
33
+ "GPUFuture",
34
+ "InputNode",
35
+ "InputAttributeNode",
36
+ "DAGInputData",
37
+ "PARENT_CLASS_NODE_KEY",
38
+ "PREV_CLASS_METHOD_CALL_KEY",
39
+ "BIND_INDEX_KEY",
40
+ "IS_CLASS_METHOD_OUTPUT_KEY",
41
+ "COLLECTIVE_OPERATION_KEY",
42
+ "DAGNODE_TYPE_KEY",
43
+ "plot",
44
+ "MultiOutputNode",
45
+ "DAGContext",
46
+ ]
lib/python3.12/site-packages/ray/dag/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (1.17 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/base.cpython-312.pyc ADDED
Binary file (611 Bytes). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/class_node.cpython-312.pyc ADDED
Binary file (13.3 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/collective_node.cpython-312.pyc ADDED
Binary file (14 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/conftest.cpython-312.pyc ADDED
Binary file (774 Bytes). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/constants.cpython-312.pyc ADDED
Binary file (1.2 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/context.cpython-312.pyc ADDED
Binary file (5.55 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/dag_node.cpython-312.pyc ADDED
Binary file (29.7 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/dag_node_operation.cpython-312.pyc ADDED
Binary file (35.3 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/dag_operation_future.cpython-312.pyc ADDED
Binary file (5.95 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/format_utils.cpython-312.pyc ADDED
Binary file (6.26 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/function_node.cpython-312.pyc ADDED
Binary file (2.57 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/input_node.cpython-312.pyc ADDED
Binary file (12.6 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/output_node.cpython-312.pyc ADDED
Binary file (2.41 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/py_obj_scanner.cpython-312.pyc ADDED
Binary file (5.12 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/utils.cpython-312.pyc ADDED
Binary file (3.1 kB). View file
 
lib/python3.12/site-packages/ray/dag/__pycache__/vis_utils.cpython-312.pyc ADDED
Binary file (4.18 kB). View file
 
lib/python3.12/site-packages/ray/dag/base.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """This module defines the base class for object scanning and gets rid of
2
+ reference cycles."""
3
+ from ray.util.annotations import DeveloperAPI
4
+
5
+
6
+ @DeveloperAPI
7
+ class DAGNodeBase:
8
+ """Common base class for a node in a Ray task graph."""
lib/python3.12/site-packages/ray/dag/class_node.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, List, Optional, Tuple, Union
2
+ from weakref import ReferenceType
3
+
4
+ import ray
5
+ from ray.dag.constants import (
6
+ BIND_INDEX_KEY,
7
+ IS_CLASS_METHOD_OUTPUT_KEY,
8
+ PARENT_CLASS_NODE_KEY,
9
+ PREV_CLASS_METHOD_CALL_KEY,
10
+ )
11
+ from ray.dag.dag_node import DAGNode
12
+ from ray.dag.format_utils import get_dag_node_str
13
+ from ray.dag.input_node import InputNode
14
+ from ray.util.annotations import DeveloperAPI
15
+
16
+
17
+ @DeveloperAPI
18
+ class ClassNode(DAGNode):
19
+ """Represents an actor creation in a Ray task DAG."""
20
+
21
+ def __init__(
22
+ self,
23
+ cls,
24
+ cls_args,
25
+ cls_kwargs,
26
+ cls_options,
27
+ other_args_to_resolve=None,
28
+ ):
29
+ self._body = cls
30
+ self._last_call: Optional["ClassMethodNode"] = None
31
+ super().__init__(
32
+ cls_args,
33
+ cls_kwargs,
34
+ cls_options,
35
+ other_args_to_resolve=other_args_to_resolve,
36
+ )
37
+
38
+ if self._contains_input_node():
39
+ raise ValueError(
40
+ "InputNode handles user dynamic input the DAG, and "
41
+ "cannot be used as args, kwargs, or other_args_to_resolve "
42
+ "in ClassNode constructor because it is not available at "
43
+ "class construction or binding time."
44
+ )
45
+
46
+ def _copy_impl(
47
+ self,
48
+ new_args: List[Any],
49
+ new_kwargs: Dict[str, Any],
50
+ new_options: Dict[str, Any],
51
+ new_other_args_to_resolve: Dict[str, Any],
52
+ ):
53
+ return ClassNode(
54
+ self._body,
55
+ new_args,
56
+ new_kwargs,
57
+ new_options,
58
+ other_args_to_resolve=new_other_args_to_resolve,
59
+ )
60
+
61
+ def _execute_impl(self, *args, **kwargs):
62
+ """Executor of ClassNode by ray.remote()
63
+
64
+ Args and kwargs are to match base class signature, but not in the
65
+ implementation. All args and kwargs should be resolved and replaced
66
+ with value in bound_args and bound_kwargs via bottom-up recursion when
67
+ current node is executed.
68
+ """
69
+ return (
70
+ ray.remote(self._body)
71
+ .options(**self._bound_options)
72
+ .remote(*self._bound_args, **self._bound_kwargs)
73
+ )
74
+
75
+ def _contains_input_node(self) -> bool:
76
+ """Check if InputNode is used in children DAGNodes with current node
77
+ as the root.
78
+ """
79
+ children_dag_nodes = self._get_all_child_nodes()
80
+ for child in children_dag_nodes:
81
+ if isinstance(child, InputNode):
82
+ return True
83
+ return False
84
+
85
+ def __getattr__(self, method_name: str):
86
+ # User trying to call .bind() without a bind class method
87
+ if method_name == "bind" and "bind" not in dir(self._body):
88
+ raise AttributeError(f".bind() cannot be used again on {type(self)} ")
89
+ # Raise an error if the method is invalid.
90
+ getattr(self._body, method_name)
91
+ call_node = _UnboundClassMethodNode(self, method_name, {})
92
+ return call_node
93
+
94
+ def __str__(self) -> str:
95
+ return get_dag_node_str(self, str(self._body))
96
+
97
+
98
+ class _UnboundClassMethodNode(object):
99
+ def __init__(self, actor: ClassNode, method_name: str, options: dict):
100
+ # TODO(sang): Theoretically, We should use weakref cuz it is
101
+ # a circular dependency but when I used weakref, it fails
102
+ # because we cannot serialize the weakref.
103
+ self._actor = actor
104
+ self._method_name = method_name
105
+ self._options = options
106
+
107
+ def bind(self, *args, **kwargs):
108
+ other_args_to_resolve = {
109
+ PARENT_CLASS_NODE_KEY: self._actor,
110
+ PREV_CLASS_METHOD_CALL_KEY: self._actor._last_call,
111
+ }
112
+
113
+ node = ClassMethodNode(
114
+ self._method_name,
115
+ args,
116
+ kwargs,
117
+ self._options,
118
+ other_args_to_resolve=other_args_to_resolve,
119
+ )
120
+ self._actor._last_call = node
121
+ return node
122
+
123
+ def __getattr__(self, attr: str):
124
+ if attr == "remote":
125
+ raise AttributeError(
126
+ ".remote() cannot be used on ClassMethodNodes. Use .bind() instead "
127
+ "to express an symbolic actor call."
128
+ )
129
+ else:
130
+ return self.__getattribute__(attr)
131
+
132
+ def options(self, **options):
133
+ self._options = options
134
+ return self
135
+
136
+
137
+ class _ClassMethodOutput:
138
+ """Represents a class method output in a Ray function DAG."""
139
+
140
+ def __init__(self, class_method_call: "ClassMethodNode", output_idx: int):
141
+ # The upstream class method call that returns multiple values.
142
+ self._class_method_call = class_method_call
143
+ # The output index of the return value from the upstream class method call.
144
+ self._output_idx = output_idx
145
+
146
+ @property
147
+ def class_method_call(self) -> "ClassMethodNode":
148
+ return self._class_method_call
149
+
150
+ @property
151
+ def output_idx(self) -> int:
152
+ return self._output_idx
153
+
154
+
155
+ @DeveloperAPI
156
+ class ClassMethodNode(DAGNode):
157
+ """Represents an actor method invocation in a Ray function DAG."""
158
+
159
+ def __init__(
160
+ self,
161
+ method_name: str,
162
+ method_args: Tuple[Any],
163
+ method_kwargs: Dict[str, Any],
164
+ method_options: Dict[str, Any],
165
+ other_args_to_resolve: Dict[str, Any],
166
+ ):
167
+ self._bound_args = method_args or []
168
+ self._bound_kwargs = method_kwargs or {}
169
+ self._bound_options = method_options or {}
170
+ self._method_name: str = method_name
171
+ # Parse other_args_to_resolve and assign to variables
172
+ self._parent_class_node: Union[
173
+ ClassNode, ReferenceType["ray._private.actor.ActorHandle"]
174
+ ] = other_args_to_resolve.get(PARENT_CLASS_NODE_KEY)
175
+ # Used to track lineage of ClassMethodCall to preserve deterministic
176
+ # submission and execution order.
177
+ self._prev_class_method_call: Optional[
178
+ ClassMethodNode
179
+ ] = other_args_to_resolve.get(PREV_CLASS_METHOD_CALL_KEY, None)
180
+ # The index/order when bind() is called on this class method
181
+ self._bind_index: Optional[int] = other_args_to_resolve.get(
182
+ BIND_INDEX_KEY, None
183
+ )
184
+ # Represent if the ClassMethodNode is a class method output. If True,
185
+ # the node is a placeholder for a return value from the ClassMethodNode
186
+ # that returns multiple values. If False, the node is a class method call.
187
+ self._is_class_method_output: bool = other_args_to_resolve.get(
188
+ IS_CLASS_METHOD_OUTPUT_KEY, False
189
+ )
190
+ # Represents the return value from the upstream ClassMethodNode that
191
+ # returns multiple values. If the node is a class method call, this is None.
192
+ self._class_method_output: Optional[_ClassMethodOutput] = None
193
+ if self._is_class_method_output:
194
+ # Set the upstream ClassMethodNode and the output index of the return
195
+ # value from `method_args`.
196
+ self._class_method_output = _ClassMethodOutput(
197
+ method_args[0], method_args[1]
198
+ )
199
+
200
+ # The actor creation task dependency is encoded as the first argument,
201
+ # and the ordering dependency as the second, which ensures they are
202
+ # executed prior to this node.
203
+ super().__init__(
204
+ method_args,
205
+ method_kwargs,
206
+ method_options,
207
+ other_args_to_resolve=other_args_to_resolve,
208
+ )
209
+
210
+ def _copy_impl(
211
+ self,
212
+ new_args: List[Any],
213
+ new_kwargs: Dict[str, Any],
214
+ new_options: Dict[str, Any],
215
+ new_other_args_to_resolve: Dict[str, Any],
216
+ ):
217
+ return ClassMethodNode(
218
+ self._method_name,
219
+ new_args,
220
+ new_kwargs,
221
+ new_options,
222
+ other_args_to_resolve=new_other_args_to_resolve,
223
+ )
224
+
225
+ def _execute_impl(self, *args, **kwargs):
226
+ """Executor of ClassMethodNode by ray.remote()
227
+
228
+ Args and kwargs are to match base class signature, but not in the
229
+ implementation. All args and kwargs should be resolved and replaced
230
+ with value in bound_args and bound_kwargs via bottom-up recursion when
231
+ current node is executed.
232
+ """
233
+ if self.is_class_method_call:
234
+ method_body = getattr(self._parent_class_node, self._method_name)
235
+ # Execute with bound args.
236
+ return method_body.options(**self._bound_options).remote(
237
+ *self._bound_args,
238
+ **self._bound_kwargs,
239
+ )
240
+ else:
241
+ assert self._class_method_output is not None
242
+ return self._bound_args[0][self._class_method_output.output_idx]
243
+
244
+ def __str__(self) -> str:
245
+ return get_dag_node_str(self, f"{self._method_name}()")
246
+
247
+ def __repr__(self) -> str:
248
+ return self.__str__()
249
+
250
+ def get_method_name(self) -> str:
251
+ return self._method_name
252
+
253
+ def _get_bind_index(self) -> int:
254
+ return self._bind_index
255
+
256
+ def _get_remote_method(self, method_name):
257
+ method_body = getattr(self._parent_class_node, method_name)
258
+ return method_body
259
+
260
+ def _get_actor_handle(self) -> Optional["ray.actor.ActorHandle"]:
261
+ if not isinstance(self._parent_class_node, ray.actor.ActorHandle):
262
+ return None
263
+ return self._parent_class_node
264
+
265
+ @property
266
+ def num_returns(self) -> int:
267
+ """
268
+ Return the number of return values from the class method call. If the
269
+ node is a class method output, return the number of return values from
270
+ the upstream class method call.
271
+ """
272
+
273
+ if self.is_class_method_call:
274
+ num_returns = self._bound_options.get("num_returns", None)
275
+ if num_returns is None:
276
+ method = self._get_remote_method(self._method_name)
277
+ num_returns = method.__getstate__()["num_returns"]
278
+ return num_returns
279
+ else:
280
+ assert self._class_method_output is not None
281
+ return self._class_method_output.class_method_call.num_returns
282
+
283
+ @property
284
+ def is_class_method_call(self) -> bool:
285
+ """
286
+ Return True if the node is a class method call, False if the node is a
287
+ class method output.
288
+ """
289
+ return not self._is_class_method_output
290
+
291
+ @property
292
+ def is_class_method_output(self) -> bool:
293
+ """
294
+ Return True if the node is a class method output, False if the node is a
295
+ class method call.
296
+ """
297
+ return self._is_class_method_output
298
+
299
+ @property
300
+ def class_method_call(self) -> Optional["ClassMethodNode"]:
301
+ """
302
+ Return the upstream class method call that returns multiple values. If
303
+ the node is a class method output, return None.
304
+ """
305
+
306
+ if self._class_method_output is None:
307
+ return None
308
+ return self._class_method_output.class_method_call
309
+
310
+ @property
311
+ def output_idx(self) -> Optional[int]:
312
+ """
313
+ Return the output index of the return value from the upstream class
314
+ method call that returns multiple values. If the node is a class method
315
+ call, return None.
316
+ """
317
+
318
+ if self._class_method_output is None:
319
+ return None
320
+ return self._class_method_output.output_idx
lib/python3.12/site-packages/ray/dag/collective_node.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
2
+
3
+ if TYPE_CHECKING:
4
+ import torch
5
+
6
+ import ray
7
+ from ray.dag import (
8
+ ClassMethodNode,
9
+ DAGNode,
10
+ )
11
+ from ray.dag.constants import COLLECTIVE_OPERATION_KEY, IS_CLASS_METHOD_OUTPUT_KEY
12
+ from ray.experimental.channel import ChannelContext
13
+ from ray.experimental.channel.torch_tensor_type import Communicator, TorchTensorType
14
+ from ray.experimental.util.types import (
15
+ AllGatherOp,
16
+ AllReduceOp,
17
+ ReduceScatterOp,
18
+ _CollectiveOp,
19
+ )
20
+ from ray.util.annotations import DeveloperAPI
21
+
22
+
23
+ class _CollectiveOperation:
24
+ """
25
+ Represent metadata for a collective communicator collective operation.
26
+
27
+ Args:
28
+ inputs: A list of lists of DAGNode. Each nested list inside
29
+ of inputs should contain exactly one object per actor.
30
+ If multiple nested lists are provided, then the order of
31
+ actors should be the same for each nested list.
32
+ op: The collective operation to perform.
33
+ transport: The transport to use for the collective operation.
34
+
35
+ Requirements:
36
+ 1. Input nodes are unique.
37
+ 2. Actor handles are unique.
38
+ 3. Actor handles match the custom communicator group if specified.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ inputs: List[List[DAGNode]],
44
+ op: _CollectiveOp,
45
+ transport: Optional[Union[str, Communicator]] = None,
46
+ ):
47
+ self._actor_handles: List["ray.actor.ActorHandle"] = []
48
+ for i, input_nodes in enumerate(inputs):
49
+ # Check non-empty input list
50
+ if len(input_nodes) == 0:
51
+ nested_list_error_msg = f" at index {i}" if len(inputs) > 1 else ""
52
+ raise ValueError(
53
+ f"Expected non-empty input list{nested_list_error_msg}."
54
+ )
55
+
56
+ # Check input nodes are DAGNode
57
+ if not all(isinstance(node, DAGNode) for node in input_nodes):
58
+ nested_list_error_msg = (
59
+ f" at list at index {i}" if len(inputs) > 1 else ""
60
+ )
61
+ raise ValueError(
62
+ f"Expected all input nodes to be DAGNode{nested_list_error_msg}, "
63
+ f"but got {input_nodes}."
64
+ )
65
+
66
+ # Check unique input nodes
67
+ if len(set(input_nodes)) != len(input_nodes):
68
+ duplicates = [
69
+ input_node
70
+ for input_node in input_nodes
71
+ if input_nodes.count(input_node) > 1
72
+ ]
73
+ nested_list_error_msg = (
74
+ f" at list at index {i}" if len(inputs) > 1 else ""
75
+ )
76
+ raise ValueError(
77
+ f"Expected unique input nodes{nested_list_error_msg}, but found duplicates: "
78
+ f"{duplicates}"
79
+ )
80
+
81
+ current_actor_handles = []
82
+ for input_node in input_nodes:
83
+ actor_handle = input_node._get_actor_handle()
84
+ if actor_handle is None:
85
+ nested_list_error_msg = (
86
+ f" at list at index {i}" if len(inputs) > 1 else ""
87
+ )
88
+ raise ValueError(
89
+ f"Expected an actor handle from the input node{nested_list_error_msg}"
90
+ )
91
+ current_actor_handles.append(actor_handle)
92
+
93
+ # Check unique actor handles
94
+ if len(set(current_actor_handles)) != len(current_actor_handles):
95
+ invalid_input_nodes = [
96
+ input_node
97
+ for input_node in input_nodes
98
+ if current_actor_handles.count(input_node._get_actor_handle()) > 1
99
+ ]
100
+ nested_list_error_msg = (
101
+ f" at list at index {i}" if len(inputs) > 1 else ""
102
+ )
103
+ raise ValueError(
104
+ f"Expected unique actor handles{nested_list_error_msg}, "
105
+ "but found duplicate actor handles from input nodes: "
106
+ f"{invalid_input_nodes}"
107
+ )
108
+
109
+ if i == 0:
110
+ first_actor_handles = current_actor_handles
111
+
112
+ # Check all lists of DAGNode have the same number of nodes
113
+ if len(inputs[0]) != len(inputs[i]):
114
+ raise ValueError(
115
+ f"Expected all input lists to have the same number of nodes. "
116
+ f"List at index 0 has length {len(inputs[0])}, but list at "
117
+ f"index {i} has length {len(inputs[i])}."
118
+ )
119
+
120
+ # Check all lists of DAGNode have same set of actor handles
121
+ if set(first_actor_handles) != set(current_actor_handles):
122
+ raise ValueError(
123
+ f"Expected all input lists to have the same set of actor handles. "
124
+ f"List at index 0 has actors {set(first_actor_handles)}, but list at "
125
+ f"index {i} has actors {set(current_actor_handles)}."
126
+ )
127
+
128
+ # Check all lists of DAGNode have same order of actor handles
129
+ for j, (first, current) in enumerate(
130
+ zip(first_actor_handles, current_actor_handles)
131
+ ):
132
+ if first != current:
133
+ raise ValueError(
134
+ f"Expected all input lists to have the same order of actor handles. "
135
+ f"List at index 0 has actor {first} at position {j}, but list at "
136
+ f"index {i} has actor {current} at position {j}."
137
+ )
138
+ self._actor_handles = current_actor_handles
139
+
140
+ self._op = op
141
+ if transport is None:
142
+ transport = TorchTensorType.ACCELERATOR
143
+ self._type_hint = TorchTensorType(transport=transport, _direct_return=True)
144
+ if isinstance(transport, Communicator):
145
+ if set(transport.get_actor_handles()) != set(self._actor_handles):
146
+ raise ValueError(
147
+ "Expected actor handles to match the custom communicator group"
148
+ )
149
+
150
+ def __str__(self) -> str:
151
+ return (
152
+ f"CollectiveOperation("
153
+ f"_actor_handles={self._actor_handles}, "
154
+ f"_op={self._op}, "
155
+ f"_type_hint={self._type_hint})"
156
+ )
157
+
158
+ @property
159
+ def actor_handles(self) -> List["ray.actor.ActorHandle"]:
160
+ return self._actor_handles
161
+
162
+ @property
163
+ def type_hint(self) -> TorchTensorType:
164
+ return self._type_hint
165
+
166
+ def get_communicator(self) -> Communicator:
167
+ if self._type_hint.communicator_id is not None:
168
+ ctx = ChannelContext.get_current()
169
+ communicator = ctx.communicators[self._type_hint.communicator_id]
170
+ elif self._type_hint.get_custom_communicator() is not None:
171
+ communicator = self._type_hint.get_custom_communicator()
172
+ else:
173
+ raise ValueError("Expected a communicator group")
174
+ return communicator
175
+
176
+ def execute(
177
+ self, *send_buf: "torch.Tensor"
178
+ ) -> Union["torch.Tensor", Tuple["torch.Tensor", ...]]:
179
+ """
180
+ Call the collective operation on the input tensor(s). Output tensor(s) are
181
+ allocated and returned.
182
+
183
+ Args:
184
+ *send_buf: A variable number of torch tensors to send to the collective
185
+ operation. The tensors have the same order as the input nodes.
186
+
187
+ Returns:
188
+ A torch tensor or a tuple of torch tensors containing the results of the
189
+ collective operation. The output tensors have the same length and order
190
+ as the input node list of the actor of this operation.
191
+ """
192
+ import torch
193
+
194
+ if not all(isinstance(t, torch.Tensor) for t in send_buf):
195
+ raise ValueError("Expected a torch tensor for each input node")
196
+
197
+ communicator = self.get_communicator()
198
+ if isinstance(self._op, AllGatherOp):
199
+ assert len(send_buf) == 1
200
+ t = send_buf[0]
201
+ world_size = len(self._actor_handles)
202
+ recv_buf = torch.empty(
203
+ (t.shape[0] * world_size, *t.shape[1:]),
204
+ dtype=t.dtype,
205
+ device=t.device,
206
+ )
207
+ communicator.allgather(t, recv_buf)
208
+ elif isinstance(self._op, AllReduceOp):
209
+ if len(send_buf) == 1:
210
+ t = send_buf[0]
211
+ recv_buf = torch.empty_like(t)
212
+ communicator.allreduce(t, recv_buf, self._op.reduceOp)
213
+ else:
214
+ if not all(t.dtype == send_buf[0].dtype for t in send_buf):
215
+ raise ValueError(
216
+ "Expected all input tensors to have the same dtype, "
217
+ f"but got {[t.dtype for t in send_buf]}"
218
+ )
219
+
220
+ def unflatten_from(flat_buf, bufs):
221
+ views = []
222
+ offset = 0
223
+ for t in bufs:
224
+ numel = t.numel()
225
+ t = flat_buf[offset : offset + numel].view(t.shape)
226
+ views.append(t)
227
+ offset += numel
228
+ return tuple(views)
229
+
230
+ flat_buf = torch.nn.utils.parameters_to_vector(send_buf)
231
+ communicator.allreduce(flat_buf, flat_buf, self._op.reduceOp)
232
+ recv_buf = unflatten_from(flat_buf, send_buf)
233
+ elif isinstance(self._op, ReduceScatterOp):
234
+ assert len(send_buf) == 1
235
+ t = send_buf[0]
236
+ world_size = len(self._actor_handles)
237
+ if t.shape[0] % world_size != 0:
238
+ raise ValueError(
239
+ "Expected the first dimension of the input tensor to be divisible "
240
+ f"by the world size {world_size}"
241
+ )
242
+ recv_buf = torch.empty(
243
+ (t.shape[0] // world_size, *t.shape[1:]),
244
+ dtype=t.dtype,
245
+ device=t.device,
246
+ )
247
+ communicator.reducescatter(t, recv_buf, self._op.reduceOp)
248
+ return recv_buf
249
+
250
+
251
+ @DeveloperAPI
252
+ class CollectiveOutputNode(ClassMethodNode):
253
+ """Represent an output node from a communicator collective operation in a Ray DAG."""
254
+
255
+ def __init__(
256
+ self,
257
+ method_name: str,
258
+ method_args: Tuple[
259
+ DAGNode,
260
+ ],
261
+ method_kwargs: Dict[str, Any],
262
+ method_options: Dict[str, Any],
263
+ other_args_to_resolve: Dict[str, Any],
264
+ ):
265
+ # Parse the input node(s).
266
+ self._inputs = method_args
267
+ # Parse the collective operation.
268
+ self._collective_op: _CollectiveOperation = other_args_to_resolve.get(
269
+ COLLECTIVE_OPERATION_KEY, None
270
+ )
271
+ self._is_class_method_output: bool = other_args_to_resolve.get(
272
+ IS_CLASS_METHOD_OUTPUT_KEY, False
273
+ )
274
+ if self._collective_op is None and not self._is_class_method_output:
275
+ raise ValueError("Expected a collective operation")
276
+
277
+ super().__init__(
278
+ method_name,
279
+ method_args,
280
+ method_kwargs,
281
+ method_options,
282
+ other_args_to_resolve,
283
+ )
284
+
285
+ def _copy_impl(
286
+ self,
287
+ new_args: List[Any],
288
+ new_kwargs: Dict[str, Any],
289
+ new_options: Dict[str, Any],
290
+ new_other_args_to_resolve: Dict[str, Any],
291
+ ):
292
+ return CollectiveOutputNode(
293
+ self._method_name,
294
+ new_args,
295
+ new_kwargs,
296
+ new_options,
297
+ other_args_to_resolve=new_other_args_to_resolve,
298
+ )
299
+
300
+ def _execute_impl(self, *args, **kwargs):
301
+ raise NotImplementedError(
302
+ "CollectiveOutputNode is only supported with dag.experimental_compile()"
303
+ )
304
+
305
+ @property
306
+ def collective_op(self) -> _CollectiveOperation:
307
+ return self._collective_op
lib/python3.12/site-packages/ray/dag/compiled_dag_node.py ADDED
The diff for this file is too large to render. See raw diff
 
lib/python3.12/site-packages/ray/dag/conftest.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import pytest
4
+
5
+ import ray
6
+
7
+ TEST_NAMESPACE = "ray_dag_test_namespace"
8
+
9
+
10
+ @pytest.fixture(scope="session")
11
+ def shared_ray_instance():
12
+ # Remove ray address for test ray cluster in case we have
13
+ # lingering RAY_ADDRESS="http://127.0.0.1:8265" from previous local job
14
+ # submissions.
15
+ if "RAY_ADDRESS" in os.environ:
16
+ del os.environ["RAY_ADDRESS"]
17
+ yield ray.init(num_cpus=16, namespace=TEST_NAMESPACE, log_to_driver=True)
lib/python3.12/site-packages/ray/dag/constants.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Reserved keys used to handle ClassMethodNode in Ray DAG building.
4
+ PARENT_CLASS_NODE_KEY = "parent_class_node"
5
+ PREV_CLASS_METHOD_CALL_KEY = "prev_class_method_call"
6
+ BIND_INDEX_KEY = "bind_index"
7
+ IS_CLASS_METHOD_OUTPUT_KEY = "is_class_method_output"
8
+
9
+ # Reserved keys used to handle CollectiveOutputNode in Ray DAG building.
10
+ COLLECTIVE_OPERATION_KEY = "collective_operation"
11
+
12
+ # Reserved key to distinguish DAGNode type and avoid collision with user dict.
13
+ DAGNODE_TYPE_KEY = "__dag_node_type__"
14
+
15
+ # Feature flag to turn off the deadlock detection.
16
+ RAY_CGRAPH_ENABLE_DETECT_DEADLOCK = (
17
+ os.environ.get("RAY_CGRAPH_ENABLE_DETECT_DEADLOCK", "1") == "1"
18
+ )
19
+
20
+ # Feature flag to turn on profiling.
21
+ RAY_CGRAPH_ENABLE_PROFILING = os.environ.get("RAY_CGRAPH_ENABLE_PROFILING", "0") == "1"
22
+
23
+ # Feature flag to turn on NVTX (NVIDIA Tools Extension Library) profiling.
24
+ # With this flag, Compiled Graph uses nvtx to automatically annotate and profile
25
+ # function calls during each actor's execution loop.
26
+ # This cannot be used together with RAY_CGRAPH_ENABLE_TORCH_PROFILING.
27
+ RAY_CGRAPH_ENABLE_NVTX_PROFILING = (
28
+ os.environ.get("RAY_CGRAPH_ENABLE_NVTX_PROFILING", "0") == "1"
29
+ )
30
+
31
+ # Feature flag to turn on torch profiling.
32
+ # This cannot be used together with RAY_CGRAPH_ENABLE_NVTX_PROFILING.
33
+ RAY_CGRAPH_ENABLE_TORCH_PROFILING = (
34
+ os.environ.get("RAY_CGRAPH_ENABLE_TORCH_PROFILING", "0") == "1"
35
+ )
36
+
37
+ # Feature flag to turn on visualization of the execution schedule.
38
+ RAY_CGRAPH_VISUALIZE_SCHEDULE = (
39
+ os.environ.get("RAY_CGRAPH_VISUALIZE_SCHEDULE", "0") == "1"
40
+ )
lib/python3.12/site-packages/ray/dag/context.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import threading
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from ray.util.annotations import DeveloperAPI
7
+
8
+ # The context singleton on this process.
9
+ _default_context: "Optional[DAGContext]" = None
10
+ _context_lock = threading.Lock()
11
+
12
+ DEFAULT_SUBMIT_TIMEOUT_S = int(os.environ.get("RAY_CGRAPH_submit_timeout", 10))
13
+ DEFAULT_GET_TIMEOUT_S = int(os.environ.get("RAY_CGRAPH_get_timeout", 10))
14
+ DEFAULT_TEARDOWN_TIMEOUT_S = int(os.environ.get("RAY_CGRAPH_teardown_timeout", 30))
15
+ DEFAULT_READ_ITERATION_TIMEOUT_S = float(
16
+ os.environ.get("RAY_CGRAPH_read_iteration_timeout_s", 0.1)
17
+ )
18
+ # Default buffer size is 1MB.
19
+ DEFAULT_BUFFER_SIZE_BYTES = int(os.environ.get("RAY_CGRAPH_buffer_size_bytes", 1e6))
20
+ # The default number of in-flight executions that can be submitted before consuming the
21
+ # output.
22
+ DEFAULT_MAX_INFLIGHT_EXECUTIONS = int(
23
+ os.environ.get("RAY_CGRAPH_max_inflight_executions", 10)
24
+ )
25
+
26
+ # The default number of results that can be buffered at the driver.
27
+ DEFAULT_MAX_BUFFERED_RESULTS = int(
28
+ os.environ.get("RAY_CGRAPH_max_buffered_results", 1000)
29
+ )
30
+
31
+ DEFAULT_OVERLAP_GPU_COMMUNICATION = bool(
32
+ os.environ.get("RAY_CGRAPH_overlap_gpu_communication", 0)
33
+ )
34
+
35
+
36
+ @DeveloperAPI
37
+ @dataclass
38
+ class DAGContext:
39
+ """Global settings for Ray DAG.
40
+
41
+ You can configure parameters in the DAGContext by setting the environment
42
+ variables, `RAY_CGRAPH_<param>` (e.g., `RAY_CGRAPH_buffer_size_bytes`) or Python.
43
+
44
+ Examples:
45
+ >>> from ray.dag import DAGContext
46
+ >>> DAGContext.get_current().buffer_size_bytes
47
+ 1000000
48
+ >>> DAGContext.get_current().buffer_size_bytes = 500
49
+ >>> DAGContext.get_current().buffer_size_bytes
50
+ 500
51
+
52
+ Args:
53
+ submit_timeout: The maximum time in seconds to wait for execute()
54
+ calls.
55
+ get_timeout: The maximum time in seconds to wait when retrieving
56
+ a result from the DAG during `ray.get`. This should be set to a
57
+ value higher than the expected time to execute the entire DAG.
58
+ teardown_timeout: The maximum time in seconds to wait for the DAG to
59
+ cleanly shut down.
60
+ read_iteration_timeout: The timeout in seconds for each read iteration
61
+ that reads one of the input channels. If the timeout is reached, the
62
+ read operation will be interrupted and will try to read the next
63
+ input channel. It must be less than or equal to `get_timeout`.
64
+ buffer_size_bytes: The initial buffer size in bytes for messages
65
+ that can be passed between tasks in the DAG. The buffers will
66
+ be automatically resized if larger messages are written to the
67
+ channel.
68
+ max_inflight_executions: The maximum number of in-flight executions that
69
+ can be submitted via `execute` or `execute_async` before consuming
70
+ the output using `ray.get()`. If the caller submits more executions,
71
+ `RayCgraphCapacityExceeded` is raised.
72
+ overlap_gpu_communication: (experimental) Whether to overlap GPU
73
+ communication with computation during DAG execution. If True, the
74
+ communication and computation can be overlapped, which can improve
75
+ the performance of the DAG execution.
76
+ """
77
+
78
+ submit_timeout: int = DEFAULT_SUBMIT_TIMEOUT_S
79
+ get_timeout: int = DEFAULT_GET_TIMEOUT_S
80
+ teardown_timeout: int = DEFAULT_TEARDOWN_TIMEOUT_S
81
+ read_iteration_timeout: float = DEFAULT_READ_ITERATION_TIMEOUT_S
82
+ buffer_size_bytes: int = DEFAULT_BUFFER_SIZE_BYTES
83
+ max_inflight_executions: int = DEFAULT_MAX_INFLIGHT_EXECUTIONS
84
+ max_buffered_results: int = DEFAULT_MAX_BUFFERED_RESULTS
85
+ overlap_gpu_communication: bool = DEFAULT_OVERLAP_GPU_COMMUNICATION
86
+
87
+ def __post_init__(self):
88
+ if self.read_iteration_timeout > self.get_timeout:
89
+ raise ValueError(
90
+ "RAY_CGRAPH_read_iteration_timeout_s "
91
+ f"({self.read_iteration_timeout}) must be less than or equal to "
92
+ f"RAY_CGRAPH_get_timeout ({self.get_timeout})"
93
+ )
94
+
95
+ @staticmethod
96
+ def get_current() -> "DAGContext":
97
+ """Get or create a singleton context.
98
+
99
+ If the context has not yet been created in this process, it will be
100
+ initialized with default settings.
101
+ """
102
+ global _default_context
103
+
104
+ with _context_lock:
105
+ if _default_context is None:
106
+ _default_context = DAGContext()
107
+
108
+ return _default_context
lib/python3.12/site-packages/ray/dag/dag_node.py ADDED
@@ -0,0 +1,705 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import copy
3
+ import uuid
4
+ from itertools import chain
5
+ from typing import (
6
+ Any,
7
+ Callable,
8
+ Dict,
9
+ List,
10
+ Literal,
11
+ Optional,
12
+ Tuple,
13
+ TypeVar,
14
+ Union,
15
+ )
16
+
17
+ import ray
18
+ from ray.dag.base import DAGNodeBase
19
+ from ray.dag.compiled_dag_node import build_compiled_dag_from_ray_dag
20
+ from ray.dag.py_obj_scanner import _PyObjScanner
21
+ from ray.experimental.channel import ChannelOutputType
22
+ from ray.experimental.channel.auto_transport_type import AutoTransportType
23
+ from ray.experimental.channel.communicator import Communicator
24
+ from ray.experimental.channel.torch_tensor_type import TorchTensorType
25
+ from ray.experimental.util.types import Device
26
+ from ray.util.annotations import DeveloperAPI
27
+
28
+ T = TypeVar("T")
29
+
30
+
31
+ @DeveloperAPI
32
+ class DAGNode(DAGNodeBase):
33
+ """Abstract class for a node in a Ray task graph.
34
+
35
+ A node has a type (e.g., FunctionNode), data (e.g., function options and
36
+ body), arguments (Python values, DAGNodes, and DAGNodes nested within Python
37
+ argument values) and options (Ray API .options() used for function, class
38
+ or class method)
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ args: Tuple[Any],
44
+ kwargs: Dict[str, Any],
45
+ options: Dict[str, Any],
46
+ other_args_to_resolve: Dict[str, Any],
47
+ ):
48
+ """
49
+ args:
50
+ args (Tuple[Any]): Bound node arguments.
51
+ ex: func_or_class.bind(1)
52
+ kwargs (Dict[str, Any]): Bound node keyword arguments.
53
+ ex: func_or_class.bind(a=1)
54
+ options (Dict[str, Any]): Bound node options arguments.
55
+ ex: func_or_class.options(num_cpus=2)
56
+ other_args_to_resolve (Dict[str, Any]): Bound kwargs to resolve
57
+ that's specific to subclass implementation without exposing
58
+ as args in base class, example: ClassMethodNode
59
+ """
60
+ self._bound_args: Tuple[Any] = args or []
61
+ self._bound_kwargs: Dict[str, Any] = kwargs or {}
62
+ self._bound_options: Dict[str, Any] = options or {}
63
+ self._bound_other_args_to_resolve: Optional[Dict[str, Any]] = (
64
+ other_args_to_resolve or {}
65
+ )
66
+
67
+ # The list of nodes that use this DAG node as an argument.
68
+ self._downstream_nodes: List["DAGNode"] = []
69
+
70
+ # UUID that is not changed over copies of this node.
71
+ self._stable_uuid = uuid.uuid4().hex
72
+
73
+ # Indicates whether this DAG node contains nested DAG nodes.
74
+ # Nested DAG nodes are allowed in traditional DAGs but not
75
+ # in Ray Compiled Graphs, except for MultiOutputNode.
76
+ self._args_contain_nested_dag_node = False
77
+
78
+ # The list of nodes that this DAG node uses as an argument.
79
+ self._upstream_nodes: List["DAGNode"] = self._collect_upstream_nodes()
80
+
81
+ # Cached values from last call to execute()
82
+ self.cache_from_last_execute = {}
83
+
84
+ self._type_hint: ChannelOutputType = ChannelOutputType()
85
+
86
+ # If the original type hint is an AutoTransportType, we make a copy
87
+ # here when it is resolved to the actual type, as additional debugging
88
+ # information. Otherwise, it is None.
89
+ self._original_type_hint: Optional[ChannelOutputType] = None
90
+
91
+ # Whether this node calls `experimental_compile`.
92
+ self.is_cgraph_output_node = False
93
+
94
+ def _collect_upstream_nodes(self) -> List["DAGNode"]:
95
+ """
96
+ Retrieve upstream nodes and update their downstream dependencies.
97
+
98
+ Currently, the DAG assumes that all DAGNodes in `args`, `kwargs`, and
99
+ `other_args_to_resolve` are upstream nodes. However, Ray Compiled Graphs
100
+ builds the upstream/downstream relationship based only on args. Be cautious
101
+ when persisting DAGNodes in `other_args_to_resolve` and kwargs in the future.
102
+
103
+ TODO (kevin85421): Currently, the upstream nodes and downstream nodes have
104
+ circular references. Therefore, it relies on the garbage collector to clean
105
+ them up instead of reference counting. We should consider using weak references
106
+ to avoid circular references.
107
+ """
108
+ upstream_nodes: List["DAGNode"] = []
109
+
110
+ # Ray Compiled Graphs do not allow nested DAG nodes in arguments.
111
+ # Specifically, a DAGNode should not be placed inside any type of
112
+ # container. However, we only know if this is a compiled graph
113
+ # when calling `experimental_compile`. Therefore, we need to check
114
+ # in advance if the arguments contain nested DAG nodes and raise
115
+ # an error after compilation.
116
+ assert hasattr(self._bound_args, "__iter__")
117
+ for arg in self._bound_args:
118
+ if isinstance(arg, DAGNode):
119
+ upstream_nodes.append(arg)
120
+ else:
121
+ scanner = _PyObjScanner()
122
+ dag_nodes = scanner.find_nodes(arg)
123
+ upstream_nodes.extend(dag_nodes)
124
+ scanner.clear()
125
+ self._args_contain_nested_dag_node = len(dag_nodes) > 0
126
+
127
+ scanner = _PyObjScanner()
128
+ other_upstream_nodes: List["DAGNode"] = scanner.find_nodes(
129
+ [
130
+ self._bound_kwargs,
131
+ self._bound_other_args_to_resolve,
132
+ ]
133
+ )
134
+ upstream_nodes.extend(other_upstream_nodes)
135
+ scanner.clear()
136
+ # Update dependencies.
137
+ for upstream_node in upstream_nodes:
138
+ upstream_node._downstream_nodes.append(self)
139
+ return upstream_nodes
140
+
141
+ def with_tensor_transport(
142
+ self,
143
+ transport: Optional[Union[str, Communicator]] = "auto",
144
+ device: Literal["default", "cpu", "gpu", "cuda"] = "default",
145
+ _static_shape: bool = False,
146
+ _direct_return: bool = False,
147
+ ):
148
+ """
149
+ Configure the torch tensor transport for this node.
150
+
151
+ Args:
152
+ transport: Specifies the tensor transport mechanism.
153
+ - "accelerator": Tensors are communicated using accelerator-specific backends
154
+ (e.g., NCCL, XLA, or vendor-provided transport). This is the recommended option
155
+ for most use cases, as it supports extensibility and future hardware backends.
156
+ - "nccl": Tensors are passed explicitly via NCCL. This option is kept for
157
+ backwards compatibility and may be removed in the future. Use "accelerator"
158
+ instead unless you have legacy requirements.
159
+ - "shm": Tensors are passed via host shared memory and gRPC. Typically used
160
+ when accelerator-based transport is unavailable or not suitable.
161
+ - "auto" (default): The system automatically selects the appropriate transport
162
+ mechanism based on the sender and receiver, usually preferring accelerator-based
163
+ transport when available.
164
+ device: The target device to use for the tensor transport.
165
+ "default": The tensor will maintain its original device placement from the sender
166
+ "cpu": The tensor will be explicitly moved to CPU device in the receiver
167
+ "gpu" or "cuda": The tensor will be explicitly moved to GPU device in the receiver
168
+ _static_shape: A hint indicating whether the shape(s) and dtype(s)
169
+ of tensor(s) contained in this value always remain the same
170
+ across different executions of the DAG. If this is True, the
171
+ transport will be more efficient.
172
+ _direct_return: Whether the tensor is sent directly or inside of
173
+ other data. If a "nccl" transport is used, this allows the
174
+ sender and receiver to eliminate performance overhead from
175
+ an additional data transfer.
176
+ """
177
+ try:
178
+ device = Device(device)
179
+ except ValueError:
180
+ valid_devices = ", ".join(f"'{d.value}'" for d in Device)
181
+ raise ValueError(
182
+ f"Invalid device '{device}'. Valid options are: {valid_devices}."
183
+ )
184
+ if transport == "auto":
185
+ self._type_hint = AutoTransportType(
186
+ device=device,
187
+ _static_shape=_static_shape,
188
+ _direct_return=_direct_return,
189
+ )
190
+ elif transport == "nccl":
191
+ self._type_hint = TorchTensorType(
192
+ transport="accelerator",
193
+ device=device,
194
+ _static_shape=_static_shape,
195
+ _direct_return=_direct_return,
196
+ )
197
+ elif transport == "accelerator":
198
+ self._type_hint = TorchTensorType(
199
+ transport="accelerator",
200
+ device=device,
201
+ _static_shape=_static_shape,
202
+ _direct_return=_direct_return,
203
+ )
204
+ elif transport == "shm":
205
+ self._type_hint = TorchTensorType(
206
+ device=device,
207
+ _static_shape=_static_shape,
208
+ _direct_return=_direct_return,
209
+ )
210
+ else:
211
+ if not isinstance(transport, Communicator):
212
+ raise ValueError(
213
+ f"Invalid transport type: {transport}. "
214
+ "Transport must be one of 'auto', 'nccl', 'shm', 'accelerator' or "
215
+ "an instance of Communicator type."
216
+ )
217
+ self._type_hint = TorchTensorType(
218
+ transport=transport,
219
+ device=device,
220
+ _static_shape=_static_shape,
221
+ _direct_return=_direct_return,
222
+ )
223
+ return self
224
+
225
+ @property
226
+ def type_hint(self) -> ChannelOutputType:
227
+ return self._type_hint
228
+
229
+ @type_hint.setter
230
+ def type_hint(self, type_hint: ChannelOutputType) -> None:
231
+ if isinstance(self._type_hint, AutoTransportType):
232
+ self._original_type_hint = self._type_hint
233
+ self._type_hint = type_hint
234
+
235
+ def get_args(self) -> Tuple[Any]:
236
+ """Return the tuple of arguments for this node."""
237
+
238
+ return self._bound_args
239
+
240
+ def get_kwargs(self) -> Dict[str, Any]:
241
+ """Return the dict of keyword arguments for this node."""
242
+
243
+ return self._bound_kwargs.copy()
244
+
245
+ def get_options(self) -> Dict[str, Any]:
246
+ """Return the dict of options arguments for this node."""
247
+
248
+ return self._bound_options.copy()
249
+
250
+ def get_other_args_to_resolve(self) -> Dict[str, Any]:
251
+ """Return the dict of other args to resolve arguments for this node."""
252
+ return self._bound_other_args_to_resolve.copy()
253
+
254
+ def get_stable_uuid(self) -> str:
255
+ """Return stable uuid for this node.
256
+ 1) Generated only once at first instance creation
257
+ 2) Stable across pickling, replacement and JSON serialization.
258
+ """
259
+ return self._stable_uuid
260
+
261
+ async def get_object_refs_from_last_execute(self) -> Dict[str, Any]:
262
+ """Gets cached object refs from the last call to execute().
263
+
264
+ After this DAG is executed through execute(), retrieves a map between node
265
+ UUID to a reference to the return value of the default executor on that node.
266
+ """
267
+ cache = {}
268
+ for node_uuid, value in self.cache_from_last_execute.items():
269
+ if isinstance(value, asyncio.Task):
270
+ cache[node_uuid] = await value
271
+ else:
272
+ cache[node_uuid] = value
273
+
274
+ return cache
275
+
276
+ def clear_cache(self):
277
+ self.cache_from_last_execute = {}
278
+
279
+ def experimental_compile(
280
+ self,
281
+ _submit_timeout: Optional[float] = None,
282
+ _buffer_size_bytes: Optional[int] = None,
283
+ enable_asyncio: bool = False,
284
+ _max_inflight_executions: Optional[int] = None,
285
+ _max_buffered_results: Optional[int] = None,
286
+ _overlap_gpu_communication: Optional[bool] = None,
287
+ _default_communicator: Optional[Union[Communicator, str]] = "create",
288
+ ) -> "ray.dag.CompiledDAG":
289
+ """Compile an accelerated execution path for this DAG.
290
+
291
+ Args:
292
+ _submit_timeout: The maximum time in seconds to wait for execute() calls.
293
+ None means using default timeout, 0 means immediate timeout
294
+ (immediate success or timeout without blocking), -1 means
295
+ infinite timeout (block indefinitely).
296
+ _buffer_size_bytes: The initial buffer size in bytes for messages
297
+ that can be passed between tasks in the DAG. The buffers will
298
+ be automatically resized if larger messages are written to the
299
+ channel.
300
+ enable_asyncio: Whether to enable asyncio for this DAG.
301
+ _max_inflight_executions: The maximum number of in-flight executions that
302
+ can be submitted via `execute` or `execute_async` before consuming
303
+ the output using `ray.get()`. If the caller submits more executions,
304
+ `RayCgraphCapacityExceeded` is raised.
305
+ _max_buffered_results: The maximum number of results that can be
306
+ buffered at the driver. If more than this number of results
307
+ are buffered, `RayCgraphCapacityExceeded` is raised. Note that
308
+ when result corresponding to an execution is retrieved
309
+ (by calling `ray.get()` on a `CompiledDAGRef` or
310
+ `CompiledDAGRef` or await on a `CompiledDAGFuture`), results
311
+ corresponding to earlier executions that have not been retrieved
312
+ yet are buffered.
313
+ _overlap_gpu_communication: (experimental) Whether to overlap GPU
314
+ communication with computation during DAG execution. If True, the
315
+ communication and computation can be overlapped, which can improve
316
+ the performance of the DAG execution. If None, the default value
317
+ will be used.
318
+ _default_communicator: The default communicator to use to transfer
319
+ tensors. Three types of values are valid. (1) Communicator:
320
+ For p2p operations, this is the default communicator
321
+ to use for nodes annotated with `with_tensor_transport()` and when
322
+ shared memory is not the desired option (e.g., when transport="nccl",
323
+ or when transport="auto" for communication between two different GPUs).
324
+ For collective operations, this is the default communicator to use
325
+ when a custom communicator is not specified.
326
+ (2) "create": for each collective operation without a custom communicator
327
+ specified, a communicator is created and initialized on its involved actors,
328
+ or an already created communicator is reused if the set of actors is the same.
329
+ For all p2p operations without a custom communicator specified, it reuses
330
+ an already created collective communicator if the p2p actors are a subset.
331
+ Otherwise, a new communicator is created.
332
+ (3) None: a ValueError will be thrown if a custom communicator is not specified.
333
+
334
+ Returns:
335
+ A compiled DAG.
336
+ """
337
+ from ray.dag import DAGContext
338
+
339
+ ctx = DAGContext.get_current()
340
+ if _buffer_size_bytes is None:
341
+ _buffer_size_bytes = ctx.buffer_size_bytes
342
+
343
+ # Validate whether this DAG node has already been compiled.
344
+ if self.is_cgraph_output_node:
345
+ raise ValueError(
346
+ "It is not allowed to call `experimental_compile` on the same DAG "
347
+ "object multiple times no matter whether `teardown` is called or not. "
348
+ "Please reuse the existing compiled DAG or create a new one."
349
+ )
350
+ # Whether this node is an output node in the DAG. We cannot determine
351
+ # this in the constructor because the output node is determined when
352
+ # `experimental_compile` is called.
353
+ self.is_cgraph_output_node = True
354
+ return build_compiled_dag_from_ray_dag(
355
+ self,
356
+ _submit_timeout,
357
+ _buffer_size_bytes,
358
+ enable_asyncio,
359
+ _max_inflight_executions,
360
+ _max_buffered_results,
361
+ _overlap_gpu_communication,
362
+ _default_communicator,
363
+ )
364
+
365
+ def execute(
366
+ self, *args, _ray_cache_refs: bool = False, **kwargs
367
+ ) -> Union[ray.ObjectRef, "ray.actor.ActorHandle"]:
368
+ """Execute this DAG using the Ray default executor _execute_impl().
369
+
370
+ Args:
371
+ _ray_cache_refs: If true, stores the default executor's return values
372
+ on each node in this DAG in a cache. These should be a mix of:
373
+ - ray.ObjectRefs pointing to the outputs of method and function nodes
374
+ - Serve handles for class nodes
375
+ - resolved values representing user input at runtime
376
+ """
377
+
378
+ def executor(node):
379
+ return node._execute_impl(*args, **kwargs)
380
+
381
+ result = self.apply_recursive(executor)
382
+ if _ray_cache_refs:
383
+ self.cache_from_last_execute = executor.cache
384
+ return result
385
+
386
+ def _get_toplevel_child_nodes(self) -> List["DAGNode"]:
387
+ """Return the list of nodes specified as top-level args.
388
+
389
+ For example, in `f.remote(a, [b])`, only `a` is a top-level arg.
390
+
391
+ This list of nodes are those that are typically resolved prior to
392
+ task execution in Ray. This does not include nodes nested within args.
393
+ For that, use ``_get_all_child_nodes()``.
394
+ """
395
+
396
+ # we use List instead of Set here because the hash key of the node
397
+ # object changes each time we create it. So if using Set here, the
398
+ # order of returned children can be different if we create the same
399
+ # nodes and dag one more time.
400
+ children = []
401
+ for a in self.get_args():
402
+ if isinstance(a, DAGNode):
403
+ if a not in children:
404
+ children.append(a)
405
+ for a in self.get_kwargs().values():
406
+ if isinstance(a, DAGNode):
407
+ if a not in children:
408
+ children.append(a)
409
+ for a in self.get_other_args_to_resolve().values():
410
+ if isinstance(a, DAGNode):
411
+ if a not in children:
412
+ children.append(a)
413
+ return children
414
+
415
+ def _get_all_child_nodes(self) -> List["DAGNode"]:
416
+ """Return the list of nodes referenced by the args, kwargs, and
417
+ args_to_resolve in current node, even they're deeply nested.
418
+
419
+ Examples:
420
+ f.remote(a, [b]) -> [a, b]
421
+ f.remote(a, [b], key={"nested": [c]}) -> [a, b, c]
422
+ """
423
+
424
+ scanner = _PyObjScanner()
425
+ # we use List instead of Set here, reason explained
426
+ # in `_get_toplevel_child_nodes`.
427
+ children = []
428
+ for n in scanner.find_nodes(
429
+ [
430
+ self._bound_args,
431
+ self._bound_kwargs,
432
+ self._bound_other_args_to_resolve,
433
+ ]
434
+ ):
435
+ if n not in children:
436
+ children.append(n)
437
+ scanner.clear()
438
+ return children
439
+
440
+ def _apply_and_replace_all_child_nodes(
441
+ self, fn: "Callable[[DAGNode], T]"
442
+ ) -> "DAGNode":
443
+ """Apply and replace all immediate child nodes using a given function.
444
+
445
+ This is a shallow replacement only. To recursively transform nodes in
446
+ the DAG, use ``apply_recursive()``.
447
+
448
+ Args:
449
+ fn: Callable that will be applied once to each child of this node.
450
+
451
+ Returns:
452
+ New DAGNode after replacing all child nodes.
453
+ """
454
+
455
+ replace_table = {}
456
+ # CloudPickler scanner object for current layer of DAGNode. Same
457
+ # scanner should be use for a full find & replace cycle.
458
+ scanner = _PyObjScanner()
459
+ # Find all first-level nested DAGNode children in args.
460
+ # Update replacement table and execute the replace.
461
+ for node in scanner.find_nodes(
462
+ [
463
+ self._bound_args,
464
+ self._bound_kwargs,
465
+ self._bound_other_args_to_resolve,
466
+ ]
467
+ ):
468
+ if node not in replace_table:
469
+ replace_table[node] = fn(node)
470
+ new_args, new_kwargs, new_other_args_to_resolve = scanner.replace_nodes(
471
+ replace_table
472
+ )
473
+ scanner.clear()
474
+
475
+ # Return updated copy of self.
476
+ return self._copy(
477
+ new_args, new_kwargs, self.get_options(), new_other_args_to_resolve
478
+ )
479
+
480
+ def apply_recursive(self, fn: "Callable[[DAGNode], T]") -> T:
481
+ """Apply callable on each node in this DAG in a bottom-up tree walk.
482
+
483
+ Args:
484
+ fn: Callable that will be applied once to each node in the
485
+ DAG. It will be applied recursively bottom-up, so nodes can
486
+ assume the fn has been applied to their args already.
487
+
488
+ Returns:
489
+ Return type of the fn after application to the tree.
490
+ """
491
+
492
+ if not type(fn).__name__ == "_CachingFn":
493
+
494
+ class _CachingFn:
495
+ def __init__(self, fn):
496
+ self.cache = {}
497
+ self.fn = fn
498
+ self.fn.cache = self.cache
499
+ self.input_node_uuid = None
500
+
501
+ def __call__(self, node: "DAGNode"):
502
+ from ray.dag.input_node import InputNode
503
+
504
+ if node._stable_uuid not in self.cache:
505
+ self.cache[node._stable_uuid] = self.fn(node)
506
+ if isinstance(node, InputNode):
507
+ if not self.input_node_uuid:
508
+ self.input_node_uuid = node._stable_uuid
509
+ elif self.input_node_uuid != node._stable_uuid:
510
+ raise AssertionError(
511
+ "Each DAG should only have one unique InputNode."
512
+ )
513
+ return self.cache[node._stable_uuid]
514
+
515
+ fn = _CachingFn(fn)
516
+ else:
517
+ if self._stable_uuid in fn.cache:
518
+ return fn.cache[self._stable_uuid]
519
+
520
+ return fn(
521
+ self._apply_and_replace_all_child_nodes(
522
+ lambda node: node.apply_recursive(fn)
523
+ )
524
+ )
525
+
526
+ def traverse_and_apply(self, fn: "Callable[[DAGNode], T]"):
527
+ """
528
+ Traverse all nodes in the connected component of the DAG that contains
529
+ the `self` node, and apply the given function to each node.
530
+ """
531
+ visited = set()
532
+ queue = [self]
533
+ cgraph_output_node: Optional[DAGNode] = None
534
+
535
+ while queue:
536
+ node = queue.pop(0)
537
+ if node._args_contain_nested_dag_node:
538
+ self._raise_nested_dag_node_error(node._bound_args)
539
+
540
+ if node not in visited:
541
+ if node.is_cgraph_output_node:
542
+ # Validate whether there are multiple nodes that call
543
+ # `experimental_compile`.
544
+ if cgraph_output_node is not None:
545
+ raise ValueError(
546
+ "The DAG was compiled more than once. The following two "
547
+ "nodes call `experimental_compile`: "
548
+ f"(1) {cgraph_output_node}, (2) {node}"
549
+ )
550
+ cgraph_output_node = node
551
+ fn(node)
552
+ visited.add(node)
553
+ """
554
+ Add all unseen downstream and upstream nodes to the queue.
555
+ This function should be called by the root of the DAG. However,
556
+ in some invalid cases, some nodes may not be descendants of the
557
+ root. Therefore, we also add upstream nodes to the queue so that
558
+ a meaningful error message can be raised when the DAG is compiled.
559
+
560
+ ```
561
+ with InputNode() as inp:
562
+ dag = MultiOutputNode([a1.inc.bind(inp), a2.inc.bind(1)])
563
+ ```
564
+
565
+ In the above example, `a2.inc` is not a descendant of inp. If we only
566
+ add downstream nodes to the queue, the `a2.inc` node will not be visited
567
+ , and the error message will be hard to understand, such as a key error
568
+ in the compiled DAG.
569
+ """
570
+ for neighbor in chain.from_iterable(
571
+ [node._downstream_nodes, node._upstream_nodes]
572
+ ):
573
+ if neighbor not in visited:
574
+ queue.append(neighbor)
575
+
576
+ def _raise_nested_dag_node_error(self, args):
577
+ """
578
+ Raise an error for nested DAGNodes in Ray Compiled Graphs.
579
+
580
+ Args:
581
+ args: The arguments of the DAGNode.
582
+ """
583
+ for arg in args:
584
+ if isinstance(arg, DAGNode):
585
+ continue
586
+ else:
587
+ scanner = _PyObjScanner()
588
+ dag_nodes = scanner.find_nodes([arg])
589
+ scanner.clear()
590
+ if len(dag_nodes) > 0:
591
+ raise ValueError(
592
+ f"Found {len(dag_nodes)} DAGNodes from the arg {arg} "
593
+ f"in {self}. Please ensure that the argument is a "
594
+ "single DAGNode and that a DAGNode is not allowed to "
595
+ "be placed inside any type of container."
596
+ )
597
+ raise AssertionError(
598
+ "A DAGNode's args should contain nested DAGNodes as args, "
599
+ "but none were found during the compilation process. This is a "
600
+ "Ray internal error. Please report this issue to the Ray team."
601
+ )
602
+
603
+ def _find_root(self) -> "DAGNode":
604
+ """
605
+ Return the root node of the DAG. The root node must be an InputNode.
606
+ """
607
+ from ray.dag.input_node import InputNode
608
+
609
+ node = self
610
+ while not isinstance(node, InputNode):
611
+ if len(node._upstream_nodes) == 0:
612
+ raise ValueError(
613
+ "No InputNode found in the DAG: when traversing upwards, "
614
+ f"no upstream node was found for {node}."
615
+ )
616
+ node = node._upstream_nodes[0]
617
+ return node
618
+
619
+ def apply_functional(
620
+ self,
621
+ source_input_list: Any,
622
+ predicate_fn: Callable,
623
+ apply_fn: Callable,
624
+ ):
625
+ """
626
+ Apply a given function to DAGNodes in source_input_list, and return
627
+ the replaced inputs without mutating or coping any DAGNode.
628
+
629
+ Args:
630
+ source_input_list: Source inputs to extract and apply function on
631
+ all children DAGNode instances.
632
+ predicate_fn: Applied on each DAGNode instance found and determine
633
+ if we should apply function to it. Can be used to filter node
634
+ types.
635
+ apply_fn: Function to apply on the node on bound attributes. Example::
636
+
637
+ apply_fn = lambda node: node._get_serve_deployment_handle(
638
+ node._deployment, node._bound_other_args_to_resolve
639
+ )
640
+
641
+ Returns:
642
+ replaced_inputs: Outputs of apply_fn on DAGNodes in
643
+ source_input_list that passes predicate_fn.
644
+ """
645
+ replace_table = {}
646
+ scanner = _PyObjScanner()
647
+ for node in scanner.find_nodes(source_input_list):
648
+ if predicate_fn(node) and node not in replace_table:
649
+ replace_table[node] = apply_fn(node)
650
+
651
+ replaced_inputs = scanner.replace_nodes(replace_table)
652
+ scanner.clear()
653
+
654
+ return replaced_inputs
655
+
656
+ def _execute_impl(
657
+ self, *args, **kwargs
658
+ ) -> Union[ray.ObjectRef, "ray.actor.ActorHandle"]:
659
+ """Execute this node, assuming args have been transformed already."""
660
+ raise NotImplementedError
661
+
662
+ def _copy_impl(
663
+ self,
664
+ new_args: List[Any],
665
+ new_kwargs: Dict[str, Any],
666
+ new_options: Dict[str, Any],
667
+ new_other_args_to_resolve: Dict[str, Any],
668
+ ) -> "DAGNode":
669
+ """Return a copy of this node with the given new args."""
670
+ raise NotImplementedError
671
+
672
+ def _copy(
673
+ self,
674
+ new_args: List[Any],
675
+ new_kwargs: Dict[str, Any],
676
+ new_options: Dict[str, Any],
677
+ new_other_args_to_resolve: Dict[str, Any],
678
+ ) -> "DAGNode":
679
+ """Return a copy of this node with the given new args."""
680
+ instance = self._copy_impl(
681
+ new_args, new_kwargs, new_options, new_other_args_to_resolve
682
+ )
683
+ instance._stable_uuid = self._stable_uuid
684
+ instance._type_hint = copy.deepcopy(self._type_hint)
685
+ instance._original_type_hint = copy.deepcopy(self._original_type_hint)
686
+ return instance
687
+
688
+ def __getstate__(self):
689
+ """Required due to overriding `__getattr__` else pickling fails."""
690
+ return self.__dict__
691
+
692
+ def __setstate__(self, d: Dict[str, Any]):
693
+ """Required due to overriding `__getattr__` else pickling fails."""
694
+ self.__dict__.update(d)
695
+
696
+ def __getattr__(self, attr: str):
697
+ if attr == "bind":
698
+ raise AttributeError(f".bind() cannot be used again on {type(self)} ")
699
+ elif attr == "remote":
700
+ raise AttributeError(
701
+ f".remote() cannot be used on {type(self)}. To execute the task "
702
+ "graph for this node, use .execute()."
703
+ )
704
+ else:
705
+ return self.__getattribute__(attr)
lib/python3.12/site-packages/ray/dag/dag_node_operation.py ADDED
@@ -0,0 +1,853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import heapq
3
+ import logging
4
+ from collections import defaultdict
5
+ from enum import Enum
6
+ from functools import total_ordering
7
+ from typing import Dict, List, Optional, Set, Tuple
8
+
9
+ import ray
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class _DAGNodeOperationType(Enum):
15
+ """
16
+ There are three types of operations that a DAG node can perform:
17
+ 1. READ: Read from an input channel.
18
+ 2. COMPUTE: Execute the method corresponding to the node.
19
+ 3. WRITE: Write to an output channel.
20
+ """
21
+
22
+ READ = "READ"
23
+ COMPUTE = "COMPUTE"
24
+ WRITE = "WRITE"
25
+
26
+ def viz_str(self):
27
+ """
28
+ A string representation of the operation type to be used in visualization.
29
+
30
+ The result string is a single character because conciseness is preferred.
31
+ """
32
+ if self == _DAGNodeOperationType.READ:
33
+ return "R"
34
+ elif self == _DAGNodeOperationType.COMPUTE:
35
+ return "C"
36
+ elif self == _DAGNodeOperationType.WRITE:
37
+ return "W"
38
+ assert False, f"Unknown operation type: {self}"
39
+
40
+
41
+ class _DAGNodeOperation:
42
+ def __init__(
43
+ self,
44
+ exec_task_idx: int,
45
+ operation_type: _DAGNodeOperationType,
46
+ method_name: Optional[str] = None,
47
+ ):
48
+ """
49
+ Args:
50
+ exec_task_idx: The index of the task that this operation belongs to
51
+ in the actor's ExecutableTask list. The index is not the same
52
+ as bind_index because there may be more tasks bound to an actor
53
+ than tasks that appear in the current compiled DAG.
54
+ operation_type: The type of operation to perform.
55
+ method_name: The name of the method that this operation originates
56
+ from. This is only for visualization and debugging purposes.
57
+ """
58
+ self.exec_task_idx = exec_task_idx
59
+ self.type = operation_type
60
+ self.method_name = method_name
61
+
62
+ def __repr__(self):
63
+ return (
64
+ f"_DAGNodeOperation("
65
+ f"exec_task_idx: {self.exec_task_idx}, "
66
+ f"type: {self.type}, "
67
+ f"method_name: {self.method_name})"
68
+ )
69
+
70
+ def viz_str(self):
71
+ """
72
+ A string representation of the node to be used in visualization.
73
+ """
74
+ return f"[{self.exec_task_idx}] {self.method_name} {self.type.viz_str()}"
75
+
76
+ def __hash__(self):
77
+ return hash((self.exec_task_idx, self.type))
78
+
79
+ def __eq__(self, other):
80
+ # An operation is uniquely identified by its `exec_task_idx` and type.
81
+ # `method_name` is only for debugging purposes.
82
+ return self.exec_task_idx == other.exec_task_idx and self.type == other.type
83
+
84
+
85
+ @total_ordering
86
+ class _DAGOperationGraphNode:
87
+ def __init__(
88
+ self,
89
+ operation: _DAGNodeOperation,
90
+ task_idx: int,
91
+ actor_handle: "ray.actor.ActorHandle",
92
+ requires_accelerator: bool,
93
+ ):
94
+ """
95
+ _DAGOperationGraphNode represents a node in the DAG operation graph.
96
+ It contains information about the node's in-degree, out-degree, edges,
97
+ and the operation it performs.
98
+
99
+ Args:
100
+ operation: The operation that this node performs. The operation
101
+ can be a READ, COMPUTE, or WRITE operation.
102
+ task_idx: A unique index which can be used to index into
103
+ `CompiledDAG.idx_to_task` to get the corresponding task.
104
+ actor_handle: The actor handle to which this operation belongs.
105
+ requires_accelerator: Whether this operation requires accelerator.
106
+ """
107
+ self.operation = operation
108
+ self.task_idx = task_idx
109
+ self.actor_handle = actor_handle
110
+ self.requires_accelerator = requires_accelerator
111
+ # The in_edges and out_edges are dicts of tuples to strings.
112
+ # Each tuple (the key) contains an integer `task_idx`, which can be
113
+ # used to index into `idx_to_task` to get the corresponding task,
114
+ # and a `_DAGNodeOperationType`, which can be READ, COMPUTE, or WRITE.
115
+ # The string (the value) is the visualization information of the edge,
116
+ # it is a tuple of a label of the edge and a boolean indicating whether
117
+ # the edge is a control dependency.
118
+ self.in_edges: Dict[Tuple[int, _DAGNodeOperationType], Tuple[str, bool]] = {}
119
+ self.out_edges: Dict[Tuple[int, _DAGNodeOperationType], Tuple[str, bool]] = {}
120
+ # The synchronous nodes are all the nodes that belong to the same accelerator
121
+ # operation. Each node is represented by a tuple of its task idx and type.
122
+ self.sync_idxs: Set[Tuple[int, _DAGNodeOperationType]] = set()
123
+ # The pending synchronous nodes are the nodes that are pending to be executed,
124
+ # i.e., their in-degrees are zero. When a synchronous node is pending, it
125
+ # will be added to the pending synchronous nodes of all the nodes in the
126
+ # accelerator operation.
127
+ self.pending_sync_idxs: Set[Tuple[int, _DAGNodeOperationType]] = set()
128
+
129
+ def __repr__(self):
130
+ return (
131
+ f"_DAGOperationGraphNode("
132
+ f"operation: {self.operation}, "
133
+ f"task_idx: {self.task_idx}, "
134
+ f"actor_id: {self.actor_handle._ray_actor_id}, "
135
+ f"requires_accelerator: {self.requires_accelerator})"
136
+ )
137
+
138
+ def __lt__(self, other: "_DAGOperationGraphNode"):
139
+ """
140
+ This function defines the order of the nodes in the priority queue used in
141
+ `_select_next_nodes`. The priority queue is a min-heap, so the node with
142
+ higher priority is considered "less than" the other node.
143
+ """
144
+ if self.is_accelerator_op != other.is_accelerator_op:
145
+ # When one node is an accelerator operation and the other is not,
146
+ # prioritize the accelerator operation.
147
+ return self.is_accelerator_op
148
+ else:
149
+ # When either both nodes are accelerator operations or both nodes
150
+ # are not accelerator operations, prioritize the earlier task within
151
+ # the same actor and load balance tasks across actors. The tie is
152
+ # broken by the `task_idx`.
153
+ return (self.operation.exec_task_idx, self.task_idx) < (
154
+ other.operation.exec_task_idx,
155
+ other.task_idx,
156
+ )
157
+
158
+ def __eq__(self, other: "_DAGOperationGraphNode"):
159
+ """
160
+ Two operations are equal only when they have the same `exec_task_idx` and `type`
161
+ and belong to the same actor.
162
+ """
163
+ return (
164
+ self.actor_handle == other.actor_handle
165
+ and self.operation.exec_task_idx == other.operation.exec_task_idx
166
+ and self.operation.type == other.operation.type
167
+ )
168
+
169
+ def __hash__(self):
170
+ """
171
+ An operation is uniquely identified by its `task_idx` and type.
172
+ """
173
+ return hash((self.operation, self.task_idx))
174
+
175
+ @property
176
+ def in_degree(self) -> int:
177
+ return len(self.in_edges)
178
+
179
+ @property
180
+ def is_ready(self) -> bool:
181
+ """
182
+ If a node is not an accelerator operation, it is ready when it has a zero
183
+ in-degree.
184
+ If it is an accelerator operation, it is ready when all the nodes in the
185
+ operation have zero in-degrees.
186
+ """
187
+ return self.in_degree == 0 and (
188
+ len(self.pending_sync_idxs) == len(self.sync_idxs)
189
+ )
190
+
191
+ @property
192
+ def is_read(self) -> bool:
193
+ return self.operation.type == _DAGNodeOperationType.READ
194
+
195
+ @property
196
+ def is_accelerator_read(self) -> bool:
197
+ """
198
+ A node is an accelerator read if it is a read node and requires accelerator.
199
+ """
200
+ return (
201
+ self.operation.type == _DAGNodeOperationType.READ
202
+ and self.requires_accelerator
203
+ )
204
+
205
+ @property
206
+ def is_accelerator_compute(self) -> bool:
207
+ """
208
+ A node is an accelerator compute if it is a compute node and requires accelerator.
209
+ """
210
+ return (
211
+ self.operation.type == _DAGNodeOperationType.COMPUTE
212
+ and self.requires_accelerator
213
+ )
214
+
215
+ @property
216
+ def is_accelerator_write(self) -> bool:
217
+ """
218
+ A node is an accelerator write if it is a write node and requires accelerator.
219
+ """
220
+ return (
221
+ self.operation.type == _DAGNodeOperationType.WRITE
222
+ and self.requires_accelerator
223
+ )
224
+
225
+ @property
226
+ def is_accelerator_op(self) -> bool:
227
+ return (
228
+ self.is_accelerator_read
229
+ or self.is_accelerator_compute
230
+ or self.is_accelerator_write
231
+ )
232
+
233
+ def viz_str(self):
234
+ """
235
+ A string representation of the node to be used in visualization.
236
+ """
237
+ return self.operation.viz_str()
238
+
239
+ @property
240
+ def _actor_id(self):
241
+ return self.actor_handle._ray_actor_id.hex()
242
+
243
+
244
+ def _add_edge(
245
+ from_node: _DAGOperationGraphNode,
246
+ to_node: _DAGOperationGraphNode,
247
+ label: str = "",
248
+ control_dependency: bool = False,
249
+ ):
250
+ """
251
+ Add an edge from `from_node` to `to_node`.
252
+
253
+ Args:
254
+ from_node: The node from which the edge originates.
255
+ to_node: The node to which the edge points.
256
+ label: The label of the edge. This will be used to annotate the edge
257
+ in the visualization of the execution schedule.
258
+ """
259
+ from_node.out_edges[(to_node.task_idx, to_node.operation.type)] = (
260
+ label,
261
+ control_dependency,
262
+ )
263
+ to_node.in_edges[(from_node.task_idx, from_node.operation.type)] = (
264
+ label,
265
+ control_dependency,
266
+ )
267
+
268
+
269
+ def _update_pending_sync_idxs(
270
+ graph: Dict[int, Dict[_DAGNodeOperationType, _DAGOperationGraphNode]],
271
+ node: _DAGOperationGraphNode,
272
+ ) -> None:
273
+ """
274
+ Update the node as pending for its synchronous nodes.
275
+ """
276
+ idx = (node.task_idx, node.operation.type)
277
+ for task_idx, op_type in node.sync_idxs:
278
+ sync_node = graph[task_idx][op_type]
279
+ sync_node.pending_sync_idxs.add(idx)
280
+
281
+
282
+ def _push_candidate_node_if_ready(
283
+ actor_to_candidates: Dict["ray._raylet.ActorID", List[_DAGOperationGraphNode]],
284
+ graph: Dict[int, Dict[_DAGNodeOperationType, _DAGOperationGraphNode]],
285
+ node: _DAGOperationGraphNode,
286
+ ) -> None:
287
+ """
288
+ Push the node with a zero in-degree to the candidates if its operation is ready.
289
+ If it has synchronous nodes, its accelerator operation is not ready until all
290
+ the nodes are pending, then all the nodes will be pushed to the candidates.
291
+ """
292
+ assert node.in_degree == 0, "Expected to have a zero in-degree"
293
+ # For the accelerator write node, update the in-degrees of the downstream
294
+ # accelerator read nodes and update them as pending. This is necessary because
295
+ # the data dependency edges between accelerator write and read nodes are only
296
+ # updated here. The accelerator P2P operation becomes ready after both the write
297
+ # and read nodes are marked as pending.
298
+ if node.is_accelerator_write:
299
+ for task_idx, op_type in node.out_edges:
300
+ read_node = graph[task_idx][op_type]
301
+ read_node.in_edges.pop((node.task_idx, node.operation.type))
302
+ assert read_node.is_accelerator_read and len(read_node.in_edges) == 0
303
+ _update_pending_sync_idxs(graph, read_node)
304
+ # For the accelerator operation node, update it as pending.
305
+ if len(node.sync_idxs) != 0:
306
+ _update_pending_sync_idxs(graph, node)
307
+ # The accelerator operation is ready when all the nodes have zero in-degrees.
308
+ # When the last node in the operation is updated as pending, push all the nodes
309
+ # to the candidates.
310
+ if node.is_ready:
311
+ if len(node.sync_idxs) == 0:
312
+ heapq.heappush(
313
+ actor_to_candidates[node.actor_handle._actor_id],
314
+ node,
315
+ )
316
+ else:
317
+ for task_idx, op_type in node.sync_idxs:
318
+ sync_node = graph[task_idx][op_type]
319
+ heapq.heappush(
320
+ actor_to_candidates[sync_node.actor_handle._actor_id],
321
+ sync_node,
322
+ )
323
+
324
+
325
+ def _select_next_nodes(
326
+ actor_to_candidates: Dict["ray._raylet.ActorID", List[_DAGOperationGraphNode]],
327
+ graph: Dict[int, Dict[_DAGNodeOperationType, _DAGOperationGraphNode]],
328
+ ) -> Optional[List[_DAGOperationGraphNode]]:
329
+ """
330
+ This function selects the next nodes for the topological sort to generate
331
+ execution schedule. If there are multiple candidate _DAGOperationGraphNodes,
332
+ select the node with the top priority. The priority is defined in
333
+ `_DAGOperationGraphNode.__lt__`.
334
+
335
+ For the implementation details, we maintain a priority queue for each actor,
336
+ where the head of the priority queue is the node with the smallest `exec_task_idx`.
337
+ When a node has a zero in-degree, it is added to the corresponding actor's
338
+ priority queue. For a node other than an accelerator collective node, it is ready to be
339
+ executed if it has a zero in-degree. For an accelerator collective node, it is ready to
340
+ be executed when all the nodes in its collective operation have zero in-degrees.
341
+
342
+ If a node is an accelerator collective node, it updates the `ready_collective_nodes` of
343
+ all the nodes in its collective operation. Unless all the nodes in its collective
344
+ group have zero in-degrees, this node is removed from the candidate list.
345
+ Eventually, exactly one accelerator collective node from its collective operation is
346
+ selected from the candidate list.
347
+
348
+ If the selected node is an accelerator write node, select all the downstream accelerator
349
+ read nodes. If the selected node is an accelerator collective node, select all the accelerator
350
+ compute nodes in its collective operation.
351
+
352
+ Args:
353
+ actor_to_candidates: A dictionary mapping an actor id to a list of
354
+ candidate nodes. The list is maintained as a priority queue, so
355
+ the head of the queue, i.e., `candidates[0]`, is the node with
356
+ the smallest `bind_index`.
357
+ graph: A dictionary mapping the index of a task to a dictionary of its
358
+ _DAGOperationGraphNodes for different operations.
359
+
360
+ Returns:
361
+ A list of _DAGOperationGraphNodes to be placed into the corresponding
362
+ execution schedules.
363
+ """
364
+ top_priority_node = None
365
+ for candidates in actor_to_candidates.values():
366
+ if len(candidates) == 0:
367
+ continue
368
+ if top_priority_node is None or candidates[0] < top_priority_node:
369
+ top_priority_node = candidates[0]
370
+
371
+ if top_priority_node is None:
372
+ return None
373
+ next_nodes = [top_priority_node]
374
+
375
+ # Select all the synchronous nodes in the accelerator operation.
376
+ if len(top_priority_node.sync_idxs) != 0:
377
+ for task_idx, op_type in top_priority_node.sync_idxs:
378
+ node = graph[task_idx][op_type]
379
+ if node != top_priority_node:
380
+ next_nodes.append(node)
381
+
382
+ # Remove the selected nodes from the candidates.
383
+ for node in next_nodes:
384
+ candidates = actor_to_candidates[node.actor_handle._actor_id]
385
+ candidates.remove(node)
386
+ heapq.heapify(candidates)
387
+
388
+ # Remove the selected nodes from the candidates.
389
+ for node in next_nodes:
390
+ candidates = actor_to_candidates[node.actor_handle._actor_id]
391
+ # The accelerator read nodes are not added to the candidates.
392
+ if node in candidates:
393
+ candidates.remove(node)
394
+ heapq.heapify(candidates)
395
+
396
+ return next_nodes
397
+
398
+
399
+ def _build_dag_node_operation_graph(
400
+ idx_to_task: Dict[int, "ray.dag.compiled_dag_node.CompiledTask"],
401
+ actor_to_operation_nodes: Dict[
402
+ "ray.actor.ActorHandle", List[List[_DAGOperationGraphNode]]
403
+ ],
404
+ ) -> Dict[int, Dict[_DAGNodeOperationType, _DAGOperationGraphNode]]:
405
+ """
406
+ Generate a DAG node operation graph by adding edges based on the
407
+ following rules:
408
+
409
+ #1 Add edges from READ to COMPUTE, and from COMPUTE to WRITE, which
410
+ belong to the same task.
411
+ #2 Add an edge from COMPUTE with bind_index i to COMPUTE with bind_index
412
+ i+1 if they belong to the same actor.
413
+ #3 Add an edge from WRITE of the writer task to READ of the reader task.
414
+
415
+ This is the step one of building an execution schedule for each actor.
416
+
417
+ Args:
418
+ idx_to_task: A dictionary that maps the `task_idx` to the `CompiledTask`.
419
+ `CompiledTask` contains information about a DAGNode and its downstream
420
+ nodes.
421
+
422
+ actor_to_operation_nodes: A dictionary that maps an actor handle to
423
+ a list of lists of _DAGOperationGraphNode. For the same actor, the
424
+ index of the outer list corresponds to the index of the ExecutableTask
425
+ in the list of `executable_tasks` in `actor_to_executable_tasks`. In
426
+ the inner list, the order of operations is READ, COMPUTE, and WRITE.
427
+
428
+ Returns:
429
+ A graph where each node is a _DAGOperationGraphNode. The key is `task_idx`,
430
+ the index to retrieve its task from `idx_to_task`, and the value is a
431
+ dictionary that maps the _DAGNodeOperationType (READ, COMPUTE, or WRITE)
432
+ to the corresponding _DAGOperationGraphNode
433
+ """
434
+ assert idx_to_task
435
+ graph: Dict[int, Dict[_DAGNodeOperationType, _DAGOperationGraphNode]] = {}
436
+
437
+ for _, operation_nodes_list in actor_to_operation_nodes.items():
438
+ prev_compute_node = None
439
+ for operation_nodes in operation_nodes_list:
440
+ task_idx = operation_nodes[0].task_idx
441
+ read_node, compute_node, write_node = (
442
+ operation_nodes[0],
443
+ operation_nodes[1],
444
+ operation_nodes[2],
445
+ )
446
+ # Add edges from READ to COMPUTE, and from COMPUTE to WRITE, which
447
+ # belong to the same task.
448
+ _add_edge(read_node, compute_node)
449
+ _add_edge(compute_node, write_node)
450
+ # Add an edge from COMPUTE with `bind_index` i to COMPUTE with
451
+ # `bind_index` i+1 if they belong to the same actor.
452
+ if prev_compute_node is not None:
453
+ _add_edge(prev_compute_node, compute_node, "", True)
454
+ prev_compute_node = compute_node
455
+ assert task_idx not in graph
456
+ graph[task_idx] = {
457
+ _DAGNodeOperationType.READ: read_node,
458
+ _DAGNodeOperationType.COMPUTE: compute_node,
459
+ _DAGNodeOperationType.WRITE: write_node,
460
+ }
461
+
462
+ # Import `ray.dag` here to avoid circular import.
463
+ from ray.dag import ClassMethodNode, CollectiveOutputNode, MultiOutputNode
464
+ from ray.dag.collective_node import _CollectiveOperation
465
+
466
+ # Add an edge from WRITE of the writer task to READ of the reader task.
467
+ # Set synchronous nodes for accelerator P2P operations.
468
+ for task_idx, task in idx_to_task.items():
469
+ if not (
470
+ isinstance(task.dag_node, ClassMethodNode)
471
+ or isinstance(task.dag_node, CollectiveOutputNode)
472
+ ):
473
+ # The graph is used to generate an execution schedule for each actor.
474
+ # The edge from the InputNode has no impact on the final execution
475
+ # schedule.
476
+ continue
477
+ if (
478
+ isinstance(task.dag_node, ClassMethodNode)
479
+ and task.dag_node.is_class_method_output
480
+ ):
481
+ # Class method output node dependencies are handled at its upstream:
482
+ # i.e., class method node
483
+ continue
484
+ for downstream_task_idx in task.downstream_task_idxs:
485
+ downstream_dag_node = idx_to_task[downstream_task_idx].dag_node
486
+ if isinstance(downstream_dag_node, MultiOutputNode):
487
+ continue
488
+ write_node = graph[task_idx][_DAGNodeOperationType.WRITE]
489
+ if (
490
+ isinstance(downstream_dag_node, ClassMethodNode)
491
+ and downstream_dag_node.is_class_method_output
492
+ ):
493
+ consumer_idxs = idx_to_task[downstream_task_idx].downstream_task_idxs
494
+ for consumer_idx in consumer_idxs:
495
+ if consumer_idx in graph:
496
+ read_node = graph[consumer_idx][_DAGNodeOperationType.READ]
497
+ _add_edge(
498
+ write_node,
499
+ read_node,
500
+ "accelerator" if write_node.requires_accelerator else "shm",
501
+ )
502
+ if write_node.requires_accelerator:
503
+ idxs = {
504
+ (task_idx, _DAGNodeOperationType.WRITE),
505
+ (consumer_idx, _DAGNodeOperationType.READ),
506
+ }
507
+ for node in [write_node, read_node]:
508
+ node.sync_idxs.update(idxs)
509
+ continue
510
+ read_node = graph[downstream_task_idx][_DAGNodeOperationType.READ]
511
+ _add_edge(
512
+ write_node,
513
+ read_node,
514
+ "accelerator" if write_node.requires_accelerator else "shm",
515
+ )
516
+ if write_node.requires_accelerator:
517
+ idxs = {
518
+ (task_idx, _DAGNodeOperationType.WRITE),
519
+ (downstream_task_idx, _DAGNodeOperationType.READ),
520
+ }
521
+ for node in [write_node, read_node]:
522
+ node.sync_idxs.update(idxs)
523
+
524
+ # Set synchronous nodes for accelerator collective operations.
525
+ collective_op_to_idxs: Dict[
526
+ _CollectiveOperation, Set[Tuple[int, _DAGNodeOperationType]]
527
+ ] = defaultdict(set)
528
+ for task_idx, task in idx_to_task.items():
529
+ if (
530
+ isinstance(task.dag_node, CollectiveOutputNode)
531
+ and not task.dag_node.is_class_method_output
532
+ ):
533
+ collective_op_to_idxs[task.dag_node.collective_op].add(
534
+ (task_idx, _DAGNodeOperationType.COMPUTE)
535
+ )
536
+ for idxs in collective_op_to_idxs.values():
537
+ for task_idx, op_type in idxs:
538
+ graph[task_idx][op_type].sync_idxs = idxs
539
+
540
+ return graph
541
+
542
+
543
+ def _actor_viz_label(actor: "ray.actor.ActorHandle"):
544
+ """
545
+ Returns the label of an actor in the visualization of the execution schedule.
546
+
547
+ Args:
548
+ actor: The actor to be represented.
549
+ """
550
+ class_name = actor._ray_actor_creation_function_descriptor.class_name
551
+ actor_id = actor._ray_actor_id.hex()
552
+ return f"Actor class name: {class_name}\nActor ID: {actor_id}"
553
+
554
+
555
+ def _node_viz_id_and_label(
556
+ node: _DAGOperationGraphNode, idx: int, optimized_index: int
557
+ ):
558
+ """
559
+ Returns the visualization id and label of a node. The visualization id is unique
560
+ across all nodes.
561
+
562
+ Args:
563
+ node: The node to be represented.
564
+ idx: The index of the node in the execution schedule.
565
+ optimized_index: The index of the node in the optimized execution schedule.
566
+ """
567
+ node_viz_label = node.viz_str() + f" {idx},{optimized_index}"
568
+ node_viz_id = f"{node._actor_id}_{node_viz_label}"
569
+ return node_viz_id, node_viz_label
570
+
571
+
572
+ def _visualize_execution_schedule(
573
+ actor_to_execution_schedule: Dict[
574
+ "ray.actor.ActorHandle", List[_DAGOperationGraphNode]
575
+ ],
576
+ actor_to_overlapped_schedule: Optional[
577
+ Dict["ray.actor.ActorHandle", List[_DAGOperationGraphNode]]
578
+ ],
579
+ graph: Dict[int, Dict[_DAGNodeOperationType, _DAGOperationGraphNode]],
580
+ ):
581
+ """
582
+ Visualize the execution schedule for each actor.
583
+
584
+ The visualization will be saved as a PNG file named `compiled_graph_schedule.png`.
585
+ Details of the visualization: # noqa
586
+
587
+ Node description format:
588
+ [<task_index>] <method_name> <operation> <orig_index>, <overlap_index>
589
+
590
+ Node description fields:
591
+ operation: is R(READ), C(COMPUTE), or W(WRITE)
592
+ orig_index: the index in the original execution schedule
593
+ overlap_index: the index in the overlap-communication optimized execution schedule
594
+ If this is different from orig_index, the node is highlighted in red color
595
+
596
+ Node grouping:
597
+ The nodes belonging to the same actor are grouped in the same rectangle
598
+ The actor class name and the actor id are shown in the rectangle
599
+
600
+ Edges:
601
+ black color (without label): data dependency
602
+ black color (annotated with "shm"): shared memory channel
603
+ blue color (annotated with "accelerator): accelerator channel
604
+ dashed edge: control dependency between compute operations
605
+
606
+ Args:
607
+ actor_to_execution_schedule: A dictionary that maps an actor handle to
608
+ the execution schedule which is a list of operation nodes.
609
+ actor_to_overlapped_schedule: A dictionary that maps an actor handle to the
610
+ optimized execution schedule which is a list of operation nodes.
611
+ graph: A graph where each node is a _DAGOperationGraphNode. The key is
612
+ `task_idx`, the index to retrieve its task from `idx_to_task`, and
613
+ the value is a dictionary that maps the _DAGNodeOperationType (READ,
614
+ COMPUTE, or WRITE) to the corresponding _DAGOperationGraphNode. It is
615
+ generated by `_build_dag_node_operation_graph`.
616
+ """
617
+ try:
618
+ import graphviz
619
+ except ImportError:
620
+ raise ImportError(
621
+ "Please install graphviz to visualize the execution schedule. "
622
+ "You can install it by running `pip install graphviz`."
623
+ )
624
+
625
+ dot = graphviz.Digraph(comment="DAG")
626
+ # A dictionary that maps a node to its visualization id
627
+ node_to_viz_id: Dict[_DAGOperationGraphNode, str] = {}
628
+
629
+ if actor_to_overlapped_schedule is None:
630
+ # TODO(rui): make the visualization more concise by only displaying
631
+ # the original schedule
632
+ actor_to_overlapped_schedule = actor_to_execution_schedule
633
+ for actor, execution_nodes in actor_to_execution_schedule.items():
634
+ overlapped_schedule = actor_to_overlapped_schedule[actor]
635
+ node_to_optimized_index = {
636
+ node: i for i, node in enumerate(overlapped_schedule)
637
+ }
638
+
639
+ actor_id = actor._ray_actor_id.hex()
640
+ with dot.subgraph(name=f"cluster_{actor_id}") as subgraph:
641
+ subgraph.attr(rank=actor_id, label=_actor_viz_label(actor))
642
+ for i, node in enumerate(execution_nodes):
643
+ optimized_index = node_to_optimized_index.get(node)
644
+ node_viz_id, node_viz_label = _node_viz_id_and_label(
645
+ node, i, optimized_index
646
+ )
647
+ color = "red" if optimized_index != i else "black"
648
+ subgraph.node(node_viz_id, node_viz_label, color=color)
649
+ node_to_viz_id[node] = node_viz_id
650
+
651
+ for actor, execution_nodes in actor_to_execution_schedule.items():
652
+ for i, node in enumerate(execution_nodes):
653
+ node_viz_id = node_to_viz_id[node]
654
+ for out_edge, viz_info in node.out_edges.items():
655
+ label, control_dependency = viz_info
656
+ out_task_idx, out_op_type = out_edge
657
+ out_node = graph[out_task_idx][out_op_type]
658
+ out_node_viz_id = node_to_viz_id[out_node]
659
+ color = "blue" if label == "accelerator" else "black"
660
+ style = "dashed" if control_dependency else "solid"
661
+ dot.edge(
662
+ node_viz_id, out_node_viz_id, label=label, color=color, style=style
663
+ )
664
+
665
+ # Add legend
666
+ with dot.subgraph(name="cluster_legend") as legend:
667
+ legend.attr(label="Legend", labelloc="t", fontsize="20", bgcolor="lightgrey")
668
+
669
+ # Single node and its explanation
670
+ legend.node("example_node", "[0] bwd C 10,10\n")
671
+ explanation = (
672
+ '<<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">' # noqa
673
+ '<TR><TD ALIGN="LEFT"><B>Node description format:</B></TD></TR>'
674
+ '<TR><TD ALIGN="LEFT">[&lt;task_index&gt;] &lt;method_name&gt; &lt;operation&gt; &lt;orig_index&gt;, &lt;overlap_index&gt;</TD></TR>' # noqa
675
+ "<TR><TD></TD></TR>"
676
+ '<TR><TD ALIGN="LEFT"><B>Node description fields:</B></TD></TR>'
677
+ '<TR><TD ALIGN="LEFT">operation: is R(READ), C(COMPUTE), or W(WRITE)</TD></TR>' # noqa
678
+ '<TR><TD ALIGN="LEFT">orig_index: the index in the original execution schedule</TD></TR>' # noqa
679
+ '<TR><TD ALIGN="LEFT">overlap_index: the index in the overlap-communication optimized execution schedule</TD></TR>' # noqa
680
+ '<TR><TD ALIGN="LEFT">If this is different from orig_index, the node is highlighted in <FONT COLOR="red">red color</FONT></TD></TR>' # noqa
681
+ "<TR><TD></TD></TR>"
682
+ '<TR><TD ALIGN="LEFT"><B>Node grouping:</B></TD></TR>'
683
+ '<TR><TD ALIGN="LEFT">The nodes belonging to the same actor are grouped in the same rectangle</TD></TR>' # noqa
684
+ '<TR><TD ALIGN="LEFT">The actor class name and the actor id are shown in the rectangle</TD></TR>' # noqa
685
+ "<TR><TD></TD></TR>"
686
+ '<TR><TD ALIGN="LEFT"><B>Edges:</B></TD></TR>'
687
+ '<TR><TD ALIGN="LEFT">black color (without label): data dependency</TD></TR>' # noqa
688
+ '<TR><TD ALIGN="LEFT">black color (annotated with "shm"): shared memory channel</TD></TR>' # noqa
689
+ '<TR><TD ALIGN="LEFT"><FONT COLOR="blue">blue color</FONT> (annotated with "accelerator): accelerator channel</TD></TR>' # noqa
690
+ '<TR><TD ALIGN="LEFT">dashed edge: control dependency between compute operations</TD></TR>' # noqa
691
+ "</TABLE>>"
692
+ )
693
+
694
+ legend.node("example_explanation", explanation, shape="plaintext")
695
+ legend.edge("example_node", "example_explanation", style="invis")
696
+
697
+ logger.info(
698
+ "Writing compiled graph schedule visualization "
699
+ "to compiled_graph_schedule.png"
700
+ )
701
+ dot.render("compiled_graph_schedule", format="png", view=False)
702
+
703
+
704
+ def _generate_actor_to_execution_schedule(
705
+ graph: Dict[int, Dict[_DAGNodeOperationType, _DAGOperationGraphNode]],
706
+ ) -> Dict["ray.actor.ActorHandle", List[_DAGOperationGraphNode]]:
707
+ """
708
+ Generate an execution schedule for each actor. The schedule is a list of
709
+ operation nodes to be executed. The function uses a topological sort
710
+ algorithm to generate the schedule.
711
+
712
+ Args:
713
+ graph: A graph where each node is a _DAGOperationGraphNode. The key is
714
+ `task_idx`, the index to retrieve its task from `idx_to_task`, and
715
+ the value is a dictionary that maps the _DAGNodeOperationType (READ,
716
+ COMPUTE, or WRITE) to the corresponding _DAGOperationGraphNode. It is
717
+ generated by `_build_dag_node_operation_graph`.
718
+
719
+ Returns:
720
+ actor_to_execution_schedule: A dictionary that maps an actor handle to
721
+ the execution schedule which is a list of operation nodes to be
722
+ executed.
723
+ """
724
+
725
+ # Mapping from the actor handle to the execution schedule which is a list
726
+ # of operations to be executed.
727
+ actor_to_execution_schedule: Dict[
728
+ "ray.actor.ActorHandle", List[_DAGOperationGraphNode]
729
+ ] = defaultdict(list)
730
+
731
+ # A dictionary mapping an actor id to a list of candidate nodes. The list
732
+ # is maintained as a priority queue, so the head of the queue, i.e.,
733
+ # `candidates[0]`, is the node with the smallest `bind_index`.
734
+ actor_to_candidates: Dict[
735
+ "ray._raylet.ActorID", List[_DAGOperationGraphNode]
736
+ ] = defaultdict(list)
737
+ for _, node_dict in graph.items():
738
+ for _, node in node_dict.items():
739
+ # A node with a zero in-degree edge means all of its dependencies
740
+ # have been satisfied, including both data and control dependencies.
741
+ # Therefore, it is a candidate for execution.
742
+ if node.in_degree == 0:
743
+ _push_candidate_node_if_ready(actor_to_candidates, graph, node)
744
+
745
+ visited_nodes = set()
746
+
747
+ # Use topological sort algorithm to generate the execution schedule.
748
+ while True:
749
+ # Select a list of nodes to be executed. There are three cases:
750
+ # 1. If a selected node is not an accelerator operation, only itself is returned.
751
+ # 2. If a selected node is an accelerator write operation, the corresponding accelerator
752
+ # read operations are also returned.
753
+ # 3. If a selected node is an accelerator collective operation, all the nodes in
754
+ # its collective operation are returned.
755
+ nodes = _select_next_nodes(actor_to_candidates, graph)
756
+ if nodes is None:
757
+ break
758
+ # Add the selected nodes to the execution schedule.
759
+ for node in nodes:
760
+ assert node not in visited_nodes
761
+ visited_nodes.add(node)
762
+ actor_to_execution_schedule[node.actor_handle].append(node)
763
+ # Update the in-degree of the downstream nodes.
764
+ for node in nodes:
765
+ for out_node_task_idx, out_node_type in node.out_edges:
766
+ out_node = graph[out_node_task_idx][out_node_type]
767
+ if out_node in visited_nodes:
768
+ # If the downstream node is already visited, it has been added
769
+ # to the execution schedule. They are the accelerator read nodes in
770
+ # case 2.
771
+ continue
772
+ out_node.in_edges.pop((node.task_idx, node.operation.type))
773
+ if out_node.in_degree == 0:
774
+ _push_candidate_node_if_ready(actor_to_candidates, graph, out_node)
775
+ assert len(visited_nodes) == len(graph) * 3, "Expected all nodes to be visited"
776
+
777
+ return actor_to_execution_schedule
778
+
779
+
780
+ def _generate_overlapped_execution_schedule(
781
+ actor_to_execution_schedule: Dict[
782
+ "ray.actor.ActorHandle", List[_DAGOperationGraphNode]
783
+ ],
784
+ ) -> Dict["ray.actor.ActorHandle", List[_DAGOperationGraphNode]]:
785
+ """
786
+ From an existing execution schedule, generate a new schedule by overlapping
787
+ computation and communication.
788
+
789
+ Currently, the algorithm generates a new schedule for each actor as follows:
790
+ For each accelerator read operation (i.e., recv), scan backwards to find the nearest
791
+ compute node to swap with so that the accelerator read operation can be overlapped
792
+ with computation.
793
+
794
+ Collective operations are not yet supported.
795
+
796
+ Args:
797
+ actor_to_execution_schedule: A dictionary that maps an actor handle to
798
+ the existing execution schedule for the actor. The schedule is a list
799
+ is a list of operations to be executed.
800
+
801
+ Returns:
802
+ A dictionary that maps an actor handle to the overlapped execution schedule
803
+ for the actor.
804
+ """
805
+
806
+ actor_to_overlapped_schedule: Dict[
807
+ "ray.actor.ActorHandle", List[_DAGOperationGraphNode]
808
+ ] = copy.deepcopy(actor_to_execution_schedule)
809
+ for overlapped_schedule in actor_to_overlapped_schedule.values():
810
+ for i in range(len(overlapped_schedule)):
811
+ if (
812
+ overlapped_schedule[i].operation.type == _DAGNodeOperationType.READ
813
+ and overlapped_schedule[i].requires_accelerator
814
+ ):
815
+ # For each accelerator read operation (i.e., recv), scan backwards
816
+ # to find the nearest compute node to swap with so that
817
+ # the accelerator read operation can be overlapped with computation.
818
+ for j in range(i - 1, -1, -1):
819
+ if (
820
+ overlapped_schedule[j].operation.type
821
+ == _DAGNodeOperationType.COMPUTE
822
+ ):
823
+ # Found a desired compute operation, make the swap
824
+ accelerator_read_op = overlapped_schedule[i]
825
+ prev_ops = overlapped_schedule[j:i]
826
+ overlapped_schedule[j + 1 : i + 1] = prev_ops
827
+ overlapped_schedule[j] = accelerator_read_op
828
+ break
829
+ if (
830
+ overlapped_schedule[j].operation.type
831
+ == _DAGNodeOperationType.READ
832
+ or overlapped_schedule[j].operation.type
833
+ == _DAGNodeOperationType.WRITE
834
+ ) and overlapped_schedule[j].requires_accelerator:
835
+ # Found an accelerator read/write operation, skip the overlap
836
+ # optimization to keep relative order of accelerator operations
837
+ break
838
+ return actor_to_overlapped_schedule
839
+
840
+
841
+ def _extract_execution_schedule(
842
+ actor_to_execution_schedule: Dict[
843
+ "ray.actor.ActorHandle", List[_DAGOperationGraphNode]
844
+ ],
845
+ ) -> Dict["ray.actor.ActorHandle", List[_DAGNodeOperation]]:
846
+ """
847
+ Extract _DAGNodeOperation from _DAGOperationGraphNode in the schedule
848
+ and discard unnecessary information.
849
+ """
850
+ return {
851
+ actor: [node.operation for node in nodes]
852
+ for actor, nodes in actor_to_execution_schedule.items()
853
+ }
lib/python3.12/site-packages/ray/dag/dag_operation_future.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, Generic, TypeVar
3
+
4
+ from ray.experimental.channel.accelerator_context import AcceleratorContext
5
+ from ray.util.annotations import DeveloperAPI
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ @DeveloperAPI
11
+ class DAGOperationFuture(ABC, Generic[T]):
12
+ """
13
+ A future representing the result of a DAG operation.
14
+
15
+ This is an abstraction that is internal to each actor,
16
+ and is not exposed to the DAG caller.
17
+ """
18
+
19
+ @abstractmethod
20
+ def wait(self):
21
+ """
22
+ Wait for the future and return the result of the operation.
23
+ """
24
+ raise NotImplementedError
25
+
26
+
27
+ @DeveloperAPI
28
+ class ResolvedFuture(DAGOperationFuture):
29
+ """
30
+ A future that is already resolved. Calling `wait()` on this will
31
+ immediately return the result without blocking.
32
+ """
33
+
34
+ def __init__(self, result):
35
+ """
36
+ Initialize a resolved future.
37
+
38
+ Args:
39
+ result: The result of the future.
40
+ """
41
+ self._result = result
42
+
43
+ def wait(self):
44
+ """
45
+ Wait and immediately return the result. This operation will not block.
46
+ """
47
+ return self._result
48
+
49
+
50
+ @DeveloperAPI
51
+ class GPUFuture(DAGOperationFuture[Any]):
52
+ """
53
+ A future for a GPU event on a CUDA stream.
54
+
55
+ This future wraps a buffer, and records an event on the given stream
56
+ when it is created. When the future is waited on, it makes the current
57
+ CUDA stream wait on the event, then returns the buffer.
58
+
59
+ The buffer must be a GPU tensor produced by an earlier operation launched
60
+ on the given stream, or it could be CPU data. Then the future guarantees
61
+ that when the wait() returns, the buffer is ready on the current stream.
62
+
63
+ The `wait()` does not block CPU.
64
+ """
65
+
66
+ # Caching GPU futures ensures CUDA events associated with futures are properly
67
+ # destroyed instead of relying on garbage collection. The CUDA event contained
68
+ # in a GPU future is destroyed right before removing the future from the cache.
69
+ # The dictionary key is the future ID, which is the task idx of the dag operation
70
+ # that produced the future. When a future is created, it is immediately added to
71
+ # the cache. When a future has been waited on, it is removed from the cache.
72
+ # When adding a future, if its ID is already a key in the cache, the old future
73
+ # is removed. This can happen when an exception is thrown in a previous execution
74
+ # of the dag, in which case the old future is never waited on.
75
+ # Upon dag teardown, all pending futures produced by the dag are removed.
76
+ gpu_futures: Dict[int, "GPUFuture"] = {}
77
+
78
+ @staticmethod
79
+ def add_gpu_future(fut_id: int, fut: "GPUFuture") -> None:
80
+ """
81
+ Cache the GPU future.
82
+ Args:
83
+ fut_id: GPU future ID.
84
+ fut: GPU future to be cached.
85
+ """
86
+ if fut_id in GPUFuture.gpu_futures:
87
+ # The old future was not waited on because of an execution exception.
88
+ GPUFuture.gpu_futures.pop(fut_id).destroy_event()
89
+ GPUFuture.gpu_futures[fut_id] = fut
90
+
91
+ @staticmethod
92
+ def remove_gpu_future(fut_id: int) -> None:
93
+ """
94
+ Remove the cached GPU future and destroy its CUDA event.
95
+ Args:
96
+ fut_id: GPU future ID.
97
+ """
98
+ if fut_id in GPUFuture.gpu_futures:
99
+ GPUFuture.gpu_futures.pop(fut_id).destroy_event()
100
+
101
+ def __init__(self, buf: Any, fut_id: int, stream: Any = None):
102
+ """
103
+ Initialize a GPU future on the given stream.
104
+
105
+ Args:
106
+ buf: The buffer to return when the future is resolved.
107
+ fut_id: The future ID to cache the future.
108
+ stream: The torch stream to record the event on, this event is waited
109
+ on when the future is resolved. If None, the current stream is used.
110
+ """
111
+ if stream is None:
112
+ stream = AcceleratorContext.get().current_stream()
113
+
114
+ self._buf = buf
115
+ self._event = AcceleratorContext.get().create_event()
116
+ self._event.record(stream)
117
+ self._fut_id = fut_id
118
+ self._waited: bool = False
119
+
120
+ # Cache the GPU future such that its CUDA event is properly destroyed.
121
+ GPUFuture.add_gpu_future(fut_id, self)
122
+
123
+ def wait(self) -> Any:
124
+ """
125
+ Wait for the future on the current CUDA stream and return the result from
126
+ the GPU operation. This operation does not block CPU.
127
+ """
128
+ current_stream = AcceleratorContext.get().current_stream()
129
+ if not self._waited:
130
+ self._waited = True
131
+ current_stream.wait_event(self._event)
132
+ # Destroy the CUDA event after it is waited on.
133
+ GPUFuture.remove_gpu_future(self._fut_id)
134
+
135
+ return self._buf
136
+
137
+ def destroy_event(self) -> None:
138
+ """
139
+ Destroy the CUDA event associated with this future.
140
+ """
141
+ if self._event is None:
142
+ return
143
+
144
+ self._event = None
lib/python3.12/site-packages/ray/dag/experimental/__init__.py ADDED
File without changes
lib/python3.12/site-packages/ray/dag/experimental/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (199 Bytes). View file
 
lib/python3.12/site-packages/ray/dag/format_utils.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ray.dag import DAGNode
2
+ from ray.util.annotations import DeveloperAPI
3
+
4
+
5
+ @DeveloperAPI
6
+ def get_dag_node_str(
7
+ dag_node: DAGNode,
8
+ body_line,
9
+ ):
10
+ indent = _get_indentation()
11
+ other_args_to_resolve_lines = _get_other_args_to_resolve_lines(
12
+ dag_node._bound_other_args_to_resolve
13
+ )
14
+ return (
15
+ f"({dag_node.__class__.__name__}, {dag_node._stable_uuid})(\n"
16
+ f"{indent}body={body_line}\n"
17
+ f"{indent}args={_get_args_lines(dag_node._bound_args)}\n"
18
+ f"{indent}kwargs={_get_kwargs_lines(dag_node._bound_kwargs)}\n"
19
+ f"{indent}options={_get_options_lines(dag_node._bound_options)}\n"
20
+ f"{indent}other_args_to_resolve={other_args_to_resolve_lines}\n"
21
+ f")"
22
+ )
23
+
24
+
25
+ def _get_indentation(num_spaces=4):
26
+ return " " * num_spaces
27
+
28
+
29
+ def _get_args_lines(bound_args):
30
+ """Pretty prints bounded args of a DAGNode, and recursively handle
31
+ DAGNode in list / dict containers.
32
+ """
33
+ indent = _get_indentation()
34
+ lines = []
35
+ for arg in bound_args:
36
+ if isinstance(arg, DAGNode):
37
+ node_repr_lines = str(arg).split("\n")
38
+ for node_repr_line in node_repr_lines:
39
+ lines.append(f"{indent}" + node_repr_line)
40
+ elif isinstance(arg, list):
41
+ for ele in arg:
42
+ node_repr_lines = str(ele).split("\n")
43
+ for node_repr_line in node_repr_lines:
44
+ lines.append(f"{indent}" + node_repr_line)
45
+ elif isinstance(arg, dict):
46
+ for _, val in arg.items():
47
+ node_repr_lines = str(val).split("\n")
48
+ for node_repr_line in node_repr_lines:
49
+ lines.append(f"{indent}" + node_repr_line)
50
+ # TODO: (jiaodong) Handle nested containers and other obj types
51
+ else:
52
+ lines.append(f"{indent}" + str(arg) + ", ")
53
+
54
+ if len(lines) == 0:
55
+ args_line = "[]"
56
+ else:
57
+ args_line = "["
58
+ for args in lines:
59
+ args_line += f"\n{indent}{args}"
60
+ args_line += f"\n{indent}]"
61
+
62
+ return args_line
63
+
64
+
65
+ def _get_kwargs_lines(bound_kwargs):
66
+ """Pretty prints bounded kwargs of a DAGNode, and recursively handle
67
+ DAGNode in list / dict containers.
68
+ """
69
+ # TODO: (jiaodong) Nits, we're missing keys and indentation was a bit off.
70
+ if not bound_kwargs:
71
+ return "{}"
72
+ indent = _get_indentation()
73
+ kwargs_lines = []
74
+ for key, val in bound_kwargs.items():
75
+ if isinstance(val, DAGNode):
76
+ node_repr_lines = str(val).split("\n")
77
+ for index, node_repr_line in enumerate(node_repr_lines):
78
+ if index == 0:
79
+ kwargs_lines.append(
80
+ f"{indent}{key}:" + f"{indent}" + node_repr_line
81
+ )
82
+ else:
83
+ kwargs_lines.append(f"{indent}{indent}" + node_repr_line)
84
+
85
+ elif isinstance(val, list):
86
+ for ele in val:
87
+ node_repr_lines = str(ele).split("\n")
88
+ for node_repr_line in node_repr_lines:
89
+ kwargs_lines.append(f"{indent}" + node_repr_line)
90
+ elif isinstance(val, dict):
91
+ for _, inner_val in val.items():
92
+ node_repr_lines = str(inner_val).split("\n")
93
+ for node_repr_line in node_repr_lines:
94
+ kwargs_lines.append(f"{indent}" + node_repr_line)
95
+ # TODO: (jiaodong) Handle nested containers and other obj types
96
+ else:
97
+ kwargs_lines.append(val)
98
+
99
+ if len(kwargs_lines) > 0:
100
+ kwargs_line = "{"
101
+ for line in kwargs_lines:
102
+ kwargs_line += f"\n{indent}{line}"
103
+ kwargs_line += f"\n{indent}}}"
104
+ else:
105
+ kwargs_line = "{}"
106
+
107
+ return kwargs_line
108
+
109
+
110
+ def _get_options_lines(bound_options):
111
+ """Pretty prints .options() in DAGNode. Only prints non-empty values."""
112
+ if not bound_options:
113
+ return "{}"
114
+ indent = _get_indentation()
115
+ options_lines = []
116
+ for key, val in bound_options.items():
117
+ if val:
118
+ options_lines.append(f"{indent}{key}: " + str(val))
119
+
120
+ options_line = "{"
121
+ for line in options_lines:
122
+ options_line += f"\n{indent}{line}"
123
+ options_line += f"\n{indent}}}"
124
+ return options_line
125
+
126
+
127
+ def _get_other_args_to_resolve_lines(other_args_to_resolve):
128
+ if not other_args_to_resolve:
129
+ return "{}"
130
+ indent = _get_indentation()
131
+ other_args_to_resolve_lines = []
132
+ for key, val in other_args_to_resolve.items():
133
+ if isinstance(val, DAGNode):
134
+ node_repr_lines = str(val).split("\n")
135
+ for index, node_repr_line in enumerate(node_repr_lines):
136
+ if index == 0:
137
+ other_args_to_resolve_lines.append(
138
+ f"{indent}{key}:"
139
+ + f"{indent}"
140
+ + "\n"
141
+ + f"{indent}{indent}{indent}"
142
+ + node_repr_line
143
+ )
144
+ else:
145
+ other_args_to_resolve_lines.append(
146
+ f"{indent}{indent}" + node_repr_line
147
+ )
148
+ else:
149
+ other_args_to_resolve_lines.append(f"{indent}{key}: " + str(val))
150
+
151
+ other_args_to_resolve_line = "{"
152
+ for line in other_args_to_resolve_lines:
153
+ other_args_to_resolve_line += f"\n{indent}{line}"
154
+ other_args_to_resolve_line += f"\n{indent}}}"
155
+ return other_args_to_resolve_line
lib/python3.12/site-packages/ray/dag/function_node.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, List
2
+
3
+ import ray
4
+ from ray.dag.dag_node import DAGNode
5
+ from ray.dag.format_utils import get_dag_node_str
6
+ from ray.util.annotations import DeveloperAPI
7
+
8
+
9
+ @DeveloperAPI
10
+ class FunctionNode(DAGNode):
11
+ """Represents a bound task node in a Ray task DAG."""
12
+
13
+ def __init__(
14
+ self,
15
+ func_body,
16
+ func_args,
17
+ func_kwargs,
18
+ func_options,
19
+ other_args_to_resolve=None,
20
+ ):
21
+ self._body = func_body
22
+ super().__init__(
23
+ func_args,
24
+ func_kwargs,
25
+ func_options,
26
+ other_args_to_resolve=other_args_to_resolve,
27
+ )
28
+
29
+ def _copy_impl(
30
+ self,
31
+ new_args: List[Any],
32
+ new_kwargs: Dict[str, Any],
33
+ new_options: Dict[str, Any],
34
+ new_other_args_to_resolve: Dict[str, Any],
35
+ ):
36
+ return FunctionNode(
37
+ self._body,
38
+ new_args,
39
+ new_kwargs,
40
+ new_options,
41
+ other_args_to_resolve=new_other_args_to_resolve,
42
+ )
43
+
44
+ def _execute_impl(self, *args, **kwargs):
45
+ """Executor of FunctionNode by ray.remote().
46
+
47
+ Args and kwargs are to match base class signature, but not in the
48
+ implementation. All args and kwargs should be resolved and replaced
49
+ with value in bound_args and bound_kwargs via bottom-up recursion when
50
+ current node is executed.
51
+ """
52
+ return (
53
+ ray.remote(self._body)
54
+ .options(**self._bound_options)
55
+ .remote(*self._bound_args, **self._bound_kwargs)
56
+ )
57
+
58
+ def __str__(self) -> str:
59
+ return get_dag_node_str(self, str(self._body))
lib/python3.12/site-packages/ray/dag/input_node.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, List, Optional, Union
2
+
3
+ from ray.dag import DAGNode
4
+ from ray.dag.format_utils import get_dag_node_str
5
+ from ray.experimental.gradio_utils import type_to_string
6
+ from ray.util.annotations import DeveloperAPI
7
+
8
+ IN_CONTEXT_MANAGER = "__in_context_manager__"
9
+
10
+
11
+ @DeveloperAPI
12
+ class InputNode(DAGNode):
13
+ r"""Ray dag node used in DAG building API to mark entrypoints of a DAG.
14
+
15
+ Should only be function or class method. A DAG can have multiple
16
+ entrypoints, but only one instance of InputNode exists per DAG, shared
17
+ among all DAGNodes.
18
+
19
+ Example:
20
+
21
+ .. code-block::
22
+
23
+ m1.forward
24
+ / \
25
+ dag_input ensemble -> dag_output
26
+ \ /
27
+ m2.forward
28
+
29
+ In this pipeline, each user input is broadcasted to both m1.forward and
30
+ m2.forward as first stop of the DAG, and authored like
31
+
32
+ .. code-block:: python
33
+
34
+ import ray
35
+
36
+ @ray.remote
37
+ class Model:
38
+ def __init__(self, val):
39
+ self.val = val
40
+ def forward(self, input):
41
+ return self.val * input
42
+
43
+ @ray.remote
44
+ def combine(a, b):
45
+ return a + b
46
+
47
+ with InputNode() as dag_input:
48
+ m1 = Model.bind(1)
49
+ m2 = Model.bind(2)
50
+ m1_output = m1.forward.bind(dag_input[0])
51
+ m2_output = m2.forward.bind(dag_input.x)
52
+ ray_dag = combine.bind(m1_output, m2_output)
53
+
54
+ # Pass mix of args and kwargs as input.
55
+ ray_dag.execute(1, x=2) # 1 sent to m1, 2 sent to m2
56
+
57
+ # Alternatively user can also pass single data object, list or dict
58
+ # and access them via list index, object attribute or dict key str.
59
+ ray_dag.execute(UserDataObject(m1=1, m2=2))
60
+ # dag_input.m1, dag_input.m2
61
+ ray_dag.execute([1, 2])
62
+ # dag_input[0], dag_input[1]
63
+ ray_dag.execute({"m1": 1, "m2": 2})
64
+ # dag_input["m1"], dag_input["m2"]
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ *args,
70
+ input_type: Optional[Union[type, Dict[Union[int, str], type]]] = None,
71
+ _other_args_to_resolve=None,
72
+ **kwargs,
73
+ ):
74
+ """InputNode should only take attributes of validating and converting
75
+ input data rather than the input data itself. User input should be
76
+ provided via `ray_dag.execute(user_input)`.
77
+
78
+ Args:
79
+ input_type: Describes the data type of inputs user will be giving.
80
+ - if given through singular InputNode: type of InputNode
81
+ - if given through InputAttributeNodes: map of key -> type
82
+ Used when deciding what Gradio block to represent the input nodes with.
83
+ _other_args_to_resolve: Internal only to keep InputNode's execution
84
+ context throughput pickling, replacement and serialization.
85
+ User should not use or pass this field.
86
+ """
87
+ if len(args) != 0 or len(kwargs) != 0:
88
+ raise ValueError("InputNode should not take any args or kwargs.")
89
+
90
+ self.input_attribute_nodes = {}
91
+
92
+ self.input_type = input_type
93
+ if input_type is not None and isinstance(input_type, type):
94
+ if _other_args_to_resolve is None:
95
+ _other_args_to_resolve = {}
96
+ _other_args_to_resolve["result_type_string"] = type_to_string(input_type)
97
+
98
+ super().__init__([], {}, {}, other_args_to_resolve=_other_args_to_resolve)
99
+
100
+ def _copy_impl(
101
+ self,
102
+ new_args: List[Any],
103
+ new_kwargs: Dict[str, Any],
104
+ new_options: Dict[str, Any],
105
+ new_other_args_to_resolve: Dict[str, Any],
106
+ ):
107
+ return InputNode(_other_args_to_resolve=new_other_args_to_resolve)
108
+
109
+ def _execute_impl(self, *args, **kwargs):
110
+ """Executor of InputNode."""
111
+ # Catch and assert singleton context at dag execution time.
112
+ assert self._in_context_manager(), (
113
+ "InputNode is a singleton instance that should be only used in "
114
+ "context manager for dag building and execution. See the docstring "
115
+ "of class InputNode for examples."
116
+ )
117
+ # If user only passed in one value, for simplicity we just return it.
118
+ if len(args) == 1 and len(kwargs) == 0:
119
+ return args[0]
120
+
121
+ return DAGInputData(*args, **kwargs)
122
+
123
+ def _in_context_manager(self) -> bool:
124
+ """Return if InputNode is created in context manager."""
125
+ if (
126
+ not self._bound_other_args_to_resolve
127
+ or IN_CONTEXT_MANAGER not in self._bound_other_args_to_resolve
128
+ ):
129
+ return False
130
+ else:
131
+ return self._bound_other_args_to_resolve[IN_CONTEXT_MANAGER]
132
+
133
+ def set_context(self, key: str, val: Any):
134
+ """Set field in parent DAGNode attribute that can be resolved in both
135
+ pickle and JSON serialization
136
+ """
137
+ self._bound_other_args_to_resolve[key] = val
138
+
139
+ def __str__(self) -> str:
140
+ return get_dag_node_str(self, "__InputNode__")
141
+
142
+ def __getattr__(self, key: str):
143
+ assert isinstance(
144
+ key, str
145
+ ), "Please only access dag input attributes with str key."
146
+ if key not in self.input_attribute_nodes:
147
+ self.input_attribute_nodes[key] = InputAttributeNode(
148
+ self, key, "__getattr__"
149
+ )
150
+ return self.input_attribute_nodes[key]
151
+
152
+ def __getitem__(self, key: Union[int, str]) -> Any:
153
+ assert isinstance(key, (str, int)), (
154
+ "Please only use int index or str as first-level key to "
155
+ "access fields of dag input."
156
+ )
157
+
158
+ input_type = None
159
+ if self.input_type is not None and key in self.input_type:
160
+ input_type = type_to_string(self.input_type[key])
161
+
162
+ if key not in self.input_attribute_nodes:
163
+ self.input_attribute_nodes[key] = InputAttributeNode(
164
+ self, key, "__getitem__", input_type
165
+ )
166
+ return self.input_attribute_nodes[key]
167
+
168
+ def __enter__(self):
169
+ self.set_context(IN_CONTEXT_MANAGER, True)
170
+ return self
171
+
172
+ def __exit__(self, *args):
173
+ pass
174
+
175
+ def get_result_type(self) -> str:
176
+ """Get type of the output of this DAGNode.
177
+
178
+ Generated by ray.experimental.gradio_utils.type_to_string().
179
+ """
180
+ if "result_type_string" in self._bound_other_args_to_resolve:
181
+ return self._bound_other_args_to_resolve["result_type_string"]
182
+
183
+
184
+ @DeveloperAPI
185
+ class InputAttributeNode(DAGNode):
186
+ """Represents partial access of user input based on an index (int),
187
+ object attribute or dict key (str).
188
+
189
+ Examples:
190
+
191
+ .. code-block:: python
192
+
193
+ with InputNode() as dag_input:
194
+ a = dag_input[0]
195
+ b = dag_input.x
196
+ ray_dag = add.bind(a, b)
197
+
198
+ # This makes a = 1 and b = 2
199
+ ray_dag.execute(1, x=2)
200
+
201
+ with InputNode() as dag_input:
202
+ a = dag_input[0]
203
+ b = dag_input[1]
204
+ ray_dag = add.bind(a, b)
205
+
206
+ # This makes a = 2 and b = 3
207
+ ray_dag.execute(2, 3)
208
+
209
+ # Alternatively, you can input a single object
210
+ # and the inputs are automatically indexed from the object:
211
+ # This makes a = 2 and b = 3
212
+ ray_dag.execute([2, 3])
213
+ """
214
+
215
+ def __init__(
216
+ self,
217
+ dag_input_node: InputNode,
218
+ key: Union[int, str],
219
+ accessor_method: str,
220
+ input_type: str = None,
221
+ ):
222
+ self._dag_input_node = dag_input_node
223
+ self._key = key
224
+ self._accessor_method = accessor_method
225
+ super().__init__(
226
+ [],
227
+ {},
228
+ {},
229
+ {
230
+ "dag_input_node": dag_input_node,
231
+ "key": key,
232
+ "accessor_method": accessor_method,
233
+ # Type of the input tied to this node. Used by
234
+ # gradio_visualize_graph.GraphVisualizer to determine which Gradio
235
+ # component should be used for this node.
236
+ "result_type_string": input_type,
237
+ },
238
+ )
239
+
240
+ def _copy_impl(
241
+ self,
242
+ new_args: List[Any],
243
+ new_kwargs: Dict[str, Any],
244
+ new_options: Dict[str, Any],
245
+ new_other_args_to_resolve: Dict[str, Any],
246
+ ):
247
+ return InputAttributeNode(
248
+ new_other_args_to_resolve["dag_input_node"],
249
+ new_other_args_to_resolve["key"],
250
+ new_other_args_to_resolve["accessor_method"],
251
+ new_other_args_to_resolve["result_type_string"],
252
+ )
253
+
254
+ def _execute_impl(self, *args, **kwargs):
255
+ """Executor of InputAttributeNode.
256
+
257
+ Args and kwargs are to match base class signature, but not in the
258
+ implementation. All args and kwargs should be resolved and replaced
259
+ with value in bound_args and bound_kwargs via bottom-up recursion when
260
+ current node is executed.
261
+ """
262
+
263
+ if isinstance(self._dag_input_node, DAGInputData):
264
+ return self._dag_input_node[self._key]
265
+ else:
266
+ # dag.execute() is called with only one arg, thus when an
267
+ # InputAttributeNode is executed, its dependent InputNode is
268
+ # resolved with original user input python object.
269
+ user_input_python_object = self._dag_input_node
270
+ if isinstance(self._key, str):
271
+ if self._accessor_method == "__getitem__":
272
+ return user_input_python_object[self._key]
273
+ elif self._accessor_method == "__getattr__":
274
+ return getattr(user_input_python_object, self._key)
275
+ elif isinstance(self._key, int):
276
+ return user_input_python_object[self._key]
277
+ else:
278
+ raise ValueError(
279
+ "Please only use int index or str as first-level key to "
280
+ "access fields of dag input."
281
+ )
282
+
283
+ def __str__(self) -> str:
284
+ return get_dag_node_str(self, f'["{self._key}"]')
285
+
286
+ def get_result_type(self) -> str:
287
+ """Get type of the output of this DAGNode.
288
+
289
+ Generated by ray.experimental.gradio_utils.type_to_string().
290
+ """
291
+ if "result_type_string" in self._bound_other_args_to_resolve:
292
+ return self._bound_other_args_to_resolve["result_type_string"]
293
+
294
+ @property
295
+ def key(self) -> Union[int, str]:
296
+ return self._key
297
+
298
+
299
+ @DeveloperAPI
300
+ class DAGInputData:
301
+ """If user passed multiple args and kwargs directly to dag.execute(), we
302
+ generate this wrapper for all user inputs as one object, accessible via
303
+ list index or object attribute key.
304
+ """
305
+
306
+ def __init__(self, *args, **kwargs):
307
+ self._args = list(args)
308
+ self._kwargs = kwargs
309
+
310
+ def __getitem__(self, key: Union[int, str]) -> Any:
311
+ if isinstance(key, int):
312
+ # Access list args by index.
313
+ return self._args[key]
314
+ elif isinstance(key, str):
315
+ # Access kwarg by key.
316
+ return self._kwargs[key]
317
+ else:
318
+ raise ValueError(
319
+ "Please only use int index or str as first-level key to "
320
+ "access fields of dag input."
321
+ )
lib/python3.12/site-packages/ray/dag/output_node.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, List, Tuple, Union
2
+
3
+ import ray
4
+ from ray.dag import DAGNode
5
+ from ray.dag.format_utils import get_dag_node_str
6
+ from ray.util.annotations import DeveloperAPI
7
+
8
+
9
+ @DeveloperAPI
10
+ class MultiOutputNode(DAGNode):
11
+ """Ray dag node used in DAG building API to mark the endpoint of DAG"""
12
+
13
+ def __init__(
14
+ self,
15
+ args: Union[List[DAGNode], Tuple[DAGNode]],
16
+ other_args_to_resolve: Dict[str, Any] = None,
17
+ ):
18
+ if isinstance(args, tuple):
19
+ args = list(args)
20
+ if not isinstance(args, list):
21
+ raise ValueError(f"Invalid input type for `args`, {type(args)}.")
22
+ super().__init__(
23
+ args,
24
+ {},
25
+ {},
26
+ other_args_to_resolve=other_args_to_resolve or {},
27
+ )
28
+
29
+ def _execute_impl(
30
+ self, *args, **kwargs
31
+ ) -> Union[ray.ObjectRef, "ray.actor.ActorHandle"]:
32
+ return self._bound_args
33
+
34
+ def _copy_impl(
35
+ self,
36
+ new_args: List[Any],
37
+ new_kwargs: Dict[str, Any],
38
+ new_options: Dict[str, Any],
39
+ new_other_args_to_resolve: Dict[str, Any],
40
+ ) -> "DAGNode":
41
+ """Return a copy of this node with the given new args."""
42
+ return MultiOutputNode(new_args, new_other_args_to_resolve)
43
+
44
+ def __str__(self) -> str:
45
+ return get_dag_node_str(self, "__MultiOutputNode__")
lib/python3.12/site-packages/ray/dag/py_obj_scanner.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import pickle # noqa: F401
3
+ from typing import Any, Dict, Generic, List, Tuple, Type, TypeVar, Union
4
+
5
+ import ray
6
+ from ray.dag.base import DAGNodeBase
7
+
8
+ # Used in deserialization hooks to reference scanner instances.
9
+ _instances: Dict[int, "_PyObjScanner"] = {}
10
+
11
+ # Generic types for the scanner to transform from and to.
12
+ SourceType = TypeVar("SourceType")
13
+ TransformedType = TypeVar("TransformedType")
14
+
15
+
16
+ def _get_node(instance_id: int, node_index: int) -> SourceType:
17
+ """Get the node instance.
18
+
19
+ Note: This function should be static and globally importable,
20
+ otherwise the serialization overhead would be very significant.
21
+ """
22
+ return _instances[instance_id]._replace_index(node_index)
23
+
24
+
25
+ class _PyObjScanner(ray.cloudpickle.CloudPickler, Generic[SourceType, TransformedType]):
26
+ """Utility to find and replace the `source_type` in Python objects.
27
+
28
+ `source_type` can either be a single type or a tuple of multiple types.
29
+
30
+ The caller must first call `find_nodes()`, then compute a replacement table and
31
+ pass it to `replace_nodes`.
32
+
33
+ This uses cloudpickle under the hood, so all sub-objects that are not `source_type`
34
+ must be serializable.
35
+
36
+ Args:
37
+ source_type: the type(s) of object to find and replace. Default to DAGNodeBase.
38
+ """
39
+
40
+ def __init__(self, source_type: Union[Type, Tuple] = DAGNodeBase):
41
+ self.source_type = source_type
42
+ # Buffer to keep intermediate serialized state.
43
+ self._buf = io.BytesIO()
44
+ # List of top-level SourceType found during the serialization pass.
45
+ self._found = None
46
+ # List of other objects found during the serialization pass.
47
+ # This is used to store references to objects so they won't be
48
+ # serialized by cloudpickle.
49
+ self._objects = []
50
+ # Replacement table to consult during deserialization.
51
+ self._replace_table: Dict[SourceType, TransformedType] = None
52
+ _instances[id(self)] = self
53
+ super().__init__(self._buf)
54
+
55
+ def reducer_override(self, obj):
56
+ """Hook for reducing objects.
57
+
58
+ Objects of `self.source_type` are saved to `self._found` and a global map so
59
+ they can later be replaced.
60
+
61
+ All other objects fall back to the default `CloudPickler` serialization.
62
+ """
63
+ if isinstance(obj, self.source_type):
64
+ index = len(self._found)
65
+ self._found.append(obj)
66
+ return _get_node, (id(self), index)
67
+
68
+ return super().reducer_override(obj)
69
+
70
+ def find_nodes(self, obj: Any) -> List[SourceType]:
71
+ """
72
+ Serialize `obj` and store all instances of `source_type` found in `_found`.
73
+
74
+ Args:
75
+ obj: The object to scan for `source_type`.
76
+ Returns:
77
+ A list of all instances of `source_type` found in `obj`.
78
+ """
79
+ assert (
80
+ self._found is None
81
+ ), "find_nodes cannot be called twice on the same PyObjScanner instance."
82
+ self._found = []
83
+ self._objects = []
84
+ self.dump(obj)
85
+ return self._found
86
+
87
+ def replace_nodes(self, table: Dict[SourceType, TransformedType]) -> Any:
88
+ """Replace previously found DAGNodes per the given table."""
89
+ assert self._found is not None, "find_nodes must be called first"
90
+ self._replace_table = table
91
+ self._buf.seek(0)
92
+ return pickle.load(self._buf)
93
+
94
+ def _replace_index(self, i: int) -> SourceType:
95
+ return self._replace_table[self._found[i]]
96
+
97
+ def clear(self):
98
+ """Clear the scanner from the _instances"""
99
+ if id(self) in _instances:
100
+ del _instances[id(self)]
101
+
102
+ def __del__(self):
103
+ self.clear()
lib/python3.12/site-packages/ray/dag/utils.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict
2
+
3
+ from ray.dag import (
4
+ ClassMethodNode,
5
+ ClassNode,
6
+ DAGNode,
7
+ FunctionNode,
8
+ InputAttributeNode,
9
+ InputNode,
10
+ MultiOutputNode,
11
+ )
12
+
13
+
14
+ class _DAGNodeNameGenerator(object):
15
+ """
16
+ Generate unique suffix for each given Node in the DAG.
17
+ Apply monotonic increasing id suffix for duplicated names.
18
+ """
19
+
20
+ def __init__(self):
21
+ self.name_to_suffix: Dict[str, int] = dict()
22
+
23
+ def get_node_name(self, node: DAGNode):
24
+ # InputNode should be unique.
25
+ if isinstance(node, InputNode):
26
+ return "INPUT_NODE"
27
+ if isinstance(node, MultiOutputNode):
28
+ return "MultiOutputNode"
29
+ # InputAttributeNode suffixes should match the user-defined key.
30
+ elif isinstance(node, InputAttributeNode):
31
+ return f"INPUT_ATTRIBUTE_NODE_{node._key}"
32
+
33
+ # As class, method, and function nodes may have duplicated names,
34
+ # generate unique suffixes for such nodes.
35
+ if isinstance(node, ClassMethodNode):
36
+ node_name = node.get_options().get("name", None) or node._method_name
37
+ elif isinstance(node, (ClassNode, FunctionNode)):
38
+ node_name = node.get_options().get("name", None) or node._body.__name__
39
+ # we use instance class name check here to avoid importing ServeNodes as
40
+ # serve components are not included in Ray Core.
41
+ elif type(node).__name__ in ("DeploymentNode", "DeploymentFunctionNode"):
42
+ node_name = node.get_deployment_name()
43
+ elif type(node).__name__ == "DeploymentFunctionExecutorNode":
44
+ node_name = node._deployment_function_handle.deployment_name
45
+ else:
46
+ raise ValueError(
47
+ "get_node_name() should only be called on DAGNode instances."
48
+ )
49
+
50
+ if node_name not in self.name_to_suffix:
51
+ self.name_to_suffix[node_name] = 0
52
+ return node_name
53
+ else:
54
+ self.name_to_suffix[node_name] += 1
55
+ suffix_num = self.name_to_suffix[node_name]
56
+
57
+ return f"{node_name}_{suffix_num}"
58
+
59
+ def reset(self):
60
+ self.name_to_suffix = dict()
61
+
62
+ def __enter__(self):
63
+ return self
64
+
65
+ def __exit__(self, *args):
66
+ self.reset()
lib/python3.12/site-packages/ray/dag/vis_utils.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+
4
+ from ray.dag import DAGNode
5
+ from ray.dag.utils import _DAGNodeNameGenerator
6
+ from ray.util.annotations import DeveloperAPI
7
+
8
+
9
+ @DeveloperAPI
10
+ def plot(dag: DAGNode, to_file=None):
11
+ if to_file is None:
12
+ tmp_file = tempfile.NamedTemporaryFile(suffix=".png")
13
+ to_file = tmp_file.name
14
+ extension = "png"
15
+ else:
16
+ _, extension = os.path.splitext(to_file)
17
+ if not extension:
18
+ extension = "png"
19
+ else:
20
+ extension = extension[1:]
21
+
22
+ graph = _dag_to_dot(dag)
23
+ graph.write(to_file, format=extension)
24
+
25
+ # Render the image directly if running inside a Jupyter notebook
26
+ try:
27
+ from IPython import display
28
+
29
+ return display.Image(filename=to_file)
30
+ except ImportError:
31
+ pass
32
+
33
+ # close temp file if needed
34
+ try:
35
+ tmp_file.close()
36
+ except NameError:
37
+ pass
38
+
39
+
40
+ def _check_pydot_and_graphviz():
41
+ """Check if pydot and graphviz are installed.
42
+
43
+ pydot and graphviz are required for plotting. We check this
44
+ during runtime rather than adding them to Ray dependencies.
45
+
46
+ """
47
+ try:
48
+ import pydot
49
+ except ImportError:
50
+ raise ImportError(
51
+ "pydot is required to plot DAG, install it with `pip install pydot`."
52
+ )
53
+ try:
54
+ pydot.Dot.create(pydot.Dot())
55
+ except (OSError, pydot.InvocationException):
56
+ raise ImportError(
57
+ "graphviz is required to plot DAG, "
58
+ "download it from https://graphviz.gitlab.io/download/"
59
+ )
60
+
61
+
62
+ def _get_nodes_and_edges(dag: DAGNode):
63
+ """Get all unique nodes and edges in the DAG.
64
+
65
+ A basic dfs with memorization to get all unique nodes
66
+ and edges in the DAG.
67
+ Unique nodes will be used to generate unique names,
68
+ while edges will be used to construct the graph.
69
+ """
70
+
71
+ edges = []
72
+ nodes = []
73
+
74
+ def _dfs(node):
75
+ nodes.append(node)
76
+ for child_node in node._get_all_child_nodes():
77
+ edges.append((child_node, node))
78
+ return node
79
+
80
+ dag.apply_recursive(_dfs)
81
+ return nodes, edges
82
+
83
+
84
+ def _dag_to_dot(dag: DAGNode):
85
+ """Create a Dot graph from dag.
86
+
87
+ TODO(lchu):
88
+ 1. add more Dot configs in kwargs,
89
+ e.g. rankdir, alignment, etc.
90
+ 2. add more contents to graph,
91
+ e.g. args, kwargs and options of each node
92
+
93
+ """
94
+ # Step 0: check dependencies and init graph
95
+ _check_pydot_and_graphviz()
96
+ import pydot
97
+
98
+ graph = pydot.Dot(rankdir="LR")
99
+
100
+ # Step 1: generate unique name for each node in dag
101
+ nodes, edges = _get_nodes_and_edges(dag)
102
+ name_generator = _DAGNodeNameGenerator()
103
+ node_names = {}
104
+ for node in nodes:
105
+ node_names[node] = name_generator.get_node_name(node)
106
+
107
+ # Step 2: create graph with all the edges
108
+ for edge in edges:
109
+ graph.add_edge(pydot.Edge(node_names[edge[0]], node_names[edge[1]]))
110
+ # if there is only one node
111
+ if len(nodes) == 1 and len(edges) == 0:
112
+ graph.add_node(pydot.Node(node_names[nodes[0]]))
113
+
114
+ return graph
lib/python3.12/site-packages/ray/experimental/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ray.experimental.dynamic_resources import set_resource
2
+ from ray.experimental.gpu_object_manager import GPUObjectManager, wait_tensor_freed
3
+ from ray.experimental.locations import get_local_object_locations, get_object_locations
4
+
5
+ __all__ = [
6
+ "get_object_locations",
7
+ "get_local_object_locations",
8
+ "set_resource",
9
+ "GPUObjectManager",
10
+ "wait_tensor_freed",
11
+ ]
lib/python3.12/site-packages/ray/experimental/collective/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ray.experimental.collective.collective import (
2
+ create_collective_group,
3
+ destroy_all_collective_groups,
4
+ destroy_collective_group,
5
+ get_collective_groups,
6
+ )
7
+ from ray.experimental.collective.operations import (
8
+ allgather,
9
+ allreduce,
10
+ reducescatter,
11
+ )
12
+ from ray.experimental.collective.util import get_tensor_transport_manager
13
+
14
+ __all__ = [
15
+ "allgather",
16
+ "allreduce",
17
+ "reducescatter",
18
+ "get_collective_groups",
19
+ "create_collective_group",
20
+ "destroy_collective_group",
21
+ "destroy_all_collective_groups",
22
+ "get_tensor_transport_manager",
23
+ ]
lib/python3.12/site-packages/ray/experimental/collective/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (688 Bytes). View file
 
lib/python3.12/site-packages/ray/experimental/collective/__pycache__/collective.cpython-312.pyc ADDED
Binary file (10.6 kB). View file
 
lib/python3.12/site-packages/ray/experimental/collective/__pycache__/collective_tensor_transport.cpython-312.pyc ADDED
Binary file (8 kB). View file
 
lib/python3.12/site-packages/ray/experimental/collective/__pycache__/communicator.cpython-312.pyc ADDED
Binary file (2.91 kB). View file
 
lib/python3.12/site-packages/ray/experimental/collective/__pycache__/conftest.cpython-312.pyc ADDED
Binary file (11.3 kB). View file
 
lib/python3.12/site-packages/ray/experimental/collective/__pycache__/nixl_tensor_transport.cpython-312.pyc ADDED
Binary file (7.91 kB). View file
 
lib/python3.12/site-packages/ray/experimental/collective/__pycache__/operations.cpython-312.pyc ADDED
Binary file (8.21 kB). View file
 
lib/python3.12/site-packages/ray/experimental/collective/__pycache__/tensor_transport_manager.cpython-312.pyc ADDED
Binary file (6.71 kB). View file
 
lib/python3.12/site-packages/ray/experimental/collective/__pycache__/util.cpython-312.pyc ADDED
Binary file (3.05 kB). View file
 
lib/python3.12/site-packages/ray/experimental/collective/collective.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import threading
2
+ import uuid
3
+ from typing import Dict, List, Optional, Union
4
+
5
+ import ray
6
+ import ray.experimental.internal_kv as internal_kv
7
+ from ray.experimental.collective.communicator import CommunicatorHandle
8
+ from ray.experimental.collective.util import get_address_and_port
9
+ from ray.util.annotations import PublicAPI
10
+ from ray.util.collective.collective_group.torch_gloo_collective_group import (
11
+ get_master_address_metadata_key,
12
+ )
13
+ from ray.util.collective.types import Backend
14
+
15
+ _remote_communicator_manager: "Optional[RemoteCommunicatorManager]" = None
16
+ _remote_communicator_manager_lock = threading.Lock()
17
+
18
+
19
+ class RemoteCommunicatorManager:
20
+ """Singleton class to store the mapping between actors and communicators
21
+ that the actors are a part of.
22
+ """
23
+
24
+ def __init__(self):
25
+ # Handles to communicators that we created. Key is a user-provided
26
+ # name or UUID.
27
+ self._remote_communicators: Dict[str, CommunicatorHandle] = {}
28
+
29
+ @staticmethod
30
+ def get() -> "RemoteCommunicatorManager":
31
+ global _remote_communicator_manager
32
+ with _remote_communicator_manager_lock:
33
+ if _remote_communicator_manager is None:
34
+ _remote_communicator_manager = RemoteCommunicatorManager()
35
+ return _remote_communicator_manager
36
+
37
+ def add_remote_communicator(self, comm_handle: CommunicatorHandle):
38
+ self._remote_communicators[comm_handle.name] = comm_handle
39
+
40
+ def remove_remote_communicator(self, name: str):
41
+ return self._remote_communicators.pop(name, None)
42
+
43
+ def get_collective_groups(
44
+ self,
45
+ actors: Optional[List[ray.actor.ActorHandle]] = None,
46
+ backend: Optional[str] = None,
47
+ ):
48
+ """
49
+ Get the collective groups that the given actors are a subset of. Filter by
50
+ backend if provided.
51
+ """
52
+ actors = actors or []
53
+ actors = set(actors)
54
+
55
+ collectives = []
56
+ # Find all collective groups that the given actors are a subset
57
+ # of, with the matching backend if provided.
58
+ for collective in self._remote_communicators.values():
59
+ if actors.issubset(set(collective.actors)):
60
+ if backend is None or collective.backend == backend:
61
+ collectives.append(collective)
62
+ return collectives
63
+
64
+
65
+ def _do_init_collective_group(
66
+ self,
67
+ world_size: int,
68
+ rank: int,
69
+ backend: str = Backend.NCCL,
70
+ name: str = "default",
71
+ ):
72
+ """Helper method that runs as a task on a remote actor to create a
73
+ collective group.
74
+ """
75
+ ray.util.collective.init_collective_group(
76
+ world_size, rank, backend, group_name=name
77
+ )
78
+
79
+
80
+ def _do_destroy_collective_group(self, name):
81
+ """Helper method that runs as a task on a remote actor to destroy a
82
+ collective group.
83
+ """
84
+ ray.util.collective.destroy_collective_group(name)
85
+
86
+
87
+ @PublicAPI(stability="alpha")
88
+ def get_collective_groups(
89
+ actors: List[ray.actor.ActorHandle], backend: Optional[str] = None
90
+ ) -> List[CommunicatorHandle]:
91
+ """
92
+ Get the collective groups that the given actors are a subset of. Filter by
93
+ backend if provided.
94
+
95
+ Args:
96
+ actors: List of actors. Return handles to all collective groups that
97
+ these actors are a subset of.
98
+ backend: An optional backend to filter by. See
99
+ ray.util.collective.types.Backend for valid backends.
100
+
101
+ Returns:
102
+ A list of communicator handles that the actors are a subset of.
103
+ """
104
+ manager = RemoteCommunicatorManager.get()
105
+ return manager.get_collective_groups(actors, backend)
106
+
107
+
108
+ @PublicAPI(stability="alpha")
109
+ def create_collective_group(
110
+ actors: List[ray.actor.ActorHandle],
111
+ backend: str,
112
+ name: Optional[str] = None,
113
+ ) -> CommunicatorHandle:
114
+ """Create a collective group on the given list of actors. If this function
115
+ returns successfully, then the collective group has been initialized on all
116
+ actors, using the given order of actors as the ranks.
117
+
118
+ Currently, an actor can only participate in one collective group per
119
+ backend at a time. To reuse an actor, destroy its collective group and
120
+ create a new one.
121
+
122
+ Args:
123
+ actors: The actors to participate in the collective group.
124
+ backend: The backend to use. See ray.util.collective.types.Backend for
125
+ valid backends.
126
+ name: A name to use for the collective group. If None is provided, a
127
+ random name will be generated.
128
+
129
+ Returns:
130
+ Handle to the communicator.
131
+ """
132
+ manager = RemoteCommunicatorManager.get()
133
+
134
+ if name is None:
135
+ name = str(uuid.uuid4())
136
+
137
+ # Validate the backend.
138
+ backend = Backend(backend)
139
+
140
+ world_size = len(actors)
141
+
142
+ for actor in actors:
143
+ if manager.get_collective_groups([actor], backend):
144
+ raise RuntimeError(
145
+ f"Actor {actor} already in group for backend {backend}. Actors can currently only participate in at most one group per backend."
146
+ )
147
+
148
+ actor_ids = [actor._ray_actor_id for actor in actors]
149
+ if len(set(actor_ids)) != len(actor_ids):
150
+ raise ValueError(f"All actors must be unique, got: {actors}")
151
+
152
+ metadata_key = None
153
+ if backend == Backend.GLOO:
154
+ # Perform extra setup for torch.distributed.
155
+ # torch.distributed requires a master address and port. Find a suitable
156
+ # port on one of the actors.
157
+ master_addr, master_port = ray.get(
158
+ actors[0].__ray_call__.remote(lambda self: get_address_and_port())
159
+ )
160
+
161
+ # Store the metadata on a named actor that all of the other
162
+ # actors can access.
163
+ metadata_key = get_master_address_metadata_key(name)
164
+ internal_kv._internal_kv_put(metadata_key, f"{master_addr}:{master_port}")
165
+
166
+ try:
167
+ init_tasks = [
168
+ actor.__ray_call__.remote(
169
+ _do_init_collective_group, world_size, rank, backend, name
170
+ )
171
+ for rank, actor in enumerate(actors)
172
+ ]
173
+ ray.get(init_tasks)
174
+ finally:
175
+ # Clean up the metadata once collective group is initialized
176
+ # (or failed to initialize).
177
+ if metadata_key is not None:
178
+ internal_kv._internal_kv_del(metadata_key)
179
+
180
+ # Group was successfully created.
181
+ # Register GLOO groups under TORCH_GLOO since GLOO uses torch.distributed.
182
+ registration_backend = Backend.TORCH_GLOO if backend == Backend.GLOO else backend
183
+ comm = CommunicatorHandle(actors, name, registration_backend)
184
+ manager.add_remote_communicator(comm)
185
+ return comm
186
+
187
+
188
+ @PublicAPI(stability="alpha")
189
+ def destroy_collective_group(group_or_name: Union[CommunicatorHandle, str]):
190
+ """
191
+ Destroy a collective group. If this functions returns successfully, then
192
+ the actors that were in the collective can be reused to create a new
193
+ collective group.
194
+
195
+ Args:
196
+ group_or_name: Either a communicator handle or the name of the group to
197
+ destroy.
198
+ """
199
+ if isinstance(group_or_name, CommunicatorHandle):
200
+ name = group_or_name.name
201
+ elif isinstance(group_or_name, str):
202
+ name = group_or_name
203
+ else:
204
+ raise ValueError("Expected CommunicatorHandle or str (group name).")
205
+
206
+ manager = RemoteCommunicatorManager.get()
207
+ group = manager.remove_remote_communicator(name)
208
+ if group is not None:
209
+ destroy_tasks = [
210
+ actor.__ray_call__.options(concurrency_group="_ray_system").remote(
211
+ _do_destroy_collective_group, name
212
+ )
213
+ for actor in group.actors
214
+ ]
215
+ try:
216
+ ray.get(destroy_tasks)
217
+ except ray.exceptions.ActorDiedError:
218
+ pass
219
+ else:
220
+ raise ValueError(f"No group with name {name} found.")
221
+
222
+
223
+ @PublicAPI(stability="alpha")
224
+ def destroy_all_collective_groups():
225
+ """
226
+ Destroy all collective groups. This will destroy all collective groups that
227
+ were previously created by this process. After this function returns, the
228
+ actors participating in those collective groups can be reused to create a
229
+ new collective group.
230
+ """
231
+ manager = RemoteCommunicatorManager.get()
232
+ for collective in manager.get_collective_groups():
233
+ destroy_collective_group(collective.name)
lib/python3.12/site-packages/ray/experimental/collective/collective_tensor_transport.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING, List, Optional
2
+
3
+ import ray
4
+ from ray.experimental.collective.tensor_transport_manager import (
5
+ TensorTransportManager,
6
+ )
7
+ from ray.util.collective.types import (
8
+ Backend,
9
+ CollectiveCommunicatorMetadata,
10
+ CollectiveTransportMetadata,
11
+ )
12
+
13
+ if TYPE_CHECKING:
14
+ import torch
15
+
16
+
17
+ class CollectiveTensorTransport(TensorTransportManager):
18
+ def __init__(self, tensor_transport_backend: Backend):
19
+ self._tensor_transport_backend = tensor_transport_backend
20
+
21
+ @property
22
+ def tensor_transport_backend(self) -> Backend:
23
+ return self._tensor_transport_backend
24
+
25
+ @staticmethod
26
+ def is_one_sided() -> bool:
27
+ return False
28
+
29
+ @staticmethod
30
+ def can_abort_transport() -> bool:
31
+ return False
32
+
33
+ def actor_has_tensor_transport(self, actor: "ray.actor.ActorHandle") -> bool:
34
+ from ray.experimental.collective import get_collective_groups
35
+
36
+ communicators = get_collective_groups(
37
+ [actor], backend=self.tensor_transport_backend
38
+ )
39
+ return len(communicators) > 0
40
+
41
+ @staticmethod
42
+ def extract_tensor_transport_metadata(
43
+ obj_id: str,
44
+ gpu_object: List["torch.Tensor"],
45
+ ) -> CollectiveTransportMetadata:
46
+ tensor_meta = []
47
+ device = None
48
+ if gpu_object:
49
+ device = gpu_object[0].device
50
+ for t in gpu_object:
51
+ if t.device.type != device.type:
52
+ raise ValueError(
53
+ "All tensors in an RDT object must have the same device type."
54
+ )
55
+ tensor_meta.append((t.shape, t.dtype))
56
+ return CollectiveTransportMetadata(
57
+ tensor_meta=tensor_meta,
58
+ tensor_device=device,
59
+ )
60
+
61
+ @staticmethod
62
+ def get_tensor_transport_metadata(
63
+ src_actor: "ray.actor.ActorHandle",
64
+ obj_id: str,
65
+ ) -> CollectiveTransportMetadata:
66
+ def __ray_get_tensor_transport_metadata__(
67
+ self: "ray.actor.ActorHandle",
68
+ obj_id: str,
69
+ ) -> CollectiveTransportMetadata:
70
+
71
+ from ray._private.worker import global_worker
72
+
73
+ gpu_object_store = global_worker.gpu_object_manager.gpu_object_store
74
+ # NOTE: We do not specify a timeout here because the user task that returns
75
+ # it could take arbitrarily long and we don't want to trigger a spurious
76
+ # timeout.
77
+ gpu_object = gpu_object_store.wait_and_get_object(obj_id)
78
+ return CollectiveTensorTransport.extract_tensor_transport_metadata(
79
+ obj_id, gpu_object
80
+ )
81
+
82
+ # Submit a Ray actor task to the source actor to get the tensor metadata.
83
+ # The metadata is a list of tuples, where each tuple contains the shape and dtype
84
+ # of a tensor in the GPU object store. This function returns an ObjectRef that
85
+ # points to the tensor metadata.
86
+ # NOTE(swang): We put this task on the background thread to avoid tasks
87
+ # executing on the main thread blocking this task.
88
+
89
+ return src_actor.__ray_call__.options(concurrency_group="_ray_system").remote(
90
+ __ray_get_tensor_transport_metadata__, obj_id
91
+ )
92
+
93
+ @staticmethod
94
+ def get_communicator_metadata(
95
+ src_actor: "ray.actor.ActorHandle",
96
+ dst_actor: "ray.actor.ActorHandle",
97
+ backend: Optional[str] = None,
98
+ ) -> CollectiveCommunicatorMetadata:
99
+
100
+ from ray.experimental.collective import get_collective_groups
101
+
102
+ communicators = get_collective_groups(
103
+ [src_actor, dst_actor],
104
+ backend=backend,
105
+ )
106
+ # TODO(kevin85421): Support multiple communicators.
107
+ if len(communicators) == 0:
108
+ raise ValueError(
109
+ f"No communicators found for actors {src_actor} and {dst_actor}. "
110
+ "Create a communicator with "
111
+ "`ray.experimental.collective.create_collective_group` "
112
+ "before calling actor tasks. with non-default tensor_transport."
113
+ )
114
+ elif len(communicators) > 1:
115
+ raise ValueError(
116
+ f"There are {len(communicators)} possible communicators that contain actors {src_actor} and {dst_actor}. "
117
+ "Currently, RDT objects only support one communicator. Please make sure only "
118
+ "one communicator exists."
119
+ )
120
+ communicator = communicators[0]
121
+ src_rank = communicator.get_rank(src_actor)
122
+ if src_rank == -1:
123
+ raise ValueError(
124
+ f"Sender actor {src_actor} not found in communicator. "
125
+ "Please make sure the sender and receiver are in the same communicator."
126
+ )
127
+ dst_rank = communicator.get_rank(dst_actor)
128
+ if dst_rank == -1:
129
+ raise ValueError(
130
+ f"Receiver actor {dst_actor} not found in communicator. "
131
+ "Please make sure the sender and receiver are in the same communicator."
132
+ )
133
+
134
+ communicator_metadata = CollectiveCommunicatorMetadata(
135
+ communicator_name=communicator.name,
136
+ src_rank=src_rank,
137
+ dst_rank=dst_rank,
138
+ )
139
+ return communicator_metadata
140
+
141
+ @staticmethod
142
+ def recv_multiple_tensors(
143
+ tensors,
144
+ obj_id: str,
145
+ tensor_transport_metadata: CollectiveTransportMetadata,
146
+ communicator_metadata: CollectiveCommunicatorMetadata,
147
+ ):
148
+ from ray.util.collective import types
149
+ from ray.util.collective.collective import recv
150
+
151
+ assert isinstance(
152
+ tensor_transport_metadata, types.CollectiveTransportMetadata
153
+ ), "metadata must be a CollectiveTransportMetadata object for non-NIXL transport"
154
+ assert isinstance(
155
+ communicator_metadata, types.CollectiveCommunicatorMetadata
156
+ ), "metadata must be a CollectiveCommunicatorMetadata object for non-NIXL transport"
157
+
158
+ for tensor in tensors:
159
+ recv(
160
+ tensor,
161
+ communicator_metadata.src_rank,
162
+ communicator_metadata.communicator_name,
163
+ )
164
+
165
+ @staticmethod
166
+ def send_multiple_tensors(
167
+ tensors: List["torch.Tensor"],
168
+ tensor_transport_metadata: CollectiveTransportMetadata,
169
+ communicator_metadata: CollectiveCommunicatorMetadata,
170
+ ):
171
+ import ray.util.collective as collective
172
+
173
+ device = tensors[0].device if tensors else None
174
+
175
+ for tensor in tensors:
176
+ if tensor.device.type != device.type:
177
+ raise ValueError(
178
+ f"tensor device {tensor.device} does not match device {device}"
179
+ )
180
+ collective.send(
181
+ tensor,
182
+ communicator_metadata.dst_rank,
183
+ communicator_metadata.communicator_name,
184
+ )
185
+
186
+ @staticmethod
187
+ def garbage_collect(
188
+ obj_id: str, tensor_transport_meta: CollectiveTransportMetadata
189
+ ):
190
+ pass
191
+
192
+ @staticmethod
193
+ def abort_transport(
194
+ obj_id: str,
195
+ communicator_metadata: CollectiveCommunicatorMetadata,
196
+ ):
197
+ raise NotImplementedError(
198
+ "Collective transport does not support abort_transport for now."
199
+ )