FlashAttention and online softmax
Before this lesson: the roofline model (lesson 2)
and tiling and fusion (lesson 3). Attention with queries,
keys and values (deep learning 9; the matrix form is in
linear algebra 15). The log-sum-exp trick
(subtract the max before exp; deep learning 2).
Time: about 80 minutes, plus the notebook.
Why this matters #
Attention is the only transformer operation whose cost grows with , where is the
sequence length. At tokens, one head's score matrix in bf16 is
bytes MiB, and there is one per head, per layer and per sequence.
FlashAttention (Dao et al. 2022) computes exact attention tile by tile and never stores that
matrix. PyTorch's scaled_dot_product_attention uses FlashAttention-2 kernels on recent NVIDIA
GPUs, and LLM inference engines build on the same kernels.
Its key formula, the merge of two partial softmax results, appears again and again: in split-KV decoding (this lesson and lesson 12), in ring attention across GPUs (lesson 10), and in any softmax over a long axis. For science, memory makes long token sequences trainable: global weather fields cut into patches, long seismic records, genomes.
1. Predict first #
2. The picture #
One query, one pass #
Start with the simplest case: one query, and a number (not a vector) as the value of each key. The query has a score for each key . We want the softmax-weighted average of the values:
Why subtract the max. In floating point, overflows quickly: , the largest fp32 number. So code first subtracts . Every exponent is then , every lies in , and nothing overflows. The factor appears in the numerator and in the denominator, so it cancels:
The problem. is known only after the last score. So the simple "safe softmax" reads the scores three times: pass 1 finds , pass 2 sums , pass 3 forms the weighted sum. On a GPU every pass over a long row is a pass over HBM.
The online idea. Read the scores in blocks, and keep three numbers:
- : the largest score seen so far;
- : the sum , relative to the current ;
- : the sum , also relative to the current .
When a new block raises the max from to , the old sums are measured against the wrong max. Fix them with one multiplication. Each old term times is , exactly the term we want. So multiply and by the rescale factor , then add the new block's terms. At the end, .
Small numbers. Scores arrive first: and . Then the score arrives: , the factor is , and the rescaled old sum is . Adding the new term gives , which is over all three scores, as if we had known the max from the start.
Press Next block and watch the yellow state. The top row shows the scores; the current block is magenta. The bars below are the weights of the scores seen so far, relative to the current max. When a block raises the max, all old bars shrink by the same rescale factor. At the end, equals the exact softmax average (difference about ). Try one huge score: the naive overflows, the online version does not.
The running state of one query has max . A new block arrives whose largest score is . By what factor must you multiply the old running sum (and the old )?
The old terms are ; we need . Multiply by . The factor is always , so it can never overflow. and are both sums relative to , so both must be rescaled by the same factor; rescaling only one of them gives a wrong output.
The GPU picture: keep the tile on chip #
Now use the real sizes. are matrices: tokens, head dimension . The output is with , the softmax taken row by row. Recall from lesson 1 that HBM is large and slow (80 GB, 3.35 TB/s on an H100 SXM), and that each SM has a small, fast on-chip memory (up to 228 KB of shared memory plus a 256 KB register file).
Standard attention runs three kernels, and each one goes through HBM:
| Step | Reads from HBM | Writes to HBM |
|---|---|---|
| () | ||
| () | ||
FlashAttention fuses the three steps into one kernel. A thread block owns query rows. It loads their tile once into on-chip memory and keeps it there. Then it streams tiles of keys and values through on-chip memory. For each tile it computes the score tile, updates the online state of each of its rows, and throws the score tile away. At the end it divides by , writes its rows of , and writes one extra number per row: the logsumexp , which the backward pass needs.
Press Play (or Step) and compare the two modes. The grid is the score matrix in tiles. In FlashAttention mode, one thread block keeps its yellow tile on chip while the magenta tile moves along the row; the score tiles it computes (blue) never go to HBM. In standard mode, the whole matrix is written to and read from HBM (orange). Compare the HBM counters, then turn on causal to see the skipped tiles.
3. Definitions and main results #
The online softmax theorem #
Theorem (online softmax). Take one query row. Its scores arrive in blocks , with value rows . Start with , , . For block :
Then exactly, and .
Proof (by induction on the blocks).
- Claim after block : is the max of all scores seen so far, and .
- Base case : is the max of block 1. The factor removes the empty start values, so and are the block-1 sums relative to .
- Step : the max of the old max and the new block's max is the max so far. By the claim for , . Multiplying by turns each old term into . Adding the new block's terms gives the sum over all seen scores, relative to . The same argument works for , term by term, because each term of is a term of times a fixed vector .
- End: after block every score has been seen and . So . The factor cancels, and this is , with no approximation.
- No overflow: and , so every exponent is and every exponential lies in .
- Logsumexp: .
Why the rescale is exact. The factor depends only on the two maxima, not on . So it is the same for every old term, and one multiplication fixes the whole sum. This is the only property the proof needs (see the quiz at the end of this section for a variant where it fails).
Keep unnormalised. FlashAttention-2 keeps as a sum and divides by once, at the end. The first FlashAttention kept a normalised output and rescaled it at every block, which costs extra non-matmul FLOPs.
The tiled forward pass. For one thread block with query rows ():
- Load into on-chip memory. Set , , for each row.
- For each key tile : load ( each). Compute the score tile () on chip.
- Update each row with the theorem: new max, rescale factor, and (, a small matmul).
- After the last tile: write and to HBM.
Steps 2 and 3 are two small matmuls ( and ) plus elementwise work, so the tensor cores do most of the arithmetic.
Merging two partial results #
Split the keys of one query into two disjoint sets and . Suppose a thread block (or a GPU) has computed attention over alone: the normalised output and the logsumexp
and the same for . The merge formula gives attention over all keys:
Proof.
- , because and are disjoint. Take the log: is the logsumexp over all keys.
- . The same holds for .
- Add the two: .
The two weights and are positive and add up to 1: is the share of the total softmax mass that lies in . So the merge is a weighted average, not a plain average and not a sum.
Compute it stably. Write
(NumPy: np.logaddexp). An empty or fully masked set has and weight 0. If both
sets are empty, return and . Never compute : it is
nan, and one nan spreads through the whole output.
Drag the split point to cut eight keys into a set A and a set B. Each set computes its own softmax average and its ; the merge weights them by . The merged output equals full attention for every split. The magenta line is the common mistake, the plain average of and . Try mask B to see a set with .
Two thread blocks return and for the same query (scalar values). What is the merged output ?
, so the weights are and : set holds three quarters of the softmax mass. . A plain average ignores how much mass each set has; a sum double-counts the normalisation.
HBM traffic #
Theorem (Dao et al. 2022, Theorem 2). Let . Standard attention needs HBM accesses. FlashAttention needs HBM accesses.
Standard attention, counted. Per head, from the table in section 2:
- : read and (), write ();
- : read , write ();
- : read and (), write ().
Total: . A mask or dropout adds more passes over matrices.
FlashAttention, counted. Use the FlashAttention-2 loop order: the outer loop runs over the query tiles. Each query tile reads its rows once, reads all of and ( elements), and writes its rows once. Over all tiles:
plus for the lse values, which we ignore. The query tile is the reuse factor: each element read from HBM is used by query rows. This is the same idea as the tile size of the tiled matmul in lesson 3.
How large can be? The tiles must fit on chip. Up to a constant, they are matrices, so the largest that fits is a constant times . Put into the count: . That is where comes from.
The on-chip budget, and the original loop order
On chip, one thread block holds and ( each), and ( each), the score tile () and ( each). Choose (at least 1) and . Then, for and :
- and take elements;
- and take ;
- the score tile takes ;
- and take .
The sum is at most , so everything fits. This choice is , and the traffic is . The term is the smaller one, because gives . Real kernels choose the tile sizes by measurement: FlashAttention-2 takes and from , depending on and the shared memory of the GPU.
The first FlashAttention paper loops the other way: the outer loop runs over tiles of size , the inner loop over query tiles of size . Then and are read once, but , , and are read and written once per tile, that is times. Each pass moves elements, so the total is again .
Dao et al. also prove a lower bound (their Proposition 3): no exact attention algorithm needs only HBM accesses for all in . So, up to constants, the tiled order is optimal.
Compare the two. For the ratio standard / FlashAttention is about . With : 4 for , and 2 for .
The roofline view (lesson 2). Both versions do the same forward FLOPs: and cost each, in total. In bf16 (2 bytes per element) the arithmetic intensity is about
- standard: FLOP/byte (32 for );
- FlashAttention: FLOP/byte (128 for ).
The H100 ridge point is FLOP/byte, so standard attention is deep in the memory-bound region. The count for FlashAttention is pessimistic: thread blocks that run at the same time usually work on the same head, so many re-reads hit the 50 MB L2 cache instead of HBM. The real HBM traffic is lower, and well-tuned kernels are close to compute-bound.
Move the sequence length and watch the two lines. The chart shows the minimum time of one head's forward pass on the roofline: standard and FlashAttention; the blue line is the compute time . Standard attention stays far above the compute line. Change to see the reuse factor at work, and turn on L2 serves re-reads to see the ideal case. The readout also shows the memory each version keeps for the backward pass.
Backward pass: recompute instead of store #
The backward pass needs . Standard autograd stores it: numbers per head. FlashAttention stores only and the per-row , and recomputes tile by tile. With the gradient of the loss with respect to , for each tile:
- recompute , then (no max and no sum needed: the lse already contains both, since );
- and ;
- , with (one number per row). This is the softmax backward (linear algebra 16); the trick avoids a sum over the whole row;
- and .
FLOPs. The standard backward has four matmuls (, , , ), each, so . FlashAttention adds the recomputation of : , which is 25% more.
Time. It is still much faster, because attention is memory-bound and FlashAttention removes most of the memory traffic. Dao et al. (2022, Figure 2) measured forward plus backward for GPT-2 medium (sequence length 1024, head dimension 64, 16 heads, batch 64) on an A100:
| Standard | FlashAttention | |
|---|---|---|
| GFLOPs | 66.6 | 75.2 |
| HBM reads and writes | 40.3 GB | 4.4 GB |
| run time | 41.7 ms | 7.3 ms |
13% more FLOPs, 9 times fewer bytes, 5.7 times faster: the run time follows the bytes.
Which statement about the FlashAttention backward pass is correct?
FlashAttention keeps and ( extra memory) and rebuilds . This adds FLOPs (recomputing ), but the standard backward reads and writes matrices in HBM, and that traffic dominates its run time. The gradients are exact up to rounding.
What FlashAttention-2 changed #
FlashAttention-2 (Dao 2023) keeps the algorithm and changes how the work is split:
- Fewer non-matmul FLOPs. It keeps unnormalised, divides once at the end, and stores only the lse. This matters because on an A100 a bf16 matmul runs at 312 TFLOP/s but non-matmul fp32 work at 19.5 TFLOP/s: each non-matmul FLOP costs 16 times more.
- Parallelism over the sequence. FlashAttention-1 used one thread block per (sequence, head). With long sequences the batch is small, and batch × heads can be smaller than the 108 SMs of an A100. FlashAttention-2 gives each query tile its own thread block, so long sequences also fill the GPU.
- Work split across warps. Inside a thread block, FlashAttention-1 split and across the 4 warps, so the warps had to add their partial outputs through shared memory. FlashAttention-2 splits the query rows across warps. All warps read the same tiles and need no communication.
Result: about 2 times faster than FlashAttention-1, at 50–73% of the A100 peak (up to 73% in the forward pass, up to 63% in the backward pass).
Which attention variant can not be computed by the one-pass online softmax as it is, with a fixed state per row?
The rescale works because changing the max multiplies every old term by the same factor . If changes, an old term must become : the factor depends on , so the old sum cannot be fixed with one multiplication. A mask and a bias only change scores inside a tile, and dropout is applied tile by tile with a saved random seed; FlashAttention supports all three.
Split-KV for decoding (flash-decoding) #
During decoding (lesson 12) each sequence has one new query per step. Parallelism over query tiles then gives only batch × heads thread blocks. With batch 1 and 32 heads, that is 32 blocks for the 132 SMs of an H100: at most 32 SMs work. Decode attention is memory-bound (it reads the whole KV cache, lesson 2), and one SM cannot pull a large share of the HBM bandwidth. So idle SMs mean wasted bandwidth.
Split-KV cuts each sequence's KV cache into chunks and gives each chunk its own thread block. Each block returns a partial for its chunk. A small second kernel merges the partials of each row with the merge formula. The extra work is tiny ( vectors per row), and now batch × heads × blocks fill the GPU. Dao, Haziza, Massa and Sizov (2023) report up to 8 times faster decoding for very long sequences.
Raise the number of splits and watch the SMs fill. Each square is one SM: orange SMs stream the KV cache from HBM, violet SMs are idle. The time model assumes that the attention time scales with the number of busy SMs. Notice that a large batch fills the GPU without splitting, and that a short KV cache gains little because the extra merge kernel costs a few microseconds.
Answers to section 1. (1) Yes. Keep the running max , the running sum and the running weighted sum ; rescale both sums by when the max grows, and output at the end. Three numbers per query (with vector values: , and a -vector). (2) Attention is memory-bound, and FlashAttention removes the traffic; the run time follows bytes, not FLOPs. It stores and one logsumexp per query row.
Causal masks and FlashAttention-3
Causal masking. With a causal mask, query sees only keys . Cut into tiles with (square tiles, ). Tiles fully above the diagonal contain only masked scores: the kernel skips them and does not even load their . Tiles on the diagonal are computed with the mask; tiles below it need no mask. So of the tiles are computed, about half for large (for and : 8,256 of 16,384 tiles). Causal attention therefore costs about half the FLOPs and half the traffic.
FlashAttention-3 (Shah et al. 2024) targets Hopper GPUs. FlashAttention-2 reaches only about 35%
of the H100 peak. FlashAttention-3 uses the asynchronous Hopper hardware (the TMA copy engine and
warp specialization, so that loads, matmuls and the softmax overlap) and fp8 tensor cores. It reports
up to 740 TFLOP/s in fp16 (75% of the H100 peak) and close to 1.2 PFLOP/s in fp8. One reason the
overlap matters: an H100 SXM5 does 989 TFLOP/s of fp16 matmul but only 3.9 TFLOP/s of special
functions such as the exponential, so the exp of the softmax is not free.
4. Worked example #
(a) Online softmax by hand #
Scores , so : whole numbers you can follow. Values . Two blocks of two scores.
Block 1, scores :
- . The factor clears the start values.
- .
- .
Block 2, scores :
- . The rescale factor is .
- .
- .
| step | factor | |||
|---|---|---|---|---|
| block 1: | 0 (start) | 1.5 | 2 | |
| block 2: | 1.875 | 5.5 |
- Output: .
- Direct check: . ✓
- Logsumexp: , and . ✓
(b) The same result by a merge #
Treat the two blocks as two key sets, computed separately.
- Set (keys 1, 2): and .
- Set (keys 3, 4): and .
- Merge: , weights and .
- . ✓
The plain average would be wrong.
(c) At scale #
tokens (for example a 2-D field cut into patches), , 16 heads, bf16, one H100 SXM (989 TFLOP/s dense bf16, 3.35 TB/s).
- Memory. is B MiB per head, 8 GiB for 16 heads, for each layer and sequence. FlashAttention keeps B MiB of fp32 lse values.
- Compute per head. FLOP, so at least µs.
- Standard traffic. elements GB, so at least µs. Memory-bound: 9 times the compute time.
- FlashAttention traffic, , no cache. elements MB, so at least 162 µs. That is 4 times less ().
- With L2. and of one head are B MiB, so they fit in the 50 MB L2. If L2 serves every re-read, the HBM traffic falls to about elements (read , write ): 8.4 MB, 2.5 µs. The kernel is then compute-bound.
Repeat step 4 of (c) with head dimension instead of 64 (same , , no cache). By about what factor does FlashAttention reduce the HBM traffic compared with standard attention?
For the ratio is . Numbers: standard elements, FlashAttention . A larger head dimension means each tile is larger, so streaming and once per query tile costs more. The traffic still falls, and the memory still disappears.
5. In code #
NumPy checks the hand computation and a full tiled forward pass, with and without a causal mask. The notebook has these cells and the exercises.
import numpy as np
from scipy.special import logsumexp
# (a) the online softmax of the worked example, two blocks of two
x, v = np.log([2., 1., 8., 4.]), np.array([1., 2., 3., 4.])
m, l, o = -np.inf, 0.0, 0.0
for blk in (slice(0, 2), slice(2, 4)):
m_new = max(m, x[blk].max())
f = np.exp(m - m_new) # rescale factor (0 at the start)
l = f * l + np.exp(x[blk] - m_new).sum()
o = f * o + (np.exp(x[blk] - m_new) * v[blk]).sum()
m = m_new
print(round(f, 4), l, o) # 0.0 1.5 2.0, then 0.25 1.875 5.5
print(o / l, 44 / 15, np.isclose(m + np.log(l), np.log(15))) # 2.9333... 2.9333... True
# (b) a tiled forward pass: Q tile on chip, K/V tiles streamed, O(N) extra memory
def flash_forward(Q, K, V, Br=16, Bc=16, causal=False):
N, d = Q.shape
O, lse = np.zeros_like(Q), np.empty(N)
for i in range(0, N, Br):
q = Q[i:i + Br]; r = len(q)
m, l, acc = np.full(r, -np.inf), np.zeros(r), np.zeros((r, d))
for j in range(0, N, Bc):
if causal and j > i + r - 1:
break # tile above the diagonal: skip
s = q @ K[j:j + Bc].T / np.sqrt(d)
if causal:
rows, cols = np.arange(i, i + r)[:, None], np.arange(j, j + s.shape[1])
s = np.where(cols <= rows, s, -np.inf)
m_new = np.maximum(m, s.max(axis=1))
p, f = np.exp(s - m_new[:, None]), np.exp(m - m_new)
l = f * l + p.sum(axis=1)
acc = f[:, None] * acc + p @ V[j:j + Bc]
m = m_new
O[i:i + r], lse[i:i + r] = acc / l[:, None], m + np.log(l)
return O, lse
def naive(Q, K, V, causal=False):
S = Q @ K.T / np.sqrt(Q.shape[1]) # the full N x N matrix
if causal:
S = np.where(np.tri(len(Q), dtype=bool), S, -np.inf)
lse = logsumexp(S, axis=1)
return np.exp(S - lse[:, None]) @ V, lse
rng = np.random.default_rng(0)
Q, K, V = (rng.normal(size=(100, 32)) for _ in range(3)) # N = 100 is not a multiple of 16
for causal in (False, True):
(O, lse), (O2, lse2) = flash_forward(Q, K, V, causal=causal), naive(Q, K, V, causal)
print(causal, np.abs(O - O2).max() < 1e-12, np.abs(lse - lse2).max() < 1e-12)
# False True True
# True True True
PyTorch has a fused attention kernel. A short check (CPU, float64) shows the two facts you rely on: the result is exact, and autograd keeps much less memory for the backward pass.
import math
import torch
import torch.nn.functional as F
torch.manual_seed(0)
N, d = 1000, 64
# shapes (batch, heads, N, d)
q, k, v = (torch.randn(1, 1, N, d, dtype=torch.float64) for _ in range(3))
O_std = torch.softmax(q @ k.transpose(-1, -2) / math.sqrt(d), dim=-1) @ v # builds S, P
print(torch.allclose(F.scaled_dot_product_attention(q, k, v), O_std)) # True
def saved_bytes(fn, *args):
"""Bytes of the distinct tensors autograd keeps for the backward pass."""
seen = {}
def pack(t):
seen[t.untyped_storage().data_ptr()] = t.untyped_storage().nbytes()
return t
with torch.autograd.graph.saved_tensors_hooks(pack, lambda t: t):
fn(*args)
return sum(seen.values())
x = [t.float().requires_grad_() for t in (q, k, v)] # fp32: 4 bytes per element
naive_t = lambda q, k, v: torch.softmax(q @ k.transpose(-1, -2) / math.sqrt(d), dim=-1) @ v
print(saved_bytes(naive_t, *x), saved_bytes(F.scaled_dot_product_attention, *x))
# 4768000 1028000
The naive version keeps (768,000 bytes) and the matrix (4,000,000 bytes). The fused kernel keeps , (256,000 bytes) and the 1,000 lse values (4,000 bytes). The gap grows as . (Checked with PyTorch 2.x on a CPU; which kernel PyTorch picks depends on the device and the inputs.)
6. Where it shows up #
- Training.
torch.nn.functional.scaled_dot_product_attentiondispatches to FlashAttention-2 kernels on recent NVIDIA GPUs (since PyTorch 2.2). Long-context training uses causal and sliding-window masks, both with tile skipping. The activation memory of attention drops from to (lesson 6). - Inference. Prefill uses the same kernel. Decoding uses split-KV and paged KV caches (lesson 12).
- Across GPUs. Ring attention (lesson 10) sends blocks around a ring of GPUs; each GPU merges the partial results with exactly the merge formula of this lesson.
- ML for science. A global 0.25° grid has points per level. Even after patching, weather and climate transformers see tens of thousands of tokens; memory makes this trainable. The same holds for long seismic waveforms and genome sequences.
- Beyond attention. Any softmax over a long axis can be streamed this way, for example the cross-entropy loss over a large vocabulary: process the vocabulary in chunks and keep .
7. Common mistakes #
- Rescaling but not when the max grows. Both sums are relative to ; both need the factor .
- Normalising each tile and adding (or averaging) the tiles. Partial results need the weights ; a plain sum or average is wrong.
nanfrom fully masked rows or tiles. isnan. Guard the case (and in a merge).- "FlashAttention approximates attention." It is exact. Only the order of the floating-point operations changes, so results agree up to rounding.
- "FlashAttention reduces FLOPs." The forward FLOPs are the same, and the backward does more. It reduces memory traffic and memory use.
- Keeping , , or in bf16. Keep them in fp32. bf16 has only about 3 significant decimal digits (lesson 5), and the sums collect many terms.
- Expecting the training speed-up at decode. With one query there is no matrix to avoid; decoding needs split-KV to use all SMs.
8. Check yourself #
- The running state of one query is , , (scalar values). A block with
scores and values arrives. Give the new , and .
Answer
and the factor is . . . The output so far is . - Merge and .
Answer
. The weights are and , so . - . What does FlashAttention store per head for the backward pass, and how does it
compare with storing in bf16?
Answer
(needed anyway, as the output) and fp32 lse values: B KiB. would be B GiB, which is 16,384 times more. - With and , the no-cache traffic ratio standard / FlashAttention is only 2.
Give two reasons why FlashAttention still wins clearly.
Answer
(1) The L2 cache serves many re-reads, so the real HBM traffic of FlashAttention is far below the no-cache count. (2) The matrices disappear: memory is , so longer sequences and larger batches fit. Also, masks and dropout add no extra passes, and one kernel replaces several. - Decoding on an H100 (132 SMs): batch 4, 16 heads, multi-head attention. How many thread blocks run
without split-KV, and with the KV cache split into 8 chunks?
Answer
Without split: blocks, so at most 64 of 132 SMs are busy. With 8 chunks: blocks, enough to fill all SMs (about 4 waves). A small merge kernel then combines 8 partials per row. - Why does the backward pass not need and separately?
Answer
. One number per row is enough, and every exponent is , so nothing overflows.
Write the online softmax update for when a new block arrives.
, then . The same rescale applies to .
Why is multiplying the old sums by exact?
Each old term times equals . The factor is the same for all old terms, so one multiplication fixes the whole sum.
Why does the safe softmax subtract the max?
So every exponent is and cannot overflow. The factor cancels between numerator and denominator, so the result does not change.
Give the formula that merges and .
and .
Merge with .
, weights and , so .
HBM traffic of standard attention and of FlashAttention, per head?
Standard: elements. FlashAttention: .
What does FlashAttention store for the backward pass, and what does it recompute?
It stores and one logsumexp per query row (). It recomputes and tile by tile.
Why is the FlashAttention backward faster despite 25% more matmul FLOPs?
Standard attention is memory-bound: its time goes to reading and writing matrices in HBM. FlashAttention removes that traffic, and run time follows bytes.
What does split-KV (flash-decoding) do?
It splits each sequence's KV cache into chunks, computes a partial per chunk in its own thread block, and merges them. Decoding then uses all SMs even at batch 1.
How large is one head's score matrix at in bf16?
B B MiB.
9. Interview angle #
- "Write online softmax, or a tiled attention forward pass, in NumPy." Keep per row, rescale by , divide once at the end, return lse. Guard fully masked rows.
- "Why is FlashAttention faster if it does the same FLOPs?" Attention is memory-bound. Fusion keeps and on chip; traffic falls from to , and in practice L2 serves most re-reads.
- "What is its memory cost?" extra per head (the lse values) instead of .
- "How does it handle a causal mask?" It skips tiles above the diagonal and masks only the diagonal tiles; about half the work.
- "Does it help decoding?" Not by itself: there is one query and no matrix. Split-KV adds parallelism over the KV length and merges the partials with lse.
- "How do you combine attention computed on different GPUs?" With the lse merge: it is associative, so partials can be merged in any order (ring attention, split-KV).
10. Practice #
The notebook for this lesson runs in Colab, with NumPy only. It has five exercises, each with a check cell and a folded solution:
online_softmax_stats(x, block): the running of a stream of scores, checked against NumPy and SciPy'slogsumexp, also with a huge score and with entries.merge_partials(o1, lse1, o2, lse2)for many rows at once, with empty (fully masked) sets.flash_attention_forward(Q, K, V, Br, Bc, causal): the tiled forward pass, checked against naive attention to , with and without a causal mask.- Count HBM traffic with a
CountingMemory: write the formulas for standard and tiled attention, check them against the counts, and plot the ratio over and the tile size. - Split-KV decoding: split a KV cache into chunks, compute the partials, merge them, and check.
11. Watch and read #
- Dao, Fu, Ermon, Rudra, Ré (2022), FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness: §3.1 (Algorithm 1, tiling and recomputation), §3.2 (Theorem 2, Proposition 3), Appendix B (backward pass), Appendix C (proofs).
- Dao (2023), FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning: §3.1 (algorithm, causal masking), §3.2 (parallelism), §3.3 (work split between warps).
- Shah et al. (2024), FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision.
- Dao, Haziza, Massa, Sizov (2023), Flash-Decoding for long-context inference.
- Milakov and Gimelshein (2018), Online normalizer calculation for softmax: the online normaliser itself.
- Triton tutorial Fused Attention: a short, readable forward and backward kernel.
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.