AI Infra Interviews logo
CUDA, Triton & Kernel Engineering / 05
easyNewFireworksTogether AIOpenAI

Why fuse kernels, how much does it save, and what can fusion not fix?

A chain of five elementwise operations reads and writes the tensor five times when once would do. The byte arithmetic for an unfused chain against a fused one, the launch overhead that matters at small sizes, the three fusion shapes, and the operations where fusion changes nothing because the matmul was at the roof.

Updated Sep 2026 · Grounded in real AI infrastructure interview loops and written to a senior-engineer editorial bar, with every number worked and every diagram hand-built.

TL;DR: Elementwise and reduction operations are bandwidth-bound: their cost is the bytes they move, not the arithmetic. A chain like y = dropout(gelu(x + b)) as three kernels reads and writes the activation three times; fused into one kernel it reads once and writes once, which is a third of the traffic and a third of the time. At small tensor sizes there is a second saving: each kernel launch costs a few microseconds of CPU and GPU overhead, and a decode step that launches 400 small kernels spends more time launching than computing. Fusion has three common shapes: elementwise chains, a reduction with its consumers (softmax as one kernel, layernorm as one kernel), and a GEMM with its epilogue (bias, activation, residual add written in the GEMM's output stage). What fusion cannot do is make a compute-bound operation faster: a large GEMM at 70% of peak FLOPS is limited by the tensor cores, and fusing its epilogue saves the epilogue's traffic, not the GEMM's time.

How to approach it

Do the byte arithmetic for a concrete chain, unfused and fused. Add launch overhead and say when it dominates. Then the three fusion shapes with an example each, and how the tools do it (torch.compile, Triton, hand-written). Close with the limits: compute-bound ops, fusions that break parallelism, and register pressure.

A strong answer

A typical situation: a transformer MLP block in eager PyTorch runs the GEMM, then a bias add, then GELU, then dropout as four kernels; the profiler shows the GEMM at 60% of the block's time and the three elementwise kernels at 40% despite doing almost no arithmetic.

The arithmetic:

activation: batch 32 × seq 4,096 × hidden 16,384 (the MLP's intermediate) in bf16 = 32 × 4,096 × 16,384 × 2 B = 4.3 GB
unfused: bias add reads 4.3 + writes 4.3; GELU reads 4.3 + writes 4.3; dropout reads 4.3 + writes 4.3 (+ mask)
  → ~26 GB moved; at 3 TB/s ≈ 8.6 ms
fused elementwise: read 4.3, write 4.3 → 8.6 GB → 2.9 ms; 3× faster, and the arithmetic (a few FLOPs per element)
  is free either way: 32 × 4,096 × 16,384 × ~20 FLOPs ≈ 4e10 FLOPs ≈ 0.04 ms at 990 TFLOPS
fused into the GEMM epilogue: the GEMM writes its output once through the epilogue, which applies bias, GELU and
  dropout in registers before the store → the 8.6 GB of the elementwise pass disappears entirely; the GEMM itself
  (2 × 32 × 4,096 × 4,096 × 16,384 ≈ 1.8e13 FLOPs ≈ 25 ms at 70% of peak) is unchanged
sanity: unfused, the elementwise chain was 8.6 ms against a 25 ms GEMM (the scenario's 40%/60%); fused into the
        epilogue, the block is 25 ms, and the GEMM is all that is left to optimize.

Kernel Fusion has the mechanism; Memory-Bound vs Compute-Bound Kernels and Arithmetic Intensity by Operation are why elementwise ops sit on the bandwidth roof.

Launch overhead, the second saving:

per kernel launch: ~3 to 5 µs of CPU-side work and ~2 to 4 µs of GPU-side scheduling gap between dependent kernels
a decode step for a 7B model at batch 1: ~32 layers × ~12 kernels = ~400 launches → 1.5 to 2 ms of overhead against
  ~5 ms of actual work; fusing to ~4 kernels per layer or capturing the step in a CUDA graph removes most of it
a training step on a 4 GB activation: the same 400 launches are 2 ms against 500 ms of work; irrelevant
rule: launch overhead matters when kernels are shorter than ~10 µs each; fusion and CUDA graphs are the two fixes,
  and they compose

torch.compile and CUDA Graphs covers the graph capture side.

The three fusion shapes:

1. elementwise chains: any sequence of per-element ops (add, mul, activation, cast, dropout) → one kernel that loads
   each element once, applies the chain in registers, stores once; torch.compile's Inductor does this automatically
   by generating a Triton kernel per chain
2. reductions with their consumers: softmax = max-reduce, subtract-exp, sum-reduce, divide → one kernel per row
   holding the row in registers or shared memory (three passes over the row in on-chip memory, one pass over HBM);
   layernorm the same (mean, variance, normalize, scale, shift in one kernel); a fused softmax reads the logits once
   instead of four times
3. GEMM epilogues: bias, activation, residual add, dtype cast, even a row-wise scale for quantization, applied in the
   GEMM's output stage before the store; CUTLASS and cuBLASLt expose this; the savings are the whole elementwise pass
   plus the intermediate tensor's memory
plus the big one: attention. FlashAttention fuses QKᵀ, softmax and the PV product so the N × N score matrix never
   exists in HBM; that is fusion of a GEMM, a reduction and a GEMM, and it changed the memory cost of attention from
   quadratic to linear in sequence length

Triton Programming Model is how most of these are written today; FlashAttention Internals is the attention case; CUTLASS and Tensor Core Kernels is the epilogue mechanism.

What fusion cannot fix:

compute-bound operations: a large GEMM at 70% of peak is limited by tensor-core throughput; fusing anything into it
  saves the other thing's traffic, not the GEMM's time; the GEMM's remaining 30% is tiling, pipelining and layout
fusions that serialize: fusing a reduction across a dimension that the unfused version parallelized can cut
  occupancy; a fused layernorm over a 16k-wide row with one block per row needs enough rows to fill the GPU
register pressure: a long fused chain holds many live values per thread; past the register budget the compiler spills
  to local memory and the fused kernel loses to the unfused one; the fix is splitting the chain or reducing the
  per-thread tile
data-dependent control flow and graph breaks: a fusion needs a static chain; a Python-side branch or a host sync
  breaks the graph and the fuser starts over at the next segment
cross-kernel reuse the cache already gives: two kernels touching a 20 MB tensor that fits L2 may already hit L2 on
  the second pass; fusion's gain is smaller there than the HBM arithmetic suggests
sanity: the test for whether fusion will help is the roofline position of the ops being fused; bandwidth-bound and
        launch-bound ops gain, compute-bound ops do not.

Roofline Model is that test; Occupancy and Register Pressure is the spill limit.

FIVE ELEMENTWISE OPS ON ONE TENSOR unfused 5 reads + 5 writes 10 traversals fused 1 read + 1 write 2 traversals Fusion often works by keeping the intermediate in L2 rather than by saving arithmetic. It cannot fix a compute-bound matmul, and launch overhead matters only for small kernels.

The reversal condition: on a system where the elementwise ops are already fused by the compiler and the GEMMs dominate, the next win is not more fusion but precision (FP8 for the GEMMs) and tiling; the candidate who reaches for fusion when the profile shows 90% GEMM time has not read the profile. Nsight Compute's memory chart shows whether the fused kernel actually stopped touching HBM, which is the only proof that the fusion did what you intended.

What interviewers probe next

  • "How does torch.compile decide what to fuse?" Inductor groups consecutive pointwise and reduction ops with compatible shapes into one Triton kernel, up to a size limit, and keeps GEMMs and convolutions as library calls with fused epilogues where the backend supports them.
  • "Fused softmax on a 128k-wide row?" The row does not fit one block's registers; the kernel uses shared memory or two passes with an online formulation (the same trick FlashAttention uses); still one HBM pass.
  • "What is the memory saving beyond bandwidth?" The intermediate tensors do not exist, so peak memory drops; for the MLP example, 4.3 GB per intermediate at batch 32.
  • "Can fusion make numerics worse?" It can make them different: a fused kernel may accumulate in a different order or precision; keep fp32 accumulation inside fused reductions and test against the unfused reference.

Common mistakes

  • Fusing into a compute-bound GEMM and expecting the GEMM to speed up.
  • A fused chain so long it spills registers.
  • Counting FLOPs to estimate elementwise cost instead of bytes.
  • Forgetting launch overhead as the reason small-batch decode is slow.

Key takeaways

  • Elementwise ops cost bytes; N unfused ops move 2N passes of the tensor; fused, 2; a 3× gain for three ops.
  • Launch overhead (a few µs per kernel) dominates when kernels are under ~10 µs; fuse and use CUDA graphs.
  • Shapes: elementwise chains, reduction-plus-consumers, GEMM epilogues; attention is all three at once.
  • Fusion cannot speed up a compute-bound GEMM, and it fails when it spills registers or serializes parallelism.
That one was free — and so are 10 answers per topic without an account. Signing in doubles that to 20, opens the Plus lessons in the courses, and remembers which topics you keep getting wrong.no card · Google sign-in · nothing to cancel
HOW DID IT GO?
0
READING SIGNED OUT

Signing in doubles your free answers, from 10 to 20 per topic, and the site starts remembering you: mastery per topic, bookmarks, and a next-focus recommendation. Free, no card.

Sign in free

The concepts behind this question

Ranked by how closely each one overlaps this question's topic, so the first card is the thing to read if the answer above moved too fast.

Foundational
Kernels & Compilers
Kernel FusionAn elementwise or reduction kernel does a few FLOPs per byte and runs at HBM speed, so a chain of five of them costs five trips through HBM for work that needs one. Fusion collapses the chain into a single kernel that keeps intermediates in registers. It is the first lever for anything memory-bound, and knowing what it cannot fix (weight reads in decode, the GEMMs themselves) is what the interview is really testing.
Advanced
Kernels & Compilers🔒 Premium
torch.compile and CUDA Graphstorch.compile captures Python into a graph with Dynamo, fuses it into Triton kernels with Inductor, and can wrap the result in a CUDA graph so a whole forward pass is one launch. CUDA graphs are what make batch-1 decode fast in every serving engine, and graph breaks, recompiles and static-shape rules are what make both bite in production. Interviewers ask when compile helps, when it hurts, and how you would know.
Foundational
Kernels & Compilers
CUDA Programming ModelCUDA splits a program into a host that allocates, copies and enqueues work, and a device that runs thousands of identical threads organized as a grid of blocks. Getting the split right, and knowing that a launch returns before the kernel runs, decides whether your first live-coding kernel produces a correct number or a silent zero.
Foundational
🧩 GPU & Accelerator Architecture
Roofline ModelThe roofline plots a kernel's attainable throughput against its arithmetic intensity, FLOPs per byte moved from memory. Below the ridge point (peak FLOPS divided by memory bandwidth, about 295 on an H100 in bf16) a kernel is memory-bound and no amount of clever code reaches the peak; above it, compute is the limit. One picture explains why decode runs at under 1% of peak and why fusion and batching are the two levers that move it.
UP NEXT ON YOUR JOURNEY
FEDITOR'S NOTE

Scored on the bytes-moved arithmetic before and after fusion, on naming launch overhead as the second saving, on the fusion categories with an example each, and on the honest limit: fusion cannot speed up a compute-bound GEMM.

DISCUSSION · 0

No comments yet — be the first to share your approach.