11import ctypes
22import ctypes .util
3+ import inspect
34import logging
45import os
56import 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+
248314def 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
325403def 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