Skip to content

Commit 717e9fc

Browse files
committed
Syntactic sugar for CG and awaiting
1 parent 55bec39 commit 717e9fc

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

megatron/core/inference/utils.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
239330
async 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

Comments
 (0)