Skip to content

Commit 165eea0

Browse files
committed
Initial weakref work
1 parent 19e9304 commit 165eea0

1 file changed

Lines changed: 50 additions & 23 deletions

File tree

cuda_core/cuda/core/experimental/_graph.py

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from __future__ import annotations
66

77
from dataclasses import dataclass
8+
import weakref
89
from typing import TYPE_CHECKING, Optional, Tuple
910

1011
if TYPE_CHECKING:
@@ -42,34 +43,55 @@ class GraphBuilder:
4243
:obj:`~_device.Device`, or a :obj:`~_stream.stream` object
4344
"""
4445

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+
4567
def __init__(self):
4668
raise NotImplementedError(
4769
"directly creating a Graph object can be ambiguous. Please either "
4870
"call Device.create_graph() or stream.creating_graph()"
4971
)
5072

5173
@staticmethod
52-
def _init(stream, is_root=True):
74+
def _init(stream, _is_primary=True):
5375
self = GraphBuilder.__new__(GraphBuilder)
5476
# TODO: I need to know if we own this stream object.
5577
# If from Device(), then we can destroy it on close
5678
# If from Stream, then we can't
57-
self._stream = stream
5879
self._capturing = False
59-
self._is_root = is_root # TODO: Is this info needed?
80+
self._is_primary = _is_primary
81+
self._mnff = GraphBuilder._MembersNeededForFinalize(self, stream)
6082
return self
6183

6284
def _check_capture_stream_provided(self, *args, **kwargs):
63-
if self._stream == None:
85+
if self._mnff.stream == None:
6486
raise RuntimeError("Tried to use a stream capture operation on a graph builder without a stream")
6587

6688
@property
6789
def legacy_stream_capture(self) -> Stream:
68-
return self._stream
90+
return self._mnff.stream
6991

7092
@property
71-
def is_root_builder(self) -> bool:
72-
return self._is_root_builder
93+
def is_primary(self) -> bool:
94+
return self._is_primary
7395

7496
@precondition(_check_capture_stream_provided)
7597
def begin_capture(self, mode="global"):
@@ -84,12 +106,12 @@ def begin_capture(self, mode="global"):
84106
else:
85107
raise ValueError(f"Only 'global', 'local' or 'relaxed' capture mode are supported, got {capture_mode}")
86108

87-
handle_return(driver.cuStreamBeginCapture(self._stream.handle, capture_mode))
109+
handle_return(driver.cuStreamBeginCapture(self._mnff.stream.handle, capture_mode))
88110
self._capturing = True
89111

90112
@precondition(_check_capture_stream_provided)
91113
def is_capture_active(self) -> bool:
92-
result = handle_return(driver.cuStreamGetCaptureInfo(self._stream.handle))
114+
result = handle_return(driver.cuStreamGetCaptureInfo(self._mnff.stream.handle))
93115

94116
capture_status = result[0]
95117
if capture_status == driver.CUstreamCaptureStatus.CU_STREAM_CAPTURE_STATUS_NONE:
@@ -107,34 +129,33 @@ def is_capture_active(self) -> bool:
107129
@precondition(_check_capture_stream_provided)
108130
def end_capture(self):
109131
if not self._capturing:
110-
raise RuntimeError("Stream is not capturing")
111-
112-
self._graph = handle_return(driver.cuStreamEndCapture(self._stream.handle))
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))
113134
self._capturing = False
114135

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

120141
# TODO: Apply each option to the value
121142
options_value = 0
122143

123-
handle_return(driver.cuGraphDebugDotPrint(self._graph, path, options_value))
144+
handle_return(driver.cuGraphDebugDotPrint(self._mnff.graph, path, options_value))
124145

125146
def fork(self, count) -> Tuple[Graph, ...]:
126147
if count <= 1:
127148
raise ValueError(f"Invalid fork count: expecting >= 2, got {count}")
128149

129150
# 1. Record an event on our stream
130-
event = self._stream.record()
151+
event = self._mnff.stream.record()
131152

132153
# TODO: Steps 2,3,4 can be combined under a single loop
133154

134155
# 2. Create a streams for each of the new forks
135156
# TODO: Optimization where one of the fork stream is allowed to use
136157
# TODO: Should use the same stream options as initial stream??
137-
fork_stream = [self._stream.device.create_stream() for i in range(count)]
158+
fork_stream = [self._mnff.stream.device.create_stream() for i in range(count)]
138159

139160
# 3. Have each new stream wait on our singular event
140161
for stream in fork_stream:
@@ -145,15 +166,21 @@ def fork(self, count) -> Tuple[Graph, ...]:
145166
event.close()
146167

147168
# 5. Create new graph builders for each new stream fork
148-
return [GraphBuilder._init(stream=stream, is_root=False) for stream in fork_stream]
169+
return [GraphBuilder._init(stream=stream, is_primary=False) for stream in fork_stream]
149170

150171
def join(self, *graph_builders):
151172
if len(graph_builders) < 1:
152173
raise ValueError("Must specify which graphs should join but none were given")
153174

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+
154180
for graph in graph_builders:
155-
self._stream.wait(graph.legacy_stream_capture)
156-
# TODO: Can we close each of those new streams? Do we need weakref?
181+
self._mnff.stream.wait(graph.legacy_stream_capture)
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?
157184
graph.close()
158185

159186
def create_conditional_handle(self, default_value=None):
@@ -169,10 +196,10 @@ def switch(self, handle, count):
169196
pass
170197

171198
def close(self):
172-
if self._capturing:
173-
raise RuntimeError("Trying to close a graph builder who is still capturing")
174-
# Can we call this directly? Or relying on weakref enough?
175-
self._stream.close()
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()
176203

177204

178205
class Graph:

0 commit comments

Comments
 (0)