Skip to content

Commit 68e67e7

Browse files
committed
Align with design
1 parent 5cb9d01 commit 68e67e7

4 files changed

Lines changed: 169 additions & 78 deletions

File tree

cuda_core/cuda/core/experimental/_device.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,14 @@ def sync(self):
12741274
handle_return(runtime.cudaDeviceSynchronize())
12751275

12761276
@precondition(_check_context_initialized)
1277-
def build_graph(self) -> GraphBuilder:
1277+
def create_graph_bulder(self) -> GraphBuilder:
1278+
"""Create a new :obj:`~_graph.GraphBuilder` object.
1279+
1280+
Returns
1281+
-------
1282+
:obj:`~_graph.GraphBuilder`
1283+
Newly created graph builder object.
1284+
1285+
"""
12781286
private_stream = self.create_stream()
1279-
return GraphBuilder._init(stream=private_stream)
1287+
return GraphBuilder._init(stream=private_stream, can_destroy_stream=True)

cuda_core/cuda/core/experimental/_graph.py

Lines changed: 119 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
1+
# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
22
#
3-
# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE
3+
# SPDX-License-Identifier: Apache-2.0
44

55
from __future__ import annotations
66

@@ -35,6 +35,32 @@ class DebugPrintOptions:
3535
CONDITIONAL_NODE_PARAMS: bool = False
3636

3737

