Skip to content

Commit a92714a

Browse files
committed
Fix Perfetto trace overlap handling
1 parent 2315acc commit a92714a

7 files changed

Lines changed: 634 additions & 16 deletions

File tree

test/test_perfetto.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import gzip
2+
import json
3+
4+
from transformer_nuggets.utils.perfetto import (
5+
default_trace_path,
6+
read_trace,
7+
split_overlapping_slices,
8+
write_trace,
9+
)
10+
11+
12+
def _duration_events(trace):
13+
return [event for event in trace["traceEvents"] if event.get("ph") == "X"]
14+
15+
16+
def test_split_overlapping_slices_creates_adjacent_lanes():
17+
trace = {
18+
"traceEvents": [
19+
{
20+
"ph": "M",
21+
"name": "thread_name",
22+
"pid": 0,
23+
"tid": 7,
24+
"args": {"name": "stream 7"},
25+
},
26+
{"ph": "X", "name": "a", "pid": 0, "tid": 7, "ts": 0, "dur": 10},
27+
{"ph": "X", "name": "b", "pid": 0, "tid": 7, "ts": 5, "dur": 10},
28+
{"ph": "X", "name": "c", "pid": 0, "tid": 7, "ts": 10, "dur": 1},
29+
]
30+
}
31+
32+
fixed = split_overlapping_slices(trace, track_pattern="stream.*")
33+
durations = _duration_events(fixed)
34+
35+
assert [event["tid"] for event in durations] == [700, 701, 700]
36+
thread_names = {
37+
event["tid"]: event["args"]["name"]
38+
for event in fixed["traceEvents"]
39+
if event.get("ph") == "M" and event.get("name") == "thread_name"
40+
}
41+
assert thread_names[700] == "stream 7 #0"
42+
assert thread_names[701] == "stream 7 #1"
43+
44+
45+
def test_split_overlapping_slices_leaves_non_overlapping_tracks_unchanged():
46+
trace = {
47+
"traceEvents": [
48+
{
49+
"ph": "M",
50+
"name": "thread_name",
51+
"pid": 0,
52+
"tid": 3,
53+
"args": {"name": "stream 3"},
54+
},
55+
{"ph": "X", "name": "a", "pid": 0, "tid": 3, "ts": 0, "dur": 10},
56+
{"ph": "X", "name": "b", "pid": 0, "tid": 3, "ts": 10, "dur": 10},
57+
]
58+
}
59+
60+
fixed = split_overlapping_slices(trace, track_pattern="stream.*")
61+
62+
assert fixed == trace
63+
64+
65+
def test_split_overlapping_slices_remaps_flow_by_correlation_and_timestamp():
66+
trace = {
67+
"traceEvents": [
68+
{
69+
"ph": "M",
70+
"name": "thread_name",
71+
"pid": 0,
72+
"tid": 7,
73+
"args": {"name": "stream 7"},
74+
},
75+
{
76+
"ph": "X",
77+
"name": "a",
78+
"pid": 0,
79+
"tid": 7,
80+
"ts": 0,
81+
"dur": 10,
82+
"args": {"correlation": 42},
83+
},
84+
{
85+
"ph": "X",
86+
"name": "b",
87+
"pid": 0,
88+
"tid": 7,
89+
"ts": 5,
90+
"dur": 10,
91+
"args": {"correlation": 42},
92+
},
93+
{"ph": "f", "pid": 0, "tid": 7, "ts": 0, "id": 42},
94+
{"ph": "f", "pid": 0, "tid": 7, "ts": 5, "id": 42},
95+
]
96+
}
97+
98+
fixed = split_overlapping_slices(trace, track_pattern="stream.*")
99+
flow_tids = [
100+
event["tid"] for event in fixed["traceEvents"] if event.get("ph") == "f"
101+
]
102+
103+
assert flow_tids == [700, 701]
104+
105+
106+
def test_split_overlapping_slices_keeps_same_tid_in_different_pids_separate():
107+
trace = {
108+
"traceEvents": [
109+
{
110+
"ph": "M",
111+
"name": "thread_name",
112+
"pid": 0,
113+
"tid": 7,
114+
"args": {"name": "stream 7"},
115+
},
116+
{
117+
"ph": "M",
118+
"name": "thread_name",
119+
"pid": 1,
120+
"tid": 7,
121+
"args": {"name": "stream 7"},
122+
},
123+
{"ph": "X", "name": "a", "pid": 0, "tid": 7, "ts": 0, "dur": 10},
124+
{"ph": "X", "name": "b", "pid": 0, "tid": 7, "ts": 5, "dur": 10},
125+
{"ph": "X", "name": "c", "pid": 1, "tid": 7, "ts": 0, "dur": 10},
126+
{"ph": "X", "name": "d", "pid": 1, "tid": 7, "ts": 5, "dur": 10},
127+
]
128+
}
129+
130+
fixed = split_overlapping_slices(trace, track_pattern="stream.*")
131+
by_pid = {}
132+
for event in _duration_events(fixed):
133+
by_pid.setdefault(event["pid"], []).append(event["tid"])
134+
135+
assert by_pid == {0: [700, 701], 1: [700, 701]}
136+
137+
138+
def test_gzip_trace_roundtrip(tmp_path):
139+
path = tmp_path / "trace.json.gz"
140+
trace = {"traceEvents": [{"ph": "X", "name": "a", "pid": 0, "tid": 0}]}
141+
142+
write_trace(path, trace)
143+
144+
with gzip.open(path, "rt", encoding="utf-8") as f:
145+
assert json.load(f) == trace
146+
assert read_trace(path) == trace
147+
148+
149+
def test_default_trace_path_prefers_gzip_for_stems_and_respects_explicit_gzip():
150+
assert default_trace_path("foo").as_posix() == "foo.json.gz"
151+
assert default_trace_path("foo.json.gz").as_posix() == "foo.json.gz"
152+
assert default_trace_path("foo", gzip_by_default=False).as_posix() == "foo.json"

