SSY
Sai Sasank Y
← All writing

An Interactive Guide to Rooflines

If your kernel runs at 30% of peak FLOPs, the most important question is how much of the remaining 70% it can actually achieve. The roofline answers that, and tells you whether you should be optimizing the implementation or changing the algorithm.

All the numbers below are for an H100 SXM in BF16. The model itself is hardware-agnostic; locking to one chip just keeps the widgets simple.

The Roofline Model

Algorithms spend their time in two places:

  1. Computation, where the time is spent on arithmetic operations such as multiplying and adding floating point numbers. An H100 can do 1979 TFLOPs of BF16 work per second.
  2. Communication, where the time is spent moving data within the GPU (between the cores and HBM) or across GPUs over NVLink (900 GB/s). The HBM bandwidth of an H100 is 3.35 TB/s.
low arithmetic intensitymemory-boundHBMcomputehigh arithmetic intensitycompute-boundHBMcompute
Assume we have the same chip in both panels. (top) When each delivered byte triggers little compute, HBM pumps as fast as it can but the compute units idle between bytes. (bottom) When each delivered byte triggers long compute, the compute units stay saturated and HBM has nothing to do between fetches. Which side is the bottleneck is determined by the kernel.

Let TmathT_{math} be the time spent in computation and TcommsT_{comms} be the time spent in communication.

Tmath=Computation FLOPsAccelerator FLOPs/sT_{math} = \frac{\text{Computation FLOPs}}{\text{Accelerator FLOPs/s}} Tcomms=Communication GBNetwork/Memory Bandwidth GB/sT_{comms} = \frac{\text{Communication GB}}{\text{Network/Memory Bandwidth GB/s}}

We can lower-bound the runtime by assuming we perfectly overlap communication with computation:

Tlowerbound=max(Tmath,Tcomms)T_{lowerbound} = \max(T_{math}, T_{comms})

And upper-bound it by assuming zero overlap:

Tupperbound=Tmath+TcommsT_{upperbound} = T_{math} + T_{comms}

We optimize toward TlowerboundT_{lowerbound}, i.e., maximizing the overlap.

Assuming perfect overlap, we land in one of two regimes:

  1. Tcomms>TmathT_{comms} > T_{math}: the algorithm is memory bound. Some fraction of the chip's FLOPs/s are spent waiting on data movement.
  2. Tmath>TcommsT_{math} > T_{comms}: the algorithm is compute bound. All of the chip's FLOPs/s are doing useful arithmetic.

A proxy that tells us which regime applies without measuring time is the arithmetic intensity of the algorithm, FLOPs per byte of data moved:

Arithmetic Intensity=Computation FLOPsCommunication Bytes\text{Arithmetic Intensity} = \frac{\text{Computation FLOPs}}{\text{Communication Bytes}}

A short derivation shows the algorithm is compute-bound exactly when its arithmetic intensity exceeds a number that depends only on the hardware:

Tmath>Tcomms    Computation FLOPsAccelerator FLOPs/s>Communication GBNetwork/Memory Bandwidth GB/s    Computation FLOPsCommunication GB>Accelerator FLOPs/sNetwork/Memory Bandwidth GB/s    Arithmetic Intensity>Accelerator FLOPs/sNetwork/Memory Bandwidth GB/s    Arithmetic Intensity>Accelerator Intensity\begin{aligned} T_{math} &> T_{comms} \\ \iff\quad \frac{\text{Computation FLOPs}}{\text{Accelerator FLOPs/s}} &> \frac{\text{Communication GB}}{\text{Network/Memory Bandwidth GB/s}} \\ \iff\quad \frac{\text{Computation FLOPs}}{\text{Communication GB}} &> \frac{\text{Accelerator FLOPs/s}}{\text{Network/Memory Bandwidth GB/s}} \\ \iff\quad \text{Arithmetic Intensity} &> \frac{\text{Accelerator FLOPs/s}}{\text{Network/Memory Bandwidth GB/s}} \\ \iff\quad \text{Arithmetic Intensity} &> \text{Accelerator Intensity} \end{aligned}

Accelerator intensity is one number per chip-dtype pair. For an H100 in BF16, it is 1979×1012/3.35×10125901979\times 10^{12} / 3.35\times 10^{12} \approx 590 FLOPs/byte. Any kernel below 590 FLOPs/byte on an H100 cannot saturate the chip's FLOPs, no matter how cleverly written.

Anatomy of a roofline · H100 SXM, BF16
Two axes
step 1 / 5
10⁻²10⁻¹10⁰10¹10²10³10⁴10^1010^1210^1410^16arithmetic intensity (FLOPs / byte)attainable performance (FLOPs/s)
Arithmetic intensity on x (FLOPs per byte moved). Attainable performance on y (FLOPs/s). Both log-scaled.
peak = 1979 TF/s
bw = 3.35 TB/s
ridge = 591 F/B

Arithmetic Intensity of Dot Product

The dot product of two vectors a\mathbf{a} and b\mathbf{b} of length NN is:

ab=i=1Naibi\mathbf{a}\cdot\mathbf{b} = \sum_{i = 1}^{N}a_{i}\cdot b_{i}

Each vector in BF16 occupies 2N2N bytes, so reading both costs 4N4N bytes and writing the scalar result adds 2 more. The op does NN multiplications and N1N - 1 additions, 2N12N - 1 FLOPs total.