38+
@dataclass
39+
class CompleteOptions:
40+
"""Customizable options for :obj:`_graph.GraphBuilder.complete()`
41+
42+
Attributes
43+
----------
44+
auto_free_on_launch : bool, optional
45+
Automatically free memory allocated in a graph before relaunching. (Default to False)
46+
upload : bool, optional
47+
Automatically upload the graph after instantiation. (Default to False)
48+
device_launch : bool, optional
49+
Configure the graph to be launchable from the device. This flag can only
50+
be used on platforms which support unified addressing. This flag cannot be
51+
used in conjunction with auto_free_on_launch. (Default to False)
52+
use_node_priority : bool, optional
53+
Run the graph using the per-node priority attributes rather than the
54+
priority of the stream it is launched into. (Default to False)
55+
56+
"""
57+
58+
auto_free_on_launch: bool = False
59+
upload: bool = False
60+
device_launch: bool = False
61+
use_node_priority: bool = False
62+
63+
3864
class GraphBuilder:
3965
"""TBD
4066
@@ -71,13 +97,14 @@ def __init__(self):
7197
)
7298

7399
@staticmethod
74-
def _init(stream, _is_primary=True):
100+
def _init(stream, can_destroy_stream, _is_primary=True):
75101
self = GraphBuilder.__new__(GraphBuilder)
76102
# TODO: I need to know if we own this stream object.
77103
# If from Device(), then we can destroy it on close
78104
# If from Stream, then we can't
79105
self._capturing = False
80106
self._is_primary = _is_primary
107+
self._can_destroy_stream = can_destroy_stream
81108
self._mnff = GraphBuilder._MembersNeededForFinalize(self, stream)
82109
return self
83110

@@ -94,9 +121,8 @@ def is_primary(self) -> bool:
94121
return self._is_primary
95122

96123
@precondition(_check_capture_stream_provided)
97-
def begin_capture(self, mode="global"):
124+
def begin_building(self, mode="global") -> GraphBuilder:
98125
# Supports "global", "local" or "relaxed"
99-
# TODO; Test case for each mode and fail
100126
if mode == "global":
101127
capture_mode = driver.CUstreamCaptureMode.CU_STREAM_CAPTURE_MODE_GLOBAL
102128
elif mode == "local":
@@ -108,6 +134,7 @@ def begin_capture(self, mode="global"):
108134

109135
handle_return(driver.cuStreamBeginCapture(self._mnff.stream.handle, capture_mode))
110136
self._capturing = True
137+
return self
111138

112139
@precondition(_check_capture_stream_provided)
113140
def is_capture_active(self) -> bool:
@@ -121,67 +148,114 @@ def is_capture_active(self) -> bool:
121148
elif capture_status == driver.CUstreamCaptureStatus.CU_STREAM_CAPTURE_STATUS_INVALIDATED:
122149
raise RuntimeError(
123150
"Stream is part of a capture sequence that has been invalidated, but "
124-
"not terminated. The capture sequence must be terminated with self.`()."
151+
"not terminated. The capture sequence must be terminated with self.end_capture()."
125152
)
126153
else:
127154
raise NotImplementedError(f"Unsupported capture stuse type received: {capture_status}")
128155

129156
@precondition(_check_capture_stream_provided)
130-
def end_capture(self):
157+
def end_building(self) -> GraphBuilder:
131158
if not self._capturing:
132-
raise RuntimeError("Stream is not capturing. Did you forget to call begin_capture()?")
159+
raise RuntimeError("Stream is not capturing. Was self.begin_capture() called?")
133160
self._mnff.graph = handle_return(driver.cuStreamEndCapture(self.stream.handle))
134161
self._capturing = False
162+
return self
163+
164+
def complete(self, options: Optional[CompleteOptions] = None) -> Graph:
165+
flags = 0
166+
if options:
167+
if options.auto_free_on_launch:
168+
flags |= driver.CU_GRAPH_COMPLETE_FLAG_AUTO_FREE_ON_LAUNCH
169+
if options.upload:
170+
flags |= driver.CU_GRAPH_COMPLETE_FLAG_UPLOAD
171+
if options.device_launch:
172+
flags |= driver.CU_GRAPH_COMPLETE_FLAG_DEVICE_LAUNCH
173+
if options.use_node_priority:
174+
flags |= driver.CU_GRAPH_COMPLETE_FLAG_USE_NODE_PRIORITY
175+
176+
return Graph._init(handle_return(driver.cuGraphInstantiate(self._mnff.graph, flags)))
135177

136178
def debug_dot_print(self, path, options: Optional[DebugPrintOptions] = None):
137-
# TODO: We should be able to print one while the capture is happening right? Just need to make sure driver version is new enough.
138179
if self._mnff.graph == None:
139180
raise RuntimeError("Graph needs to be built before generating a DOT debug file")
140181

141-
# TODO: Apply each option to the value
142-
options_value = 0
143-
144-
handle_return(driver.cuGraphDebugDotPrint(self._mnff.graph, path, options_value))
145-
146-
def fork(self, count) -> Tuple[GraphBuilder, ...]:
182+
flags = 0
183+
if options:
184+
if options.VERBOSE:
185+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_VERBOSE
186+
if options.RUNTIME_TYPES:
187+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_RUNTIME_TYPES
188+
if options.KERNEL_NODE_PARAMS:
189+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_KERNEL_NODE_PARAMS
190+
if options.MEMCPY_NODE_PARAMS:
191+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_MEMCPY_NODE_PARAMS
192+
if options.MEMSET_NODE_PARAMS:
193+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_MEMSET_NODE_PARAMS
194+
if options.HOST_NODE_PARAMS:
195+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_HOST_NODE_PARAMS
196+
if options.EVENT_NODE_PARAMS:
197+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_EVENT_NODE_PARAMS
198+
if options.EXT_SEMAS_SIGNAL_NODE_PARAMS:
199+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_EXT_SEMAS_SIGNAL_NODE_PARAMS
200+
if options.EXT_SEMAS_WAIT_NODE_PARAMS:
201+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_EXT_SEMAS_WAIT_NODE_PARAMS
202+
if options.KERNEL_NODE_ATTRIBUTES:
203+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_KERNEL_NODE_ATTRIBUTES
204+
if options.HANDLES:
205+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_HANDLES
206+
if options.MEM_ALLOC_NODE_PARAMS:
207+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_MEM_ALLOC_NODE_PARAMS
208+
if options.MEM_FREE_NODE_PARAMS:
209+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_MEM_FREE_NODE_PARAMS
210+
if options.BATCH_MEM_OP_NODE_PARAMS:
211+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_BATCH_MEM_OP_NODE_PARAMS
212+
if options.EXTRA_TOPO_INFO:
213+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_EXTRA_TOPO_INFO
214+
if options.CONDITIONAL_NODE_PARAMS:
215+
flags |= driver.CU_GRAPH_DEBUG_DOT_PRINT_CONDITIONAL_NODE_PARAMS
216+
217+
handle_return(driver.cuGraphDebugDotPrint(self._mnff.graph, path, flags))
218+
219+
def split(self, count) -> Tuple[GraphBuilder, ...]:
147220
if count <= 1:
148-
raise ValueError(f"Invalid fork count: expecting >= 2, got {count}")
221+
raise ValueError(f"Invalid split count: expecting >= 2, got {count}")
149222

150-
# 1. Record an event on our stream
151223
event = self._mnff.stream.record()
152-
153-
# TODO: Steps 2,3,4 can be combined under a single loop
154-
155-
# 2. Create a streams for each of the new forks
156-
# TODO: Optimization where one of the fork stream is allowed to use
157-
# TODO: Should use the same stream options as initial stream??
158-
fork_stream = [self._mnff.stream.device.create_stream() for i in range(count)]
159-
160-
# 3. Have each new stream wait on our singular event
161-
for stream in fork_stream:
224+
result = [self]
225+
for i in range(count-1):
226+
stream = self._mnff.stream.device.create_stream()
162227
stream.wait(event)
163-
164-
# 4. Discard the event
165-
# TODO: Is this actually allowed when using with a graph? Surely, since it just needs to create an edge for us... right?
228+
result.append(GraphBuilder._init(stream=stream, is_primary=False))
166229
event.close()
230+
return result
167231

168-
# 5. Create new graph builders for each new stream fork
169-
return [GraphBuilder._init(stream=stream, is_primary=False) for stream in fork_stream]
232+
@staticmethod
233+
def join(*graph_builders):
234+
if not all(isinstance(builder, GraphBuilder) for builder in graph_builders):
235+
raise TypeError("All arguments must be GraphBuilder instances")
170236

171-
def join(self, *graph_builders):
172237
if len(graph_builders) < 1:
173-
raise ValueError("Must specify which graphs should join but none were given")
174-
175-
# Assert that none of the graph_builders are primary
176-
for graph in graph_builders:
177-
if graph.is_primary:
178-
raise ValueError("The primary graph builder should not be joined. Others builders should instead be joined onto it.")
179-
180-
for graph in graph_builders:
181-
self._mnff.stream.wait(graph.stream)
182-
# TODO: Do we close them now or let weakref handle it during garbage collection?
183-
# This is a perf question, is there a good default?
184-
graph.close()
238+
raise ValueError("Must join with at least two graph builders")
239+
240+
# Discover which builder should join
241+
join_idx = 0
242+
for i, builder in enumerate(graph_builders):
243+
if builder.is_primary:
244+
join_idx = i
245+
break
246+
247+
# Join builder waits on all builders
248+
for i, builder in enumerate(graph_builders):
249+
if i == join_idx:
250+
continue
251+
builder.stream.wait(builder.stream)
252+
builder.close()
253+
254+
return graph_builders[join_idx]
255+
256+
def __cuda_stream__(self) -> Tuple[int, int]:
257+
"""Return an instance of a __cuda_stream__ protocol."""
258+
return self.stream.__cuda_stream__
185259

186260
def create_conditional_handle(self, default_value=None):
187261
pass

cuda_core/cuda/core/experimental/_stream.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,18 @@ def __cuda_stream__(self):
343343

344344
return Stream._init(obj=_stream_holder())
345345

346-
def build_graph(self) -> GraphBuilder:
347-
return GraphBuilder._init(stream=self)
346+
def create_graph_builder(self) -> GraphBuilder:
347+
"""Create a new :obj:`~_graph.GraphBuilder` object.
348348
349+
The new graph builder will be associated with this stream.
350+
351+
Returns
352+
-------
353+
:obj:`~_graph.GraphBuilder`
354+
Newly created graph builder object.
355+
356+
"""
357+
return GraphBuilder._init(stream=self, can_destroy_stream=False)
349358

350359
LEGACY_DEFAULT_STREAM = Stream._legacy_default()
351360
PER_THREAD_DEFAULT_STREAM = Stream._per_thread_default()

cuda_core/tests/test_graph.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77
# is strictly prohibited.
88

99

10-
from cuda.core.experimental import Device, LaunchConfig, Program, ProgramOptions, launch
10+
from cuda.core.experimental import Device, LaunchConfig, Program, ProgramOptions, launch, GraphBuilder
1111

1212
# from cuda.core.experimental import Device, Stream, StreamOptions
1313
# from cuda.core.experimental._event import Event
1414
# from cuda.core.experimental._stream import LEGACY_DEFAULT_STREAM, PER_THREAD_DEFAULT_STREAM, default_stream
1515

1616

1717
def test_graph_is_capture_alive(init_cuda):
18-
graph_builder = Device().build_graph()
19-
assert graph_builder.is_capture_active() is False
20-
graph_builder.begin_capture()
21-
assert graph_builder.is_capture_active() is True
22-
graph_builder.end_capture()
23-
assert graph_builder.is_capture_active() is False
18+
gb = Device().create_gb()
19+
assert gb.is_capture_active() is False
20+
gb.begin_building()
21+
assert gb.is_capture_active() is True
22+
gb.end_building()
23+
assert gb.is_capture_active() is False
2424

2525

2626
def test_graph_straight(init_cuda):
@@ -35,15 +35,11 @@ def test_graph_straight(init_cuda):
3535
empty_kernel = mod.get_kernel("empty_kernel")
3636

3737
# Test start
38-
graph_builder = Device().build_graph()
39-
config = LaunchConfig(grid=1, block=1, stream=graph_builder.stream)
40-
41-
assert graph_builder.is_capture_active() is False
42-
graph_builder.begin_capture()
43-
launch(empty_kernel, {"grid": 1, "block": 1, "stream": graph_builder.stream})
44-
launch(empty_kernel, {"grid": 1, "block": 1, "stream": graph_builder.stream})
45-
launch(empty_kernel, {"grid": 1, "block": 1, "stream": graph_builder.stream})
46-
graph_builder.end_capture()
38+
gb = Device().create_gb().begin_building()
39+
launch(gb, LaunchConfig(grid=1, block=1), empty_kernel)
40+
launch(gb, LaunchConfig(grid=1, block=1), empty_kernel)
41+
launch(gb, LaunchConfig(grid=1, block=1), empty_kernel)
42+
graph = gb.end_building().complete()
4743

4844

4945
def test_graph_fork_join(init_cuda):
@@ -58,19 +54,23 @@ def test_graph_fork_join(init_cuda):
5854
empty_kernel = mod.get_kernel("empty_kernel")
5955

6056
# Test start
61-
graph_builder = Device().build_graph()
62-
assert graph_builder.is_capture_active() is False
63-
graph_builder.begin_capture()
64-
launch(empty_kernel, {"grid": 1, "block": 1, "stream": graph_builder.stream})
57+
gb = Device().create_gb().begin_building()
58+
launch(gb, LaunchConfig(grid=1, block=1), empty_kernel)
59+
60+
left, right = gb.split(2)
61+
launch(left, LaunchConfig(grid=1, block=1), empty_kernel)
62+
launch(left, LaunchConfig(grid=1, block=1), empty_kernel)
63+
launch(right, LaunchConfig(grid=1, block=1), empty_kernel)
64+
launch(right, LaunchConfig(grid=1, block=1), empty_kernel)
65+
gb = GraphBuilder.join(left, right)
6566

66-
left, right = graph_builder.fork(2)
67-
launch(empty_kernel, {"grid": 1, "block": 1, "stream": left.stream})
68-
launch(empty_kernel, {"grid": 1, "block": 1, "stream": left.stream})
69-
launch(empty_kernel, {"grid": 1, "block": 1, "stream": right.stream})
70-
launch(empty_kernel, {"grid": 1, "block": 1, "stream": right.stream})
71-
graph_builder.join(left, right)
67+
launch(gb, LaunchConfig(grid=1, block=1), empty_kernel)
68+
graph = gb.end_building().complete()
69+
# gb.debug_dot_print(b"vlad.dot")
7270

73-
launch(empty_kernel, {"grid": 1, "block": 1, "stream": graph_builder.stream})
74-
graph_builder.end_capture()
7571

76-
graph_builder.debug_dot_print(b"vlad.dot")
72+
# TODO: Test with subgraph
73+
# TODO: Test with conditional
74+
# TODO: Test using graph builder created from device
75+
# TODO: Test using graph builder created from stream
76+
# TODO: Check that the split invalidates the original builder

0 commit comments

Comments
 (0)