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
55from __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+
3864class 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
0 commit comments