Skip to content

Commit 0cf46e6

Browse files
committed
add optional clock lock
1 parent ad48efe commit 0cf46e6

3 files changed

Lines changed: 138 additions & 20 deletions

File tree

test/test_utils.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,84 @@
1+
import ctypes
2+
import os
13
import tempfile
24
import unittest.mock as mock
35
from pathlib import Path
46

57
import pytest
68
import torch
9+
from transformer_nuggets.utils.benchmark import (
10+
_get_max_sm_clock,
11+
_get_nvml_handle,
12+
_nvml,
13+
locked_clocks,
14+
benchmark_cuda_function_in_microseconds_triton,
15+
)
716
from transformer_nuggets.utils.shape_trace import open_logs, ShapeLog
817
from transformer_nuggets.utils.tracing import NanInfDetect
918

1019

20+
requires_root = pytest.mark.skipif(os.geteuid() != 0, reason="Locking GPU clocks requires root")
21+
22+
23+
def _read_sm_clock() -> int:
24+
nvml = _nvml()
25+
nvml.nvmlInit()
26+
handle = _get_nvml_handle(nvml)
27+
clk = ctypes.c_uint()
28+
nvml.nvmlDeviceGetClockInfo(handle, ctypes.c_uint(1), ctypes.byref(clk))
29+
val = clk.value
30+
nvml.nvmlShutdown()
31+
return val
32+
33+
34+
@requires_root
35+
def test_locked_clocks_context_manager():
36+
with locked_clocks() as target_mhz:
37+
assert isinstance(target_mhz, int)
38+
assert target_mhz > 0
39+
40+
x = torch.randn(1024, 1024, device="cuda")
41+
for _ in range(10):
42+
x @ x
43+
torch.cuda.synchronize()
44+
45+
assert _read_sm_clock() == target_mhz
46+
47+
48+
@requires_root
49+
def test_locked_clocks_custom_frequency():
50+
nvml = _nvml()
51+
nvml.nvmlInit()
52+
handle = _get_nvml_handle(nvml)
53+
max_clk = _get_max_sm_clock(nvml, handle)
54+
nvml.nvmlShutdown()
55+
56+
with locked_clocks(clock_mhz=max_clk) as target_mhz:
57+
assert target_mhz == max_clk
58+
59+
60+
@requires_root
61+
def test_locked_clocks_resets_on_exception():
62+
with pytest.raises(ValueError, match="intentional"):
63+
with locked_clocks():
64+
raise ValueError("intentional")
65+
66+
67+
@requires_root
68+
def test_benchmark_lock_clocks_kwarg():
69+
x = torch.randn(1024, 1024, device="cuda")
70+
t = benchmark_cuda_function_in_microseconds_triton(lambda: x @ x, LOCK_CLOCKS=True)
71+
assert t > 0
72+
73+
74+
def test_locked_clocks_no_permission():
75+
if os.geteuid() == 0:
76+
pytest.skip("Running as root, cannot test permission failure")
77+
with pytest.raises(RuntimeError, match="Requires root"):
78+
with locked_clocks():
79+
pass
80+
81+
1182
def test_nan():
1283
a = torch.tensor(
1384
[

transformer_nuggets/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
benchmark_torch_function_in_microseconds,
33
benchmark_cuda_function_in_microseconds,
44
benchmark_cuda_function_in_microseconds_triton,
5+
locked_clocks,
56
max_memory_usage,
67
cuda_memory_usage,
78
profile_function,

transformer_nuggets/utils/benchmark.py

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,44 @@
1818
logger.addHandler(logging.NullHandler())
1919

2020

21+
@contextmanager
22+
def locked_clocks(device: int = 0, clock_mhz: int | None = None):
23+
"""Lock GPU SM clocks for stable benchmarking.
24+
25+
Uses ``sudo nvidia-smi -lgc`` to lock and ``sudo nvidia-smi -rgc`` to
26+
reset. Will prompt for a password in interactive terminals.
27+
28+
Args:
29+
device: CUDA device index.
30+
clock_mhz: SM clock frequency in MHz. If None, locks to the GPU's max SM clock.
31+
"""
32+
import subprocess
33+
34+
if clock_mhz is None:
35+
clock_mhz = int(
36+
subprocess.check_output(
37+
[
38+
"nvidia-smi",
39+
"-i",
40+
str(device),
41+
"--query-gpu=clocks.max.sm",
42+
"--format=csv,noheader,nounits",
43+
],
44+
text=True,
45+
).strip()
46+
)
47+
48+
subprocess.check_call(
49+
["sudo", "nvidia-smi", "-i", str(device), "-lgc", f"{clock_mhz},{clock_mhz}"]
50+
)
51+
logger.info(f"Locked GPU {device} SM clocks to {clock_mhz} MHz")
52+
try:
53+
yield clock_mhz
54+
finally:
55+
subprocess.call(["sudo", "nvidia-smi", "-i", str(device), "-rgc"])
56+
logger.info(f"Reset GPU {device} SM clocks")
57+
58+
2159
def lazy_import_error(error_msg: str):
2260
"""Decorator that allows functions with imports to be defined without the dependency"""
2361

@@ -60,40 +98,48 @@ class ProfileConfig:
6098

6199

62100
def benchmark_torch_function_in_microseconds(func: Callable, *args, **kwargs) -> float:
63-
# warmup
64-
for _ in range(5):
65-
func(*args, **kwargs)
66-
t0 = benchmark.Timer(
67-
stmt="func(*args, **kwargs)",
68-
globals={"args": args, "kwargs": kwargs, "func": func},
69-
)
70-
return t0.adaptive_autorange(min_run_time=0.1).median * 1e6
101+
lock = kwargs.pop("LOCK_CLOCKS", False)
102+
ctx = locked_clocks() if lock else nullcontext()
103+
with ctx:
104+
for _ in range(5):
105+
func(*args, **kwargs)
106+
t0 = benchmark.Timer(
107+
stmt="func(*args, **kwargs)",
108+
globals={"args": args, "kwargs": kwargs, "func": func},
109+
)
110+
return t0.adaptive_autorange(min_run_time=0.1).median * 1e6
71111

72112

73113
def benchmark_cuda_function_in_microseconds(func: Callable, *args, **kwargs) -> float:
74114
"""Thin wrapper around do_bench_using_profiling.
75115
76-
Accepts NUM_ITERS as a kwarg but removes it before calling func so it
77-
never leaks into the benchmarked callable.
116+
Accepts NUM_ITERS, IS_VETTED_BENCHMARKING, and lock_clocks as kwargs but
117+
removes them before calling func so they never leak into the benchmarked callable.
78118
"""
79119
num_iters = kwargs.pop("NUM_ITERS", 100)
80120
is_vetted_benchmarking = kwargs.pop("IS_VETTED_BENCHMARKING", False)
81-
no_args = lambda: func(*args, **kwargs)
82-
time = do_bench_using_profiling(
83-
no_args, rep=num_iters, is_vetted_benchmarking=is_vetted_benchmarking
84-
)
85-
return time * 1e3
121+
lock = kwargs.pop("LOCK_CLOCKS", False)
122+
ctx = locked_clocks() if lock else nullcontext()
123+
with ctx:
124+
no_args = lambda: func(*args, **kwargs)
125+
return (
126+
do_bench_using_profiling(
127+
no_args, rep=num_iters, is_vetted_benchmarking=is_vetted_benchmarking
128+
)
129+
* 1e3
130+
)
86131

87132

88133
@lazy_import_error("This function requires Triton. Please install it with: pip install triton")
89134
def benchmark_cuda_function_in_microseconds_triton(func: Callable, *args, **kwargs) -> float:
90135
"""Thin wrapper around do_bench"""
91-
from triton.testing import do_bench # Python caches this automatically
92-
93-
no_args = lambda: func(*args, **kwargs)
94-
time = do_bench(no_args)
136+
from triton.testing import do_bench
95137

96-
return time * 1e3
138+
lock = kwargs.pop("LOCK_CLOCKS", False)
139+
ctx = locked_clocks() if lock else nullcontext()
140+
with ctx:
141+
no_args = lambda: func(*args, **kwargs)
142+
return do_bench(no_args) * 1e3
97143

98144

99145
def profile_function(

0 commit comments

Comments
 (0)