Model op performance without a trace#

2026-07-22

3 min read time

In normal use, TraceLens fuses two sources per kernel: the perf model supplies analytical FLOPs and bytes from an op’s shapes and dtypes, and the trace supplies the measured wall-clock time. Dividing one by the other gives the achieved TFLOPS/s and TB/s in every report.

The perf model is just a Python library — you can call the analytical half on its own, with no trace and no kernel launch. Hand it shapes, get back FLOPs and bytes; arithmetic intensity and roofline-bounded time follow. This is useful for sizing studies, end-to-end ceilings, and regression bounds before you run anything.

Before you begin#

  • TraceLens installed (see Install TraceLens).

  • The shapes and dtypes of the ops you want to model (no trace required).

Op coverage#

The perf model ships curated formulas for the fundamental op classes in modern workloads — GEMM, SDPA, CONV, Softmax, Normalization, GroupedGemm, MoEComm, MambaSSD, FusedRoPE, CrossEntropy, CausalConv1d, plus the elementwise and reduction primitives — over a dozen base classes, all maintained and tested against real traces. Every base class in TraceLens.PerfModel.perf_model exposes flops_func and bytes_func (and *_bwd_func variants where a backward pass applies).

Compute FLOPs, bytes, and arithmetic intensity#

Import the op class and call its flops_func / bytes_func with the op’s shapes. For a GEMM of shape M × N × K:

from TraceLens.PerfModel.perf_model import GEMM

M, N, K = 40960, 6144, 1536
bpe = 2  # bytes per element (bf16)

flops = GEMM.flops_func(M, N, K, bias=False)                 # 2 * M * N * K
bytes_ = GEMM.bytes_func(M, N, K, False, bpe, bpe, bpe, bpe)  # read A + read B + write C
arithmetic_intensity = flops / bytes_                         # FLOPs per byte

Attention works the same way, with forward and backward variants:

from TraceLens.PerfModel.perf_model import SDPA

# B, S_q, H_Q, S_kv, H_KV, d_h_qk, d_h_v, causal
flops_fwd = SDPA.flops_func(1, 4096, 64, 4096, 8, 128, 128, True)
bytes_fwd = SDPA.bytes_func(1, 4096, 64, 4096, 8, 128, 128, True, bpe)
flops_bwd = SDPA.flops_bwd_func(1, 4096, 64, 4096, 8, 128, 128, True, flash_impl=True)

Bound the time with a roofline#

Arithmetic intensity tells you which side of the roofline an op sits on. Compare it against the hardware’s roofline knee (peak compute divided by peak bandwidth), then take the larger of the compute-bound and bandwidth-bound times as the achievable lower bound:

PEAK_TFLOPS_BF16 = 708.0   # MI300X matrix bf16
HBM_BW_GBPS = 5300.0       # HBM3

ai_knee = (PEAK_TFLOPS_BF16 * 1e12) / (HBM_BW_GBPS * 1e9)   # FLOPs/byte crossover

def roofline_time_us(flops, bytes_):
    compute_us = flops / (PEAK_TFLOPS_BF16 * 1e12) * 1e6
    bandwidth_us = bytes_ / (HBM_BW_GBPS * 1e9) * 1e6
    return max(compute_us, bandwidth_us)

This is the same analytical model TraceLens’s built-in roofline analysis uses — here applied to shapes directly instead of to a trace. The roofline is an upper bound on achievable performance (a real kernel won’t reach it), but the ratios between layers and regimes are solid planning numbers that fall out of pure arithmetic.

Worked example#

The perf_model_without_trace.ipynb notebook works a full example end to end: every linear in a Llama-3-70B decoder layer plotted as arithmetic intensity, SDPA forward and backward across sequence length, and a per-token roofline rollup (predicted ms per token for prefill and decode, bf16, AMD Instinct™ MI300X) — all without a single trace.