6.1 FlashAttention V1 in detail
A deep dive into the core principles and implementation details of FlashAttention V1
This article digs into the core ideas and implementation details of FlashAttention V1: how Tiling + Online Softmax cut Attention’s extra GPU memory from to , slash HBM traffic, and speed things up with no accuracy loss.
📑 Contents
- 1. Performance bottlenecks of standard Attention
- 2. Core idea of FlashAttention
- 3. Online Softmax algorithm
- 4. Tiling strategy
- 5. Full forward pass
- 6. Backward pass and recomputation
- 7. IO complexity analysis
- 8. Implementation notes and code examples
- Summary
- Self-check list
- References
1. Performance bottlenecks of standard Attention
1.1 Standard Attention compute flow
Scaled Dot-Product Attention runs as:
where , is sequence length, and is head dimension.
1.2 Memory bottleneck analysis
The key problem in the standard implementation is that intermediate matrices and are :
| 📊 Metric | 📝 Value | 💡 Note |
|---|---|---|
| Intermediate matrix size | S and P each need storage | |
| HBM read/write | Repeatedly read/write matrices | |
| Compute | Two matrix multiplies |
At sequence length , , a single head’s alone needs MB (FP16). With multiple heads and batches, memory cost is huge.
1.3 Insufficient arithmetic intensity
Arithmetic Intensity of standard Attention is:
Since is usually only 64 or 128, arithmetic intensity is low — the bottleneck is memory bandwidth, not compute. A lot of time goes to data movement while GPU compute units starve.
💡 Tip: like a chef who cooks fast but waits forever for ingredients from the warehouse — the core bottleneck of standard Attention is data movement between HBM and SRAM.
2. Core idea of FlashAttention
2.1 IO-Awareness: focus on data movement
FlashAttention’s core insight: on modern GPUs, Attention is Memory-Bound; the win is fewer HBM accesses, not fewer FLOPs.
Traditional optimization chases fewer FLOPs (e.g. sparse Attention). FlashAttention takes another path — clever tiling keeps data in SRAM (Shared Memory) as much as possible and avoids landing intermediate results in HBM.
2.2 Two key techniques
FlashAttention V1 combines two techniques:
- Tiling: cut Q, K, V into small tiles; load one tile at a time into SRAM to compute
- Online Softmax: without knowing the global max, correct Softmax results as new tiles arrive
📌 Key point: both are required — Tiling solves “how not to materialize the full matrix”; Online Softmax solves “Softmax needs global info when computing in tiles”.
2.3 Intuition
Imagine ranking every student by percentile from the whole class’s scores. The usual way collects every score (all on the blackboard) then computes. FlashAttention is more like: each time a group of scores arrives, immediately update running stats (max, sum) and correct previous results — the final answer matches collecting everything first.
3. Online Softmax algorithm
3.1 Three passes of standard Softmax
For numerical stability, standard (safe) Softmax needs three passes over :
Pass 1: global max for numerical stability
Pass 2: normalization denominator (exp-sum)
Pass 3: normalized output per element
Passes 2 and 3 both need the global max from pass 1; pass 3 also needs from pass 2. You must finish a full scan before the next — which conflicts with tiling.
📖 Further reading: Online Softmax (NVIDIA, 2018) merges the three passes into two — maintain and while scanning; FlashAttention further fuses “multiply by for the output” into that flow.
3.2 Online Softmax recurrence
The key innovation: maintain correctable local stats and update them as new data tiles arrive.
Suppose the first tiles are done, with local max and local exp-sum . When tile , , arrives:
Step 1: update max
Step 2: correct and update exp-sum
Step 3: correct existing output
3.3 Correctness
Mathematical correctness comes from a simple identity:
Whenever the global max updates, every previous exp term is fixed by multiplying . The operation is exact — no approximation.
⚠️ Note: Online Softmax is mathematically equivalent to standard Softmax (aside from tiny rounding differences from floating-point order). It is not an approximate algorithm.
4. Tiling strategy
4.1 Tiling scheme
Split , , along the sequence dimension into tiles of size (Q tile size) and (K/V tile size):
- into tiles
- into tiles
Tile size depends on SRAM capacity (in elements; think M = SRAM_bytes / sizeof(dtype)) and head dimension (feature dim per token, e.g. in LLaMA-7B):
Why ?
Computing one tile, SRAM must hold — four tensors roughly (see §4.2), about elements. Require , so each tile holds at most rows. Relatively small () and stats () are ignored because when they do not dominate SRAM.
Why also with ?
Online Softmax’s correction is a per-row scalar. The original paper constrains to simplify analysis and avoid complicated stats management when rows greatly exceed the column dim; V2 relaxes this.
4.2 SRAM layout
During compute, SRAM must hold:
| 📊 Data | Size | 📝 Use |
|---|---|---|
| tile | Current Query tile | |
| tile | Current Key tile | |
| tile | Current Value tile | |
| tile | Local attention scores | |
| tile | Accumulated output tile | |
| Row max, row exp-sum (one scalar per row) |
Total SRAM is about ; the dominant term is , which is why the tile-size formula has in the denominator — stay under Shared Memory capacity .
4.3 Outer and inner loops
FlashAttention V1 uses outer loop over K/V tiles, inner loop over Q tiles:
outer: for j = 1 to T_c (iterate K/V tiles)
load K_j, V_j from HBM into SRAM
inner: for i = 1 to T_r (iterate Q tiles)
load Q_i, O_i, m_i, l_i from HBM into SRAM
compute local scores S_ij = Q_i * K_j^T
update stats and output
write updated O_i, m_i, l_i back to HBM
💡 Tip: outer over K/V means each K/V tile loads from HBM once, while Q and O load many times. That is a V1 weakness (V2 flips to outer over Q).
5. Full forward pass
5.1 Step-by-step derivation
For the -th Q tile and -th K/V tile:
Step 1: local attention scores
Step 2: current tile row-max
Step 3: update global row-max
Step 4: correction factor and update exp-sum
rescales the old exp-sum (under old max ) onto the new max .
Step 5: update output
5.2 Algorithm pseudocode
# FlashAttention V1 forward pseudocode
def flash_attention_forward(Q, K, V, B_r, B_c):
N, d = Q.shape
O = zeros(N, d)
m = full(N, -inf) # row max
l = zeros(N) # row exp-sum
# Outer loop: iterate K/V tiles
for j in range(0, N, B_c):
K_j = K[j:j+B_c] # load from HBM
V_j = V[j:j+B_c] # load from HBM
# Inner loop: iterate Q tiles
for i in range(0, N, B_r):
Q_i = Q[i:i+B_r] # load from HBM
O_i = O[i:i+B_r] # load from HBM
m_i = m[i:i+B_r] # load from HBM
l_i = l[i:i+B_r] # load from HBM
# Local attention scores
S_ij = Q_i @ K_j.T / sqrt(d) # [B_r, B_c]
# Update stats
m_new = max(m_i, rowmax(S_ij))
l_new = l_i * exp(m_i - m_new) + rowsum(exp(S_ij - m_new))
# Update output
O_new = O_i * (l_i * exp(m_i - m_new) / l_new).unsqueeze(1)
O_new += (exp(S_ij - m_new) / l_new.unsqueeze(1)) @ V_j
# Write back to HBM
O[i:i+B_r] = O_new
m[i:i+B_r] = m_new
l[i:i+B_r] = l_new
return O
5.3 Numerical stability
The whole path stays stable by subtracting the max:
- Every exp input is shifted by the current known max, so has
- Correction also has , hence
No overflow in the full computation.
6. Backward pass and recomputation
6.1 Backward challenge
Standard Attention backward needs intermediate and () for gradients. is pre-softmax logits; is post-softmax attention weights; output :
FlashAttention uses the identity (written ) to avoid building the full matrix.
Saving those intermediates would waste the memory the forward pass saved.
6.2 Recomputation strategy
FlashAttention V1 does not save and ; it recomputes them in backward:
- Forward only saves: (inputs, output, stats)
- Backward: with saved and , reload to form , then recover
6.3 Cost of recomputation
Recomputation adds FLOPs (about one extra forward matmul), but HBM traffic drops:
| ✅ Gain | ❌ Cost |
|---|---|
| Memory → | Extra FLOPs (~50% of forward compute) |
| Much less HBM traffic | SRAM must hold more data at once |
| Longer sequences trainable | Backward kernel more complex |
Because Attention is Memory-Bound, the extra compute is “hidden” by fewer HBM accesses — wall-clock time is often shorter.
7. IO complexity analysis
7.1 Standard Attention IO complexity
comes from reading/writing (); from intermediate and (each ). When , dominates → .
7.2 FlashAttention IO complexity
FlashAttention HBM traffic:
Breakdown:
- From §4.1, , so outer iterations
- Each outer pass sweeps full and ( elements), so and traffic is
- are each read once: , negligible next to the above
Total . Intuition: larger SRAM () → larger tiles → fewer outer loops → less repeated traffic.
is SRAM size in elements. Since (typical , ≈ FP16 elements, ), this is much smaller than .
7.3 Lower-bound proof
The paper also shows any exact Attention algorithm needs at least HBM accesses — FlashAttention’s IO complexity is asymptotically optimal.
7.4 Practical speedup
Typical A100 comparison:
| 📊 Sequence length | Standard Attention | FlashAttention V1 | Speedup |
|---|---|---|---|
| 1024 | 1.0x | 2.4x | 2.4x |
| 2048 | 1.0x | 2.8x | 2.8x |
| 4096 | 1.0x | 3.5x | 3.5x |
| 8192 | OOM | runnable | - |
8. Implementation notes and code examples
8.1 Tensor shapes end to end
Inputs are 4-D; the first two dims are batch and head (passed through as “batch”); the last two do the matmuls:
Q: [B, H, N, D]
K: [B, H, N, D] → K^T (transpose last two dims): [B, H, D, N]
V: [B, H, N, D]
Step 1 Attention scores:
S = Q @ K^T / sqrt(D)
= [B, H, N, D] @ [B, H, D, N]
= [B, H, N, N]
Step 2 Softmax (along last N, row-wise):
P = softmax(S, dim=-1)
shape: [B, H, N, N]
Step 3 Output:
O = P @ V
= [B, H, N, N] @ [B, H, N, D]
= [B, H, N, D] (same shape as Q)
Auxiliary tensors (FlashAttention-specific):
M: [B, H, N]— per-row max (numerical stability + Online Softmax correction)L: [B, H, N]— per-row exp-sum (softmax denominator)
8.2 Kernel design notes
📌 On parallel partitioning (grid and block design):
- grid (program granularity) = (B, H): each program owns full attention for one
(batch, head)slice; programs are independent, no communication.- Threads inside a block: within a program, Triton splits
tl.dot,tl.max,tl.sum, etc. across warps of a thread block. Tile size is set byBLOCK_M(Q rows ),BLOCK_N(K/V rows ),BLOCK_D(vectorized width on D).- Serial inside a program: outer loop over K/V tiles, inner over Q tiles
Different Q tiles share stats and accumulated across outer iterations; one program’s registers cannot hold m/l/O for “all Q tiles”, so HBM auxiliaries
M, Lpersist across iterations (the root cause of V1’s extra IO — V2 swaps loop order, puts Q on the grid, and keeps m/l/O in registers the whole time, removing that HBM traffic).⚠️ V1’s natural parallelism bottleneck: grid size in inference is often only tens (e.g. batch=1, heads=32 → 32 programs), while modern GPUs have 80–140 SMs (A100=108, H100=132), so SM utilization is ~30%. That is V1’s main weakness and the reason V2 had to appear.
- Thread Block mapping: in V1 each Thread Block owns one outer K/V tile (one ) and walks all Q tiles
- Shared Memory: pre-allocate SRAM for , (resident in the outer loop) plus ,
- Registers: keep stats , and accumulated in registers when possible
- Loop structure: outer loads K/V tiles; inner does GEMM and Softmax updates
8.3 Simplified Triton implementation
import math
import torch
import triton
import triton.language as tl
@triton.jit
def flash_attn_v1_kernel(
Q, K, V, O, # [B, H, N, D]
M, L, # row max and row exp-sum
stride_qb, stride_qh, stride_qn, stride_qd,
stride_kb, stride_kh, stride_kn, stride_kd,
stride_vb, stride_vh, stride_vn, stride_vd,
stride_ob, stride_oh, stride_on, stride_od,
stride_mb, stride_mh,
N,
D: tl.constexpr, # head dimension (feature length per token); used in scale = 1/sqrt(D)
BLOCK_M: tl.constexpr, # Q tile rows B_r
BLOCK_N: tl.constexpr, # K/V tile rows B_c
BLOCK_D: tl.constexpr, # load/compute width on head dim; when D is a power of 2, BLOCK_D==D; else BLOCK_D > D and excess positions are masked
):
# ========== V1 parallel partition: grid = (batch, head) ==========
# Each program serially sweeps all K/V and Q tiles for one (batch, head)
batch_id = tl.program_id(0)
head_id = tl.program_id(1)
# Base offsets of current (batch, head) on each tensor
qkvo_bh_q = batch_id * stride_qb + head_id * stride_qh
qkvo_bh_k = batch_id * stride_kb + head_id * stride_kh
qkvo_bh_v = batch_id * stride_vb + head_id * stride_vh
qkvo_bh_o = batch_id * stride_ob + head_id * stride_oh
ml_bh = batch_id * stride_mb + head_id * stride_mh
d_range = tl.arange(0, BLOCK_D)
scale = 1.0 / math.sqrt(D)
# ============ Outer loop: iterate K/V tiles (V1 key design) ============
for kv_start in range(0, N, BLOCK_N):
kv_range = kv_start + tl.arange(0, BLOCK_N)
kv_mask = kv_range < N
# Load K_j, V_j once; stay in registers/SRAM; reused by all Q tiles
k = tl.load(K + qkvo_bh_k + kv_range[:, None] * stride_kn + d_range[None, :] * stride_kd,
mask=kv_mask[:, None] & (d_range[None, :] < D), other=0.0)
v = tl.load(V + qkvo_bh_v + kv_range[:, None] * stride_vn + d_range[None, :] * stride_vd,
mask=kv_mask[:, None] & (d_range[None, :] < D), other=0.0)
# ============ Inner loop: iterate Q tiles ============
for q_start in range(0, N, BLOCK_M):
q_range = q_start + tl.arange(0, BLOCK_M)
q_mask = q_range < N
# Load Q_i, O_i, m_i, l_i — in V1 every outer iter reloads from HBM
q = tl.load(Q + qkvo_bh_q + q_range[:, None] * stride_qn + d_range[None, :] * stride_qd,
mask=q_mask[:, None] & (d_range[None, :] < D), other=0.0)
o_i = tl.load(O + qkvo_bh_o + q_range[:, None] * stride_on + d_range[None, :] * stride_od,
mask=q_mask[:, None] & (d_range[None, :] < D), other=0.0).to(tl.float32)
m_i = tl.load(M + ml_bh + q_range, mask=q_mask, other=float('-inf'))
l_i = tl.load(L + ml_bh + q_range, mask=q_mask, other=0.0)
# S_ij = Q_i @ K_j^T / sqrt(d)
s = tl.dot(q, tl.trans(k)) * scale
# Mask out-of-range K/V columns so fake 0s do not pollute the softmax denominator
s = tl.where(kv_mask[None, :], s, float('-inf'))
# Online Softmax update
m_new = tl.maximum(m_i, tl.max(s, axis=1))
alpha = tl.exp(m_i - m_new)
p = tl.exp(s - m_new[:, None])
l_new = alpha * l_i + tl.sum(p, axis=1)
# Output correction + accumulate (matches §5.1 Step 5)
o_new = o_i * (alpha * l_i / l_new)[:, None] \
+ tl.dot(p.to(v.dtype), v) / l_new[:, None]
# Write updated O_i, m_i, l_i back to HBM for the next outer iter
tl.store(O + qkvo_bh_o + q_range[:, None] * stride_on + d_range[None, :] * stride_od,
o_new.to(O.dtype.element_ty),
mask=q_mask[:, None] & (d_range[None, :] < D))
tl.store(M + ml_bh + q_range, m_new, mask=q_mask)
tl.store(L + ml_bh + q_range, l_new, mask=q_mask)
def flash_attn_v1(Q, K, V, BLOCK_M=64, BLOCK_N=64):
# Q, K, V: [B, H, N, D]
B, H, N, D = Q.shape
O = torch.zeros_like(Q)
M = torch.full((B, H, N), float('-inf'), device=Q.device, dtype=torch.float32)
L = torch.zeros((B, H, N), device=Q.device, dtype=torch.float32)
BLOCK_D = triton.next_power_of_2(D)
grid = (B, H) # V1 parallel: only on batch×head
flash_attn_v1_kernel[grid](
Q, K, V, O, M, L,
Q.stride(0), Q.stride(1), Q.stride(2), Q.stride(3),
K.stride(0), K.stride(1), K.stride(2), K.stride(3),
V.stride(0), V.stride(1), V.stride(2), V.stride(3),
O.stride(0), O.stride(1), O.stride(2), O.stride(3),
M.stride(0), M.stride(1),
N, D=D, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, BLOCK_D=BLOCK_D,
)
return O
📌 Code takeaways:
- V1 parallel partition: grid is
(batch, head), K/V outer, Q inner — each K/V tile loads from HBM once, but Q/O/m/l are read/written times - Auxiliary M, L: stats for Q must be shared across outer iters, so persist
[B, H, N]tensors in HBM - K/V OOB mask (
tl.where(kv_mask, s, -inf)) is critical for correctness; missing it pollutes the softmax denominator with fake zeros - Initial
m, lare-infand0; withalpha = exp(-inf - m_new) = 0, the first iter naturally becomes a normal accumulate - V1 limit:
(batch, head)is usually far below SM count (A100 has 108 SMs; batch×head in inference is often only tens), so SM utilization is low; V2 puts Q on the grid to fix that
📝 Summary
FlashAttention V1 rethinks Attention through IO-Awareness and proposes Tiling + Online Softmax:
- No full intermediate matrix — tiled streaming avoids memory
- Online Softmax stays exact — maintain and correct local stats for results equivalent to standard Softmax
- Recompute instead of store — backward recomputes and , trading a little compute for large memory savings
- Asymptotically optimal IO — HBM traffic drops from to
🎯 Self-check list
- Explain why standard Attention is Memory-Bound rather than Compute-Bound
- Manually derive the Online Softmax recurrence update
- Show how tile sizes , follow from SRAM capacity
- Describe what the outer and inner loops traverse in FlashAttention forward
- Explain why recomputation does not lengthen wall-clock time
- Derive FlashAttention HBM IO complexity
- Implement a simplified FlashAttention forward Kernel in Triton or CUDA
- State the limitation of outer-looping K/V in FlashAttention V1