Skip to content

Commit dad6a42

Browse files
Andy-Jostcursoragentrwgk
authored
cuda.core: Cythonize GraphBuilder and Graph with handle-layer cleanup (#2008)
* cuda.core: convert GraphBuilder to cdef class with explicit state machine Refactor GraphBuilder from a Python class using _MembersNeededForFinalize to a cdef class with explicit _BuilderKind (PRIMARY/FORKED/CONDITIONAL_BODY) and _CaptureState (NOT_STARTED/CAPTURING/ENDED) tracking. Cleanup moves into __dealloc__/close, and the builder now uses GraphHandle/StreamHandle from _resource_handles instead of holding raw driver objects. Drop the is_stream_owner flag now that StreamHandle owns the lifetime. End-capture paths in __dealloc__ and close guard on _h_stream so cleanup is safe even if _init* fails before completing assignment. Made-with: Cursor * cuda.core: convert Graph to cdef class with GraphExecHandle Add a GraphExecHandle to the resource-handle layer (parallel to GraphHandle) wrapping CUgraphExec with RAII cleanup via cuGraphExecDestroy on shared_ptr release. Convert Graph from a Python class using _MembersNeededForFinalize to a cdef class holding a typed _h_graph_exec attribute, dropping the weakref.finalize machinery. update/upload/launch move to nogil cydriver paths consistent with the GraphBuilder rewrite. Also drop quoted forward-reference annotations on create_graph_builder and _instantiate_graph/complete now that GraphBuilder is cimported in _device.pyx and _stream.pyx and Cython accepts the in-module forward reference to Graph. Clears the related "Strings should no longer be used for type declarations" warnings. Made-with: Cursor * fix(cuda.core): drop unused handle cimports flagged by cython-lint The cdef-class member declarations live in the .pxd, so the .pyx does not need to re-cimport GraphExecHandle, GraphHandle, or StreamHandle. Made-with: Cursor * fix(cuda.core): break _stream/_device <-> graph._graph_builder import cycle cimport-ing GraphBuilder at the top of _stream.pyx and _device.pyx made Cython emit a Python-level import of cuda.core.graph._graph_builder during _stream module init. That triggered the chain graph -> _graph_node -> _kernel_arg_handler -> _memory._buffer -> _device, which then re-entered the still-initializing _stream module via "from cuda.core._stream import IsStreamT", failing with ImportError: cannot import name IsStreamT. Restore the original lazy "import GraphBuilder" inside create_graph_builder (Stream and Device) and Stream_accept. The return annotations stay as bare names; "from __future__ import annotations" in both files defers their evaluation, so they need not resolve at function-definition time. Made-with: Cursor * fix(cuda.core): expose GraphBuilder._init as a Python-callable factory The previous import-cycle fix changed _stream/_device.create_graph_builder to a lazy Python "import GraphBuilder" instead of a module-level cimport. With _init declared as @staticmethod cdef, Python attribute lookup cannot find it, so every test that builds a graph failed with "AttributeError: type object 'GraphBuilder' has no attribute '_init'" at _device.pyx:1376 / _stream.pyx:376. Convert _init from @staticmethod cdef to @staticmethod def (matches the Stream._init pattern) and drop the cdef declaration from the .pxd. _init runs once per builder creation, so the loss of cdef-level dispatch is irrelevant. Graph._init stays cdef; it is only called intra-module. Made-with: Cursor * fix(cuda.core): pass non-NULL captureStatus to cuStreamGetCaptureInfo Every graph-builder test failed with CUDA_ERROR_INVALID_VALUE on the new ``GraphBuilder.begin_building`` path. The driver rejects ``cuStreamGetCaptureInfo`` when ``captureStatus_out`` is NULL, but the new ``_get_capture_info`` helper accepted a NULL status pointer and ``begin_building`` was calling it that way (it just wanted the freshly captured graph handle and assumed the status was implied by the preceding ``cuStreamBeginCapture``). Pass a stack-local ``CUstreamCaptureStatus`` and document the helper's requirement that ``status`` be non-NULL. ``graph`` is still allowed to be NULL (``is_building`` calls it that way and the driver accepts it). Co-authored-by: Cursor <cursoragent@cursor.com> * test: verify Graph.close() is idempotent (Glasswing V18.1) Add coverage that repeated close() does not double-destroy the graph exec handle. Addresses NVBugs 6268912 / cuda-python-private#370 via the handle-layer refactor in this PR. * cuda.core: harden GraphBuilder state machine and expand graph tests Add CLOSED state with GB_check_open guards, fix begin_building ordering so capture cleanup runs on partial failure, preserve FORKED primary-graph refs for conditional nodes, and defer Graph._init until instantiate succeeds. Add tests for Leo's review gaps including closed-builder errors and forked conditionals. * fix(cuda.core): resolve merge fallout in resource handle stubs Drop an unnecessary cimport from _resource_handles.pyx that broke stubgen/mypy after merging main, regenerate the affected .pyi files, and fix debug_dot_print path encoding for Cython 3.2. * fix(cuda.core): pass non-NULL phGraph to cuStreamEndCapture Use a stack-local CUgraph output for end_building() and GB_end_capture_if_needed(), and release the GIL inside the helper so driver calls are nogil regardless of caller (__dealloc__ or close()). --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Ralf W. Grosse-Kunstleve <rwgkio@gmail.com>
1 parent 66cb8cb commit dad6a42

12 files changed

Lines changed: 575 additions & 224 deletions

cuda_core/cuda/core/_cpp/resource_handles.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ decltype(&cuLibraryGetKernel) p_cuLibraryGetKernel = nullptr;
6969

7070
// Graph
7171
decltype(&cuGraphDestroy) p_cuGraphDestroy = nullptr;
72+
decltype(&cuGraphExecDestroy) p_cuGraphExecDestroy = nullptr;
7273

7374
// Linker
7475
decltype(&cuLinkDestroy) p_cuLinkDestroy = nullptr;
@@ -1136,6 +1137,28 @@ GraphHandle create_graph_handle_ref(CUgraph graph, const GraphHandle& h_parent)
11361137
return GraphHandle(box, &box->resource);
11371138
}
11381139