Arithmetic Intensity=2N14N+2\text{Arithmetic Intensity} = \frac{2N - 1}{4N + 2}

As NN grows, the intensity approaches 12\frac{1}{2}, half a FLOP per byte. The H100's hardware intensity is 590, so dot product is memory bound by three orders of magnitude. The most optimized dot-product kernel still cannot exceed about 1.7 TFLOPs out of the chip's 1979 TFLOPs, because the algorithm doesn't do enough math per byte to justify them.

Compare with a square matmul of size N×NN \times N. FLOPs grow as 2N32N^3 while bytes grow as 6N26N^2 (read AA, read BB, write CC, all BF16):

Arithmetic Intensity=2N36N2=N3\text{Arithmetic Intensity} = \frac{2N^3}{6N^2} = \frac{N}{3}

The intensity grows with NN. Drag the slider below to watch matmul climb the memory slope and cross the ridge into the compute-bound regime.

Dot product vs. matmul
Same multiply-and-add, different data organization. Slide N and watch matmul cross the ridge while dot stays stuck.
10^-110^010^110^210^310^410^1010^1210^1410^16arithmetic intensity (FLOPs / byte)attainable perf (FLOPs/s)ridge 591memory-boundcompute-bounddot0.50 F/Bmatmul · N=6421.3 F/B → 71 TF/s
Matmul size N
N = 64 (memory-bound)
H100 SXM · BF16
peak = 1979 TF/s
bw = 3.35 TB/s
ridge = 591 F/B
matmul crosses at
N ≈ 1773
Time breakdown (assuming perfect overlap)
dot (1M)
T_math
1.06 ns
T_comms
1.25 µs
comms
1.2k× lead
matmul (N=64)
T_math
0.26 ns
T_comms
7.34 ns
comms
28× lead
Matmul is still memory-bound at N=64: T_comms (7.34 ns) dominates T_math (0.26 ns). Crank N higher to cross the ridge.

The dot product stays on the memory slope no matter how large NN gets. The matmul climbs because data reuse is baked into the algorithm: each element of AA participates in NN multiplications.

Kernels in the wild

The same exercise applies to any kernel: count FLOPs, count bytes moved, divide. Five common ones from a forward pass, all in BF16:

  • Elementwise activation (GELU, ReLU): a few FLOPs per element against 4 bytes (read input + write output, bf16).
  • LayerNorm (row length dd): ~5d5d FLOPs against 8d8d bytes (input, output, scale, bias).
  • GEMV (y=Axy = Ax, AA is n×nn \times n): 2n22n^2 FLOPs against ~2n22n^2 bytes since the matrix dominates.
  • GEMM (square N×NN \times N): as derived above. Crosses the H100 ridge around N1770N \approx 1770.
  • Flash Attention (head dim 128, seq length ss): ~4s2d4s^2 d FLOPs against ~8sd8sd bytes (no materialized score matrix). Crosses the ridge around s1180s \approx 1180.
10⁻¹10⁰10¹10²10³10⁴10^1010^1210^1410^16arithmetic intensity (FLOPs / byte)attainable performance (FLOPs/s)ridge · 591 F/Bmemory-boundcompute-boundLayerNormGEMVelementwise (GELU)GEMM (N=64)Attention (s=128)GEMM (N=512)Attention (s=2048)GEMM (N=4096)
Most everyday neural-network ops sit well to the left of the ridge. Only large GEMMs and long-context attention earn the chip’s peak FLOPs; the rest are paying for memory bandwidth.

Most everyday ops sit well left of the ridge. That's why kernel fusion matters in practice: folding a chain of memory-bound ops into one kernel cuts the round trips through HBM, even when each individual op stays memory bound.

Try it on your own kernel

Arithmetic intensity calculator · H100 SXM, BF16
Pick a kernel (or enter your own FLOPs and bytes) and watch where it lands on the roofline.
10⁻¹10⁰10¹10²10³10⁴10^1010^1210^1410^16arithmetic intensity (FLOPs / byte)attainable performance (FLOPs/s)ridge · 591 F/BGEMM (square matmul)AI = 85.3 · 285.9 TF/s
Kernel
all matrices 256×256
Size N
N = 256
FLOPs = 33.55M
bytes = 393.22K
AI = 85.3 F/B
attainable = 285.9 TF/s
memory-bound· 14.4% of peak

The widget has presets for the kernels above plus a custom mode where you can plug in your own FLOPs and bytes. The readout on the right tells you which regime the kernel is in and what fraction of peak it can attain.

A kernel that lands far below the ridge cannot be saved by implementation work alone; the only way up is to raise the arithmetic intensity itself through fusion, blocking, recomputation, or mixed precision. A kernel that lands near the ridge but attains only a fraction of the attainable performance is implementation limited, and the things to look at are bandwidth utilization, occupancy, and memory access patterns.

Drawing the roofline from real silicon

All numbers above are NVIDIA's datasheet figures. Real hardware achieves some fraction of those, and the gap matters before you trust the roofline as a target. In Machine Baseline for CPU Performance Engineering on an M4 Pro, the first entry of a separate series on CPU performance engineering, I do this exercise empirically by microbenchmarking the peak FP32 throughput and DRAM bandwidth of a single P-core. The ridge there sits at ~1 FLOP/byte, three orders of magnitude below the H100's. On that machine, any kernel above 1 FLOP/byte is compute bound.


Reference: Roofline analysis (JAX scaling book)