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

269 lines
7.8 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 json
import pytest
import tokenspeed_kernel.profiling as profiling
import torch
class _FakeScope:
def __init__(self, log: list[str]):
self._log = log
def __enter__(self):
self._log.append("enter")
return self
def __exit__(self, exc_type, exc, tb):
_ = exc_type, exc, tb
self._log.append("exit")
class _FakeProton:
def __init__(self):
self.start_calls: list[tuple] = []
self.finalize_calls: list[tuple] = []
self.scope_calls: list[tuple[str, dict[str, object]]] = []
self.scope_log: list[str] = []
def start(self, *args, **kwargs):
self.start_calls.append((args, kwargs))
return 123
def finalize(self, *args, **kwargs):
self.finalize_calls.append((args, kwargs))
def scope(self, name: str, metrics: dict[str, object] | None = None):
self.scope_calls.append((name, {} if metrics is None else dict(metrics)))
return _FakeScope(self.scope_log)
@pytest.fixture(autouse=True)
def _reset_state():
profiling.stop_profiling()
profiling.ProfilingState.reset()
capture = profiling.ShapeCapture.get()
capture.enabled = False
capture.clear()
profiling.ShapeCapture.reset()
profiling._BOOTSTRAPPED = False
yield
profiling.stop_profiling()
profiling.ProfilingState.reset()
capture = profiling.ShapeCapture.get()
capture.enabled = False
capture.clear()
profiling.ShapeCapture.reset()
profiling._BOOTSTRAPPED = False
def test_start_and_stop_calls_proton(monkeypatch):
fake = _FakeProton()
monkeypatch.setattr(profiling, "_HAS_PROTON", True)
monkeypatch.setattr(profiling, "proton", fake)
cfg = profiling.ProfilingConfig(
output="run_profile",
data="trace",
backend="cupti",
mode="pcsampling",
hook="triton",
output_format="chrome_trace",
)
session = profiling.start_profiling(cfg)
assert session == 123
assert profiling.ProfilingState.get().active
profiling.stop_profiling()
assert not profiling.ProfilingState.get().active
assert fake.start_calls == [
(
("run_profile",),
{
"data": "trace",
"backend": "cupti",
"mode": "pcsampling",
"hook": "triton",
},
)
]
assert fake.finalize_calls == [((123, "chrome_trace"), {})]
def test_start_warns_when_proton_missing(monkeypatch):
monkeypatch.setattr(profiling, "_HAS_PROTON", False)
monkeypatch.setattr(profiling, "proton", None)
with pytest.warns(UserWarning, match="Proton not installed"):
session = profiling.start_profiling()
assert session is None
assert not profiling.ProfilingState.get().active
def test_kernel_scope_noop_when_inactive():
scope = profiling.kernel_scope(
"gemm",
"mm",
torch.float16,
kernel_name="triton_mm_fp8_scaled",
M=16,
N=32,
K=64,
)
assert scope is profiling._NOOP_SCOPE
with scope:
pass
def test_kernel_scope_uses_proton_scope_when_active(monkeypatch):
fake = _FakeProton()
monkeypatch.setattr(profiling, "_HAS_PROTON", True)
monkeypatch.setattr(profiling, "proton", fake)
profiling.start_profiling()
with profiling.kernel_scope(
"gemm",
"mm",
torch.float16,
kernel_name="triton_mm_fp8_scaled",
M=32,
N=64,
K=128,
):
pass
assert fake.scope_calls == [
(
"gemm.mm[triton_mm_fp8_scaled]",
{
"dtype": "torch.float16",
"M": 32,
"N": 64,
"K": 128,
},
)
]
assert fake.scope_log == ["enter", "exit"]
def test_bootstrap_reads_env_and_only_runs_once(monkeypatch):
fake = _FakeProton()
registrations: list[object] = []
monkeypatch.setattr(profiling, "_HAS_PROTON", True)
monkeypatch.setattr(profiling, "proton", fake)
monkeypatch.setattr(
profiling.atexit,
"register",
lambda fn: registrations.append(fn),
)
monkeypatch.setenv("TOKENSPEED_KERNEL_PROFILE", "1")
monkeypatch.setenv("TOKENSPEED_KERNEL_PROFILE_OUTPUT", "env_profile")
monkeypatch.setenv("TOKENSPEED_KERNEL_PROFILE_DATA", "trace")
profiling.bootstrap_profiling_from_env()
profiling.bootstrap_profiling_from_env()
assert len(fake.start_calls) == 1
assert fake.start_calls[0] == (
("env_profile",),
{
"data": "trace",
"backend": None,
"mode": None,
"hook": "triton",
},
)
assert len(registrations) == 2
def test_shape_capture_records_dump_and_clear(tmp_path):
output = tmp_path / "shapes.json"
profiling.start_shape_capture()
cap = profiling.ShapeCapture.get()
cap.record("gemm", "mm", "k1", torch.float16, {"M": 16, "N": 32, "K": 64})
cap.record(
"attention",
"decode",
"k2",
torch.bfloat16,
{"batch": 2, "seq_len": 128, "num_q_heads": 8, "head_dim": 64},
)
profiling.stop_shape_capture(output)
payload = json.loads(output.read_text())
assert len(payload) == 2
assert payload[0]["family"] == "gemm"
assert payload[0]["shape_params"] == {"K": 64, "M": 16, "N": 32}
assert payload[1]["family"] == "attention"
assert payload[1]["shape_params"]["batch"] == 2
assert not profiling.ShapeCapture.get().enabled
def test_shape_capture_context_manager_writes_output(tmp_path):
output = tmp_path / "ctx_shapes.json"
with profiling.shape_capture(output):
profiling.ShapeCapture.get().record(
"gemm",
"mm",
"k1",
torch.float32,
{"M": 8, "N": 8, "K": 8},
)
payload = json.loads(output.read_text())
assert len(payload) == 1
assert payload[0]["kernel_name"] == "k1"
def test_bootstrap_shape_capture_from_env_and_atexit_dump(tmp_path, monkeypatch):
registrations: list[object] = []
output = tmp_path / "env_shapes.json"
monkeypatch.setattr(
profiling.atexit,
"register",
lambda fn: registrations.append(fn),
)
monkeypatch.setenv("TOKENSPEED_KERNEL_CAPTURE_SHAPES", "1")
monkeypatch.setenv("TOKENSPEED_KERNEL_CAPTURE_SHAPES_OUTPUT", str(output))
profiling.bootstrap_profiling_from_env()
cap = profiling.ShapeCapture.get()
assert cap.enabled
cap.record("gemm", "mm", "k1", torch.float16, {"M": 4, "N": 4, "K": 4})
for fn in registrations:
fn()
payload = json.loads(output.read_text())
assert len(payload) == 1
assert payload[0]["shape_params"] == {"K": 4, "M": 4, "N": 4}
assert not profiling.ShapeCapture.get().enabled