← Plasmient Labs
Code Walk 04read · don't run

FlashAttention — the same attention,
made memory-aware

Walk 02's attention builds a giant T×T score matrix in slow memory. On long sequences that memory traffic — not the math — is the whole cost. FlashAttention never builds that matrix. It does MORE arithmetic and runs several times faster, because attention was never compute-bound. We read the ~100-line kernel that proves it.

00 Start here — the bill Walk 02 quietly ran up

Remember the attention block from Walk 02. For a sequence of lengthT, it does this:

# nanoGPT's manual attention — Walk 02, the slow path
att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))  # (B, nh, T, T)
att = att.masked_fill(mask == 0, float('-inf'))
att = F.softmax(att, dim=-1)                                     # (B, nh, T, T)
y   = att @ v                                                    # (B, nh, T, hs)

Look at the shapes: att is (B, nh, T, T). For every pair of tokens, one number. At T = 8192 that's a 67-million-entry matrix, per head, per layer — and it gets written to slow global memory (HBM), read back for the softmax, written again, read again for the@ v. The multiplies are cheap. Shuffling that giant matrix in and out of HBM is the cost, and it grows with .

the one idea to hold ontoAttention is memory-bound, not compute-bound. The fix isn't fewer FLOPs — FlashAttention doesmore. The fix is to never store the T×T matrix at all, so it never touches slow memory.

01 The move: tile it, and never write the big matrix down

This is Walk 03's lesson — keep data in fast on-chip memory, cross the slow cliff as little as possible — applied to attention. FlashAttention chops Q, K, V into tiles small enough to live in SRAM, and computes attention one tile-pair at a time, accumulating the final output as it goes. The full T×T scores exist only momentarily, one small block at a time, on-chip.

// Everything lives in SRAM (on-chip), never a full T×T matrix in HBM
extern __shared__ float sram[];
int tile_size = Bc * d;          // one tile of Q, K, or V
float* Qi = sram;                // a block of queries
float* Kj = &sram[tile_size];    // a block of keys
float* Vj = &sram[tile_size * 2];// a block of values
float* S  = &sram[tile_size * 3];// scores for THIS tile only — Bc×Br, not T×T

Notice S: it's sized Bc × Brone tile, maybe 32×32 — notT × T. That single decision is the whole algorithm. But it raises a problem: softmax needs the whole row at once (you divide by the sum over all keys). If you only ever see one tile of keys at a time, how do you normalize? That's the next section, and it's the clever bit.

02 The trick that makes it possible: online softmax

Softmax over a row is exp(sᵢ − max) / Σ exp(sⱼ − max) — you subtract the row max for numerical safety, then divide by the row sum. Both the max and the sum need the entire row. FlashAttention computes them incrementally, updating a running max m and running sum l as each new tile arrives — the same math a streaming average uses.

// S = QKᵀ for this tile, and track the running row-max
float row_m = -INFINITY;
for (int y = 0; y < Bc; y++) {
    float sum = 0;
    for (int x = 0; x < d; x++)
        sum += Qi[(tx * d) + x] * Kj[(y * d) + x];   // one query · one key
    sum *= softmax_scale;
    S[(Bc * tx) + y] = sum;
    if (sum > row_m) row_m = sum;                    // rowmax(S), online
}

// P = exp(S − row_m), and the running row-sum
float row_l = 0;
for (int y = 0; y < Bc; y++) {
    S[(Bc * tx) + y] = __expf(S[(Bc * tx) + y] - row_m);
    row_l += S[(Bc * tx) + y];                       // rowsum(P), online
}

For the current tile it computes the scores S = QKᵀ, this tile's local max (row_m), the exponentials P = exp(S − row_m), and this tile's local sum (row_l). All in SRAM. Nothing full-width is stored.

why this is the keyOnline softmax is what lets you process attention tile by tile and still get the exactsame answer as the all-at-once version. Not an approximation — identical numbers, computed in a memory pattern the GPU actually likes.

First get a feel for the softmax itself — the same function attention runs over its scores. Drag the logits and watch the probabilities redistribute; the running max and sumthe kernel tracks are just the incremental way to compute exactly this:

logits → softmax → cross-entropy · drag the z sliders, click a bar to set the true class
true class0
p(true)
cross-entropy
Softmax over four scores — reused live from Study Module 2. FlashAttention computes this identical distribution, but one tile at a time, carrying the running max and sum.

The full derivation — logits, softmax, and the cross-entropy loss it feeds — is inStudy · Module 2 — Probability & the loss.

03 Reconcile: merge each tile into the running answer

