Kernel Fusion and the Memory Wall
Toy chain: 6.0 tensor transfers across three kernels versus 2.0 across one fused kernel. Counts include input load and final store; equal tensor sizes are assumed. Animation timing is schematic.
Scroll horizontally to inspect the diagram.
Consider three operations: a matrix multiply, a bias addition, and ReLU. If each launches a separate kernel, intermediate results can be written to GPU memory and then read back by the next operation. A fused implementation can keep some of those values close to the arithmetic units instead.
Fusion can save both intermediate memory transfers and kernel-launch overhead. Its benefit depends on the operation’s shape and bottleneck.
Compute needs a supply of bytes
A GPU has several storage levels. HBM holds large tensors; caches retain some recently used data; shared memory and registers support work within a streaming multiprocessor. Registers have finite capacity and access costs, and shared memory is explicitly managed rather than simply another automatic cache.
Where a GPU kernel keeps its values
- HBM / device memory
- Large off-chip store for weights, activations, and KV cache. Global loads and stores can move data through the cache hierarchy.
- L2 cache
- Shared on-chip cache. Reusing a recently accessed address may avoid another HBM transfer, but residency is limited.
- Shared memory
- On-chip storage explicitly managed by cooperating threads in a block. Tiles compete with other blocks for each SM's capacity.
- Registers
- Thread-local values used by arithmetic instructions. Too many live values can reduce occupancy or spill into local memory backed by device memory.
Shared memory and registers are distinct resources, not nested boxes. Fusion tries to retain intermediates on chip, but register pressure, shared-memory limits, and synchronization can constrain the benefit.
Arithmetic intensity is the workload's arithmetic operations divided by the bytes transferred across the memory boundary being studied. The roofline compares that intensity with peak compute and bandwidth. A small BF16 matrix-vector product may perform roughly two FLOPs per two-byte weight read; a matrix product over many token positions reuses each weight more.
Idealized ceilings and workload intensities, not measured throughput. Kernel overheads and other memory traffic lower achieved performance.
decode (batch 1)
Arithmetic intensity: 1 FLOPs/byte.
Idealized ceiling: 2.0 TFLOP/s (0.6% of peak).
Fusion can remove bytes or launch overhead; register pressure and occupancy can offset those savings.
Fusing bias and activation into a matrix-multiply epilogue avoids writing and rereading the intermediate result. The original inputs and weights must still be read, and the final output written.
y = ReLU(xW + b)
- 1. Matrix multiplication: xW
↓ Write the intermediate to global memory; the next kernel reads it.
- 2. Add bias: + b
↓ Write the intermediate to global memory; the next kernel reads it.
- 3. Apply ReLU: max(0, ·)
Write the final output to global memory.
Intermediate full-tensor writes: 2; reads: 2.
This counts only intermediate traffic. Both versions still read inputs and weights and write the result; bias reads also remain. Whether an epilogue can be fused efficiently depends on the operation, layout, and kernel. Arbitrary attention and projection chains cannot be assumed to fuse this way.
Softmax needs a global row statistic
For a row of scores , stable softmax computes
An unfused implementation might read the row for its maximum, read it again to write exponentials, read those exponentials for their sum, then read them again to write normalized output. Ignoring the small reduction vectors, that is four reads and two writes of row-sized arrays.
A fused kernel can read and write the row once if it can retain the needed values on chip. If a row is too large, the algorithm needs tiling or additional passes. Normalizing each tile independently would be wrong because the denominator must include the whole row.
Row-wise softmax of an N × N matrix: global-memory passes
- 1. Find row maximum
Read S and reduce each row. Store one maximum per row.
1 full read + 0 full write
- 2. Exponentiate S − max
Read S again and write the full intermediate E.
1 full read + 1 full write
- 3. Sum exponentials
Read E and reduce each row. Store one sum per row.
1 full read + 0 full write
- 4. Normalize
Read E again and write O = E / row_sum.
1 full read + 1 full write
6 × N² elements = 12 × N² bytes at 16 bits
2 × N² elements = 4 × N² bytes at 16 bits
The fused case reads each row once, retains it on chip for max, exp, sum, and division, then writes once. This requires the row to fit the kernel’s on-chip strategy. Counts omit O(N) row statistics and cache effects. They are traffic accounting, not runtime or FLOP measurements.
FlashAttention: avoid the full score matrix
Dense attention computes . Materializing the score matrix for one head needs elements. At FP16, means 128 MiB for that matrix alone.
FlashAttention works on tiles and maintains online softmax statistics. It avoids storing the complete score and probability matrices in HBM. Its auxiliary memory is linear in sequence length, but its total HBM traffic is not universally linear: keys and values can be loaded repeatedly for different query tiles. The original paper analyzes an IO bound proportional to over its stated SRAM-capacity regime. FlashAttention paper.
Resident tensor sizes, one head, d = 128, FP16
FlashAttention avoids storing the full N × N matrix in HBM. Its tile buffers and row statistics require additional space. These bars show storage, not total memory traffic: tiled attention may read K and V more than once.
For one query tile, retain a running maximum , normalization sum , and unnormalized output numerator . When the next score tile has maximum , use and rescale the old accumulators by :
After all key tiles, return . The rescaling makes earlier and later tiles share one softmax denominator.
Scroll horizontally to inspect the diagram.
Query and key/value tiles contribute to one running row normalization. Tile-local probabilities cannot be normalized independently and then simply concatenated.
FlashAttention computes the same attention function, subject to floating-point differences from the order and precision of its operations. Output comparisons therefore need an appropriate numerical tolerance.
A small executable reference
This CPU example demonstrates the online reduction for one unmasked query. It is a correctness sketch, not a GPU implementation or a speed benchmark.
import math
import numpy as np
rng = np.random.default_rng(7)
q = rng.normal(size=4)
k = rng.normal(size=(11, 4))
v = rng.normal(size=(11, 3))
scores = k @ q / math.sqrt(q.size)
prob = np.exp(scores - scores.max())
prob /= prob.sum()
reference = prob @ v
maximum = -np.inf
normalizer = 0.0
numerator = np.zeros(v.shape[1])
for start in range(0, len(k), 3):
tile_scores = scores[start:start + 3]
new_maximum = max(maximum, tile_scores.max())
correction = np.exp(maximum - new_maximum)
tile_exp = np.exp(tile_scores - new_maximum)
normalizer = correction * normalizer + tile_exp.sum()
numerator = correction * numerator + tile_exp @ v[start:start + 3]
maximum = new_maximum
result = numerator / normalizer
np.testing.assert_allclose(result, reference, rtol=1e-12, atol=1e-12)
print(result)A causal kernel must also apply the correct mask, including partially masked tiles, and safely handle rows with no valid keys. Production implementations add parallel work partitioning, precision choices, and hardware-specific data movement.
FlashAttention-2 improves work partitioning and reduces non-matmul overhead. FlashAttention-3 exploits Hopper's asynchronous execution and low-precision facilities. FlashAttention-2, FlashAttention-3.
The attention operation ends with the weighted value sum; the transformer's learned output projection is a separate operation unless an implementation explicitly combines it. Likewise, an entire gated MLP is not necessarily one kernel. Inspect the actual execution trace before deciding which intermediate tensors were eliminated.
Next, PagedAttention addresses a different memory problem: where growing per-request KV data lives.