Skip to content

Commit 8197e9c

Browse files
committed
Emit profiler traces as native Perfetto TrackEvents
1 parent 127f6ef commit 8197e9c

11 files changed

Lines changed: 1292 additions & 79 deletions

File tree

examples/warp_specialized_static.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def run():
110110
device=device,
111111
)
112112

113-
trace_path = transformer_nuggets.DATA_DIR / "profiler_warp_static_trace.json"
113+
trace_path = transformer_nuggets.DATA_DIR / "profiler_warp_static_trace.pftrace"
114114

115115
process_names = {i: f"CTA {i}" for i in range(NUM_BLOCKS)}
116116

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies = [
2828
"seaborn",
2929
"typer",
3030
"jinja2",
31+
"perfetto",
3132
]
3233

3334
[project.optional-dependencies]

test/test_perfetto.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import gzip
22
import json
33

4+
import pytest
5+
46
from transformer_nuggets.utils.perfetto import (
7+
chrome_trace_to_track_event_trace,
58
default_trace_path,
9+
default_track_event_path,
610
read_trace,
711
split_overlapping_slices,
812
write_trace,
@@ -146,5 +150,152 @@ def test_gzip_trace_roundtrip(tmp_path):
146150

147151
def test_default_trace_path_prefers_gzip_for_stems_and_respects_explicit_gzip():
148152
assert default_trace_path("foo").as_posix() == "foo.json.gz"
153+
assert default_trace_path("foo.json").as_posix() == "foo.json"
149154
assert default_trace_path("foo.json.gz").as_posix() == "foo.json.gz"
150155
assert default_trace_path("foo", gzip_by_default=False).as_posix() == "foo.json"
156+
157+
158+
def test_default_track_event_path_uses_native_perfetto_suffix():
159+
assert default_track_event_path("foo").as_posix() == "foo.pftrace"
160+
assert default_track_event_path("foo.json").as_posix() == "foo.pftrace"
161+
assert default_track_event_path("foo.json.gz").as_posix() == "foo.pftrace"
162+
assert default_track_event_path("foo.pftrace").as_posix() == "foo.pftrace"
163+
164+
165+
def test_track_event_conversion_preserves_instants_counters_and_warns_on_unsupported():
166+
from perfetto.protos.perfetto.trace.perfetto_trace_pb2 import TrackEvent, Trace
167+
168+
trace = {
169+
"traceEvents": [
170+
{"ph": "M", "name": "thread_name", "pid": 0, "tid": 1, "args": {"name": "worker"}},
171+
{"ph": "i", "name": "marker", "pid": 0, "tid": 1, "ts": 1},
172+
{"ph": "C", "name": "memory", "pid": 0, "tid": 1, "ts": 2, "args": {"bytes": 42}},
173+
{"ph": "B", "name": "unsupported", "pid": 0, "tid": 1, "ts": 3},
174+
]
175+
}
176+
177+
with pytest.warns(RuntimeWarning, match="unsupported Chrome trace phases: B"):
178+
payload = chrome_trace_to_track_event_trace(trace)
179+
180+
parsed = Trace()
181+
parsed.ParseFromString(payload)
182+
event_types = [
183+
packet.track_event.type for packet in parsed.packet if packet.HasField("track_event")
184+
]
185+
assert TrackEvent.TYPE_INSTANT in event_types
186+
assert TrackEvent.TYPE_COUNTER in event_types
187+
188+
189+
def test_track_event_conversion_puts_gpu_annotations_on_separate_track():
190+
from perfetto.protos.perfetto.trace.perfetto_trace_pb2 import TrackEvent, Trace
191+
192+
trace = {
193+
"traceEvents": [
194+
{"ph": "M", "name": "thread_name", "pid": 0, "tid": 7, "args": {"name": "stream 7"}},
195+
{"ph": "X", "cat": "kernel", "name": "kernel", "pid": 0, "tid": 7, "ts": 0, "dur": 10},
196+
{
197+
"ph": "X",
198+
"cat": "gpu_user_annotation",
199+
"name": "burst_0",
200+
"pid": 0,
201+
"tid": 7,
202+
"ts": 0,
203+
"dur": 10,
204+
},
205+
]
206+
}
207+
208+
parsed = Trace()
209+
parsed.ParseFromString(chrome_trace_to_track_event_trace(trace))
210+
names_by_uuid = {
211+
packet.track_descriptor.uuid: packet.track_descriptor.name
212+
for packet in parsed.packet
213+
if packet.HasField("track_descriptor") and packet.track_descriptor.name
214+
}
215+
event_tracks = {
216+
packet.track_event.name: names_by_uuid[packet.track_event.track_uuid]
217+
for packet in parsed.packet
218+
if packet.HasField("track_event")
219+
and packet.track_event.type == TrackEvent.TYPE_SLICE_BEGIN
220+
}
221+
222+
assert event_tracks["kernel"] == "stream 7"
223+
assert event_tracks["burst_0"] == "GPU annotations stream 7"
224+
225+
226+
def test_track_event_conversion_attaches_paired_flows_to_slices():
227+
from perfetto.protos.perfetto.trace.perfetto_trace_pb2 import TrackEvent, Trace
228+
229+
trace = {
230+
"traceEvents": [
231+
{"ph": "M", "name": "thread_name", "pid": 0, "tid": 1, "args": {"name": "cpu"}},
232+
{"ph": "M", "name": "thread_name", "pid": 0, "tid": 2, "args": {"name": "gpu"}},
233+
{"ph": "X", "name": "cudaLaunchKernel", "pid": 0, "tid": 1, "ts": 0, "dur": 10},
234+
{"ph": "X", "name": "kernel", "pid": 0, "tid": 2, "ts": 20, "dur": 5},
235+
{"ph": "s", "name": "ac2g", "pid": 0, "tid": 1, "ts": 1, "id": 99},
236+
{"ph": "f", "name": "ac2g", "pid": 0, "tid": 2, "ts": 20, "id": 99},
237+
{"ph": "f", "name": "single-ended-noise", "pid": 0, "tid": 2, "ts": 22, "id": 100},
238+
]
239+
}
240+
241+
parsed = Trace()
242+
parsed.ParseFromString(chrome_trace_to_track_event_trace(trace))
243+
begins = [
244+
packet.track_event
245+
for packet in parsed.packet
246+
if packet.HasField("track_event")
247+
and packet.track_event.type == TrackEvent.TYPE_SLICE_BEGIN
248+
]
249+
flow_ids_by_name = {event.name: tuple(event.flow_ids) for event in begins}
250+
251+
assert flow_ids_by_name["cudaLaunchKernel"] == (99,)
252+
assert flow_ids_by_name["kernel"] == (99,)
253+
assert all(100 not in flow_ids for flow_ids in flow_ids_by_name.values())
254+
255+
256+
def test_track_event_conversion_splits_crossing_slices_and_keeps_nested_slices():
257+
from perfetto.protos.perfetto.trace.perfetto_trace_pb2 import (
258+
TrackDescriptor,
259+
TrackEvent,
260+
Trace,
261+
)
262+
263+
trace = {
264+
"traceEvents": [
265+
{"ph": "M", "name": "thread_name", "pid": 0, "tid": 1, "args": {"name": "stream 1"}},
266+
{"ph": "X", "name": "outer", "pid": 0, "tid": 1, "ts": 0, "dur": 10},
267+
{"ph": "X", "name": "inner", "pid": 0, "tid": 1, "ts": 2, "dur": 2},
268+
{"ph": "X", "name": "crossing", "pid": 0, "tid": 1, "ts": 5, "dur": 10},
269+
]
270+
}
271+
272+
parsed = Trace()
273+
parsed.ParseFromString(chrome_trace_to_track_event_trace(trace, track_pattern="stream.*"))
274+
275+
descriptors_by_uuid = {
276+
packet.track_descriptor.uuid: packet.track_descriptor
277+
for packet in parsed.packet
278+
if packet.HasField("track_descriptor") and packet.track_descriptor.name
279+
}
280+
names_by_uuid = {uuid: descriptor.name for uuid, descriptor in descriptors_by_uuid.items()}
281+
begin_events = [
282+
packet.track_event
283+
for packet in parsed.packet
284+
if packet.HasField("track_event")
285+
and packet.track_event.type == TrackEvent.TYPE_SLICE_BEGIN
286+
]
287+
event_tracks = {event.name: names_by_uuid[event.track_uuid] for event in begin_events}
288+
event_track_uuids = {event.name: event.track_uuid for event in begin_events}
289+
290+
assert set(names_by_uuid.values()) >= {"stream 1"}
291+
assert event_tracks["outer"] == event_tracks["inner"] == event_tracks["crossing"] == "stream 1"
292+
assert event_track_uuids["outer"] == event_track_uuids["inner"]
293+
assert event_track_uuids["crossing"] != event_track_uuids["outer"]
294+
295+
outer_desc = descriptors_by_uuid[event_track_uuids["outer"]]
296+
crossing_desc = descriptors_by_uuid[event_track_uuids["crossing"]]
297+
assert outer_desc.sibling_merge_behavior == (
298+
TrackDescriptor.SIBLING_MERGE_BEHAVIOR_BY_SIBLING_MERGE_KEY
299+
)
300+
assert crossing_desc.sibling_merge_behavior == outer_desc.sibling_merge_behavior
301+
assert crossing_desc.sibling_merge_key == outer_desc.sibling_merge_key

transformer_nuggets/cute/profiler/README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ with profile_session(
2020
max_events_per_unit=64,
2121
num_units=(num_blocks, "Block"),
2222
tag_names=["compute"],
23-
trace_path="trace.json.gz",
23+
trace_path="trace.pftrace",
2424
) as (prof, _):
2525
my_kernel(output, prof.tensor, prof.max_events_per_unit)
2626
```
@@ -69,7 +69,7 @@ You set `max_events_per_unit` to something larger than you need; the decoder sca
6969
| `profile_session(...)` | Context manager: allocate, yield, decode, write trace |
7070
| `allocate_profile_buffer(max_events_per_unit, num_units, device)` | Allocate buffer |
7171
| `decode_events(buf, tag_table)` | Decode to `Event` list |
72-
| `events_to_perfetto(events, path)` | Write Chrome trace JSON/JSON.GZ and split overlapping slices into adjacent lanes by default |
72+
| `events_to_perfetto(events, path)` | Write native Perfetto TrackEvent `.pftrace` by default, or Chrome JSON/JSON.GZ with `trace_format="chrome_json"` |
7373
| `TagTable(names)` | Map tag names ↔ integer IDs |
7474
| `PostProcessContext` | Context passed to post-processing callbacks |
7575

@@ -81,9 +81,17 @@ You set `max_events_per_unit` to something larger than you need; the decoder sca
8181
| `warp_start/warp_stop(...)` | Low-level start/stop (lane 0 of target_warp) |
8282
| `warp_atomic_alloc(...)` | Allocate event index atomically |
8383

84+
## Trace Formats
85+
86+
Two output formats are supported:
87+
88+
- `track_event` (default): native Perfetto protobuf (`.pftrace` / `.perfetto-trace`). This is the preferred format for programmatically generated traces. Crossing overlaps on one logical track are encoded as multiple backing TrackEvent tracks with the same merge key, so Perfetto can display them as one logical row.
89+
- `chrome_json`: legacy Chrome JSON/JSON.GZ. Use this only for compatibility with tools that require Chrome JSON. Perfetto handles this format on a best-effort basis and requires duration events on a track to nest cleanly.
90+
91+
Pass `trace_format="chrome_json"` to `profile_session` or `events_to_perfetto` to opt into legacy JSON output. Pass `split_overlaps=False` to keep raw tracks.
92+
8493
## Post-Processing
8594

86-
Trace paths ending in `.gz` are written compressed. Overlapping duration slices on the same Perfetto track are split into adjacent `#0`, `#1`, ... lanes by default; pass `split_overlaps=False` to `profile_session` or `events_to_perfetto` to keep raw tracks.
8795

8896
You can pass callbacks to `profile_session` to mutate events or the Perfetto trace before writing:
8997

transformer_nuggets/cute/profiler/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""NVIDIA Intra-Kernel Profiling for CUTE DSL Kernels.
22
33
This package provides utilities for profiling code regions *inside* GPU kernels,
4-
generating Chrome trace format files viewable in Perfetto (https://ui.perfetto.dev/).
4+
generating native Perfetto TrackEvent traces viewable in Perfetto (https://ui.perfetto.dev/).
55
66
Two modes are supported:
77
- Atomic mode: No event_idx needed, indices allocated via atomics (simple)
@@ -21,7 +21,7 @@ def my_kernel(output, prof_buf, max_events):
2121
max_events_per_unit=64,
2222
num_units=(num_blocks, "Block"),
2323
tag_names=["compute"],
24-
trace_path="trace.json.gz",
24+
trace_path="trace.pftrace",
2525
) as (prof, tag_table):
2626
my_kernel(output, prof.tensor, prof.max_events_per_unit)
2727

transformer_nuggets/cute/profiler/example.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Run with:
88
python -m transformer_nuggets.cute.profiler.example
99
10-
View the generated trace.json at https://ui.perfetto.dev/
10+
View the generated .pftrace files at https://ui.perfetto.dev/
1111
"""
1212

1313
import torch
@@ -158,7 +158,7 @@ def run_atomic_mode():
158158

159159
import transformer_nuggets
160160

161-
trace_path = transformer_nuggets.DATA_DIR / "profiler_atomic_trace.json"
161+
trace_path = transformer_nuggets.DATA_DIR / "profiler_atomic_trace.pftrace"
162162

163163
with profile_session(
164164
max_events_per_unit=3 * NUM_ITERATIONS + 2,
@@ -191,7 +191,7 @@ def run_static_mode():
191191

192192
import transformer_nuggets
193193

194-
trace_path = transformer_nuggets.DATA_DIR / "profiler_static_trace.json"
194+
trace_path = transformer_nuggets.DATA_DIR / "profiler_static_trace.pftrace"
195195

196196
with profile_session(
197197
max_events_per_unit=3 * NUM_ITERATIONS + 2,

0 commit comments

Comments
 (0)