Skip to content

Commit eb4754a

Browse files
committed
Add CUPTI monitor profiler support
## Human Note ## Agent note Add an opt-in CUPTI monitor configuration to the shared profiler wrapper, including PM metric sampling, environment counters, CUDA Graph dependency recording, event-node attribution, and metric discovery. Constructing the profiler now prepares the backend immediately, which lets graph recorders arm before capture while preserving existing context-manager usage. Monitor traces use PyTorch's native Perfetto exporter and normalize its unconditional `.gz` suffix, so callers receive the requested `.pftrace` or JSON path instead of having to discover a differently named artifact. The optional dependency keeps cupti-python out of the default installation. ## Test Plan ```bash ~/.venvs/nightly/bin/python -m pytest test/test_profiler.py test/test_perfetto.py -q uvx ruff check transformer_nuggets/utils/benchmark.py transformer_nuggets/utils/__init__.py test/test_profiler.py uvx ruff format --check transformer_nuggets/utils/benchmark.py transformer_nuggets/utils/__init__.py test/test_profiler.py prek ```
1 parent 833b8a1 commit eb4754a

4 files changed

Lines changed: 311 additions & 55 deletions

File tree

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ dev = [
4242
"docstring-parser",
4343
]
4444

45+
cupti-monitor = [
46+
"cupti-python>=13.3",
47+
]
48+
4549
llama = [
4650
"sentencepiece==0.1.99",
4751
"datasets==2.15.0",

test/test_profiler.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import gzip
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from transformer_nuggets.utils import benchmark
7+
from transformer_nuggets.utils.benchmark import (
8+
DEFAULT_CUPTI_MONITOR_PM_METRICS,
9+
CuptiMonitorConfig,
10+
_write_cupti_monitor_trace,
11+
)
12+
13+
14+
def test_cupti_monitor_defaults_enable_rich_trace_data():
15+
config = CuptiMonitorConfig()
16+
17+
assert config.environment_counters
18+
assert config.pm_metrics == DEFAULT_CUPTI_MONITOR_PM_METRICS
19+
assert config.graph_dependencies
20+
assert config.event_node_ids
21+
22+
23+
def test_cupti_monitor_config_builds_pytorch_options():
24+
config = CuptiMonitorConfig(
25+
environment_counters=True,
26+
pm_metrics=("sm__cycles_active.avg.pct_of_peak_sustained_elapsed",),
27+
graph_dependencies=True,
28+
event_node_ids=True,
29+
cuda_sync_events=True,
30+
pftrace_compression_level=4,
31+
)
32+
33+
assert config.custom_profiler_config() == {
34+
"backend": "cupti_monitor",
35+
"enable_environment_counters": True,
36+
"enable_pm_sampling": True,
37+
"pm_metrics": ["sm__cycles_active.avg.pct_of_peak_sustained_elapsed"],
38+
"enable_graph_dependencies": True,
39+
"enable_event_node_ids": True,
40+
"enable_cuda_sync_events": True,
41+
"pftrace_compression_level": 4,
42+
}
43+
44+
45+
@pytest.mark.parametrize("level", [-1, 10])
46+
def test_cupti_monitor_config_rejects_invalid_compression(level):
47+
with pytest.raises(ValueError, match="between 0 and 9"):
48+
CuptiMonitorConfig(pftrace_compression_level=level)
49+
50+
51+
class GzipExportProfiler:
52+
def __init__(self, payload: bytes) -> None:
53+
self.payload = payload
54+
55+
def export_chrome_trace(self, path: str) -> None:
56+
with gzip.open(f"{path}.gz", "wb") as output:
57+
output.write(self.payload)
58+
59+
60+
def test_cupti_monitor_pftrace_export_hides_gzip_suffix(tmp_path):
61+
path = tmp_path / "trace.pftrace"
62+
profiler = GzipExportProfiler(b"compressed native trace")
63+
64+
_write_cupti_monitor_trace(profiler, path, trace_format="track_event")
65+
66+
assert path.exists()
67+
assert not Path(f"{path}.gz").exists()
68+
with gzip.open(path, "rb") as trace:
69+
assert trace.read() == b"compressed native trace"
70+
71+
72+
def test_cupti_monitor_json_export_hides_gzip_suffix(tmp_path):
73+
path = tmp_path / "trace.json"
74+
profiler = GzipExportProfiler(b'{"traceEvents": []}')
75+
76+
_write_cupti_monitor_trace(profiler, path, trace_format="chrome_json")
77+
78+
assert path.read_bytes() == b'{"traceEvents": []}'
79+
assert not Path(f"{path}.gz").exists()
80+
81+
82+
def test_cupti_monitor_profiler_is_constructed_before_context_entry(monkeypatch, tmp_path):
83+
experimental_config = object()
84+
constructed = []
85+
86+
class FakeProfiler:
87+
def start(self):
88+
pass
89+
90+
def stop(self):
91+
pass
92+
93+
def fake_profile(**kwargs):
94+
constructed.append(kwargs)
95+
return FakeProfiler()
96+
97+
monkeypatch.setattr(
98+
benchmark,
99+
"_cupti_monitor_experimental_config",
100+
lambda config: experimental_config,
101+
)
102+
monkeypatch.setattr(benchmark.torch.profiler, "profile", fake_profile)
103+
104+
context = benchmark.profiler(
105+
tmp_path / "trace",
106+
backend="cupti_monitor",
107+
)
108+
109+
assert constructed[0]["experimental_config"] is experimental_config
110+
with context:
111+
pass
112+
113+
114+
def test_cupti_monitor_config_selects_monitor_backend(monkeypatch, tmp_path):
115+
constructed = []
116+
117+
class FakeProfiler:
118+
def start(self):
119+
pass
120+
121+
def stop(self):
122+
pass
123+
124+
monkeypatch.setattr(
125+
benchmark,
126+
"_cupti_monitor_experimental_config",
127+
lambda config: constructed.append(config) or object(),
128+
)
129+
monkeypatch.setattr(benchmark.torch.profiler, "profile", lambda **kwargs: FakeProfiler())
130+
131+
benchmark.profiler(
132+
tmp_path / "trace",
133+
cupti_monitor_config=CuptiMonitorConfig(environment_counters=False),
134+
)
135+
136+
assert not constructed[0].environment_counters
Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
from transformer_nuggets.utils.benchmark import (
2-
benchmark_torch_function_in_microseconds,
2+
DEFAULT_CUPTI_MONITOR_PM_METRICS,
3+
CudaBenchmarkStats,
4+
CuptiMonitorConfig,
5+
ProfileConfig,
6+
attach_oom_observer,
37
benchmark_cuda_function_in_microseconds,
48
benchmark_cuda_function_in_microseconds_triton,
59
benchmark_cuda_function_stats,
10+
benchmark_torch_function_in_microseconds,
11+
cuda_memory_usage,
612
locked_clocks,
713
max_memory_usage,
8-
cuda_memory_usage,
914
profile_function,
10-
ProfileConfig,
11-
CudaBenchmarkStats,
12-
save_memory_snapshot,
1315
profiler,
14-
attach_oom_observer,
16+
save_memory_snapshot,
17+
supported_cupti_monitor_metrics,
1518
)
16-
from transformer_nuggets.utils.tracing import LoggingMode, NanInfDetect
17-
from transformer_nuggets.utils.triton import print_sass
18-
from transformer_nuggets.utils.merge_traces import merge_traces
1919
from transformer_nuggets.utils.memory_viz import generate_memory_comparison_html
20+
from transformer_nuggets.utils.merge_traces import merge_traces
2021
from transformer_nuggets.utils.perfetto import (
2122
TraceFormat,
2223
default_trace_path,
@@ -28,5 +29,7 @@
2829
write_trace,
2930
write_track_event_trace,
3031
)
32+
from transformer_nuggets.utils.tracing import LoggingMode, NanInfDetect
3133
from transformer_nuggets.utils.track_event import chrome_trace_to_track_event_trace
34+
from transformer_nuggets.utils.triton import print_sass
3235
# from transformer_nuggets.utils.model_extraction import extract_attention_data

0 commit comments

Comments
 (0)