Triton kernel performance model#
2026-07-22
11 min read time
This topic explains how TraceLens computes GFLOPS, TB/s, and other performance
metrics for torch.compile-generated Triton kernels.
The performance model is implemented in
TraceLens/PerfModel/triton_compiled_perf_model.py.
How Triton kernel naming works#
When you run torch.compile(model),
TorchDynamo
captures the model’s operations as an FX graph, and the
Inductor
backend fuses groups of ATen ops into Triton kernels. Each kernel’s name
encodes what it does:
triton_poi_fused__unsafe_view_mul_silu_2
│ │ │ │
│ │ │ └── index (2nd kernel in the graph)
│ │ └── fused ATen ops (alphabetical)
│ └── kernel type: poi=pointwise, red=reduction, per=persistent-reduction
└── generated by Triton/Inductor
Pointwise kernels (triton_poi_*) process each element independently.
One loop dimension: xnumel (total elements).
Reduction kernels (triton_red_*, triton_per_*) reduce along one axis.
Two loop dimensions: xnumel (outer) and rnumel (reduction axis).
Requirements and PyTorch version compatibility#
Trace capture requirements#
TraceLens uses a two-tier approach (V2 primary, V1 fallback) to extract
perf metrics. For V2 to work, the trace must be captured with
record_shapes=True:
with torch.profiler.profile(
activities=[
torch.profiler.ProfilerActivity.CPU,
torch.profiler.ProfilerActivity.CUDA,
],
record_shapes=True, # required for V2 metadata
) as prof:
model(inputs)
Note
Without record_shapes=True, the trace won’t contain Concrete Inputs,
Input Dims, or Input type — and V2 falls back to V1.
PyTorch version compatibility#
V2 requires specific trace event fields. The following table shows verified availability (confirmed from actual traces captured on each version):
Trace Field |
PyTorch 2.4 |
PyTorch 2.11 |
Used By |
|---|---|---|---|
|
Yes |
Yes |
V2: element counts ( |
|
Yes |
Yes |
V2: per-tensor byte calculation |
|
Yes |
Yes |
V2: bytes per element |
|
No |
Yes |
Not used (informational) |
|
No |
Yes |
Not used (informational) |
|
No |
Yes |
Not used (informational) |
|
No |
Yes |
Not used (informational) |
V2 core fields are available since PyTorch 2.4. The extra fields
(kernel_file, num_warps, and so on) were added later but aren’t required
by TraceLens.
TORCHINDUCTOR_UNIQUE_KERNEL_NAMES#
Inductor controls kernel naming using TORCHINDUCTOR_UNIQUE_KERNEL_NAMES.
When enabled, kernels get descriptive names that encode the kernel
category and fused ops (for example, triton_poi_fused_add_mul_silu_0).
When disabled, kernels are named generically (triton_0, triton_1).
Note
This is a compile-time setting — it must be set when the trace is captured, not when TraceLens analyzes it.
Default by PyTorch version:
PyTorch Version |
Default |
Notes |
|---|---|---|
2.4 – 2.5 |
Disabled |
Must opt in: |
2.6+ |
Enabled |
Source: torch/_inductor/config.py
— verified at tags
v2.4.0 and
v2.6.0.
Impact on TraceLens:
Metric |
Unique names enabled |
Unique names disabled |
|---|---|---|
V2 element counts ( |
Works |
Works |
V2 bytes ( |
Works |
Works |
FLOPs ( |
Works — parses ops from name |
Returns |
V1 cache fallback |
Works |
Works |
For users on PyTorch 2.4–2.5, set the environment variable before trace capture to get FLOPs:
TORCHINDUCTOR_UNIQUE_KERNEL_NAMES=1 python your_training_script.py
Recommended setup#
Verified on PyTorch 2.4 and 2.11 (with record_shapes=True):
PyTorch 2.11 — recommended, everything works out of the box
PyTorch 2.4–2.5 — works with
TORCHINDUCTOR_UNIQUE_KERNEL_NAMES=1for FLOPsPyTorch 2.6–2.10 — expected to work (unique names enabled by default per source code) but not directly verified
How it works#
Data flow#
Chrome Trace (.json.gz)
│
├── event["name"]
│ │
│ └── resolve_perf_model_class()
│ └── _match_triton_compiled() → TritonCompiledPerfModel
│
├── event["args"] GPU events
│ ├── Concrete Inputs │
│ ├── Input Dims │
│ └── Input type │
│ │ │
│ ┌────┴─────┐ │
│ │ V2 path │ ──── _meta_from_trace_args() │
│ └────┬─────┘ ├── scalars → xnumel, rnumel
│ │ ├── dims + dtypes → total_bytes
│ success? └── _parse_kernel_name() → aten_ops
│ ├── yes ── _meta ─────────┐ │
│ └── no │ │
│ │ │ │
│ ┌────┴─────┐ │ │
│ │ V1 path │ │ │
│ └────┬─────┘ │ │
│ │ │ │
│ Inductor Cache (.py) │ │
│ ├── Original ATen │ GPUEventAnalyser
│ ├── size_hints │ busy_time
│ └── signature │ │
│ │ │ │
│ └── _lookup() ── _meta │
│ │ │
│ ┌────┴────┐ ┌─────┴─────┐
│ │ flops() │ │ kernel │
│ │ bytes() │ │ time (µs) │
│ └────┬────┘ └─────┬─────┘
│ │ │
│ └───────┬───────┘
│ │
│ TB/s, TFLOPS/s
│ (tree_perf.py:compute_perf_metrics)
Two-tier metadata extraction#
TraceLens needs three pieces of information to compute metrics:
Element counts —
xnumelandrnumelData types and shapes — to calculate bytes moved
Fused ATen ops — to calculate FLOPs
These are obtained through a two-tier approach. V2 is tried first; V1 is the fallback:
# TritonCompiledPerfModel.__init__
self._meta = _meta_from_trace_args(event) # V2: trace event args
if self._meta is None:
self._meta = _lookup(self.name, kwargs.get("inductor_cache_dir")) # V1: cache fallback
V2: trace-intrinsic (primary)#
Extracts metadata directly from the Chrome trace event’s args dict.
Available in PyTorch 2.4+ traces (see PyTorch version compatibility).
No disk I/O needed — the trace is self-contained.
{
"name": "triton_poi_fused__unsafe_view_mul_silu_2",
"args": {
"Concrete Inputs": ["", "", "268435456"],
"Input Dims": [[8, 4096, 8192], [32768, 8192], []],
"Input type": ["c10::BFloat16", "c10::BFloat16", "Scalar"]
}
}
Field |
What it provides |
|---|---|
|
Scalar values including |
|
Per-tensor shapes for exact byte calculation |
|
C10 dtype strings ( |
V1: cache-based (fallback)#
Parses the Inductor wrapper .py files from the torch.compile cache directory.
Used when trace args are missing (older PyTorch traces) or when the trace was
collected without record_shapes=True.
Cache dirs searched (first match wins):
--inductor_cache_dirCLI argument (explicit)$TORCHINDUCTOR_CACHE_DIRenvironment variable~/.cache/torchinductor/tmp/torchinductor_<user>
Three fields are extracted using regex:
# 1. ATen ops (from comment above kernel)
# Original ATen: [aten.mul, aten.silu]
# 2. Element counts (from decorator)
size_hints={'x': 268435456} # PyTorch 2.7+ dict format
size_hints=[268435456] # older list format
# 3. Pointer dtypes (from triton_meta)
'signature': {'in_out_ptr0': '*bf16', 'in_ptr0': '*bf16', 'xnumel': 'i32'}
V1 compared with V2#
V2 (trace-intrinsic) |
V1 (cache-based) |
|
|---|---|---|
Data source |
Chrome trace |
Inductor cache |
PyTorch version |
2.4+ |
Any |
ATen ops source |
Parsed from kernel name using DP segmentation against |
|
Element counts |
Exact (from |
Rounded to power-of-2 ( |
Bytes calculation |
Per-tensor: |
Per-pointer: |
External dependency |
None — trace is self-contained |
Requires cache directory on disk |
Works on shared traces |
Yes |
Only if cache is accessible |
Inductor Triton kernel types#
Inductor generates Triton kernels in several categories, each with a
distinct 3-character prefix derived from the category name (category[:3]).
These 6 categories have been stable from PyTorch 2.3 through 2.11+
(defined in torch/_inductor/wrapper_benchmark.py):
Prefix |
Category |
Description |
|---|---|---|
|
|
Element-wise ops (add, mul, relu, silu, and so on). One loop dimension: |
|
|
Reduction ops (sum, mean, batch norm, and so on) using a looped strategy. Two dimensions: |
|
|
Same reduction ops, but keeps the entire reduction axis in registers/SRAM. Used when |
|
|
Complex ops like matrix multiplication (mm, addmm, _scaled_mm) using pre-defined Triton templates with optional fused epilogues. |
|
|
Fused operations across lists of tensors (for example, optimizer step updates applied to all parameters at once). |
|
|
Split scan operations (for example, cumulative sums with decoupled lookback). |
TraceLens coverage:
Category |
Modeled by TraceLens? |
Notes |
|---|---|---|
|
Yes |
|
|
Yes |
Same perf model, two-dimensional (xnumel × rnumel) |
|
Yes |
Treated identically to |
|
No (not needed) |
General matrix multiplication (GEMM) ops already modeled by |
|
No |
Typically optimizer-step ops, not performance-critical |
|
No |
Scan operations, uncommon in typical workloads |
Naming pattern:
triton_{category}_fused_{op1}_{op2}_{...}_{index}
The fused ops are listed alphabetically. The index is a sequential
counter within the compiled graph. When unique_kernel_names is disabled,
the entire name collapses to triton_{index}.
Metric formulas#
FLOPs#
flops = sum(flops_per_elem[op] for op in fused_aten_ops) * xnumel * rnumel
The flops_per_elem table is in _FLOPS_PER_ELEM. Examples: add=1, mul=1,
silu=4, gelu=8, exp=4, pow=2, rsqrt=2. Memory-only ops (clone,
_to_copy, _unsafe_view) contribute 0.
Bytes#
V2 (trace args):
bytes = sum(prod(shape_i) * bytes_per_elem_i for each tensor input)
+ ptr_bytes[0] * xnumel # only for pointwise (output write)
V1 (cache files):
# Reduction (rnumel > 1):
bytes = sum(ptr_bytes[:-1]) * xnumel * rnumel + ptr_bytes[-1] * xnumel
# Pointwise (rnumel == 1):
bytes = (sum(ptr_bytes) + sum(in_out_extra_bytes)) * xnumel
The in_out_extra_bytes accounts for in_out_ptr parameters that are both
read and written — they appear once in the signature but move data twice.
Throughput#
All throughput metrics use the busy kernel time from GPUEventAnalyser
(aggregated GPU execution time, not the CPU-side event["dur"]):
TB/s = (bytes_moved / 1e12) / (kernel_time_us / 1e6)
TFLOPS/s = (gflops / 1e3) / (kernel_time_us / 1e6)
FLOPS/Byte = flops / bytes_moved
Data Moved (MB) = bytes_moved / (1024 * 1024)
Worked example: pointwise kernel (SwiGLU)#
Kernel: triton_poi_fused__unsafe_view_mul_silu_2
Operation: silu(gate_proj(x)) * up_proj(x) in the SwiGLU MLP
Trace event args (V2 input)#
Concrete Inputs: ['', '', '268435456']
Input Dims: [[8, 4096, 8192], [32768, 8192], []]
Input type: ['c10::BFloat16', 'c10::BFloat16', 'Scalar']
Element count extraction#
Integer scalars from Concrete Inputs (empty strings and non-integer values
like floats or booleans are skipped): [268435456]
Pointwise kernel → last integer scalar is xnumel:
xnumel = 268,435,456(= 8 x 4,096 x 8,192)rnumel = 1
Bytes calculation#
Step 1: Calculate bytes for each tensor input:
Input |
Dims |
Type |
Bytes per elem |
Elements |
Bytes |
|---|---|---|---|---|---|
|
[8, 4096, 8192] |
bf16 |
2 |
268,435,456 |
536,870,912 |
|
[32768, 8192] |
bf16 |
2 |
268,435,456 |
536,870,912 |
|
[] |
Scalar |
— |
— |
skipped |
input_bytes = (268,435,456 * 2) + (268,435,456 * 2) = 1,073,741,824
Step 2: Add output write for pointwise kernels.
In pointwise kernels, in_out_ptr0 is both read and written — the output
overwrites the input buffer. The input bytes above already count the read.
V2 adds one extra write of ptr_bytes[0] * xnumel:
output_write = 2 * 268,435,456 = 536,870,912
Total:
bytes_moved = 1,073,741,824 + 536,870,912
= 1,610,612,736 bytes
= 1,536 MB
FLOPs calculation#
Fused ops parsed from the kernel name: aten._unsafe_view, aten.mul, aten.silu
Op |
FLOPs per element |
|---|---|
|
0 (memory-only) |
|
1 |
|
4 |
Total |
5 |
flops = 5 * xnumel * rnumel
= 5 * 268,435,456 * 1
= 1,342,177,280
GFLOPS = 1,342,177,280 / 1e9 = 1.342
Throughput metrics#
GPU kernel time from the Chrome trace: 505.3 us
TB/s = (1,610,612,736 / 1e12) / (505.3 / 1e6) = 3.19 TB/s
TFLOPS/s = (1.342 / 1e3) / (505.3 / 1e6) = 2.66 TFLOPS/s
Summary#
Metric |
Value |
|---|---|
Data Moved |
1,536 MB |
GFLOPS |
1.342 |
FLOPS/Byte |
0.833 |
TB/s |
3.19 |
TFLOPS/s |
2.66 |
Worked example: reduction kernel (RMSNorm)#
Kernel: triton_red_fused_add_mean_mul_pow_rsqrt_0
Operation: RMSNorm — x * rsqrt(mean(x^2) + eps) * weight
Trace event args (V2 input)#
Concrete Inputs: ['', '', '', '32768', '2048']
Input Dims: [[8, 4096, 2048], [2048], [8, 4096, 2048], [], []]
Input type: ['c10::BFloat16', 'c10::BFloat16', 'c10::BFloat16', 'Scalar', 'Scalar']
Element count extraction#
Integer scalars from Concrete Inputs (non-integer values skipped): [32768, 2048]
Reduction kernel → last two integer scalars are xnumel and rnumel:
xnumel = 32,768(= 8 x 4,096 = batch x seq_len)rnumel = 2,048(= hidden_dim, the reduction axis)
Bytes calculation (V2)#
Input |
Dims |
Type |
Bytes per elem |
Elements |
Bytes |
|---|---|---|---|---|---|
|
[8, 4096, 2048] |
bf16 |
2 |
67,108,864 |
134,217,728 |
|
[2048] |
bf16 |
2 |
2,048 |
4,096 |
|
[8, 4096, 2048] |
bf16 |
2 |
67,108,864 |
134,217,728 |
No extra output write for reduction kernels (the output pointer is already listed as a separate tensor).
bytes_moved = 134,217,728 + 4,096 + 134,217,728
= 268,439,552 bytes
≈ 256 MB
Bytes calculation (V1 — for comparison)#
V1 uses xnumel, rnumel, and per-pointer byte widths from the signature
instead of per-tensor shapes:
signature: {'in_ptr0': '*bf16', 'in_ptr1': '*bf16', 'out_ptr1': '*bf16',
'xnumel': 'i32', 'r0_numel': 'i32'}
ptr_bytes = [2, 2, 2] # three *bf16 pointers
Reduction formula: all inputs read xnumel * rnumel elements, the output
(last pointer) writes xnumel elements:
bytes = sum(ptr_bytes[:-1]) * xnumel * rnumel + ptr_bytes[-1] * xnumel
= (2 + 2) * 32,768 * 2,048 + 2 * 32,768
= 268,435,456 + 65,536
= 268,500,992 bytes
≈ 256 MB
Note
V1 and V2 give slightly different values because V1 assumes all input
pointers access the full xnumel * rnumel grid, while V2 uses exact tensor
shapes (the weight tensor in_ptr1 is only [2048], not [32768, 2048]).
FLOPs calculation#
Fused ops: aten.add, aten.mean, aten.mul, aten.pow, aten.rsqrt
Op |
FLOPs per element |
|---|---|
|
1 |
|
1 |
|
1 |
|
2 |
|
2 |
Total |
7 |
flops = 7 * 32,768 * 2,048 = 469,762,048
GFLOPS = 0.470
Throughput metrics#
GPU kernel time: 79.4 us
TB/s = (268,439,552 / 1e12) / (79.4 / 1e6) = 3.38 TB/s
TFLOPS/s = (0.470 / 1e3) / (79.4 / 1e6) = 5.92 TFLOPS/s
Results for the example transformer block#
Model: TransformerBlock(dim=2048, n_heads=16, mlp_ratio=4), batch=8,
seq_len=4096, dtype=bf16. Traced with PyTorch 2.11+rocm7.2 on AMD Instinct™ MI300X.
Kernel |
GFLOPS |
Data Moved (MB) |
FLOPS/Byte |
TB/s |
TFLOPS/s |
|---|---|---|---|---|---|
|
0.470 |
256 |
1.75 |
3.38 |
5.92 |
|
0.470 |
384 |
1.17 |
4.32 |
5.04 |
|
1.342 |
1,536 |
0.83 |
3.19 |
2.66 |
|
0.067 |
512 |
0.12 |
3.81 |
0.48 |