Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

425 lines
14 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations
import math
import time
from contextlib import nullcontext
from datetime import datetime, timezone
from typing import Any, Callable, Iterable
import torch
from tokenspeed_kernel.benchmark.config import BenchmarkConfig
from tokenspeed_kernel.benchmark.result import BenchmarkResult
from tokenspeed_kernel.benchmark.throughput import ThroughputCalculator
from tokenspeed_kernel.numerics.comparison import compare_outputs
from tokenspeed_kernel.numerics.inputs import (
get_benchmark_shapes,
get_input_generator,
)
from tokenspeed_kernel.numerics.tolerance import get_family_tolerance
from tokenspeed_kernel.platform import current_platform
from tokenspeed_kernel.profiling import ProfilingConfig, profiling
from tokenspeed_kernel.registry import KernelRegistry, KernelSpec
from tokenspeed_kernel.selection import (
ref_compatible_with_spec,
spec_matches_shape_traits,
)
# isort: split
import tokenspeed_kernel.numerics.gemm # noqa: F401
__all__ = ["BenchmarkRunner"]
def _percentile(sorted_values: list[float], percentile: float) -> float:
if not sorted_values:
return 0.0
if len(sorted_values) == 1:
return float(sorted_values[0])
rank = (len(sorted_values) - 1) * (percentile / 100.0)
low = int(math.floor(rank))
high = int(math.ceil(rank))
if low == high:
return float(sorted_values[low])
weight = rank - float(low)
return float(sorted_values[low] * (1.0 - weight) + sorted_values[high] * weight)
class BenchmarkRunner:
"""Benchmarks kernel implementations."""
def __init__(self, config: BenchmarkConfig | None = None):
if not torch.cuda.is_available():
raise RuntimeError("BenchmarkRunner requires CUDA")
self.config = config or BenchmarkConfig()
self.config.validate()
def _resolve_profiling_config(self, default_output: str) -> ProfilingConfig | None:
if not self.config.proton_profile:
return None
if self.config.proton_config is not None:
return self.config.proton_config
return ProfilingConfig(output=default_output, data="trace")
def _profiling_context(self, default_output: str):
proton_cfg = self._resolve_profiling_config(default_output)
if proton_cfg is None:
return nullcontext()
return profiling(proton_cfg)
def _time_kernel(
self, kernel: Callable[..., Any], inputs: dict[str, Any]
) -> list[float]:
with torch.no_grad():
for _ in range(self.config.warmup_iters):
kernel(**inputs)
torch.cuda.synchronize()
if self.config.use_cuda_events:
start_events = [
torch.cuda.Event(enable_timing=True)
for _ in range(self.config.bench_iters)
]
end_events = [
torch.cuda.Event(enable_timing=True)
for _ in range(self.config.bench_iters)
]
with torch.no_grad():
for i in range(self.config.bench_iters):
start_events[i].record()
kernel(**inputs)
end_events[i].record()
torch.cuda.synchronize()
times = [
start.elapsed_time(end) * 1000.0
for start, end in zip(start_events, end_events, strict=False)
]
else:
times: list[float] = []
with torch.no_grad():
for _ in range(self.config.bench_iters):
torch.cuda.synchronize()
t0 = time.perf_counter()
kernel(**inputs)
torch.cuda.synchronize()
t1 = time.perf_counter()
times.append((t1 - t0) * 1e6)
times.sort()
return times
def _benchmark_one_shape(
self,
spec: KernelSpec,
kernel: Callable[..., Any],
shape: dict[str, Any],
dtype: torch.dtype,
dtype_role: str | Iterable[str],
) -> BenchmarkResult | None:
if not spec_matches_shape_traits(spec, shape):
return None
signature = spec.format_signature_for_storage_dtype(dtype, dtype_role)
if signature is None:
return None
generator = get_input_generator(
spec.family,
spec.mode,
dtype=dtype,
traits=spec.traits,
format_signature=signature,
device="cuda",
seed=self.config.seed,
)
inputs = generator.generate(**shape)
times = self._time_kernel(kernel, inputs)
if not times:
return None
wanted_percentiles = set(self.config.percentiles)
wanted_percentiles.update({50.0, 90.0, 99.0})
percentile_values = {
percentile: _percentile(times, percentile)
for percentile in sorted(wanted_percentiles)
}
p50 = percentile_values[50.0]
p90 = percentile_values[90.0]
p99 = percentile_values[99.0]
min_latency = float(times[0])
max_latency = float(times[-1])
throughput_dtype = inputs.get("out_dtype")
if not isinstance(throughput_dtype, torch.dtype):
throughput_dtype = dtype
tflops, bandwidth = ThroughputCalculator.compute(
spec.family,
spec.mode,
shape,
p50,
dtype=throughput_dtype,
)
numerics_passed: bool | None = None
max_abs_diff: float | None = None
max_rel_diff: float | None = None
if self.config.verify:
numerics_passed, max_abs_diff, max_rel_diff = self._verify_one_shape(
spec,
kernel,
shape,
dtype,
dtype_role,
generator,
)
platform = current_platform()
return BenchmarkResult(
kernel_name=spec.name,
op_family=spec.family,
op_mode=spec.mode,
solution=spec.solution,
dtype=str(dtype),
platform_arch=f"{platform.vendor}:{platform.arch}",
shape_params=dict(shape),
median_latency_us=p50,
p90_latency_us=p90,
p99_latency_us=p99,
min_latency_us=min_latency,
max_latency_us=max_latency,
tflops=tflops,
bandwidth_gb_s=bandwidth,
numerics_passed=numerics_passed,
max_abs_diff=max_abs_diff,
max_rel_diff=max_rel_diff,
timestamp=datetime.now(timezone.utc).isoformat(),
num_iters=self.config.bench_iters,
)
def _verify_one_shape(
self,
spec: KernelSpec,
kernel: Callable[..., Any],
shape: dict[str, Any],
dtype: torch.dtype,
dtype_role: str | Iterable[str],
generator: Any,
) -> tuple[bool | None, float | None, float | None]:
if spec.solution == "reference":
return None, None, None
registry = KernelRegistry.get()
signature = spec.format_signature_for_storage_dtype(dtype, dtype_role)
if signature is None:
return None, None, None
ref_specs = registry.get_for_operator(
spec.family,
spec.mode,
format_signature=signature,
solution="reference",
)
if not ref_specs:
return None, None, None
ref_spec = None
for ref in ref_specs:
if ref.name == spec.name:
continue
if ref_compatible_with_spec(ref, spec):
ref_spec = ref
break
if ref_spec is None:
return None, None, None
if not spec_matches_shape_traits(ref_spec, shape):
return None, None, None
ref_kernel = registry.get_impl(ref_spec.name)
if ref_kernel is None:
return None, None, None
verify_inputs = generator.generate(**shape)
with torch.no_grad():
expected = ref_kernel(**verify_inputs)
actual = kernel(**verify_inputs)
if not isinstance(actual, torch.Tensor) or not isinstance(
expected, torch.Tensor
):
return None, None, None
try:
tol_fn = get_family_tolerance(spec.family)
except KeyError:
return None, None, None
tolerance = tol_fn(dtype, inputs=verify_inputs, **shape)
comparison = compare_outputs(actual, expected, tolerance=tolerance)
return (
comparison.passed,
comparison.max_abs_diff,
comparison.max_rel_diff,
)
def _benchmark_kernel_impl(
self,
kernel_name: str,
*,
shapes: list[dict[str, Any]] | None = None,
dtype: torch.dtype = torch.bfloat16,
dtype_role: str | Iterable[str],
) -> list[BenchmarkResult]:
"""Benchmark a single kernel across shapes."""
registry = KernelRegistry.get()
spec = registry.get_by_name(kernel_name)
if spec is None:
raise ValueError(f"Kernel {kernel_name!r} is not registered")
if spec.format_signature_for_storage_dtype(dtype, dtype_role) is None:
raise ValueError(
f"Kernel {kernel_name!r} does not support storage dtype={dtype} "
f"on dtype role(s) {dtype_role}"
)
platform = current_platform()
if not spec.capability.satisfied_by(platform):
raise ValueError(
f"Kernel {kernel_name!r} is not compatible with platform {platform.device_name}"
)
kernel = registry.get_impl(kernel_name)
if kernel is None:
raise ValueError(f"Kernel implementation for {kernel_name!r} is missing")
test_shapes = shapes or get_benchmark_shapes(spec.family, spec.mode)
results: list[BenchmarkResult] = []
for shape in test_shapes:
result = self._benchmark_one_shape(
spec,
kernel,
shape,
dtype,
dtype_role,
)
if result is not None:
results.append(result)
return results
def benchmark_kernel(
self,
kernel_name: str,
*,
shapes: list[dict[str, Any]] | None = None,
dtype: torch.dtype = torch.bfloat16,
dtype_role: str | Iterable[str],
) -> list[BenchmarkResult]:
with self._profiling_context(default_output=f"bench_{kernel_name}"):
return self._benchmark_kernel_impl(
kernel_name,
shapes=shapes,
dtype=dtype,
dtype_role=dtype_role,
)
def _benchmark_op_impl(
self,
op_family: str,
op_mode: str,
*,
shapes: list[dict[str, Any]] | None = None,
dtype: torch.dtype = torch.bfloat16,
dtype_role: str | Iterable[str],
) -> list[BenchmarkResult]:
"""Benchmark all implementations of an op."""
registry = KernelRegistry.get()
platform = current_platform()
specs = [
spec
for spec in registry.get_for_operator(
op_family,
op_mode,
platform=platform,
)
if spec.format_signatures_for_storage_dtype(dtype, dtype_role)
]
results: list[BenchmarkResult] = []
for spec in sorted(specs, key=lambda item: (item.solution, item.name)):
results.extend(
self._benchmark_kernel_impl(
spec.name, shapes=shapes, dtype=dtype, dtype_role=dtype_role
)
)
return results
def benchmark_op(
self,
op_family: str,
op_mode: str,
*,
shapes: list[dict[str, Any]] | None = None,
dtype: torch.dtype = torch.bfloat16,
dtype_role: str | Iterable[str],
) -> list[BenchmarkResult]:
with self._profiling_context(default_output=f"bench_{op_family}_{op_mode}"):
return self._benchmark_op_impl(
op_family,
op_mode,
shapes=shapes,
dtype=dtype,
dtype_role=dtype_role,
)
def _benchmark_all_impl(
self,
*,
dtype: torch.dtype = torch.bfloat16,
dtype_role: str | Iterable[str],
) -> list[BenchmarkResult]:
"""Benchmark all registered kernels on this platform."""
registry = KernelRegistry.get()
results: list[BenchmarkResult] = []
for family, mode in sorted(registry.list_operators()):
try:
op_results = self._benchmark_op_impl(
family, mode, dtype=dtype, dtype_role=dtype_role
)
except KeyError:
continue
if op_results:
results.extend(op_results)
return results
def benchmark_all(
self,
*,
dtype: torch.dtype = torch.bfloat16,
dtype_role: str | Iterable[str],
) -> list[BenchmarkResult]:
with self._profiling_context(default_output="bench_all"):
return self._benchmark_all_impl(dtype=dtype, dtype_role=dtype_role)