When a new tile gives a bigger max than we've seen, everything accumulated so far was normalized against the old max — so it must be rescaled. That's these lines: combine old and new stats, correct the running output O, add the new tile's contribution, carry the updatedm and l forward.

// merge THIS tile's stats with everything seen so far
float row_m_new = max(row_m_prev, row_m);
float row_l_new = (__expf(row_m_prev - row_m_new) * row_l_prev)
                + (__expf(row_m     - row_m_new) * row_l);

// rescale the running output O and add this tile's contribution — no T×T matrix ever stored
for (int x = 0; x < d; x++) {
    float pv = 0;                                     // Pij · Vj
    for (int y = 0; y < Bc; y++)
        pv += S[(Bc * tx) + y] * Vj[(y * d) + x];
    O[... i ...] = (1 / row_l_new)
        * (row_l_prev * __expf(row_m_prev - row_m_new) * O[... i ...]   // rescale old
        +              __expf(row_m      - row_m_new) * pv);            // add new
}
m[...] = row_m_new;   l[...] = row_l_new;             // carry the stats forward

The output O is updated in place, tile by tile. At no point does aT × T matrix exist in HBM. When the loops finish, O holds exactly whatsoftmax(QKᵀ) @ V would have produced — computed without ever paying to store the intermediate.

04 The whole shape of the kernel

Stepping back, the entire forward pass is two nested loops over tiles:

for (int j = 0; j < Tc; j++) {                 // outer loop over KEY/VALUE tiles
  // pull one tile of K and V from slow HBM into fast SRAM, once
  for (int x = 0; x < d; x++) {
    Kj[(tx * d) + x] = K[qkv_offset + (tile_size * j) + (tx * d) + x];
    Vj[(tx * d) + x] = V[qkv_offset + (tile_size * j) + (tx * d) + x];
  }
  __syncthreads();

  for (int i = 0; i < Tr; i++)  {              // inner loop over QUERY tiles
    // ... the whole attention math for this (query-tile × key-tile) block ...
  }
  __syncthreads();
}
outer (j)walk over tiles of K and V. Load each tile from HBM into SRAM once.
inner (i)walk over tiles of Q. For each, compute scores against the current K/V tile, update the online softmax stats, and fold the result into the running output O.
__syncthreadsbarriers so the whole block agrees a tile is loaded before anyone reads it — the same coordination as Walk 03's shared-memory kernel.
HBM trafficeach element of Q, K, V crosses the slow cliff a handful of times total — not T times. That's the win.

05 Why doing more work is faster

FlashAttention performs extra arithmetic — the rescaling, and in the backward pass it evenrecomputes the scores instead of storing them. By a FLOP count it does more than naive attention. Yet it runs several times faster and uses memory that grows with T instead of. The only way that reconciles:

naive attention (Walk 02)FlashAttention
T×T matrix in HBMyes — written & read repeatedlynever
memory usedO(T²)O(T)
arithmeticlessmore (rescale + recompute)
wall-clockslowseveral× faster

This is only possible because attention was memory-bound — the math units were idle, waiting on HBM. Trading cheap idle FLOPs for expensive memory traffic is a winning trade every time the limiter is memory. FlashAttention is the most famous instance of the exact principle Walk 03 taught kernel by kernel.

06 The taste to take away

know your limiterNaive attention burned time on memory while the tensor cores idled. You can't fix what you don't measure — the first move is always identifying the bottleneck.
IO-awarenessThe paper's own word. Design the kernel around the memory hierarchy, not the FLOP count. The fast kernel is the one that respects the cliff.
exact, not approximateOnline softmax gives the identical answer. This wasn't a quality trade — it was a pure systems win, which is why it shipped everywhere within a year.
recompute > storeWhen memory is the limiter, redoing arithmetic can be cheaper than saving its result. Counter-intuitive until you've internalized the cliff.
the ladder closes here · the endgame link

Four walks, one arc. micrograd: what a model computes.nanoGPT: it's matmuls and one attention block.the CUDA matmul: a matmul's real speed spans 70×, invisible to "utilization." FlashAttention: the single most important kernel of the LLM era is famous for exactly one reason — it fixed a memory-traffic problem that no compute metric could see.

That is the company. The naive attention pinned the GPU at 100% while starving on HBM. The whole industry ran it that way for years because the standard dashboard said everything was fine.utilization-truth — our flagship lab — measures precisely this: it runs amemory-bound workload next to a compute-bound one and prints achieved bandwidth and % of peak FLOPS beside what nvidia-smi claims. FlashAttention is the proof that the gap is worth billions. Our lab is how you find it on your own hardware.

Full references — go to the source

We explain the kernel in our own words and never rehost the source. These are the originals — the Dao et al. paper is the canonical read.