Skip to content

Commit 666c1b1

Browse files
committed
cuda-graphs for small kerenls
1 parent 797b867 commit 666c1b1

1 file changed

Lines changed: 134 additions & 33 deletions

File tree

transformer_nuggets/utils/benchmark.py

Lines changed: 134 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ctypes
22
import ctypes.util
3+
import inspect
34
import logging
45
import os
56
import random
@@ -245,12 +246,78 @@ def p95_us(self) -> float:
245246
return self.quantiles_us[2]
246247

247248

249+
def _call_do_bench_using_profiling(
250+
fn: Callable[[], object],
251+
*,
252+
rep: int,
253+
is_vetted_benchmarking: bool,
254+
) -> float:
255+
"""Call Inductor's profiler benchmark across torch versions.
256+
257+
Torch versions differ in whether ``do_bench_using_profiling`` accepts the
258+
``is_vetted_benchmarking`` kwarg. Detect that at runtime so the benchmark
259+
helper works on both older and newer torch builds.
260+
"""
261+
params = inspect.signature(do_bench_using_profiling).parameters
262+
call_kwargs = {"rep": rep}
263+
if "is_vetted_benchmarking" in params:
264+
call_kwargs["is_vetted_benchmarking"] = is_vetted_benchmarking
265+
return do_bench_using_profiling(fn, **call_kwargs)
266+
267+
268+
269+
def _benchmark_cuda_graph_replay_samples_us(
270+
func: Callable,
271+
*args,
272+
**kwargs,
273+
) -> list[float]:
274+
"""Capture one CUDA graph and return per-replay CUDA-event timings in us.
275+
276+
This measures steady-state replay latency. It intentionally excludes host
277+
launch gaps that dominate tiny eager kernels. Any GPU-side work inside the
278+
captured callable is included. Host-to-device copy-in from CPU tensors is
279+
not represented unless the callable stages data into static GPU buffers as
280+
part of the captured region.
281+
"""
282+
num_iters = kwargs.pop("NUM_ITERS", 100)
283+
warmup_iters = kwargs.pop("CUDAGRAPH_WARMUP_ITERS", max(10, min(25, num_iters)))
284+
lock = kwargs.pop("LOCK_CLOCKS", False)
285+
ctx = locked_clocks() if lock else nullcontext()
286+
with ctx:
287+
no_args = lambda: func(*args, **kwargs)
288+
for _ in range(warmup_iters):
289+
no_args()
290+
torch.cuda.synchronize()
291+
292+
graph = torch.cuda.CUDAGraph()
293+
with torch.cuda.graph(graph):
294+
no_args()
295+
torch.cuda.synchronize()
296+
297+
for _ in range(warmup_iters):
298+
graph.replay()
299+
torch.cuda.synchronize()
300+
301+
start = torch.cuda.Event(enable_timing=True)
302+
end = torch.cuda.Event(enable_timing=True)
303+
samples_us = []
304+
for _ in range(num_iters):
305+
start.record()
306+
graph.replay()
307+
end.record()
308+
torch.cuda.synchronize()
309+
samples_us.append(start.elapsed_time(end) * 1e3)
310+
return samples_us
311+
312+
313+
248314
def benchmark_cuda_function_stats(func: Callable, *args, **kwargs) -> CudaBenchmarkStats:
249315
"""Benchmark a CUDA callable and return median-centered summary stats.
250316
251-
This collects per-iteration timings from Inductor's GPU benchmarker and
252-
returns the raw samples, the sample median, a bootstrap confidence interval
253-
for that median, and `(p05, p50, p95)` sample quantiles.
317+
By default this collects per-iteration timings from Inductor's GPU
318+
benchmarker. With ``USE_CUDA_GRAPHS=True`` it instead captures one static
319+
CUDA graph and returns per-replay timings, which is often closer to NCU for
320+
tiny static kernels.
254321
255322
Args:
256323
func: Callable to benchmark.
@@ -259,7 +326,8 @@ def benchmark_cuda_function_stats(func: Callable, *args, **kwargs) -> CudaBenchm
259326
``func``. The following benchmark-control keys are consumed by this
260327
helper before calling ``func``: ``NUM_ITERS``,
261328
``MEMORY_WARMUP_ITERS``, ``CONFIDENCE``, ``N_RESAMPLES``, ``SEED``,
262-
and ``IS_VETTED_BENCHMARKING``.
329+
``IS_VETTED_BENCHMARKING``, ``LOCK_CLOCKS``, ``USE_CUDA_GRAPHS``,
330+
and ``CUDAGRAPH_WARMUP_ITERS``.
263331
264332
Returns:
265333
CudaBenchmarkStats with raw samples, the sample median, a bootstrap
@@ -271,36 +339,46 @@ def benchmark_cuda_function_stats(func: Callable, *args, **kwargs) -> CudaBenchm
271339
after warmup, when samples are not dominated by obvious drift or phase
272340
changes such as autotuning, thermal throttling, or one-time allocator
273341
effects.
274-
275-
Examples:
276-
Basic usage::
277-
278-
stats = benchmark_cuda_function_stats(lambda: kernel(x, y), NUM_ITERS=200)
279-
print(stats.median_us)
280-
print(stats.median_ci_us)
281-
print(stats.quantiles_us)
282-
283-
With locked clocks::
284-
285-
with locked_clocks():
286-
stats = benchmark_cuda_function_stats(lambda: kernel(x, y), NUM_ITERS=200)
287342
"""
288343
num_iters = kwargs.pop("NUM_ITERS", 100)
289344
memory_warmup_iters = kwargs.pop("MEMORY_WARMUP_ITERS", 100)
290345
confidence = kwargs.pop("CONFIDENCE", 0.95)
291346
n_resamples = kwargs.pop("N_RESAMPLES", 1000)
292347
seed = kwargs.pop("SEED", 0)
293348
is_vetted_benchmarking = kwargs.pop("IS_VETTED_BENCHMARKING", False)
294-
no_args = lambda: func(*args, **kwargs)
295-
from torch._inductor.runtime.benchmarking import benchmarker
296-
297-
samples_ms = benchmarker.benchmark_gpu(
298-
no_args,
299-
benchmark_iters=num_iters,
300-
memory_warmup_iters=memory_warmup_iters,
301-
return_mode="all",
302-
is_vetted_benchmarking=is_vetted_benchmarking,
303-
)
349+
use_cuda_graphs = kwargs.pop("USE_CUDA_GRAPHS", False)
350+
351+
if use_cuda_graphs:
352+
samples_us = _benchmark_cuda_graph_replay_samples_us(
353+
func,
354+
*args,
355+
NUM_ITERS=num_iters,
356+
CUDAGRAPH_WARMUP_ITERS=kwargs.pop(
357+
"CUDAGRAPH_WARMUP_ITERS", memory_warmup_iters
358+
),
359+
LOCK_CLOCKS=kwargs.pop("LOCK_CLOCKS", False),
360+
**kwargs,
361+
)
362+
return CudaBenchmarkStats.from_samples(
363+
samples_us,
364+
confidence=confidence,
365+
n_resamples=n_resamples,
366+
seed=seed,
367+
)
368+
369+
lock = kwargs.pop("LOCK_CLOCKS", False)
370+
ctx = locked_clocks() if lock else nullcontext()
371+
with ctx:
372+
no_args = lambda: func(*args, **kwargs)
373+
from torch._inductor.runtime.benchmarking import benchmarker
374+
375+
samples_ms = benchmarker.benchmark_gpu(
376+
no_args,
377+
benchmark_iters=num_iters,
378+
memory_warmup_iters=memory_warmup_iters,
379+
return_mode="all",
380+
is_vetted_benchmarking=is_vetted_benchmarking,
381+
)
304382
return CudaBenchmarkStats.from_samples(
305383
(float(sample) * 1e3 for sample in samples_ms),
306384
confidence=confidence,
@@ -323,20 +401,43 @@ def benchmark_torch_function_in_microseconds(func: Callable, *args, **kwargs) ->
323401

324402

325403
def benchmark_cuda_function_in_microseconds(func: Callable, *args, **kwargs) -> float:
326-
"""Thin wrapper around do_bench_using_profiling.
327-
328-
Accepts NUM_ITERS, IS_VETTED_BENCHMARKING, and lock_clocks as kwargs but
329-
removes them before calling func so they never leak into the benchmarked callable.
404+
"""Benchmark a CUDA callable and return median latency in microseconds.
405+
406+
By default this uses Inductor's profiler-based benchmark helper. With
407+
``USE_CUDA_GRAPHS=True`` it instead captures one static CUDA graph and times
408+
replay latency with CUDA events.
409+
410+
Consumed benchmark kwargs:
411+
- ``NUM_ITERS``
412+
- ``IS_VETTED_BENCHMARKING``
413+
- ``LOCK_CLOCKS``
414+
- ``USE_CUDA_GRAPHS``
415+
- ``CUDAGRAPH_WARMUP_ITERS``
330416
"""
331417
num_iters = kwargs.pop("NUM_ITERS", 100)
332418
is_vetted_benchmarking = kwargs.pop("IS_VETTED_BENCHMARKING", False)
419+
use_cuda_graphs = kwargs.pop("USE_CUDA_GRAPHS", False)
420+
421+
if use_cuda_graphs:
422+
samples_us = _benchmark_cuda_graph_replay_samples_us(
423+
func,
424+
*args,
425+
NUM_ITERS=num_iters,
426+
LOCK_CLOCKS=kwargs.pop("LOCK_CLOCKS", False),
427+
CUDAGRAPH_WARMUP_ITERS=kwargs.pop("CUDAGRAPH_WARMUP_ITERS", max(10, min(25, num_iters))),
428+
**kwargs,
429+
)
430+
return statistics.median(samples_us)
431+
333432
lock = kwargs.pop("LOCK_CLOCKS", False)
334433
ctx = locked_clocks() if lock else nullcontext()
335434
with ctx:
336435
no_args = lambda: func(*args, **kwargs)
337436
return (
338-
do_bench_using_profiling(
339-
no_args, rep=num_iters, is_vetted_benchmarking=is_vetted_benchmarking
437+
_call_do_bench_using_profiling(
438+
no_args,
439+
rep=num_iters,
440+
is_vetted_benchmarking=is_vetted_benchmarking,
340441
)
341442
* 1e3
342443
)

0 commit comments

Comments
 (0)