3.3 A deep dive into Self-Attention
Self-Attention is the heart of the Transformer, and the module where modern LLMs concentrate the most compute and the richest set of optimizations
Self-Attention is the heart of the Transformer, and the module where modern LLMs concentrate the most compute and the richest set of optimizations. Whether you want to understand the IO ideas behind FlashAttention, or why Attention variants like GQA and MLA cut inference cost, you need a solid grip on Self-Attention. This article starts from the history of Attention, walks through every math step of Scaled Dot-Product Attention, writes a PyTorch implementation by hand, looks at compute bottlenecks, and then goes into FlashAttention and the main Attention variants, so you can build a full chain from intuition to formulas to engineering.
📑 Table of contents
- 1. The history of Attention
- 2. Understanding QKV through information retrieval
- 3. Scaled Dot-Product Attention in detail
- 4. Manual PyTorch implementation
- 5. Step-by-step dimension walkthrough
- 6. Why divide by : a variance-stability derivation
- 7. Numerical stability of Softmax
- 8. Multi-Head Attention
- 9. MHA vs MQA vs GQA vs MLA
- 10. Causal Mask: role and implementation
- 11. Bottleneck analysis for Self-Attention
- 12. The core idea of FlashAttention
- Self-check list
- References
1. The history of Attention
Before the Transformer landed in 2017, Attention had already been brewing for a few years in sequence modeling. Following that history helps you see what problem Self-Attention actually solves, and why it looks the way it does today.
1.1 Bahdanau Attention (2014): the starting point
Classic Encoder-Decoder setups (for example RNNs for machine translation) had a hard failure mode: the Encoder squeezed the whole input sequence into one fixed-length vector, then handed that to the Decoder. When the input got long, that fixed vector could not hold everything, and translation quality collapsed.
Bahdanau et al. proposed a real fix in 2014: stop forcing the compression. When the Decoder generates each word, let it look back at all Encoder hidden states and take what it needs. Concretely, the Decoder’s current hidden state acts as a “query”, is matched against every Encoder hidden state (a small feed-forward net scores the match), and those scores weight the Encoder states into a “context vector” that becomes extra input for the current decode step.
That is the early form of Attention: teach the model to “attend” to different parts of the input. Bahdanau Attention still used an extra feed-forward net as the match function (additive attention), so it was not cheap to compute.
1.2 Luong Attention (2015): cheaper matching
In 2015 Luong simplified Bahdanau Attention in two ways: a cheaper match function (dot product instead of a feed-forward net), and several alignment styles (global vs local). The dot-product Attention idea (score similarity by a plain inner product) later became the base of the Transformer.
Dot products are much cheaper than a feed-forward match: they are matrix multiplication, which GPUs parallelize well. That simple change opened the door for Self-Attention at scale.
1.3 Self-Attention (2017): Attention Is All You Need
The 2017 Transformer paper made a bold call: drop RNNs entirely and model sequences with Attention alone.
Earlier Attention was cross-sequence: the Decoder attended to the Encoder. Self-Attention is within one sequence: every position attends to every position in the same sequence (including itself). That gives two big wins:
- Parallelism: RNNs must run step by step (step depends on ). In Self-Attention every position can be computed at once, which fits GPUs.
- Long-range dependency: In an RNN, distant tokens only interact through many steps, and the signal decays. In Self-Attention any two positions interact in one step, so there is no distance decay in theory.
The cost? Self-Attention is : every pair of positions needs a match score, while RNNs are . That quadratic cost is where a huge amount of later optimization work starts.
2. Understanding QKV through information retrieval
The three core Attention ideas are Query (Q), Key (K), and Value (V). The names come straight from information retrieval. A everyday library analogy helps build intuition.
2.1 A library search analogy
You walk into a library with a fuzzy need: “I want material on parallel computing.” That need is your Query.
Every book spine has a label: “Operating Systems”, “Computer Networks”, “Intro to Parallel Computing”, “Data Structures”, and so on. Those labels are each book’s Key.
You compare your Query with every Key. “Intro to Parallel Computing” matches highly; “Operating Systems” is somewhat related; “Data Structures” is barely related.
After you have match scores, you do not take the spine labels (Keys). You take each book’s actual content (Value), weighted by the scores. Parallel-computing content dominates, OS contributes a bit, data structures almost nothing, and you end up with a blended summary centered on parallel computing with a little OS on the side.
2.2 Formal definition of the triple
Back to Self-Attention. Given input sequence with shape , three linear maps send into different spaces:
- : each token builds its own “query vector”: “what information do I need”
- : each token builds its own “index vector”: “what clues I offer about the information I have”
- : each token builds its own “content vector”: “the information I actually carry”
Q, K, and V come from three different linear maps of the same , rather than from itself, because the roles need to be decoupled. What a token needs and what it can provide are often different. In a sentence, a verb may need to attend to its subject and object (Query direction), while as something others attend to it mainly supplies action semantics (Key/Value direction). Three independent projection matrices give the model room to learn those different maps.
3. Scaled Dot-Product Attention in detail
With Q, K, and V in hand, Attention breaks into five clear steps.
3.1 Step 1: linear projection
Input has shape . The three weight matrices , , each have shape (single-head case):
These three matmuls are three independent GEMM (General Matrix Multiply) ops. In practice, to raise GPU utilization, people often fuse , , into one big of shape , run one GEMM, then split. That uses the GPU better.
3.2 Step 2: raw attention scores
Multiply by to score every token pair:
Each entry is the dot product of token ‘s query and token ‘s key: how much token attends to token before scaling.
This step builds an matrix. That is the root of Self-Attention’s quadratic cost.
3.3 Step 3: scaling
Divide the raw scores by ( is the Key dimension):
Why scale? Briefly: when is large, dot products get huge and Softmax gradients go to zero. Full math is in section 6.
3.4 Step 4: Softmax normalization
Softmax each row of the scaled score matrix into a distribution:
Softmax does two jobs: map arbitrary reals into so they can act as weights, and make each row sum to 1 so you get a valid distribution.
3.5 Step 5: weighted sum
Weight the Value matrix with the attention weights:
Each token position ends up with a -dim vector that mixes the tokens it “attended” to, with weights from .
3.6 Full formula
Put together, Self-Attention is one line:
Then apply an output projection of shape to map back into the model’s hidden space. That projection matters even more in Multi-Head Attention: it remixes the concatenated heads.
4. Manual PyTorch implementation
Theory is cheaper than writing the code. Below are Single-Head and Multi-Head Self-Attention by hand.
4.1 Single-Head Self-Attention
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class SingleHeadSelfAttention(nn.Module):
"""Manual single-head Self-Attention, without PyTorch's built-in MHA"""
def __init__(self, d_model: int):
super().__init__()
self.d_model = d_model
# Three linear projections: in dim d_model, out dim d_model
self.W_Q = nn.Linear(d_model, d_model, bias=False)
self.W_K = nn.Linear(d_model, d_model, bias=False)
self.W_V = nn.Linear(d_model, d_model, bias=False)
# Output projection
self.W_O = nn.Linear(d_model, d_model, bias=False)
def forward(self, x: torch.Tensor, mask: torch.Tensor = None):
# x: (batch, seq_len, d_model)
# Step 1: linear projections for Q, K, V
Q = self.W_Q(x) # (batch, seq_len, d_model)
K = self.W_K(x) # (batch, seq_len, d_model)
V = self.W_V(x) # (batch, seq_len, d_model)
# Step 2: attention scores = Q @ K^T
scores = torch.matmul(Q, K.transpose(-2, -1)) # (batch, seq_len, seq_len)
# Step 3: scale so large dots do not kill softmax gradients
scores = scores / math.sqrt(self.d_model)
# Step 4: if a mask is given, set masked positions to -inf
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# Step 5: Softmax to get attention weights
attn_weights = F.softmax(scores, dim=-1) # (batch, seq_len, seq_len)
# Step 6: weighted sum over V
context = torch.matmul(attn_weights, V) # (batch, seq_len, d_model)
# Step 7: output projection
output = self.W_O(context) # (batch, seq_len, d_model)
return output
4.2 Multi-Head Self-Attention
class MultiHeadSelfAttention(nn.Module):
"""Manual multi-head Self-Attention: split d_model into num_heads subspaces"""
def __init__(self, d_model: int, num_heads: int):
super().__init__()
assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
self.d_model = d_model
self.num_heads = num_heads
self.head_dim = d_model // num_heads # dim per head
# QKV projections: in d_model, out d_model (all heads packed inside)
self.W_Q = nn.Linear(d_model, d_model, bias=False)
self.W_K = nn.Linear(d_model, d_model, bias=False)
self.W_V = nn.Linear(d_model, d_model, bias=False)
self.W_O = nn.Linear(d_model, d_model, bias=False)
def forward(self, x: torch.Tensor, mask: torch.Tensor = None):
batch_size, seq_len, _ = x.shape
# Step 1: linear projections
Q = self.W_Q(x) # (batch, seq_len, d_model)
K = self.W_K(x)
V = self.W_V(x)
# Step 2: reshape to multi-head, transpose so head sits before seq_len
# (batch, seq_len, d_model) -> (batch, seq_len, num_heads, head_dim)
# -> (batch, num_heads, seq_len, head_dim)
Q = Q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
K = K.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
V = V.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# Step 3: attention scores per head
# Q @ K^T: (batch, num_heads, seq_len, head_dim)
# @ (batch, num_heads, head_dim, seq_len)
# = (batch, num_heads, seq_len, seq_len)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
# Step 4: optional causal mask
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# Step 5: Softmax + weighted sum
attn_weights = F.softmax(scores, dim=-1)
# (batch, num_heads, seq_len, seq_len) @ (batch, num_heads, seq_len, head_dim)
# = (batch, num_heads, seq_len, head_dim)
context = torch.matmul(attn_weights, V)
# Step 6: concat heads: transpose back then merge last two dims
context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
# Step 7: output projection remixes the concatenated heads
output = self.W_O(context) # (batch, seq_len, d_model)
return output
The code walks every step from input to output. Note that .transpose(1, 2) turns (batch, seq_len, num_heads, head_dim) into (batch, num_heads, seq_len, head_dim), so later matmuls run independently inside each head and finish all heads in one batch-parallel pass.
5. Step-by-step dimension walkthrough
Abstract shapes get confusing. Below we track every tensor shape in Multi-Head Self-Attention with concrete numbers.
5.1 Setup
seq_len = 6 # sequence length (6 tokens)
d_model = 512 # model hidden dim
num_heads = 8 # number of attention heads
head_dim = 512 / 8 = 64 # dim per head
batch = 1 # simplify to batch=1
5.2 Step tracking
Input
X: (1, 6, 512) # 1 sample, 6 tokens, each 512-dim
Linear projection
W_Q: (512, 512) # nn.Linear weight matrix
Q = X @ W_Q^T: (1, 6, 512) @ (512, 512) = (1, 6, 512)
K = X @ W_K^T: (1, 6, 512) @ (512, 512) = (1, 6, 512)
V = X @ W_V^T: (1, 6, 512) @ (512, 512) = (1, 6, 512)
Split heads + transpose
Q.view(1, 6, 8, 64): (1, 6, 8, 64)
Q.transpose(1, 2): (1, 8, 6, 64) # 8 heads, each sees 6 tokens of 64-dim
K reshape likewise: (1, 8, 6, 64)
V reshape likewise: (1, 8, 6, 64)
Attention scores
K.transpose(-2, -1): (1, 8, 64, 6) # transpose last two dims
Q @ K^T: (1, 8, 6, 64) @ (1, 8, 64, 6) = (1, 8, 6, 6)
# each head produces a 6x6 score matrix
Scaling
scores / sqrt(64): (1, 8, 6, 6) # sqrt(64) = 8, divide each score by 8
Softmax
softmax(scores, dim=-1): (1, 8, 6, 6) # normalize each row (last dim) to a distribution
# each 6-dim row sums to 1
Weighted sum
attn_weights @ V: (1, 8, 6, 6) @ (1, 8, 6, 64) = (1, 8, 6, 64)
# each head outputs 6 tokens of 64-dim
Concat heads
transpose(1, 2): (1, 6, 8, 64) # move head dim back
view(1, 6, 512): (1, 6, 512) # 8 * 64 = 512, concat to d_model
Output projection
W_O: (512, 512)
context @ W_O^T: (1, 6, 512) @ (512, 512) = (1, 6, 512)
Final output
Output: (1, 6, 512) # same shape as input X
End to end, input and output stay , so Transformer Blocks can stack like Lego.
6. Why divide by : a variance-stability derivation
Dividing by is easy to dismiss as a “tuning trick”, but there is a hard math reason.
6.1 The problem
Assume every component of and is an independent random variable with mean 0 and variance 1. Compute the variance of the dot product .
Let , , with each and i.i.d., , .
The dot product is:
6.2 Derivation
Expectation and variance of one component product:
Because and are independent:
So:
The dot product is a sum of independent terms. By additivity of variance:
6.3 Conclusion
Variance of is . So when , the std of the dot product is about 8; when , about 11.3. Larger dimensions mean larger absolute dots and a more spread-out distribution.
Softmax is very sensitive to scale. When gaps get huge (say 50 vs -10), Softmax collapses toward a one-hot, gradients go near zero, and training stalls.
After dividing by :
Variance returns to 1. Whatever is, Softmax inputs stay in a sane range and gradients stay healthy. That is where the “Scaled” in Scaled Dot-Product Attention comes from.
7. Numerical stability of Softmax
Softmax looks like a simple formula, but GPU implementations hide an easy overflow trap.
7.1 The overflow problem
Standard Softmax:
grows fast. When is large (say ), overflows float32 and even float16 into inf. If numerator and denominator both overflow, inf / inf becomes NaN and the whole compute is toast.
7.2 Subtract the max
The fix uses Softmax shift invariance: subtract the max from every input, and the result is unchanged.
The proof is short: multiply numerator and denominator by , which is the same as dividing both by . After the subtract, the largest exponent input is 0, so is at most 1 and cannot overflow.
That costs an efficiency hit: a naive implementation needs three passes over the data:
- Pass 1: scan all elements for the max
- Pass 2: scan all elements, compute and their sum
- Pass 3: scan all elements, divide each by the sum
Each pass reads from HBM (high-bandwidth memory). Three passes means three HBM reads. When is large (for example seq length 128K), that IO cost hurts.
7.3 Online Softmax
Milakov and Gimelshein (2018) proposed Online Softmax, which folds three passes into one pass. The idea is to maintain the running max and the normalization denominator while you scan.
While visiting element , keep two variables:
- : max of the first elements
- : sum of exponentials of the first elements, relative to
Recurrence:
The key is the second line: when the max moves from to , rescale the previous sum by to change base. One pass gets you max and exp-sum; one more pass finishes normalization. Two passes total, one fewer than the naive version.
That idea is critical for FlashAttention: FlashAttention tiles the Attention matrix and updates Softmax intermediates per tile. Without online Softmax updates you would have to write the full matrix to HBM and Softmax globally, which kills the point of tiling. Online Softmax makes tiling and exact Softmax compatible.
8. Multi-Head Attention
8.1 Why multiple heads
Single-head Attention has one QKV projection set, so the model learns one “attention pattern”. Token relations in language are multi-dimensional: the same word can hold syntactic ties (subject-verb agreement), semantic ties (synonymy), positional patterns (local neighbors), and more at once.
Analogy: in a project review, one reviewer can only push the angles they are good at. A review board (architecture, code quality, test coverage, docs) scores independently and merges into one report, and coverage is much broader.
Multi-Head Attention is that review board. Each head has its own , , and catches different relation types in different subspaces.
8.2 Math
Assume , , so .
Full Multi-Head Attention:
Each head’s , , has shape . In practice you do not keep small matrices. You project once with a big of shape , then reshape to . Same math: the big matrix is eight small ones stacked along the output dim.
8.3 Parameter count
With :
| Parameter matrix | Shape | Params |
|---|---|---|
| 262,144 | ||
| 262,144 | ||
| 262,144 | ||
| 262,144 | ||
| Total |
General rule: MHA params are (no bias). Head count does not change total params; it only changes the split. Add a head and each head gets thinner, so the product (total projected dim) stays .
8.4 Engineering angle
Multi-head structure parallelizes naturally. Eight independent heads can:
- Parallelize inside a GPU: use the batch dim and process all heads in one CUDA kernel launch
- Tensor Parallelism across GPUs: put different heads on different GPUs. For example 8 heads on 4 GPUs means 2 heads each. One AllReduce merges the output projection results
9. MHA vs MQA vs GQA vs MLA
As LLMs moved into an inference-efficiency era, Attention head layouts kept evolving. The driver is simple: KV Cache is too big.
9.1 MHA (Multi-Head Attention)
In standard MHA every head has its own Q, K, V. With heads you cache K sets and V sets.
- Q heads, K heads, V heads
- Each Q/K/V set is independent; nothing shared
9.2 MQA (Multi-Query Attention)
Shazeer (2019) proposed: all Q heads share one K and one V. Still Q heads, but only 1 K head and 1 V head.
- Q heads, 1 K head, 1 V head
- All Q heads share the same K and V
KV Cache shrinks to , so inference gets much faster. Expressiveness drops: every head is forced to pull from the same KV, so diversity is limited.
9.3 GQA (Grouped-Query Attention)
GQA is the MHA/MQA middle ground (Ainslie et al., 2023). Split Q heads into groups ( Q heads each); each group shares one K and one V.
- Q heads, K heads, V heads
- Every Q heads share one K/V set
- falls back to MHA; falls back to MQA
GQA balances quality and inference cost better. LLaMA-2-70B, Mistral-7B, and many other mainstream models use GQA.
9.4 Comparison table
With , num_heads = 32, head_dim = 128, sequence length , FP16:
| Metric | MHA () | GQA () | MQA () |
|---|---|---|---|
| Q heads | 32 | 32 | 32 |
| KV heads | 32 | 8 | 1 |
| params | |||
| params | |||
| KV Cache / token | |||
| KV Cache / layer @ 4096 tokens | |||
| Model quality | Best | Near MHA | Some drop |
Note: Cache size = 2 × B × L × num_heads × head_dim × d_type_bytes (2 is K and V, B is batch_size, L is generated sequence length so far) From MHA to GQA(), KV Cache shrinks to 1/4 and params drop, while quality barely moves. That is why GQA became the default choice.
9.5 MLA (Multi-Latent Attention)
DeepSeek-V2 proposed MLA (Multi-Latent Attention), another way to compress KV Cache.
The idea: do not cache full K and V. Compress them into a low-dimensional latent space and cache only that. At inference, decompress back to full K and V when needed.
- Standard MHA: (cache , dim = num_kv_heads head_dim)
- MLA: (cache , much smaller than ), then at inference on demand
MLA’s low-rank compression shrinks KV Cache a lot (DeepSeek-V2 reports about 93.3% compression) while carefully designed up-projections keep quality. Unlike GQA (fewer KV heads), MLA shrinks the dim of each representation. Same goal: make inference KV Cache as small as possible.
10. Causal Mask: role and implementation
10.1 Why mask
In autoregressive LMs (GPT family, LLaMA family), the training target is “predict the next token from the past”. During Attention, each position may only see itself and earlier tokens, never the future. Otherwise training peeks at the answer and never learns real prediction.
10.2 Implementation
A Causal Mask is an upper-triangular cover over the score matrix that sets future scores to -inf:
# Build a causal mask
def create_causal_mask(seq_len: int) -> torch.Tensor:
"""
Build a lower-triangular matrix (including the diagonal); upper triangle is False.
True positions are kept; False positions are set to -inf in Attention scores.
"""
mask = torch.tril(torch.ones(seq_len, seq_len, dtype=torch.bool))
return mask
# Example: seq_len = 4
# mask:
# [[True, False, False, False],
# [True, True, False, False],
# [True, True, True, False],
# [True, True, True, True ]]
#
# Masked Attention scores (upper triangle = -inf):
# [[s00, -inf, -inf, -inf],
# [s10, s11, -inf, -inf],
# [s20, s21, s22, -inf],
# [s30, s31, s32, s33 ]]
#
# After Softmax, -inf positions become weight 0; future info is fully blocked
Another engineering angle: only the lower triangle is live, so in theory you can skip the upper triangle. FlashAttention-2 does exactly that: skip upper-triangle tiles and cut nearly half the work.
11. Bottleneck analysis for Self-Attention
Self-Attention performance has two different regimes.
11.1 FLOPs breakdown
Four main matmuls sit inside Self-Attention:
| Op | Shape | FLOPs |
|---|---|---|
Total: .
When is small (for example ), dominates: bottleneck is QKV projection GEMM. When is large (for example ), dominates: bottleneck is the Attention matrix.
11.2 Compute Bound vs Memory Bound
GPU performance is limited by two numbers:
- Compute: how many FLOPS per second
- Bandwidth (Memory Bandwidth): how many GB/s you can read/write from HBM
Whether an op is Compute Bound or Memory Bound depends on Arithmetic Intensity = FLOPs / Bytes.
- If intensity > the GPU’s compute/bandwidth ratio, the op is Compute Bound: the ALU stays busy
- If intensity < that ratio, the op is Memory Bound: the ALU waits on data movement
A100 example: ~312 TFLOPS FP16, ~2 TB/s HBM, ratio about 156 FLOP/Byte.
Prefill (full prompt):
QKV projections are large matmuls with a large batch dim (seq_len), high intensity, usually Compute Bound. is also a large matmul, also Compute Bound.
Decode (token by token):
Each step has one token. QKV projections become GEMV, intensity crashes, Memory Bound. Attention becomes a vector against the whole KV Cache, also Memory Bound. Most time goes into hauling weights and KV Cache from HBM.
11.3 The memory bottleneck
Naive Attention materializes the full score matrix. With seq_len = 128, num_heads = 32:
That is far beyond any single GPU. Even at “only” 8K, the Attention matrix needs , which is already serious.
That memory wall is exactly what FlashAttention targets.
12. The core idea of FlashAttention
FlashAttention (Dao et al., 2022) is one of the most influential Attention optimizations in recent years. It does not change the math result (exact, not approximate). It reorders the work so HBM traffic drops a lot.
12.1 GPU memory hierarchy refresher
To understand FlashAttention you need the GPU memory stack:
- HBM (High Bandwidth Memory): main GPU memory, large capacity (for example A100 80 GB) but relatively slow (~2 TB/s)
- SRAM (on-chip cache): shared memory and registers on each SM (Streaming Multiprocessor), tiny capacity (A100 ~192 KB shared memory per SM, ~20 MB across all SMs) but very fast (~19 TB/s)
Naive Attention: finish in HBM and write the full matrix back; read it for Softmax and write back; read Softmax output and for the matmul. Every step reads/writes that huge matrix in HBM. That wastes bandwidth.
12.2 Tiling
FlashAttention’s first key technique is Tiling: do not build the full Attention matrix at once. Split Q, K, V into tiles. Load one Q tile and one K/V tile into SRAM, finish that tile’s Attention there, write results back to HBM.
Standard:
Load all Q, K into HBM → full N x N matrix → write HBM → read Softmax → write HBM → ...
FlashAttention:
Split Q into T_r tiles, K/V into T_c tiles
For each Q_i tile:
For each K_j, V_j tile:
Load Q_i, K_j, V_j from HBM into SRAM (tiles are small enough to fit)
In SRAM: Q_i @ K_j^T → local Softmax → multiply by V_j
Accumulate into the output for this Q_i
Write the final Q_i output back to HBM
Each step only holds one tile of the Attention matrix in SRAM. The full matrix never lands in HBM.
12.3 Why Online Softmax matters
Tiling creates a hard problem: Softmax needs a full-row max and sum, but each tile only sees part of a row. How do you get global Softmax from local pieces?
That is where Online Softmax comes in. Recall section 7.3: when a new block arrives you can update the global max and denominator and correct earlier block results.
Concretely, after Attention of against you have a local output and Softmax stats (local max , local denominator ). Against you get and . Online Softmax correction:
Keep accumulating until every tile is done. The final result matches standard global Softmax exactly.
12.4 Why HBM traffic drops
Standard Attention HBM traffic:
- Write (): writes
- Read for Softmax: reads
- Write : writes
- Read for : reads
- Total HBM traffic: (plus reads of Q, K, V themselves)
FlashAttention HBM traffic:
- Read Q, K, V:
- Write final output O:
- Intermediate Attention tiles live only in SRAM and are discarded; the full matrix never hits HBM
- But Q, K, V, O tiles are reread: for V1, the outer loop walks K/V tiles and each time sweeps full Q and O ()
- Total HBM traffic: ( is SRAM size in elements)
Because is much smaller than (for example gives , while 192KB SRAM holds about 98304 FP16 elements), HBM traffic is several times below the standard . It still grows with ; the constant is just much smaller. What truly drops to is extra memory (no more stored matrix). FLOPs stay ; only the IO pattern changes. FlashAttention speeds up by cutting slow HBM traffic and keeping work in fast SRAM.
12.5 FlashAttention-2 and FlashAttention-3
FlashAttention-2 (Dao, 2023) improves parallelism and warp scheduling on top of FlashAttention:
- Reorders inner/outer loops (outer over Q tiles, inner over K/V), cutting shared-memory traffic
- Better warp-level work assignment, higher GPU occupancy
- Skips all-zero Causal Mask tiles, cutting more compute
FlashAttention-3 (Dao et al., 2024) targets Hopper (H100), using TMA (Tensor Memory Accelerator) and wgmma (Warp Group MMA) to raise hardware utilization further.
FlashAttention is now standard in major inference and training stacks. PyTorch 2.0+ ships torch.nn.functional.scaled_dot_product_attention, which defaults to a FlashAttention kernel underneath.
🎯 Self-check list
After this article, test how deep your understanding goes:
- Trace Attention from Bahdanau to Self-Attention and say what each step fixed
- Write Scaled Dot-Product Attention from memory and explain what Q, K, V each do
- Derive from scratch why we divide by : assume i.i.d. standard normal and show dot-product variance is
- Hand-write Single-Head and Multi-Head Self-Attention in PyTorch and track tensor shapes at each step
- Explain why Softmax subtracts the max, and the Online Softmax recurrence idea
- Given concrete seq_len, , num_heads, track every tensor shape end to end
- Sketch MHA / MQA / GQA structure differences and estimate each KV Cache size
- Explain Causal Mask (upper triangle to -inf, Softmax zeros those weights) and how it is implemented
- Tell Compute Bound from Memory Bound, and which bottleneck Prefill vs Decode hits
- Explain FlashAttention: how Tiling + Online Softmax drops extra memory from to and HBM traffic from to
📚 References
Papers
- Neural Machine Translation by Jointly Learning to Align and Translate (Bahdanau et al., 2014): https://arxiv.org/abs/1409.0473 — Bahdanau Attention, the starting point of Attention
- Effective Approaches to Attention-based Neural Machine Translation (Luong et al., 2015): https://arxiv.org/abs/1508.04025 — Luong Attention, introduced dot-product Attention
- Attention Is All You Need (Vaswani et al., 2017): https://arxiv.org/abs/1706.03762 — original Transformer paper
- Fast Transformer Decoding: One Write-Head is All You Need (Shazeer, 2019): https://arxiv.org/abs/1911.02150 — Multi-Query Attention
- GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints (Ainslie et al., 2023): https://arxiv.org/abs/2305.13245 — Grouped-Query Attention
- DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model (DeepSeek-AI, 2024): https://arxiv.org/abs/2405.04434 — Multi-Latent Attention
- FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness (Dao et al., 2022): https://arxiv.org/abs/2205.14135 — FlashAttention v1
- FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning (Dao, 2023): https://arxiv.org/abs/2307.08691 — FlashAttention v2
- Online Normalizer Calculation for Softmax (Milakov & Gimelshein, 2018): https://arxiv.org/abs/1805.02867 — Online Softmax algorithm
Tutorials and blogs
- The Illustrated Transformer (Jay Alammar): https://jalammar.github.io/illustrated-transformer/ — illustrated Transformer intro
- The Annotated Transformer (Harvard NLP): https://nlp.seas.harvard.edu/annotated-transformer/ — paper lined up with a PyTorch implementation
- Andrej Karpathy: Let’s build GPT from scratch: https://www.youtube.com/watch?v=kCc8FmEb1nY — GPT from scratch
- ELI5: FlashAttention (Aleksa Gordic): https://gordicaleksa.medium.com/eli5-flash-attention-5c44017022ad — approachable FlashAttention walkthrough