Online Softmax (the FlashAttention Trick) - LLM Internals
Implement Online Softmax (the FlashAttention Trick) in Python with NumPy. hard exercise with examples, hints, and instant feedback. No setup or signup required.
Online Softmax: the trick behind FlashAttention
What you'll build
Two functions that compute a softmax in one streaming pass over blocks: online_softmax for a 1-D vector, and online_attention, which computes exact attention block by block over the keys without ever forming the full score matrix.
Background
Standard attention materializes the (n_q, n_k) score matrix, then takes a row softmax. For long sequences that matrix doesn't fit in fast on-chip memory (SRAM), and moving it to and from slow memory (HBM) dominates the runtime. FlashAttention (Dao et al., 2022) walks over the keys in blocks and keeps three running statistics per query. It is exact, not an approximation; it saves memory traffic, not FLOPs. The enabling idea is the online softmax (Milakov & Gimelshein, 2018): when the running max increases, rescale everything accumulated so far by exp(m_old - m_new).
Formula: online_softmax(x, block_size)
Start with a running max m = -inf and running sum l = 0. For each block b of x (the last block may be shorter):
m_new = max(m, max(b))
l = l * exp(m - m_new) + sum(exp(b - m_new)) # rescale the old sum to the new max
m = m_new
After the pass: softmax(x) = exp(x - m) / l.
Formula: online_attention(Q, K, V, block_size)
Keep per query row a running max m (n_q, 1), a running denominator l (n_q, 1) and an unnormalized output acc (n_q, d_v). For each block (K_b, V_b) of block_size keys:
S = Q @ K_b.T / sqrt(d) # (n_q, B)
m_new = maximum(m, rowmax(S)) # (n_q, 1)
scale = exp(m - m_new) # shrink the old statistics
P = exp(S - m_new) # (n_q, B)
l = l * scale + rowsum(P)
acc = acc * scale + P @ V_b # (n_q, d_v)
m = m_new
At the end, output = acc / l, which equals softmax(Q K^T / sqrt(d)) @ V up to float rounding.
Inputs
x: 1-D float array;block_size: positive intQ(n_q, d),K(n_k, d),V(n_k, d_v);block_sizekeys per block
Expected Return Format
online_softmax(x, block_size): 1-D array, same shape asx, sums to 1online_attention(Q, K, V, block_size): array(n_q, d_v)
Edge cases and conventions
exp(-inf - m_new) = 0, so the first block needs no special case.- Any
block_size(1, a divisor ofn_k, or larger thann_k) must give the same answer. - Logits around 1000 must not overflow.
Constraints
NumPy only. Process the keys block by block; don't take a softmax over a whole row of scores at once.
Follow-up
- What is the memory footprint of the score matrix for
n = 32ktokens and 32 heads in fp16? What does FlashAttention store instead? - FlashAttention-2 delays the division by
lto the very end and parallelises over query blocks. Why does that help?
Examples
Input: online_softmax(np.array([1000.0, 1001.0, 1002.0]), block_size=2) Output: [0.09, 0.2447, 0.6652]
Every exp is taken relative to the running max, so huge logits are fine.
Input: online_softmax(np.array([0.0, 1.0, 5.0, 2.0, 9.0]), block_size=2) Output: [0.0001, 0.0003, 0.018, 0.0009, 0.9807]
The max changes from 1 to 5 to 9; each time, the running sum is rescaled by exp(m_old - m_new).
Practice in Python
import numpy as np
def online_softmax(x, block_size):
"""
Softmax of a 1-D array computed with one streaming pass over blocks
(running max + running sum), then one normalization pass.
Returns:
probs: same shape as x, sums to 1
"""
# Your code here
pass
def online_attention(Q, K, V, block_size):
"""
Attention softmax(Q K^T / sqrt(d)) V computed block by block over the keys,
FlashAttention style.
Args:
Q: (n_q, d), K: (n_k, d), V: (n_k, d_v)
block_size: number of keys per block
Returns:
output: (n_q, d_v)
"""
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 7 tests, or use the progressive hints.