transformer_nuggets/cute/profiler/README.md

Lines changed: 4 additions & 2 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",
23+
trace_path="trace.json.gz",
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 |
72+
| `events_to_perfetto(events, path)` | Write Chrome trace JSON/JSON.GZ and split overlapping slices into adjacent lanes by default |
7373
| `TagTable(names)` | Map tag names ↔ integer IDs |
7474
| `PostProcessContext` | Context passed to post-processing callbacks |
7575

@@ -83,6 +83,8 @@ You set `max_events_per_unit` to something larger than you need; the decoder sca
8383

8484
## Post-Processing
8585

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.
87+
8688
You can pass callbacks to `profile_session` to mutate events or the Perfetto trace before writing:
8789

8890
- `post_process_events(events, ctx) -> events`: Rename, filter, or regroup events.

transformer_nuggets/cute/profiler/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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",
24+
trace_path="trace.json.gz",
2525
) as (prof, tag_table):
2626
my_kernel(output, prof.tensor, prof.max_events_per_unit)
2727

transformer_nuggets/cute/profiler/host.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,24 @@
2222
self.kernel(x_c, prof.to_cute(), prof.max_events_per_unit, produce_tag, consume_tag).launch(...)
2323
2424
events = decode_events(prof, tag_table)
25-
events_to_perfetto(events, "trace.json", pid=0)
25+
events_to_perfetto(events, "trace.json.gz", pid=0)
2626
2727
Or use the context manager:
28-
with profile_session(64, num_units=4, tag_names=["produce", "consume"], trace_path="trace.json") as (prof, tags):
28+
with profile_session(64, num_units=4, tag_names=["produce", "consume"], trace_path="trace.json.gz") as (prof, tags):
2929
produce_tag = tags.id("produce")
3030
self.kernel(x_c, prof.to_cute(), prof.max_events_per_unit, produce_tag).launch(...)
3131
"""
3232

3333
from __future__ import annotations
3434

35-
import json
3635
from contextlib import contextmanager
3736
from dataclasses import dataclass
3837
from collections.abc import Callable, Iterator
3938

4039
import torch
4140

41+
from transformer_nuggets.utils.perfetto import split_overlapping_slices, write_trace
42+
4243
try:
4344
import cutlass.cute as cute
4445
from cutlass.cute.runtime import from_dlpack
@@ -305,6 +306,7 @@ def events_to_perfetto(
305306
pid: int = 0,
306307
tid_prefix: int | None = None,
307308
unit_name: str = "Unit",
309+
split_overlaps: bool = True,
308310
) -> dict:
309311
"""Export events to Perfetto-compatible Chrome trace JSON format.
310312
@@ -323,15 +325,16 @@ def events_to_perfetto(
323325
tid_prefix: Optional prefix for thread IDs. If provided, tid = tid_prefix + event.tid.
324326
This can satisfy Perfetto's pid/tid quirk where tid should start with pid.
325327
unit_name: Name for units in trace (e.g., "Block", "Warp"). Defaults to "Unit".
328+
split_overlaps: If true, split overlapping duration slices on the same
329+
track into adjacent lanes before returning/writing the trace.
326330
327331
Returns:
328332
The trace dict (can be further modified before writing).
329333
"""
330334
if not events:
331335
trace = {"traceEvents": []}
332336
if trace_path is not None:
333-
with open(trace_path, "w") as f:
334-
json.dump(trace, f)
337+
write_trace(trace_path, trace)
335338
return trace
336339

337340
trace_events = []
@@ -412,9 +415,11 @@ def events_to_perfetto(
412415
"traceEvents": trace_events,
413416
}
414417

418+
if split_overlaps:
419+
trace = split_overlapping_slices(trace)
420+
415421
if trace_path is not None:
416-
with open(trace_path, "w") as f:
417-
json.dump(trace, f, indent=2)
422+
write_trace(trace_path, trace)
418423

419424
return trace
420425

@@ -430,6 +435,7 @@ def profile_session(
430435
pid: int = 0,
431436
post_process_events: Callable[[list[Event], PostProcessContext], list[Event]] | None = None,
432437
post_process_trace: Callable[[dict, PostProcessContext], dict] | None = None,
438+
split_overlaps: bool = True,
433439
) -> Iterator[tuple[ProfileBuf, TagTable]]:
434440
"""Context manager for profiling a kernel session.
435441
@@ -441,7 +447,7 @@ def profile_session(
441447
max_events_per_unit=64,
442448
num_units=(num_blocks, "Block"), # Named units for nicer traces
443449
tag_names=["produce", "consume"],
444-
trace_path="trace.json"
450+
trace_path="trace.json.gz"
445451
) as (prof, tags):
446452
TAG_PRODUCE = tags.id("produce")
447453
TAG_CONSUME = tags.id("consume")
@@ -475,6 +481,8 @@ def group_by_block_warp(events, ctx):
475481
Signature: (events, context) -> events. Can rename, filter, or regroup events.
476482
post_process_trace: Optional callback to mutate the Perfetto trace dict before writing.
477483
Signature: (trace_dict, context) -> trace_dict. Can add flow events, counters, etc.
484+
split_overlaps: If true, split overlapping duration slices on the same
485+
Perfetto track into adjacent lanes before writing the trace.
478486
479487
Yields:
480488
Tuple of (ProfileBuf, TagTable).
@@ -506,10 +514,18 @@ def group_by_block_warp(events, ctx):
506514
events = post_process_events(events, ctx)
507515

508516
if trace_path is not None:
509-
trace = events_to_perfetto(events, trace_path=None, pid=pid, unit_name=prof.unit_name)
517+
trace = events_to_perfetto(
518+
events,
519+
trace_path=None,
520+
pid=pid,
521+
unit_name=prof.unit_name,
522+
split_overlaps=False,
523+
)
510524

511525
if post_process_trace is not None:
512526
trace = post_process_trace(trace, ctx)
513527

514-
with open(trace_path, "w") as f:
515-
json.dump(trace, f, indent=2)
528+
if split_overlaps:
529+
trace = split_overlapping_slices(trace)
530+
531+
write_trace(trace_path, trace)

transformer_nuggets/utils/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,10 @@
1717
from transformer_nuggets.utils.triton import print_sass
1818
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.perfetto import (
21+
default_trace_path,
22+
read_trace,
23+
split_overlapping_slices,
24+
write_trace,
25+
)
2026
# from transformer_nuggets.utils.model_extraction import extract_attention_data

transformer_nuggets/utils/benchmark.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919
from torch.cuda._memory_viz import profile_plot # type: ignore
2020
from torch.profiler import profile, ProfilerActivity, record_function, schedule
2121

22+
from transformer_nuggets.utils.perfetto import (
23+
default_trace_path,
24+
read_trace,
25+
split_overlapping_slices,
26+
write_trace,
27+
)
28+
2229
logger = logging.getLogger(__name__)
2330
logger.addHandler(logging.NullHandler())
2431

@@ -161,6 +168,9 @@ class ProfileConfig:
161168
extra_kwargs: dict = field(default_factory=dict)
162169
memory_profile_path: str | None = None
163170
row_limit: int = 10
171+
gzip_trace: bool = True
172+
fix_overlapping_events: bool = True
173+
overlap_track_pattern: str | None = "stream.*"
164174

165175

166176
@dataclass(frozen=True)
@@ -521,8 +531,24 @@ def profile_function(
521531
torch.cuda.synchronize()
522532

523533
if config.file_path is not None:
524-
trace_path = Path(config.file_path).with_suffix(".json")
525-
prof.export_chrome_trace(str(trace_path))
534+
trace_path = default_trace_path(config.file_path, gzip_by_default=config.gzip_trace)
535+
export_path = trace_path
536+
if trace_path.suffix == ".gz":
537+
export_path = trace_path.with_name(f"{trace_path.name}.tmp.json")
538+
539+
prof.export_chrome_trace(str(export_path))
540+
541+
if config.fix_overlapping_events or export_path != trace_path:
542+
trace = read_trace(export_path)
543+
if config.fix_overlapping_events:
544+
trace = split_overlapping_slices(
545+
trace,
546+
track_pattern=config.overlap_track_pattern,
547+
)
548+
write_trace(trace_path, trace)
549+
if export_path != trace_path:
550+
export_path.unlink()
551+
526552
logger.info(f"💾 Trace file 📄 saved to: {bcolors.OKGREEN}{trace_path}{bcolors.ENDC}")
527553

528554
if profile_memory and config.memory_profile_path is not None:

0 commit comments

Comments
 (0)