@@ -236,6 +236,97 @@ def tensor_swap(x, src_idxs, dst_idxs):
236236 x [dst_idxs ], x [src_idxs ] = x [src_idxs ], x [dst_idxs ]
237237
238238
239+ class CUDAGraphCache :
240+ """Syntactic sugar for capturing and replaying graphs over in-line code blocks.
241+
242+ In Python, neither context managers nor function calls allow the flexibility of this sugar.
243+ This class allows for the following type of usage:
244+
245+ ```
246+ cache_for_my_op = CUDAGraphCache()
247+ token_count = some_token_count_value
248+ request_count = some_request_count_value
249+
250+ for _ in cache_for_my_op(token_count, request_count):
251+ logits = extract_logits(n).float()
252+ probs = torch.softmax(logits, dim=-1)
253+ output.copy_(sample(probs))
254+ ```
255+
256+ The above example handles both capture and replay.
257+ During capture, the for-loop yields twice; during replay, the for-loop body is skipped.
258+ """
259+
260+ def __init__ (self ):
261+ self ._graphs : dict = {}
262+
263+ def __call__ (self , * keys , pool = None , skip_warmup = False , eager = False ):
264+ """Default use: captures if the keys are new, replays if the keys are cached.
265+
266+ Args:
267+ *keys: Arguments to key the graph on.
268+ pool (Optional): CUDA memory pool to build the graphs within.
269+ skip_warmup (Optional): If True, skip the warmup run before capture.
270+ eager (Optional): If True, run the body once without capturing or replaying.
271+ """
272+ return self .capture (
273+ * keys , pool = pool , replay_on_hit = True , skip_warmup = skip_warmup , eager = eager
274+ )
275+
276+ def __contains__ (self , key ) -> bool :
277+ return key in self ._graphs
278+
279+ def __delitem__ (self , key ) -> None :
280+ del self ._graphs [key ]
281+
282+ def clear (self ) -> None :
283+ """Remove all cached graphs."""
284+ self ._graphs .clear ()
285+
286+ def capture (self , * keys , pool = None , replay_on_hit = False , skip_warmup = False , eager = False ):
287+ """Inline capture: captures if the keys are new, does nothing if the keys are cached.
288+
289+ Args:
290+ *keys: Cache key components (stored as a tuple).
291+ pool (Optional): CUDA memory pool to build the graphs within.
292+ replay_on_hit (Optional): If True, replay the graph on cache hit.
293+ skip_warmup (Optional): If True, skip the warmup run before capture.
294+ eager (Optional): If True, run the body once without capturing or replaying.
295+ """
296+ if eager :
297+ yield
298+ return
299+ key = keys if len (keys ) != 1 else keys [0 ]
300+ if key in self ._graphs :
301+ if replay_on_hit :
302+ self ._graphs [key ].replay ()
303+ return
304+ if not skip_warmup :
305+ # Suggested best practice: run the body once before recording to ensure
306+ # the kernels are JIT-compiled and the allocator state is stable.
307+ yield
308+ # Now perform the actual graph capture.
309+ g = torch .cuda .CUDAGraph ()
310+ kwargs = {"pool" : pool } if pool is not None else {}
311+ with torch .cuda .graph (g , ** kwargs ):
312+ yield
313+ self ._graphs [key ] = g
314+
315+ def replay (self , * keys ) -> None :
316+ """Replay a previously captured graph."""
317+ key = keys if len (keys ) != 1 else keys [0 ]
318+ self ._graphs [key ].replay ()
319+
320+
321+ async def torch_awaitable (stream : torch .cuda .Stream | None = None ):
322+ """Syntactic sugar for returning an awaitable handle for non-distributed torch."""
323+ if stream is None :
324+ stream = torch .cuda .current_stream ()
325+ event = stream .record_event ()
326+ while not event .query ():
327+ await asyncio .sleep (0 )
328+
329+
239330async def await_process_call (call , process : multiprocessing .Process , timeout : float = 1.0 ):
240331 """Repeatedly wait for a multiprocessing callable to resolve, aborting upon process failure.
241332
0 commit comments