blob: 89824026364483eb591cd78a9d9c1d5b4076db40 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
"""Tests for tvm.tirx.bench utilities."""
import importlib
import inspect
from types import SimpleNamespace
import pytest
import torch
pytest.importorskip("triton") # tvm.tirx.bench imports triton.profiler
from tvm.testing import env
from tvm.tirx.bench import DistributedBenchContext, bench
bench_module = importlib.import_module("tvm.tirx.bench")
class _FakeStream:
cuda_stream = 123
def synchronize(self):
pass
def _distributed_context(max_reduce=lambda value: value):
return DistributedBenchContext(
rank=0,
world_size=4,
barrier=lambda: None,
max_reduce=max_reduce,
stream=_FakeStream(),
)
def test_bench_cooldown_precedes_every_impl(monkeypatch):
"""cooldown_s sleeps immediately before each impl's warmup+measurement.
2 impls x 2 rounds = 4 timed calls, so 4 sleeps (the first impl in the
first round is included). Pins the #29 per-impl cooldown semantics.
"""
calls = []
sleeps = []
def fake_timer(fn, warmup=25, rep=100):
del warmup, rep
fn()
return 0.001
monkeypatch.setattr(bench_module, "_do_bench_event", fake_timer)
monkeypatch.setattr(bench_module.time, "sleep", sleeps.append)
results = bench(
{"a": lambda: calls.append("a"), "b": lambda: calls.append("b")},
warmup=0,
repeat=1,
timer="event",
cooldown_s=1.0,
rounds=2,
)
assert calls == ["a", "b", "a", "b"]
assert sleeps == [1.0, 1.0, 1.0, 1.0]
assert results["benchmark_protocol"]["cooldown_s"] == 1.0
assert results["benchmark_protocol"]["round_aggregate"] == "mean"
def test_bench_retains_round_samples_and_uses_arithmetic_mean(monkeypatch):
values = iter([0.001, 0.002, 0.100])
def fake_timer(_fn, warmup=25, rep=100):
del warmup, rep
return next(values)
monkeypatch.setattr(bench_module, "_do_bench_event", fake_timer)
results = bench({"tir": lambda: None}, timer="event", cooldown_s=0, rounds=3)
assert results["round_samples"] == {"tir": [1.0, 2.0, 100.0]}
assert results["impls"] == {"tir": 103.0 / 3.0}
def test_bench_l2_flush_buffer_matches_triton_256_mib(monkeypatch):
captured = {}
def fake_empty(size, *, dtype, device):
captured.update(size=size, dtype=dtype, device=device)
return object()
monkeypatch.setattr(bench_module.torch, "empty", fake_empty)
bench_module._empty_cache_for_benchmark()
assert captured == {
"size": 256 * 1024 * 1024 // 4,
"dtype": torch.int,
"device": "cuda",
}
@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
def test_bench_event_pure_launch():
"""New Triton-standard bench(): no-arg launch closures, event timer."""
M, N = 256, 256
A = torch.randn(M, N, device="cuda", dtype=torch.float16)
B = torch.randn(M, N, device="cuda", dtype=torch.float16)
funcs = {"mm": lambda: torch.mm(A, B)}
results = bench(funcs, warmup=5, repeat=10, timer="event")
assert "mm" in results["impls"]
assert results["impls"]["mm"] > 0
assert results["timer"] == "event"
def test_bench_default_timer_is_proton(monkeypatch):
"""Omitting timer resolves to Proton and invokes only the Proton timer."""
calls = []
def fake_proton(fn, warmup=25, rep=100):
calls.append((warmup, rep))
fn()
return 0.001
monkeypatch.setattr(bench_module, "_do_bench_proton", fake_proton)
results = bench({"noop": lambda: None}, cooldown_s=0)
assert results["timer"] == "proton"
assert results["impls"] == {"noop": 1.0}
assert calls == [(25, 100)]
def test_distributed_kineto_span_uses_cross_stream_samples_and_rank_max(monkeypatch):
reductions = []
local_samples = [float(index + 1) for index in range(30)]
def fake_profile(name, func, prepare, distributed):
assert name == "impl"
assert callable(func)
assert callable(prepare["impl"])
assert distributed.world_size == 4
return local_samples, [2] * len(local_samples)
def max_reduce(value):
reductions.append(value)
return value + 100.0
monkeypatch.setattr(bench_module, "_profile_distributed_kineto_span", fake_profile)
result = bench(
{"impl": lambda: None},
distributed=_distributed_context(max_reduce),
prepare={"impl": lambda: None},
cooldown_s=0,
)
assert result["timer"] == "kineto"
assert result["impls"] == {"impl": 115.5}
assert reductions == local_samples
protocol = result["benchmark_protocol"]
assert protocol["timing_scope"] == "complete correlated GPU activity span"
assert protocol["span_definition"] == "latest activity end minus earliest activity start"
assert protocol["rank_aggregate"] == "sample_wise_max"
assert protocol["rank_local_scope_stream_counts"] == {"impl": [{"min": 2, "max": 2}]}
def test_kineto_span_collector_uses_earliest_start_and_latest_end():
def event(name, device_type, stream, start, end):
return SimpleNamespace(
name=name,
device_type=device_type,
device_resource_id=stream,
time_range=SimpleNamespace(start=start, end=end),
)
profiler = SimpleNamespace(
events=lambda: [
event("sample.0", torch.autograd.DeviceType.CPU, 0, 0.0, 10.0),
event("sample.0", torch.autograd.DeviceType.CUDA, 11, 2.0, 5.0),
event("sample.0", torch.autograd.DeviceType.CUDA, 17, 3.0, 8.0),
event("sample.1", torch.autograd.DeviceType.CUDA, 11, 20.0, 21.5),
]
)
samples, stream_counts = bench_module._collect_kineto_span_samples(
profiler, ["sample.0", "sample.1"]
)
assert samples == [6.0, 1.5]
assert stream_counts == [2, 1]
def test_distributed_kineto_span_keeps_fixed_order_without_ab_ba():
source = inspect.getsource(bench_module._bench_distributed_kineto_span)
assert "reversed" not in source
assert "round_orders" not in source
@pytest.mark.parametrize(
"kwargs, match",
[
({"timer": "event"}, "only timer='kineto'"),
({"timer": "proton"}, "only timer='kineto'"),
({"warmup": 1}, "rejects overrides: warmup"),
({"repeat": 1}, "rejects overrides: repeat"),
({"cudagraph_rep": 1}, "rejects overrides: cudagraph_rep"),
],
)
def test_distributed_timers_reject_invalid_timer_and_budgets(kwargs, match):
with pytest.raises(ValueError, match=match):
bench(
{"noop": lambda: None},
distributed=_distributed_context(),
cooldown_s=0,
**kwargs,
)
@pytest.mark.parametrize(
("overrides", "error", "match"),
[
({"world_size": 0}, ValueError, "world_size must be a positive integer"),
({"world_size": True}, ValueError, "world_size must be a positive integer"),
({"rank": -1}, ValueError, "rank must be in"),
({"rank": 4}, ValueError, "rank must be in"),
({"barrier": None}, TypeError, "barrier must be callable"),
({"max_reduce": None}, TypeError, "max_reduce must be callable"),
({"stream": object()}, TypeError, "stream must be an actual CUDA stream"),
],
)
def test_distributed_timer_rejects_invalid_context(overrides, error, match):
values = {
"rank": 0,
"world_size": 4,
"barrier": lambda: None,
"max_reduce": lambda value: value,
"stream": _FakeStream(),
}
values.update(overrides)
with pytest.raises(error, match=match):
bench(
{"noop": lambda: None},
distributed=DistributedBenchContext(**values),
cooldown_s=0,
)
def test_prepare_requires_distributed_context():
with pytest.raises(ValueError, match="only with a distributed context"):
bench({"noop": lambda: None}, prepare={"noop": lambda: None}, cooldown_s=0)
def test_bench_cudagraph_proton_wiring(monkeypatch):
calls = []
def fake_cudagraph_proton(fn, rep=20):
calls.append(rep)
fn()
return 0.002
monkeypatch.setattr(bench_module, "_do_bench_cudagraph_proton", fake_cudagraph_proton)
results = bench({"noop": lambda: None}, timer="cudagraph_proton", cudagraph_rep=7, cooldown_s=0)
assert results["impls"] == {"noop": 2.0}
assert results["timer"] == "cudagraph_proton"
assert calls == [7]
def test_bench_never_silently_falls_back_from_proton(monkeypatch):
def unavailable(_fn, warmup=25, rep=100):
del warmup, rep
raise RuntimeError("Proton profiler session could not be created")
monkeypatch.setattr(bench_module, "_do_bench_proton", unavailable)
with pytest.raises(RuntimeError, match="Proton profiler session"):
bench({"noop": lambda: None}, timer="proton", cooldown_s=0)
@pytest.mark.parametrize(
("timer", "alternative"),
[("proton", "event"), ("cudagraph_proton", "event")],
)
def test_missing_proton_session_is_an_explicit_error(monkeypatch, timer, alternative):
monkeypatch.setattr(bench_module.proton, "start", lambda *_args, **_kwargs: None)
with pytest.raises(RuntimeError, match=rf"{timer}.*timer='{alternative}'"):
bench_module._start_proton_session("profile", timer=timer, explicit_alternative=alternative)
@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
def test_bench_references_pure_launch():
"""New bench(): reference builders return no-arg callables and get timed."""
M, N = 128, 128
A = torch.randn(M, N, device="cuda", dtype=torch.float16)
B = torch.randn(M, N, device="cuda", dtype=torch.float16)
funcs = {"tir": lambda: torch.mm(A, B)}
def _addmm():
C = torch.zeros(M, N, device="cuda", dtype=torch.float16)
return lambda: torch.addmm(C, A, B)
results = bench(funcs, warmup=5, repeat=10, timer="event", references={"addmm": _addmm})
assert set(results["impls"].keys()) == {"tir", "addmm"}
assert all(v > 0 for v in results["impls"].values())
@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
def test_bench_rejects_unknown_timer():
"""Unknown timer names fail instead of changing measurement method."""
A = torch.randn(8, 8, device="cuda", dtype=torch.float16)
with pytest.raises(ValueError):
bench({"mm": lambda: torch.mm(A, A)}, timer="unknown")
if __name__ == "__main__":
pytest.main([__file__, "-v"])