Skip to content

Commit 5cb9d01

Browse files
committed
Draft
Draft Initial weakref work Use stream as name stash
1 parent 41fdb86 commit 5cb9d01

5 files changed

Lines changed: 302 additions & 0 deletions

File tree

cuda_core/cuda/core/experimental/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from cuda.core.experimental import utils
66
from cuda.core.experimental._device import Device
77
from cuda.core.experimental._event import Event, EventOptions
8+
from cuda.core.experimental._graph import Graph, GraphBuilder
89
from cuda.core.experimental._launcher import LaunchConfig, launch
910
from cuda.core.experimental._linker import Linker, LinkerOptions
1011
from cuda.core.experimental._module import ObjectCode

cuda_core/cuda/core/experimental/_device.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from cuda.core.experimental._context import Context, ContextOptions
99
from cuda.core.experimental._event import Event, EventOptions
10+
from cuda.core.experimental._graph import GraphBuilder
1011
from cuda.core.experimental._memory import Buffer, MemoryResource, _DefaultAsyncMempool, _SynchronousMemoryResource
1112
from cuda.core.experimental._stream import Stream, StreamOptions, default_stream
1213
from cuda.core.experimental._utils.clear_error_support import assert_type
@@ -1271,3 +1272,8 @@ def sync(self):
12711272
12721273
"""
12731274
handle_return(runtime.cudaDeviceSynchronize())
1275+
1276+
@precondition(_check_context_initialized)
1277+
def build_graph(self) -> GraphBuilder:
1278+
private_stream = self.create_stream()
1279+
return GraphBuilder._init(stream=private_stream)
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2+
#
3+
# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE
4+
5+
from __future__ import annotations
6+
7+
from dataclasses import dataclass
8+
import weakref
9+
from typing import TYPE_CHECKING, Optional, Tuple
10+
11+
if TYPE_CHECKING:
12+
from cuda.core.experimental._stream import Stream
13+
from cuda.core.experimental._utils import driver, handle_return, precondition
14+
15+
16+
@dataclass
17+
class DebugPrintOptions:
18+
""" """
19+
20+
VERBOSE: bool = False
21+
RUNTIME_TYPES: bool = False
22+
KERNEL_NODE_PARAMS: bool = False
23+
MEMCPY_NODE_PARAMS: bool = False
24+
MEMSET_NODE_PARAMS: bool = False
25+
HOST_NODE_PARAMS: bool = False
26+
EVENT_NODE_PARAMS: bool = False
27+
EXT_SEMAS_SIGNAL_NODE_PARAMS: bool = False
28+
EXT_SEMAS_WAIT_NODE_PARAMS: bool = False
29+
KERNEL_NODE_ATTRIBUTES: bool = False
30+
HANDLES: bool = False
31+
MEM_ALLOC_NODE_PARAMS: bool = False
32+
MEM_FREE_NODE_PARAMS: bool = False
33+
BATCH_MEM_OP_NODE_PARAMS: bool = False
34+
EXTRA_TOPO_INFO: bool = False
35+
CONDITIONAL_NODE_PARAMS: bool = False
36+
37+
38+
class GraphBuilder:
39+
"""TBD
40+
41+
Directly creating a :obj:`~_graph.GraphBuilder` is not supported due
42+
to ambiguity. New graph builders should instead be created through a
43+
:obj:`~_device.Device`, or a :obj:`~_stream.stream` object
44+
"""
45+
46+
class _MembersNeededForFinalize:
47+
__slots__ = ("stream", "graph")
48+
49+
def __init__(self, graph_builder_obj, stream_obj):
50+
self.stream = stream_obj
51+
self.graph = None
52+
weakref.finalize(graph_builder_obj, self.close)
53+
54+
def close(self):
55+
# FIXME: Are the stream and graph builder racing for the weakref callback?
56+
# If so, maybe we need to enforce that all capture is completed
57+
status = handle_return(driver.cuStreamGetCaptureInfo(self.stream.handle))[0]
58+
if status != driver.CUstreamCaptureStatus.CU_STREAM_CAPTURE_STATUS_NONE:
59+
# Callback routine needs to end capture for error free handling
60+
handle_return(driver.cuStreamEndCapture(self.stream.handle))
61+
if self.graph:
62+
handle_return(driver.cuGraphDestroy(self.graph))
63+
self.graph = None
64+
65+
__slots__ = ("__weakref__", "_mnff", "_is_primary", "_capturing")
66+
67+
def __init__(self):
68+
raise NotImplementedError(
69+
"directly creating a Graph object can be ambiguous. Please either "
70+
"call Device.create_graph() or stream.creating_graph()"
71+
)
72+
73+
@staticmethod
74+
def _init(stream, _is_primary=True):
75+
self = GraphBuilder.__new__(GraphBuilder)
76+
# TODO: I need to know if we own this stream object.
77+
# If from Device(), then we can destroy it on close
78+
# If from Stream, then we can't
79+
self._capturing = False
80+
self._is_primary = _is_primary
81+
self._mnff = GraphBuilder._MembersNeededForFinalize(self, stream)
82+
return self
83+
84+
def _check_capture_stream_provided(self, *args, **kwargs):
85+
if self._mnff.stream == None:
86+
raise RuntimeError("Tried to use a stream capture operation on a graph builder without a stream")
87+
88+
@property
89+
def stream(self) -> Stream:
90+
return self._mnff.stream
91+
92+
@property
93+
def is_primary(self) -> bool:
94+
return self._is_primary
95+
96+
@precondition(_check_capture_stream_provided)
97+
def begin_capture(self, mode="global"):
98+
# Supports "global", "local" or "relaxed"
99+
# TODO; Test case for each mode and fail
100+
if mode == "global":
101+
capture_mode = driver.CUstreamCaptureMode.CU_STREAM_CAPTURE_MODE_GLOBAL
102+
elif mode == "local":
103+
capture_mode = driver.CUstreamCaptureMode.CU_STREAM_CAPTURE_MODE_THREAD_LOCAL
104+
elif mode == "relaxed":
105+
capture_mode = driver.CUstreamCaptureMode.CU_STREAM_CAPTURE_MODE_RELAXED
106+
else:
107+
raise ValueError(f"Only 'global', 'local' or 'relaxed' capture mode are supported, got {capture_mode}")
108+
109+
handle_return(driver.cuStreamBeginCapture(self._mnff.stream.handle, capture_mode))
110+
self._capturing = True
111+
112+
@precondition(_check_capture_stream_provided)
113+
def is_capture_active(self) -> bool:
114+
result = handle_return(driver.cuStreamGetCaptureInfo(self._mnff.stream.handle))
115+
116+
capture_status = result[0]
117+
if capture_status == driver.CUstreamCaptureStatus.CU_STREAM_CAPTURE_STATUS_NONE:
118+
return False
119+
elif capture_status == driver.CUstreamCaptureStatus.CU_STREAM_CAPTURE_STATUS_ACTIVE:
120+
return True
121+
elif capture_status == driver.CUstreamCaptureStatus.CU_STREAM_CAPTURE_STATUS_INVALIDATED:
122+
raise RuntimeError(
123+
"Stream is part of a capture sequence that has been invalidated, but "
124+
"not terminated. The capture sequence must be terminated with self.`()."
125+
)
126+
else:
127+
raise NotImplementedError(f"Unsupported capture stuse type received: {capture_status}")
128+
129+
@precondition(_check_capture_stream_provided)
130+
def end_capture(self):
131+
if not self._capturing:
132+
raise RuntimeError("Stream is not capturing. Did you forget to call begin_capture()?")
133+
self._mnff.graph = handle_return(driver.cuStreamEndCapture(self.stream.handle))
134+
self._capturing = False
135+
136+
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.
138+
if self._mnff.graph == None:
139+
raise RuntimeError("Graph needs to be built before generating a DOT debug file")
140+
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, ...]:
147+
if count <= 1:
148+
raise ValueError(f"Invalid fork count: expecting >= 2, got {count}")
149+
150+
# 1. Record an event on our stream
151+
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:
162+
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?
166+
event.close()
167+
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]
170+
171+
def join(self, *graph_builders):
172+
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()
185+
186+
def create_conditional_handle(self, default_value=None):
187+
pass
188+
189+
def if_cond(self, handle):
190+
pass
191+
192+
def if_else(self, handle):
193+
pass
194+
195+
def switch(self, handle, count):
196+
pass
197+
198+
def close(self):
199+
if self._mnff.capturing:
200+
# Explicitly trying to close a graph builder who is still capturing is not allowed
201+
raise RuntimeError("Trying to close a graph builder who is still capturing. Did you forget to call end_capture()?")
202+
self._mnff.close()
203+
204+
205+
class Graph:
206+
""" """
207+
208+
def __init__(self):
209+
raise RuntimeError("directly constructing a Graph instance is not supported")
210+
211+
@staticmethod
212+
def _init(graph):
213+
self = Graph.__new__(Graph)
214+
self._graph = graph
215+
return self

cuda_core/cuda/core/experimental/_stream.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
handle_return,
2424
runtime,
2525
)
26+
from cuda.core.experimental._graph import GraphBuilder
2627

2728

2829
@dataclass
@@ -342,6 +343,9 @@ def __cuda_stream__(self):
342343

343344
return Stream._init(obj=_stream_holder())
344345

346+
def build_graph(self) -> GraphBuilder:
347+
return GraphBuilder._init(stream=self)
348+
345349

346350
LEGACY_DEFAULT_STREAM = Stream._legacy_default()
347351
PER_THREAD_DEFAULT_STREAM = Stream._per_thread_default()

cuda_core/tests/test_graph.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright 2025 NVIDIA Corporation. All rights reserved.
2+
#
3+
# Please refer to the NVIDIA end user license agreement (EULA) associated
4+
# with this source code for terms and conditions that govern your use of
5+
# this software. Any use, reproduction, disclosure, or distribution of
6+
# this software and related documentation outside the terms of the EULA
7+
# is strictly prohibited.
8+
9+
10+
from cuda.core.experimental import Device, LaunchConfig, Program, ProgramOptions, launch
11+
12+
# from cuda.core.experimental import Device, Stream, StreamOptions
13+
# from cuda.core.experimental._event import Event
14+
# from cuda.core.experimental._stream import LEGACY_DEFAULT_STREAM, PER_THREAD_DEFAULT_STREAM, default_stream
15+
16+
17+
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
24+
25+
26+
def test_graph_straight(init_cuda):
27+
# TODO: Maybe share these between tests?
28+
code = """
29+
__global__ void empty_kernel() {}
30+
"""
31+
arch = "".join(f"{i}" for i in Device().compute_capability)
32+
program_options = ProgramOptions(std="c++17", arch=f"sm_{arch}")
33+
prog = Program(code, code_type="c++", options=program_options)
34+
mod = prog.compile("cubin", name_expressions=("empty_kernel",))
35+
empty_kernel = mod.get_kernel("empty_kernel")
36+
37+
# 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()
47+
48+
49+
def test_graph_fork_join(init_cuda):
50+
# TODO: Maybe share these between tests?
51+
code = """
52+
__global__ void empty_kernel() {}
53+
"""
54+
arch = "".join(f"{i}" for i in Device().compute_capability)
55+
program_options = ProgramOptions(std="c++17", arch=f"sm_{arch}")
56+
prog = Program(code, code_type="c++", options=program_options)
57+
mod = prog.compile("cubin", name_expressions=("empty_kernel",))
58+
empty_kernel = mod.get_kernel("empty_kernel")
59+
60+
# 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})
65+
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)
72+
73+
launch(empty_kernel, {"grid": 1, "block": 1, "stream": graph_builder.stream})
74+
graph_builder.end_capture()
75+
76+
graph_builder.debug_dot_print(b"vlad.dot")

0 commit comments

Comments
 (0)