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

268 lines
8.6 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
from typing import Any
import torch
from tokenspeed_kernel.numerics.inputs import (
InputGenerator,
set_benchmark_shapes,
set_input_generator,
set_standard_shapes,
)
from tokenspeed_kernel.numerics.tolerance import Tolerance, set_family_tolerance
from tokenspeed_kernel.signature import TensorFormat
# ---------------------------------------------------------------------------
# Tolerance
# ---------------------------------------------------------------------------
_ATOL = {
torch.float32: 1e-5,
# bf16/fp16: error is dominated by the output cast (~1 ulp_rel = 2^-7 ≈ 8e-3
# for bf16; 2^-10 ≈ 1e-3 for fp16), not by fp32 accumulation, so we set the
# baseline at the rounding floor and use a K-independent scale.
torch.float16: 1.5e-2,
torch.bfloat16: 1.5e-2,
torch.float8_e4m3fn: 5e-3,
torch.float8_e4m3fnuz: 5e-3,
}
_FP8_DTYPES: set[torch.dtype] = {
torch.float8_e4m3fn,
torch.float8_e4m3fnuz,
}
_BF16_FP16_DTYPES: set[torch.dtype] = {
torch.float16,
torch.bfloat16,
}
def tolerance(
dtype: torch.dtype,
*,
K: int | None = None,
inputs: dict[str, Any] | None = None,
acc_dtype: torch.dtype = torch.float32,
**_: Any,
) -> Tolerance:
"""Shape-aware GEMM tolerance.
- fp32: error grows as sqrt(K) under fp32 accumulation noise.
- fp16/bf16: K-independent — fp32 accumulation is well below the output
dtype's rounding floor, so error is dominated by the final cast.
- fp8: error grows linearly with K for blockwise kernels, with a floor for
the output dtype rounding error on small K.
"""
if dtype not in _ATOL:
raise KeyError(f"No GEMM tolerance baseline for dtype={dtype}")
if K is None and inputs is not None and "A" in inputs:
K = int(inputs["A"].shape[-1])
if K is None:
raise ValueError("GEMM tolerance requires K or inputs['A']")
base = _ATOL[dtype]
if dtype in _FP8_DTYPES:
output_rounding_floor = max(_ATOL[d] for d in _BF16_FP16_DTYPES)
scale = max(base * max(K, 1) / 128.0, output_rounding_floor) / base
elif dtype in _BF16_FP16_DTYPES:
scale = 1.0
else:
scale = math.sqrt(max(K, 1) / 128.0)
if acc_dtype != torch.float32:
scale *= 8.0
return Tolerance(atol=base * scale, rtol=base * scale)
set_family_tolerance("gemm", tolerance)
# ---------------------------------------------------------------------------
# Input Generator
# ---------------------------------------------------------------------------
class GemmInputGenerator(InputGenerator):
def _generate_value(self, shape: tuple[int, ...], dtype) -> torch.Tensor:
values = torch.randn(
*shape,
dtype=torch.float32,
device=self.device,
generator=self.rng,
)
return values.to(dtype)
def _generate_scales(self, shape: tuple[int, ...], dtype) -> torch.Tensor:
scales = torch.rand(
*shape,
dtype=torch.float32,
device=self.device,
generator=self.rng,
)
return scales.to(dtype)
def _format(self, role: str) -> TensorFormat | None:
if self.format_signature is None:
return None
return self.format_signature.format_for(role)
def _block_size(
self,
*formats: TensorFormat | None,
) -> list[int] | None:
for tensor_format in formats:
scale = tensor_format.scale if tensor_format is not None else None
if scale is not None and scale.block_shape is not None:
return list(scale.block_shape)
return None
def _scale_for_format(
self,
tensor_format: TensorFormat | None,
role: str,
*,
M: int,
N: int,
K: int,
block_size: list[int] | None,
) -> torch.Tensor | None:
scale = tensor_format.scale if tensor_format is not None else None
if scale is None:
return None
if scale.granularity == "block" and tensor_format.format == "mxfp8":
if block_size is None:
raise ValueError(
"mxfp8 block scale format requires concrete block_shape"
)
block_n, block_k = block_size
k_tiles = math.ceil(K / block_k)
if role == "a":
return self._generate_scales((M, k_tiles), scale.storage_dtype)
if role == "b":
n_tiles = math.ceil(N / block_n)
return self._generate_scales((n_tiles, k_tiles), scale.storage_dtype)
if scale.granularity == "channel":
return self._generate_scales(
(M,) if role == "a" else (N,),
scale.storage_dtype,
)
return self._generate_scales((1,), scale.storage_dtype)
def generate(
self,
M: int,
N: int,
K: int,
) -> dict[str, Any]:
a_layout = self.traits.get("a_layout")
b_layout = self.traits.get("b_layout")
a_format = self._format("a")
b_format = self._format("b")
a_dtype = a_format.storage_dtype if a_format is not None else self.dtype
b_dtype = b_format.storage_dtype if b_format is not None else self.dtype
A = (
self._generate_value((K, M), a_dtype)
if a_layout == {"KM"}
else self._generate_value((M, K), a_dtype)
)
B = (
self._generate_value((K, N), b_dtype)
if b_layout == {"KN"}
else self._generate_value((N, K), b_dtype)
)
block_size = self._block_size(a_format, b_format)
A_scales = self._scale_for_format(
a_format,
"a",
M=M,
N=N,
K=K,
block_size=block_size,
)
B_scales = self._scale_for_format(
b_format,
"b",
M=M,
N=N,
K=K,
block_size=block_size,
)
out_dtype = torch.bfloat16
alpha = None
return {
"A": A,
"B": B,
"A_scales": A_scales,
"B_scales": B_scales,
"out_dtype": out_dtype,
"alpha": alpha,
"block_size": block_size,
}
set_input_generator("gemm", "mm", GemmInputGenerator)
# ---------------------------------------------------------------------------
# Shape Presets
# ---------------------------------------------------------------------------
GEMM_MM_STANDARD_SHAPES: list[dict[str, int]] = [
{"M": 16, "N": 16, "K": 64},
{"M": 64, "N": 128, "K": 128},
{"M": 128, "N": 128, "K": 256},
{"M": 256, "N": 256, "K": 512},
# DSv3 hot-path shapes — exercise hand-rolled kernels in trtllm dsv3_router /
# dsv3_fused_a (M ≤ 16, K = 7168, N = num_experts=256 or fused_a=2112) plus
# off-shape (M = 64) which falls back to cuBLAS inside the same op.
{"M": 1, "N": 256, "K": 7168},
{"M": 8, "N": 256, "K": 7168},
{"M": 16, "N": 256, "K": 7168},
{"M": 64, "N": 256, "K": 7168},
{"M": 1, "N": 2112, "K": 7168},
{"M": 8, "N": 2112, "K": 7168},
{"M": 16, "N": 2112, "K": 7168},
{"M": 64, "N": 2112, "K": 7168},
]
GEMM_MM_BENCHMARK_SHAPES: list[dict[str, int]] = [
{"M": 1, "N": 4096, "K": 4096},
{"M": 16, "N": 4096, "K": 4096},
{"M": 128, "N": 4096, "K": 4096},
{"M": 512, "N": 4096, "K": 4096},
{"M": 4096, "N": 4096, "K": 4096},
]
set_standard_shapes("gemm", "mm", GEMM_MM_STANDARD_SHAPES)
set_benchmark_shapes("gemm", "mm", GEMM_MM_BENCHMARK_SHAPES)