1140+
// ============================================================================
1141+
// Graph Exec Handles
1142+
// ============================================================================
1143+
1144+
namespace {
1145+
struct GraphExecBox {
1146+
CUgraphExec resource;
1147+
};
1148+
} // namespace
1149+
1150+
GraphExecHandle create_graph_exec_handle(CUgraphExec graph_exec) {
1151+
auto box = std::shared_ptr<const GraphExecBox>(
1152+
new GraphExecBox{graph_exec},
1153+
[](const GraphExecBox* b) {
1154+
GILReleaseGuard gil;
1155+
p_cuGraphExecDestroy(b->resource);
1156+
delete b;
1157+
}
1158+
);
1159+
return GraphExecHandle(box, &box->resource);
1160+
}
1161+
11391162
namespace {
11401163
struct GraphNodeBox {
11411164
mutable CUgraphNode resource;

cuda_core/cuda/core/_cpp/resource_handles.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ extern decltype(&cuLibraryGetKernel) p_cuLibraryGetKernel;
108108

109109
// Graph
110110
extern decltype(&cuGraphDestroy) p_cuGraphDestroy;
111+
extern decltype(&cuGraphExecDestroy) p_cuGraphExecDestroy;
111112

112113
// Linker
113114
extern decltype(&cuLinkDestroy) p_cuLinkDestroy;
@@ -183,6 +184,7 @@ using MemoryPoolHandle = std::shared_ptr<const CUmemoryPool>;
183184
using LibraryHandle = std::shared_ptr<const CUlibrary>;
184185
using KernelHandle = std::shared_ptr<const CUkernel>;
185186
using GraphHandle = std::shared_ptr<const CUgraph>;
187+
using GraphExecHandle = std::shared_ptr<const CUgraphExec>;
186188
using GraphNodeHandle = std::shared_ptr<const CUgraphNode>;
187189
using GraphicsResourceHandle = std::shared_ptr<const CUgraphicsResource>;
188190
using NvrtcProgramHandle = std::shared_ptr<const nvrtcProgram>;
@@ -464,6 +466,14 @@ GraphHandle create_graph_handle(CUgraph graph);
464466
// but h_parent will be prevented from destruction while this handle exists.
465467
GraphHandle create_graph_handle_ref(CUgraph graph, const GraphHandle& h_parent);
466468

469+
// ============================================================================
470+
// Graph exec handle functions
471+
// ============================================================================
472+
473+
// Wrap an externally-created CUgraphExec with RAII cleanup.
474+
// When the last reference is released, cuGraphExecDestroy is called automatically.
475+
GraphExecHandle create_graph_exec_handle(CUgraphExec graph_exec);
476+
467477
// ============================================================================
468478
// Graph node handle functions
469479
// ============================================================================
@@ -649,6 +659,10 @@ inline CUgraph as_cu(const GraphHandle& h) noexcept {
649659
return h ? *h : nullptr;
650660
}
651661

662+
inline CUgraphExec as_cu(const GraphExecHandle& h) noexcept {
663+
return h ? *h : nullptr;
664+
}
665+
652666
inline CUgraphNode as_cu(const GraphNodeHandle& h) noexcept {
653667
return h ? *h : nullptr;
654668
}
@@ -729,6 +743,10 @@ inline std::intptr_t as_intptr(const GraphHandle& h) noexcept {
729743
return reinterpret_cast<std::intptr_t>(as_cu(h));
730744
}
731745

746+
inline std::intptr_t as_intptr(const GraphExecHandle& h) noexcept {
747+
return reinterpret_cast<std::intptr_t>(as_cu(h));
748+
}
749+
732750
inline std::intptr_t as_intptr(const GraphNodeHandle& h) noexcept {
733751
return reinterpret_cast<std::intptr_t>(as_cu(h));
734752
}
@@ -855,6 +873,10 @@ inline PyObject* as_py(const GraphHandle& h) noexcept {
855873
return detail::make_py("cuda.bindings.driver", "CUgraph", as_intptr(h));
856874
}
857875

876+
inline PyObject* as_py(const GraphExecHandle& h) noexcept {
877+
return detail::make_py("cuda.bindings.driver", "CUgraphExec", as_intptr(h));
878+
}
879+
858880
inline PyObject* as_py(const GraphNodeHandle& h) noexcept {
859881
if (!as_intptr(h)) {
860882
Py_RETURN_NONE;

cuda_core/cuda/core/_device.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1454,7 +1454,7 @@ class Device:
14541454
from cuda.core.graph._graph_builder import GraphBuilder
14551455

14561456
self._check_context_initialized()
1457-
return GraphBuilder._init(stream=self.create_stream(), is_stream_owner=True)
1457+
return GraphBuilder._init(self.create_stream())
14581458

14591459

14601460
cdef inline int Device_ensure_cuda_initialized() except? -1:

cuda_core/cuda/core/_resource_handles.pxd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core":
2828
ctypedef shared_ptr[const cydriver.CUlibrary] LibraryHandle
2929
ctypedef shared_ptr[const cydriver.CUkernel] KernelHandle
3030
ctypedef shared_ptr[const cydriver.CUgraph] GraphHandle
31+
ctypedef shared_ptr[const cydriver.CUgraphExec] GraphExecHandle
3132
ctypedef shared_ptr[const cydriver.CUgraphNode] GraphNodeHandle
3233
ctypedef shared_ptr[const cydriver.CUgraphicsResource] GraphicsResourceHandle
3334
ctypedef shared_ptr[const cynvrtc.nvrtcProgram] NvrtcProgramHandle
@@ -66,6 +67,7 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core":
6667
cydriver.CUlibrary as_cu(LibraryHandle h) noexcept nogil
6768
cydriver.CUkernel as_cu(KernelHandle h) noexcept nogil
6869
cydriver.CUgraph as_cu(GraphHandle h) noexcept nogil
70+
cydriver.CUgraphExec as_cu(GraphExecHandle h) noexcept nogil
6971
cydriver.CUgraphNode as_cu(GraphNodeHandle h) noexcept nogil
7072
cydriver.CUgraphicsResource as_cu(GraphicsResourceHandle h) noexcept nogil
7173
cynvrtc.nvrtcProgram as_cu(NvrtcProgramHandle h) noexcept nogil
@@ -87,6 +89,7 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core":
8789
intptr_t as_intptr(LibraryHandle h) noexcept nogil
8890
intptr_t as_intptr(KernelHandle h) noexcept nogil
8991
intptr_t as_intptr(GraphHandle h) noexcept nogil
92+
intptr_t as_intptr(GraphExecHandle h) noexcept nogil
9093
intptr_t as_intptr(GraphNodeHandle h) noexcept nogil
9194
intptr_t as_intptr(GraphicsResourceHandle h) noexcept nogil
9295
intptr_t as_intptr(NvrtcProgramHandle h) noexcept nogil
@@ -109,6 +112,7 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core":
109112
object as_py(LibraryHandle h)
110113
object as_py(KernelHandle h)
111114
object as_py(GraphHandle h)
115+
object as_py(GraphExecHandle h)
112116
object as_py(GraphNodeHandle h)
113117
object as_py(GraphicsResourceHandle h)
114118
object as_py(NvrtcProgramHandle h)
@@ -219,6 +223,9 @@ cdef LibraryHandle get_kernel_library(const KernelHandle& h) noexcept nogil
219223
cdef GraphHandle create_graph_handle(cydriver.CUgraph graph) except+ nogil
220224
cdef GraphHandle create_graph_handle_ref(cydriver.CUgraph graph, const GraphHandle& h_parent) except+ nogil
221225

226+
# Graph exec handles
227+
cdef GraphExecHandle create_graph_exec_handle(cydriver.CUgraphExec graph_exec) except+ nogil
228+
222229
# Graph node handles
223230
cdef GraphNodeHandle create_graph_node_handle(cydriver.CUgraphNode node, const GraphHandle& h_graph) except+ nogil
224231
cdef GraphHandle graph_node_get_graph(const GraphNodeHandle& h) noexcept nogil

cuda_core/cuda/core/_resource_handles.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ DevicePtrHandle = shared_ptr
1313
LibraryHandle = shared_ptr
1414
KernelHandle = shared_ptr
1515
GraphHandle = shared_ptr
16+
GraphExecHandle = shared_ptr
1617
GraphNodeHandle = shared_ptr
1718
GraphicsResourceHandle = shared_ptr
1819
NvrtcProgramHandle = shared_ptr

cuda_core/cuda/core/_resource_handles.pyx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core":
151151
GraphHandle create_graph_handle_ref "cuda_core::create_graph_handle_ref" (
152152
cydriver.CUgraph graph, const GraphHandle& h_parent) except+ nogil
153153

154+
# Graph exec handles
155+
GraphExecHandle create_graph_exec_handle "cuda_core::create_graph_exec_handle" (
156+
cydriver.CUgraphExec graph_exec) except+ nogil
157+
154158
# Graph node handles
155159
GraphNodeHandle create_graph_node_handle "cuda_core::create_graph_node_handle" (
156160
cydriver.CUgraphNode node, const GraphHandle& h_graph) except+ nogil
@@ -299,6 +303,7 @@ cdef extern from "_cpp/resource_handles.hpp" namespace "cuda_core":
299303

300304
# Graph
301305
void* p_cuGraphDestroy "reinterpret_cast<void*&>(cuda_core::p_cuGraphDestroy)"
306+
void* p_cuGraphExecDestroy "reinterpret_cast<void*&>(cuda_core::p_cuGraphExecDestroy)"
302307

303308
# Linker
304309
void* p_cuLinkDestroy "reinterpret_cast<void*&>(cuda_core::p_cuLinkDestroy)"
@@ -358,7 +363,7 @@ cdef void _init_driver_fn_pointers() noexcept:
358363
global p_cuMemFreeAsync, p_cuMemFree, p_cuMemFreeHost
359364
global p_cuMemPoolImportPointer
360365
global p_cuLibraryLoadFromFile, p_cuLibraryLoadData, p_cuLibraryUnload, p_cuLibraryGetKernel
361-
global p_cuGraphDestroy
366+
global p_cuGraphDestroy, p_cuGraphExecDestroy
362367
global p_cuLinkDestroy
363368
global p_cuGraphicsUnmapResources, p_cuGraphicsUnregisterResource
364369
global p_cuDevSmResourceSplit
@@ -418,6 +423,7 @@ cdef void _init_driver_fn_pointers() noexcept:
418423

419424
# Graph
420425
p_cuGraphDestroy = _get_driver_fn("cuGraphDestroy")
426+
p_cuGraphExecDestroy = _get_driver_fn("cuGraphExecDestroy")
421427

422428
# Linker
423429
p_cuLinkDestroy = _get_driver_fn("cuLinkDestroy")

cuda_core/cuda/core/_stream.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ cdef class Stream:
414414
"""
415415
from cuda.core.graph._graph_builder import GraphBuilder
416416

417-
return GraphBuilder._init(stream=self, is_stream_owner=False)
417+
return GraphBuilder._init(self)
418418

419419

420420
LEGACY_DEFAULT_STREAM: Stream = Stream._legacy_default()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
from cuda.bindings cimport cydriver
6+
7+
from cuda.core._resource_handles cimport GraphExecHandle, GraphHandle, StreamHandle
8+
from cuda.core._stream cimport Stream
9+
10+
11+
cdef class GraphBuilder:
12+
cdef:
13+
GraphHandle _h_graph
14+
StreamHandle _h_stream
15+
int _kind
16+
int _state
17+
Stream _stream # cached to avoid reconstruction from _h_stream handle
18+
object __weakref__
19+
20+
21+
cdef class Graph:
22+
cdef:
23+
GraphExecHandle _h_graph_exec
24+
object __weakref__
25+
26+
@staticmethod
27+
cdef Graph _init(cydriver.CUgraphExec graph_exec)

cuda_core/cuda/core/graph/_graph_builder.pyi

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ from cuda.core._stream import Stream
88
from cuda.core._utils.cuda_utils import driver
99
from cuda.core.graph._graph_definition import GraphCondition, GraphDefinition
1010

11+
_BuilderKind = int
12+
_CaptureState = int
1113

1214
@dataclass
1315
class GraphDebugPrintOptions:
@@ -106,23 +108,19 @@ class GraphBuilder:
106108
107109
"""
108110

109-
class _MembersNeededForFinalize:
110-
__slots__ = ('conditional_graph', 'graph', 'is_join_required', 'is_stream_owner', 'stream')
111-
112-
def __init__(self, graph_builder_obj: GraphBuilder, stream_obj: Stream | None, is_stream_owner: bool, conditional_graph, is_join_required: bool) -> None:
113-
...
114-
115-
def close(self) -> None:
116-
...
117-
__slots__ = ('__weakref__', '_building_ended', '_mnff')
111+
def __init__(self):
112+
...
118113

119-
def __init__(self) -> None:
114+
def __dealloc__(self):
120115
...
121116

122-
@classmethod
123-
def _init(cls, stream: Stream | None, is_stream_owner: bool, conditional_graph: object=None, is_join_required: bool=False) -> GraphBuilder:
117+
@staticmethod
118+
def _init(stream: Stream):
124119
...
125120

121+
def close(self):
122+
"""Destroy the graph builder."""
123+
126124
@property
127125
def stream(self) -> Stream:
128126
"""Returns the stream associated with the graph builder."""
@@ -155,7 +153,7 @@ class GraphBuilder:
155153
def end_building(self) -> GraphBuilder:
156154
"""Ends the building process."""
157155

158-
def complete(self, options: GraphCompleteOptions | None=None) -> 'Graph':
156+
def complete(self, options: GraphCompleteOptions | None=None) -> Graph:
159157
"""Completes the graph builder and returns the built :obj:`~graph.Graph` object.
160158
161159
Parameters
@@ -245,9 +243,6 @@ class GraphBuilder:
245243
A condition variable for controlling conditional execution.
246244
"""
247245

248-
def _cond_with_params(self, node_params: object) -> tuple[GraphBuilder, ...]:
249-
...
250-
251246
def if_then(self, condition: GraphCondition) -> GraphBuilder:
252247
"""Adds an if condition branch and returns a new graph builder for it.
253248
@@ -335,15 +330,7 @@ class GraphBuilder:
335330
336331
"""
337332

338-
def close(self) -> None:
339-
"""Destroy the graph builder.
340-
341-
Closes the associated stream if we own it. Borrowed stream
342-
object will instead have their references released.
343-
344-
"""
345-
346-
def embed(self, child: GraphBuilder) -> None:
333+
def embed(self, child: GraphBuilder):
347334
"""Embed a previously-built :obj:`~graph.GraphBuilder` as a child node.
348335
349336
Parameters
@@ -392,21 +379,7 @@ class Graph:
392379
393380
"""
394381

395-
class _MembersNeededForFinalize:
396-
__slots__ = 'graph'
397-
398-
def __init__(self, graph_obj: Graph, graph: driver.CUgraphExec) -> None:
399-
...
400-
401-
def close(self) -> None:
402-
...
403-
__slots__ = ('__weakref__', '_mnff')
404-
405-
def __init__(self) -> None:
406-
...
407-
408-
@classmethod
409-
def _init(cls, graph: driver.CUgraphExec) -> Graph:
382+
def __init__(self):
410383
...
411384

412385
def close(self) -> None:
@@ -457,5 +430,5 @@ class Graph:
457430
"""
458431
__all__ = ['Graph', 'GraphBuilder', 'GraphCompleteOptions', 'GraphDebugPrintOptions']
459432

460-
def _instantiate_graph(h_graph, options: GraphCompleteOptions | None=None) -> 'Graph':
433+
def _instantiate_graph(h_graph, options: GraphCompleteOptions | None=None) -> Graph:
461434
...

0 commit comments

Comments
 (0)