Skip to content

Commit 13679ae

Browse files
committed
cute/profiler: token-mode pairing, tid fix, bounds_check, raw event escape hatch
- Add region_start/region_end + RegionToken for explicit pairing across Python scopes or cutlass.range iterations. Token carries unit_id, event_idx, start_ns, target_warp so the start/end pair can't drift. - Fix profile_region passing unit_id as tid to warp_stop; add a tid parameter (defaults to unit_id for back-compat). - Add bounds_check (Constexpr) on static_start/stop/warp_start/warp_stop/ profile_region to opt out of the inner scf.if when calling from deeply nested control flow. - Add raw_event_stop primitive (no warp guard, lane-0-only stores) for call sites that already sit inside a warp election and trigger cf.br lowering failures with the standard profile_region path. - Update example.py with a token-mode demo; README documents all three modes and notes the IKET analogue.
1 parent ef294ff commit 13679ae

4 files changed

Lines changed: 355 additions & 80 deletions

File tree

transformer_nuggets/cute/profiler/README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ with profile_session(
2525
my_kernel(output, prof.tensor, prof.max_events_per_unit)
2626
```
2727

28-
## Two Modes
28+
## Three Modes
2929

30-
### Atomic Mode (simple)
30+
### Atomic mode (default)
3131

3232
Omit `event_idx` and indices are allocated via atomics at runtime:
3333

3434
```python
35-
with profile_region(prof_buf, max_events, TAG, tid):
35+
with profile_region(prof_buf, max_events, TAG, unit_id):
3636
do_work()
3737
```
3838

3939
Works in loops without manual bookkeeping. The tradeoff: **nested regions cause timing skew**. The inner region's atomic runs while the outer region's timer is still running, inflating the outer duration.
4040

41-
### Manual Mode (accurate for nesting)
41+
### Static mode (accurate for nesting)
4242

4343
Pass an explicit `event_idx` to avoid atomics entirely:
4444

@@ -60,6 +60,25 @@ for i in cutlass.range(4):
6060

6161
You set `max_events_per_unit` to something larger than you need; the decoder scans all slots and skips empty ones.
6262

63+
### Token mode (explicit pairing across scopes)
64+
65+
`with profile_region(...)` already pairs start and end structurally, but it can't bridge two Python scopes or open in iteration `i` and close in `i+1`. For those cases, use `region_start` / `region_end` and pass the returned `RegionToken` explicitly:
66+
67+
```python
68+
from transformer_nuggets.cute.profiler import region_start, region_end, RegionToken
69+
70+
outer = region_start(prof_buf, unit_id, max_events)
71+
for i in cutlass.range(N):
72+
inner = region_start(prof_buf, unit_id, max_events)
73+
do_work(i)
74+
region_end(prof_buf, TAG_INNER, inner, max_events)
75+
region_end(prof_buf, TAG_OUTER, outer, max_events)
76+
```
77+
78+
`RegionToken` captures `(unit_id, event_idx, start_ns, target_warp)`, so `region_end` only needs the token plus the tag, and the start/end pair can't drift on those fields. It's a `NamedTuple`, so the DSL can thread it through `cutlass.range` as a loop-carried value if you need to keep a region open across iterations.
79+
80+
This is the closest analogue to NVIDIA IKET's `iket.range_start` / `iket.range_end` SSA-token pairing in `cutlass.cute.experimental.iket`. The shape is similar but the mechanism is different: IKET emits MLIR ops and lowers via the proprietary `iket` dialect, while this profiler emits inline PTX (`%globaltimer`, `st.global.cs.u64`) directly.
81+
6382
## API
6483

6584
### Host (`host.py`)
@@ -77,7 +96,9 @@ You set `max_events_per_unit` to something larger than you need; the decoder sca
7796

7897
| Function | Description |
7998
|----------|-------------|
80-
| `profile_region(buf, max_events, tag, tid, event_idx=None)` | Context manager |
99+
| `profile_region(buf, max_events, tag, unit_id, target_warp=None, event_idx=None, tid=None)` | Context-manager API |
100+
| `region_start(buf, unit_id, max_events, target_warp=None, event_idx=None) -> RegionToken` | Open a region, return a pairing token |
101+
| `region_end(buf, tag, token, max_events_per_unit, tid=None)` | Close a region using its token |
81102
| `warp_start/warp_stop(...)` | Low-level start/stop (lane 0 of target_warp) |
82103
| `warp_atomic_alloc(...)` | Allocate event index atomically |
83104

transformer_nuggets/cute/profiler/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def my_kernel(output, prof_buf, max_events):
4747
warp_start,
4848
warp_stop,
4949
profile_region,
50+
region_start,
51+
region_end,
52+
RegionToken,
53+
raw_event_stop,
5054
)
5155

5256
from transformer_nuggets.cute.profiler.postprocessors import (
@@ -78,6 +82,10 @@ def my_kernel(output, prof_buf, max_events):
7882
"warp_start",
7983
"warp_stop",
8084
"profile_region",
85+
"region_start",
86+
"region_end",
87+
"RegionToken",
88+
"raw_event_stop",
8189
# Post-processors
8290
"group_by_unit",
8391
"group_by_tag",

transformer_nuggets/cute/profiler/example.py

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Example demonstrating NVIDIA intra-kernel profiling with CUTE DSL.
22
3-
This example shows BOTH profiling modes:
4-
1. Atomic mode: No event_idx needed, indices allocated via atomics (simple)
5-
2. Static mode: Explicit event_idx via runtime expressions (no atomics)
3+
This example shows THREE profiling shapes:
4+
1. Atomic mode (recommended default): ``with profile_region(...)``, no event_idx.
5+
2. Static mode: ``with profile_region(..., event_idx=...)`` for zero atomics.
6+
3. Token mode: ``region_start(...) -> token`` / ``region_end(..., token)`` for
7+
explicit pairing across Python scopes or loop iterations.
68
79
Run with:
810
python -m transformer_nuggets.cute.profiler.example
@@ -19,7 +21,7 @@
1921

2022
from transformer_nuggets.cute.base import CuteOp
2123
from transformer_nuggets.cute.profiler.host import profile_session
22-
from transformer_nuggets.cute.profiler.ops import profile_region
24+
from transformer_nuggets.cute.profiler.ops import profile_region, region_start, region_end
2325
from transformer_nuggets.cute.profiler.postprocessors import group_by_unit
2426

2527

@@ -214,10 +216,94 @@ def run_static_mode():
214216
print("✗ Output verification failed!")
215217

216218

219+
class ProfiledKernelToken(CuteOp):
220+
"""Kernel using TOKEN mode: ``region_start`` returns a token consumed by ``region_end``.
221+
222+
The token carries (event_idx, start_ns); pairing is data-flow, not scope.
223+
Use this when a region naturally spans a loop iteration boundary or two
224+
Python scopes that ``with`` can't bridge cleanly.
225+
"""
226+
227+
def __init__(self, num_iterations: int = 4):
228+
super().__init__()
229+
self.num_iterations = num_iterations
230+
231+
@cute.kernel
232+
def kernel(
233+
self,
234+
output: cute.Tensor,
235+
prof_buf: cute.Tensor,
236+
max_events_per_unit: cutlass.Int32,
237+
):
238+
tidx, _, _ = cute.arch.thread_idx()
239+
bidx, _, _ = cute.arch.block_idx()
240+
bdim, _, _ = cute.arch.block_dim()
241+
242+
prof_tid = bidx
243+
global_idx = bidx * bdim + tidx
244+
245+
outer_tok = region_start(prof_buf, prof_tid, max_events_per_unit)
246+
for i in cutlass.range(self.num_iterations):
247+
compute_tok = region_start(prof_buf, prof_tid, max_events_per_unit)
248+
if global_idx < cute.size(output):
249+
val = output[global_idx]
250+
output[global_idx] = val + 1
251+
region_end(prof_buf, TAG_COMPUTE, compute_tok, max_events_per_unit)
252+
region_end(prof_buf, TAG_ITERATION, outer_tok, max_events_per_unit)
253+
254+
@cute.jit()
255+
def __call__(
256+
self,
257+
output: cute.Tensor,
258+
prof_buf: cute.Tensor,
259+
max_events_per_unit: cutlass.Int32,
260+
):
261+
self.kernel(output, prof_buf, max_events_per_unit).launch(
262+
grid=(NUM_BLOCKS, 1, 1),
263+
block=(THREADS_PER_BLOCK, 1, 1),
264+
)
265+
266+
def interface(self, output: torch.Tensor, prof_buf: torch.Tensor, max_events: int):
267+
self.__call__(from_dlpack(output), from_dlpack(prof_buf), Int32(max_events))
268+
269+
270+
def run_token_mode():
271+
"""Run the token mode example."""
272+
print("\n" + "=" * 60)
273+
print("TOKEN MODE: region_start returns a token, region_end consumes it")
274+
print("=" * 60)
275+
276+
device = torch.device("cuda")
277+
output = torch.zeros(256, dtype=torch.float32, device=device)
278+
279+
import transformer_nuggets
280+
281+
trace_path = transformer_nuggets.DATA_DIR / "profiler_token_trace.pftrace"
282+
283+
with profile_session(
284+
max_events_per_unit=2 * NUM_ITERATIONS + 2,
285+
num_units=(NUM_BLOCKS, "Block"),
286+
tag_names=["iteration", "compute", "store"],
287+
trace_path=str(trace_path),
288+
device=device,
289+
) as (prof, tag_table):
290+
print(f"Tags: {tag_table.names}")
291+
kernel = ProfiledKernelToken(num_iterations=NUM_ITERATIONS)
292+
kernel.interface(output, prof.tensor, prof.max_events_per_unit)
293+
294+
print(f"Trace: {trace_path}")
295+
296+
expected = torch.full_like(output, float(NUM_ITERATIONS))
297+
if torch.allclose(output, expected):
298+
print("✓ Output verification passed!")
299+
else:
300+
print("✗ Output verification failed!")
301+
302+
217303
def main():
218304
print("=" * 60)
219305
print("NVIDIA Intra-Kernel Profiling Example")
220-
print("Demonstrating BOTH atomic and static modes")
306+
print("Demonstrating atomic, static, and token modes")
221307
print("=" * 60)
222308

223309
if not torch.cuda.is_available():
@@ -226,6 +312,7 @@ def main():
226312

227313
run_atomic_mode()
228314
run_static_mode()
315+
run_token_mode()
229316

230317
print("\n" + "=" * 60)
231318
print("Done! View traces at https://ui.perfetto.dev/")

0 commit comments

Comments
 (0)