55from __future__ import annotations
66
77from dataclasses import dataclass
8+ import weakref
89from typing import TYPE_CHECKING , Optional , Tuple
910
1011if 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
178205class Graph :
0 commit comments