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

528 lines
15 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 pytest
import tokenspeed_kernel.benchmark.cli as benchmark_cli
import tokenspeed_kernel.benchmark.runner as benchmark_runner_module
import tokenspeed_kernel.numerics.gemm # noqa: F401
import torch
from tokenspeed_kernel.benchmark.config import BenchmarkConfig
from tokenspeed_kernel.benchmark.report import format_report
from tokenspeed_kernel.benchmark.result import export_results, import_results
from tokenspeed_kernel.benchmark.runner import BenchmarkRunner
from tokenspeed_kernel.benchmark.throughput import ThroughputCalculator
from tokenspeed_kernel.platform import Platform
from tokenspeed_kernel.profiling import ProfilingConfig
from tokenspeed_kernel.registry import KernelRegistry, KernelSpec
from tokenspeed_kernel.signature import format_signatures
pytestmark = [
pytest.mark.usefixtures("fresh_registry"),
pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required"),
]
_TEST_GEMM_TRAITS = {"b_layout": frozenset({"KN"})}
_TEST_SHAPES = [{"M": 8, "N": 8, "K": 8}]
def _torch_mm(
A: torch.Tensor,
B: torch.Tensor,
A_scales: torch.Tensor | None,
B_scales: torch.Tensor | None,
out_dtype: torch.dtype,
*,
alpha: torch.Tensor | None = None,
block_size: list[int] | None = None,
) -> torch.Tensor:
_ = A_scales, B_scales, alpha, block_size
return (A @ B).to(out_dtype)
def _register_test_gemm_kernels() -> None:
registry = KernelRegistry.get()
dtype = torch.float32
ref_spec = KernelSpec(
name="test_gemm_reference",
family="gemm",
mode="mm",
solution="reference",
format_signatures=format_signatures(("a", "b"), "dense", {dtype}),
traits=_TEST_GEMM_TRAITS,
priority=0,
)
fast_spec = KernelSpec(
name="test_gemm_fast",
family="gemm",
mode="mm",
solution="triton",
format_signatures=format_signatures(("a", "b"), "dense", {dtype}),
traits=_TEST_GEMM_TRAITS,
priority=10,
)
registry.register(ref_spec, _torch_mm)
registry.register(fast_spec, _torch_mm)
@pytest.fixture
def setup_gemm_case():
Platform.reset()
_register_test_gemm_kernels()
yield
Platform.reset()
def test_benchmark_kernel_returns_result(setup_gemm_case):
runner = BenchmarkRunner(
BenchmarkConfig(
warmup_iters=0,
bench_iters=3,
)
)
results = runner.benchmark_kernel(
"test_gemm_fast",
shapes=_TEST_SHAPES,
dtype=torch.float32,
dtype_role="a",
)
assert len(results) == 1
result = results[0]
assert result.kernel_name == "test_gemm_fast"
assert result.tflops is not None
assert result.bandwidth_gb_s is not None
assert result.numerics_passed is True
assert result.max_abs_diff is not None
assert result.max_rel_diff is not None
def test_benchmark_kernel_supports_cpu_wall_time(setup_gemm_case):
runner = BenchmarkRunner(
BenchmarkConfig(
warmup_iters=0,
bench_iters=3,
use_cuda_events=False,
)
)
results = runner.benchmark_kernel(
"test_gemm_fast",
shapes=_TEST_SHAPES,
dtype=torch.float32,
dtype_role="a",
)
assert len(results) == 1
result = results[0]
assert result.kernel_name == "test_gemm_fast"
assert result.median_latency_us >= 0.0
assert result.max_latency_us >= result.min_latency_us
assert result.numerics_passed is True
def test_benchmark_kernel_can_disable_verification(setup_gemm_case):
runner = BenchmarkRunner(
BenchmarkConfig(
warmup_iters=0,
bench_iters=3,
verify=False,
)
)
results = runner.benchmark_kernel(
"test_gemm_fast",
shapes=_TEST_SHAPES,
dtype=torch.float32,
dtype_role="a",
)
assert len(results) == 1
result = results[0]
assert result.numerics_passed is None
assert result.max_abs_diff is None
assert result.max_rel_diff is None
def test_benchmark_op_includes_reference(setup_gemm_case):
runner = BenchmarkRunner(
BenchmarkConfig(
warmup_iters=0,
bench_iters=2,
)
)
results = runner.benchmark_op(
"gemm",
"mm",
shapes=_TEST_SHAPES,
dtype=torch.float32,
dtype_role="a",
)
names = {result.kernel_name for result in results}
assert names == {"test_gemm_reference", "test_gemm_fast"}
def test_report_format_contains_expected_columns(setup_gemm_case):
runner = BenchmarkRunner(
BenchmarkConfig(
warmup_iters=0,
bench_iters=2,
)
)
results = runner.benchmark_op(
"gemm",
"mm",
shapes=_TEST_SHAPES,
dtype=torch.float32,
dtype_role="a",
)
report = format_report(results)
assert "Kernel" in report
assert "p50 (us)" in report
assert "TFLOPs" in report
assert "test_gemm_fast" in report
assert "Numerics" not in report
def test_export_import_roundtrip(tmp_path, setup_gemm_case):
runner = BenchmarkRunner(
BenchmarkConfig(
warmup_iters=0,
bench_iters=2,
)
)
results = runner.benchmark_op(
"gemm",
"mm",
shapes=_TEST_SHAPES,
dtype=torch.float32,
dtype_role="a",
)
export_path = tmp_path / "bench_results.json"
export_results(results, export_path)
loaded = import_results(export_path)
assert len(loaded) == len(results)
assert loaded[0].kernel_name == results[0].kernel_name
assert loaded[0].shape_params == results[0].shape_params
assert loaded[0].numerics_passed == results[0].numerics_passed
assert loaded[0].max_abs_diff == results[0].max_abs_diff
assert loaded[0].max_rel_diff == results[0].max_rel_diff
def test_attention_decode_throughput_with_explicit_kv_heads():
shape = {
"batch": 2,
"seq_len": 4,
"num_q_heads": 8,
"num_kv_heads": 2,
"head_dim": 16,
}
tflops, bandwidth = ThroughputCalculator.compute(
"attention",
"decode",
shape,
latency_us=1000.0,
dtype=torch.float16,
)
assert tflops == pytest.approx(4.096e-6)
assert bandwidth == pytest.approx(0.002048)
def test_attention_decode_throughput_with_shape_aliases():
shape = {
"batch_size": 1,
"max_seq_len": 2,
"heads": 4,
"head_dim": 8,
}
tflops, bandwidth = ThroughputCalculator.compute(
"attn",
"decode",
shape,
latency_us=1000.0,
dtype=torch.float16,
)
assert tflops == pytest.approx(2.56e-7)
assert bandwidth == pytest.approx(0.000384)
def test_attention_decode_throughput_missing_required_shape_returns_none():
shape = {
"batch": 2,
"seq_len": 4,
"num_q_heads": 8,
}
tflops, bandwidth = ThroughputCalculator.compute(
"attention",
"decode",
shape,
latency_us=1000.0,
dtype=torch.float16,
)
assert tflops is None
assert bandwidth is None
def test_benchmark_config_rejects_invalid_proton_data():
cfg = BenchmarkConfig(
proton_profile=True,
proton_config=ProfilingConfig(data="invalid"),
)
with pytest.raises(ValueError, match="proton_config.data"):
cfg.validate()
def test_benchmark_kernel_uses_profiling_context_when_enabled(
setup_gemm_case, monkeypatch
):
configs: list[ProfilingConfig] = []
trace: list[str] = []
class _Ctx:
def __enter__(self):
trace.append("enter")
return self
def __exit__(self, exc_type, exc, tb):
_ = exc_type, exc, tb
trace.append("exit")
def _fake_profiling(cfg: ProfilingConfig):
configs.append(cfg)
return _Ctx()
monkeypatch.setattr(benchmark_runner_module, "profiling", _fake_profiling)
runner = BenchmarkRunner(
BenchmarkConfig(
warmup_iters=0,
bench_iters=1,
verify=False,
proton_profile=True,
)
)
results = runner.benchmark_kernel(
"test_gemm_fast",
shapes=_TEST_SHAPES,
dtype=torch.float32,
dtype_role="a",
)
assert len(results) == 1
assert trace == ["enter", "exit"]
assert len(configs) == 1
assert configs[0].output == "bench_test_gemm_fast"
assert configs[0].data == "trace"
def test_benchmark_op_profiles_once_when_enabled(setup_gemm_case, monkeypatch):
configs: list[ProfilingConfig] = []
trace: list[str] = []
class _Ctx:
def __enter__(self):
trace.append("enter")
return self
def __exit__(self, exc_type, exc, tb):
_ = exc_type, exc, tb
trace.append("exit")
def _fake_profiling(cfg: ProfilingConfig):
configs.append(cfg)
return _Ctx()
monkeypatch.setattr(benchmark_runner_module, "profiling", _fake_profiling)
runner = BenchmarkRunner(
BenchmarkConfig(
warmup_iters=0,
bench_iters=1,
verify=False,
proton_profile=True,
)
)
results = runner.benchmark_op(
"gemm",
"mm",
shapes=_TEST_SHAPES,
dtype=torch.float32,
dtype_role="a",
)
assert {result.kernel_name for result in results} == {
"test_gemm_reference",
"test_gemm_fast",
}
assert trace == ["enter", "exit"]
assert len(configs) == 1
assert configs[0].output == "bench_gemm_mm"
assert configs[0].data == "trace"
def test_benchmark_kernel_uses_explicit_proton_config(setup_gemm_case, monkeypatch):
configs: list[ProfilingConfig] = []
class _Ctx:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
_ = exc_type, exc, tb
def _fake_profiling(cfg: ProfilingConfig):
configs.append(cfg)
return _Ctx()
monkeypatch.setattr(benchmark_runner_module, "profiling", _fake_profiling)
custom_cfg = ProfilingConfig(
output="custom_profile",
data="tree",
backend="cupti",
mode="pcsampling",
hook="triton",
output_format="chrome_trace",
)
runner = BenchmarkRunner(
BenchmarkConfig(
warmup_iters=0,
bench_iters=1,
verify=False,
proton_profile=True,
proton_config=custom_cfg,
)
)
runner.benchmark_kernel(
"test_gemm_fast",
shapes=_TEST_SHAPES,
dtype=torch.float32,
dtype_role="a",
)
assert len(configs) == 1
assert configs[0] is custom_cfg
def test_benchmark_cli_proton_flag_sets_profile_mode(monkeypatch):
class _FakeRunner:
last_config: BenchmarkConfig | None = None
last_call: tuple | None = None
def __init__(self, config: BenchmarkConfig):
config.validate()
type(self).last_config = config
def benchmark_kernel(self, *args, **kwargs):
type(self).last_call = ("kernel", args, kwargs)
return []
def benchmark_op(self, *args, **kwargs):
type(self).last_call = ("op", args, kwargs)
return []
def benchmark_all(self, *args, **kwargs):
type(self).last_call = ("all", args, kwargs)
return []
monkeypatch.setattr(benchmark_cli, "BenchmarkRunner", _FakeRunner)
monkeypatch.setattr(benchmark_cli, "load_builtin_kernels", lambda: None)
monkeypatch.setattr(benchmark_cli, "format_report", lambda _results: "ok")
rc = benchmark_cli.main(["--op", "gemm.mm", "--dtype-role", "a", "--proton"])
assert rc == 0
assert _FakeRunner.last_config is not None
assert _FakeRunner.last_config.proton_profile is True
assert _FakeRunner.last_config.proton_config is None
assert _FakeRunner.last_call is not None
assert _FakeRunner.last_call[0] == "op"
def test_benchmark_cli_builds_proton_config_from_flags(monkeypatch):
class _FakeRunner:
last_config: BenchmarkConfig | None = None
def __init__(self, config: BenchmarkConfig):
config.validate()
type(self).last_config = config
def benchmark_kernel(self, *args, **kwargs):
_ = args, kwargs
return []
def benchmark_op(self, *args, **kwargs):
_ = args, kwargs
return []
def benchmark_all(self, *args, **kwargs):
_ = args, kwargs
return []
monkeypatch.setattr(benchmark_cli, "BenchmarkRunner", _FakeRunner)
monkeypatch.setattr(benchmark_cli, "load_builtin_kernels", lambda: None)
monkeypatch.setattr(benchmark_cli, "format_report", lambda _results: "ok")
rc = benchmark_cli.main(
[
"--op",
"gemm.mm",
"--dtype-role",
"a",
"--proton-output",
"bench_cli",
"--proton-data",
"tree",
"--proton-backend",
"cupti",
"--proton-mode",
"pcsampling",
"--proton-hook",
"none",
"--proton-output-format",
"chrome_trace",
]
)
assert rc == 0
assert _FakeRunner.last_config is not None
assert _FakeRunner.last_config.proton_profile is True
assert _FakeRunner.last_config.proton_config is not None
proton_cfg = _FakeRunner.last_config.proton_config
assert proton_cfg.output == "bench_cli"
assert proton_cfg.data == "tree"
assert proton_cfg.backend == "cupti"
assert proton_cfg.mode == "pcsampling"
assert proton_cfg.hook is None
assert proton_cfg.output_format == "chrome_trace"