Files
nvbench/python/cuda/bench/_decorators.py
Oleksandr Pavlyk e07f87910a Add decorators for registering benchmarks and adding axis
cuda.bench.register(fn) continues returning Benchmark, and supports
legacy use.

New signature added:
   cuda.bench.register():
      Returns a decorator

```
@bench.register()
@bench.axis.float64("Duration (s)", [7e-5, 1e-4, 5e-4])
@bench.option.min_samples(120)
def single_float64_axis(state: bench.State):
   ...
```
2026-05-12 15:50:47 -05:00

286 lines
11 KiB
Python

# Copyright 2026 NVIDIA Corporation
#
# Licensed under the Apache License, Version 2.0 with the LLVM exception
# (the "License"); you may not use this file except in compliance with
# the License.
#
# You may obtain a copy of the License at
#
# http://llvm.org/foundation/relicensing/LICENSE.txt
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Function decorators for registering and configuring NVBench benchmarks."""
from __future__ import annotations
from collections.abc import Callable, Sequence
from typing import Any, TypeVar
_F = TypeVar("_F", bound=Callable[..., Any])
_Benchmark = Any
_RawRegister = Callable[[Callable[..., Any]], _Benchmark]
_RegisterGetter = Callable[[], _RawRegister]
_BenchmarkAction = Callable[[_Benchmark], None]
_BENCHMARK_ACTIONS_ATTR = "__cuda_bench_actions__"
_BENCHMARK_REGISTERED_ATTR = "__cuda_bench_registered__"
def _append_benchmark_action(action: _BenchmarkAction) -> Callable[[_F], _F]:
"""Return a function-preserving decorator that records a benchmark action."""
def decorator(fn: _F) -> _F:
"""Attach a delayed benchmark action to a benchmark function."""
if getattr(fn, _BENCHMARK_REGISTERED_ATTR, False):
raise RuntimeError(
"NVBench axis and option decorators must be placed below "
"@bench.register()"
)
actions = getattr(fn, _BENCHMARK_ACTIONS_ATTR, None)
if actions is None:
actions = []
try:
setattr(fn, _BENCHMARK_ACTIONS_ATTR, actions)
except AttributeError as e:
raise TypeError(
"NVBench benchmark decorators require a callable object"
) from e
actions.append(action)
return fn
return decorator
def _apply_benchmark_actions(
benchmark: _Benchmark, fn: Callable[..., Any]
) -> _Benchmark:
"""Apply delayed benchmark actions to a registered benchmark."""
for action in reversed(getattr(fn, _BENCHMARK_ACTIONS_ATTR, ())):
action(benchmark)
return benchmark
def _mark_registered(fn: Callable[..., Any]) -> None:
"""Mark a callable as registered when it supports attribute assignment."""
try:
setattr(fn, _BENCHMARK_REGISTERED_ATTR, True)
except AttributeError:
pass
def make_register(get_register: _RegisterGetter) -> Callable[..., Any]:
"""Create the public ``register`` function around a raw register function."""
def register(fn=None, /):
"""Register a Python benchmark function with NVBench.
Called as ``bench.register(fn)``, this returns the registered
``Benchmark``. Called as ``@bench.register()``, this returns a decorator
that registers the function and leaves the decorated symbol unchanged.
"""
if fn is None:
def decorator(benchmark_fn):
benchmark = get_register()(benchmark_fn)
_apply_benchmark_actions(benchmark, benchmark_fn)
_mark_registered(benchmark_fn)
return benchmark_fn
return decorator
benchmark = get_register()(fn)
_apply_benchmark_actions(benchmark, fn)
_mark_registered(fn)
return benchmark
register.__name__ = "register"
register.__qualname__ = "register"
return register
class _AxisDecorators:
"""Namespace for decorators that add axes to a benchmark."""
def int64(self, name: str, values: Sequence[int]) -> Callable[[_F], _F]:
"""Add an ``int64`` axis to the decorated benchmark."""
return _append_benchmark_action(
lambda benchmark: benchmark.add_int64_axis(name, values)
)
def add_int64_axis(self, name: str, values: Sequence[int]) -> Callable[[_F], _F]:
"""Alias for :meth:`int64`."""
return self.int64(name, values)
def int64_power_of_two(
self, name: str, values: Sequence[int]
) -> Callable[[_F], _F]:
"""Add a power-of-two ``int64`` axis to the decorated benchmark."""
return _append_benchmark_action(
lambda benchmark: benchmark.add_int64_power_of_two_axis(name, values)
)
def power_of_two(self, name: str, values: Sequence[int]) -> Callable[[_F], _F]:
"""Alias for :meth:`int64_power_of_two`."""
return self.int64_power_of_two(name, values)
def add_int64_power_of_two_axis(
self, name: str, values: Sequence[int]
) -> Callable[[_F], _F]:
"""Alias for :meth:`int64_power_of_two`."""
return self.int64_power_of_two(name, values)
def float64(self, name: str, values: Sequence[float]) -> Callable[[_F], _F]:
"""Add a ``float64`` axis to the decorated benchmark."""
return _append_benchmark_action(
lambda benchmark: benchmark.add_float64_axis(name, values)
)
def add_float64_axis(
self, name: str, values: Sequence[float]
) -> Callable[[_F], _F]:
"""Alias for :meth:`float64`."""
return self.float64(name, values)
def string(self, name: str, values: Sequence[str]) -> Callable[[_F], _F]:
"""Add a string axis to the decorated benchmark."""
return _append_benchmark_action(
lambda benchmark: benchmark.add_string_axis(name, values)
)
def add_string_axis(self, name: str, values: Sequence[str]) -> Callable[[_F], _F]:
"""Alias for :meth:`string`."""
return self.string(name, values)
class _OptionDecorators:
"""Namespace for decorators that set benchmark options."""
def name(self, value: str) -> Callable[[_F], _F]:
"""Set the benchmark name."""
return self.set_name(value)
def set_name(self, value: str) -> Callable[[_F], _F]:
"""Set the benchmark name."""
return _append_benchmark_action(lambda benchmark: benchmark.set_name(value))
def run_once(self, value: bool = True) -> Callable[[_F], _F]:
"""Set whether each benchmark configuration runs only once."""
return self.set_run_once(value)
def set_run_once(self, value: bool) -> Callable[[_F], _F]:
"""Set whether each benchmark configuration runs only once."""
return _append_benchmark_action(lambda benchmark: benchmark.set_run_once(value))
def skip_time(self, duration_seconds: float) -> Callable[[_F], _F]:
"""Set the threshold below which benchmark runs are skipped."""
return self.set_skip_time(duration_seconds)
def set_skip_time(self, duration_seconds: float) -> Callable[[_F], _F]:
"""Set the threshold below which benchmark runs are skipped."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_skip_time(duration_seconds)
)
def throttle_recovery_delay(self, delay_seconds: float) -> Callable[[_F], _F]:
"""Set the delay after GPU clock throttling is detected."""
return self.set_throttle_recovery_delay(delay_seconds)
def set_throttle_recovery_delay(self, delay_seconds: float) -> Callable[[_F], _F]:
"""Set the delay after GPU clock throttling is detected."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_throttle_recovery_delay(delay_seconds)
)
def throttle_threshold(self, threshold: float) -> Callable[[_F], _F]:
"""Set the GPU clock throttle threshold."""
return self.set_throttle_threshold(threshold)
def set_throttle_threshold(self, threshold: float) -> Callable[[_F], _F]:
"""Set the GPU clock throttle threshold."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_throttle_threshold(threshold)
)
def timeout(self, duration_seconds: float) -> Callable[[_F], _F]:
"""Set the benchmark timeout in seconds."""
return self.set_timeout(duration_seconds)
def set_timeout(self, duration_seconds: float) -> Callable[[_F], _F]:
"""Set the benchmark timeout in seconds."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_timeout(duration_seconds)
)
def stopping_criterion(self, criterion: str) -> Callable[[_F], _F]:
"""Set the benchmark stopping criterion."""
return self.set_stopping_criterion(criterion)
def set_stopping_criterion(self, criterion: str) -> Callable[[_F], _F]:
"""Set the benchmark stopping criterion."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_stopping_criterion(criterion)
)
def criterion_param_float64(self, name: str, value: float) -> Callable[[_F], _F]:
"""Set a floating-point parameter for the stopping criterion."""
return self.set_criterion_param_float64(name, value)
def set_criterion_param_float64(
self, name: str, value: float
) -> Callable[[_F], _F]:
"""Set a floating-point parameter for the stopping criterion."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_criterion_param_float64(name, value)
)
def criterion_param_int64(self, name: str, value: int) -> Callable[[_F], _F]:
"""Set an integer parameter for the stopping criterion."""
return self.set_criterion_param_int64(name, value)
def set_criterion_param_int64(self, name: str, value: int) -> Callable[[_F], _F]:
"""Set an integer parameter for the stopping criterion."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_criterion_param_int64(name, value)
)
def criterion_param_string(self, name: str, value: str) -> Callable[[_F], _F]:
"""Set a string parameter for the stopping criterion."""
return self.set_criterion_param_string(name, value)
def set_criterion_param_string(self, name: str, value: str) -> Callable[[_F], _F]:
"""Set a string parameter for the stopping criterion."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_criterion_param_string(name, value)
)
def min_samples(self, count: int) -> Callable[[_F], _F]:
"""Set the minimum number of samples to collect."""
return self.set_min_samples(count)
def set_min_samples(self, count: int) -> Callable[[_F], _F]:
"""Set the minimum number of samples to collect."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_min_samples(count)
)
def is_cpu_only(self, value: bool = True) -> Callable[[_F], _F]:
"""Set whether the benchmark only performs CPU work."""
return self.set_is_cpu_only(value)
def set_is_cpu_only(self, value: bool) -> Callable[[_F], _F]:
"""Set whether the benchmark only performs CPU work."""
return _append_benchmark_action(
lambda benchmark: benchmark.set_is_cpu_only(value)
)
axis = _AxisDecorators()
option = _OptionDecorators()