FlashAttention Implementation › How FlashAttention Uses Tiling
Language AI HandbookAll chapters FlashAttention Implementation › How FlashAttention Uses TilingOne section · Sections in this chapter4 / 10 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
We've established that standard attention is memory-bound: the algorithm spends most of its time moving data between slow HBM and fast SRAM, rather than performing arithmetic. The attention matrix is the culprit, requiring multiple read-write cycles to HBM. FlashAttention's key insight starts with a simple question: what if we never stored that matrix at all?
The answer lies in a technique called tiling. Rather than computing all attention scores at once and then applying softmax, FlashAttention processes attention in small chunks that fit entirely in SRAM. Each chunk computes a portion of the final output, and careful bookkeeping ensures the pieces combine to produce exactly the same result as standard attention.
The Tiling Strategy
To understand how tiling works, imagine you're computing attention for a 4096-token sequence. Standard attention would:
- Compute all million attention scores
- Store this entire matrix in memory
- Apply softmax across each row
- Multiply by the value matrix
FlashAttention takes a different approach. Instead of processing all tokens at once, it divides the computation into manageable blocks:
- Query blocks: Groups of consecutive query positions (rows of Q)
- Key-Value blocks: Groups of consecutive key-value positions (rows of K and V)
For each query block, the algorithm iterates through all key-value blocks, computing partial attention scores and accumulating results. The block sizes and are chosen specifically so that all active data fits in SRAM at once.
import numpy as np
def compute_block_sizes(sram_bytes, d, dtype_bytes=2):
"""
Compute optimal block sizes for FlashAttention given SRAM constraints.
Args:
sram_bytes: Available SRAM per streaming multiprocessor
d: Head dimension
dtype_bytes: Bytes per element
Returns:
Block sizes B_r and B_c
"""
# We need to fit:
# - Q block: B_r x d
# - K block: B_c x d
# - V block: B_c x d
# - Output accumulator: B_r x d
# - Statistics (m, l): B_r x 2
# - Score block: B_r x B_c
# Simplified calculation assuming square blocks (B_r = B_c = B)
# 3*B*d + B*d + 2*B + B*B = SRAM
# Solving: B^2 + 4*B*d + 2*B = SRAM/dtype_bytes
# For d=64, typical block size is 64-128
# Exact calculation depends on hardware and implementation details
elements_available = sram_bytes // dtype_bytes
# Approximate: prioritize fitting Q, K, V, O blocks
# B * d * 4 should fit comfortably
B = int(np.sqrt(elements_available / (4 + d)))
B = min(B, 128) # Cap at typical maximum
B = max(B, 16) # Minimum for efficiency
return B, B
# A100 has ~192KB SRAM per SM
sram_per_sm = 192 * 1024
d_head = 64
B_r, B_c = compute_block_sizes(sram_per_sm, d_head, dtype_bytes=2)FlashAttention Block Size Calculation: SRAM available: 192 KB Head dimension: 64 Data type: fp16 (2 bytes) Computed block sizes: B_r = 38, B_c = 38 Each Q block: 38 x 64 = 4.0 KB Each K block: 38 x 64 = 4.0 KB

With 192 KB of SRAM available, we can fit query and key-value blocks of size 128, plus room for intermediate computations. The critical observation is that these block sizes are independent of sequence length: whether processing 1K or 100K tokens, each tile uses the same fixed amount of fast memory.
But there's a catch. Tiling the attention computation creates a fundamental problem that lies at the heart of FlashAttention's innovation.
The Online Softmax Trick
Here's the challenge: softmax requires global information, but we're processing local tiles.
Consider what softmax does. It converts a vector of raw scores into a probability distribution where all values sum to 1. Given a vector of attention scores, standard softmax computes each output element as:
where:
- : the -th attention score (the value we want to convert to a probability)
- : the exponential of . This keeps all values become positive
- : the sum of exponentials across all scores, serving as the normalizing constant
- : the total number of scores (equal to sequence length in attention)
The denominator is the problem. To compute the softmax output for any single element, we need , the sum of exponentials across all scores. But when processing tiles, we don't have all scores at once. We see positions 1-64, then 65-128, then 129-192, and so on. How can we compute a global normalization constant from local pieces?
FlashAttention solves this with the online softmax algorithm, a technique that maintains running statistics and retroactively corrects previous computations as new information arrives. The key insight is that softmax can be decomposed and incrementally refined.
Building Intuition: Two Blocks
Let's start with the simplest case. Imagine we have just two blocks of scores, and . The full softmax over both is:
Now suppose we process first, before is available. At that moment, is the only score we know, so its "local softmax" is , a probability of 100%. This is wrong, of course, but we didn't have complete information.
When arrives, we can fix our mistake. The correct probability for should be . We can get this by multiplying our initial answer (1) by the correction factor:
This factor adjusts for the new, larger denominator. The pattern generalizes: as each new block arrives, we correct all previous accumulations by the ratio of old denominator to new denominator.
The Running Statistics
To make this correction efficient, FlashAttention tracks two running statistics for each query position:
- : the running maximum score seen so far
- : the running sum of exponentials (shifted by the maximum for numerical stability)
Why track the maximum? Exponentials grow quickly: is extremely large while is essentially zero. Without care, we'd overflow or underflow. By subtracting the maximum before exponentiating, we ensure all values remain in a reasonable range. This is the same trick used in numerically stable softmax implementations.
As each new tile of scores arrives, FlashAttention updates these statistics:
where:
- : the running maximum from previous blocks
- : the updated maximum after seeing the current block
- : the running sum of exponentials from previous blocks
- : the updated sum after incorporating the current block
- : the vector of attention scores in the current block
- : the correction factor that rescales previous accumulations
Why the Correction Factor Works
The correction factor is the mathematical heart of online softmax. Let's understand why it works.
When the new block contains a score larger than any we've seen before, . The difference is negative, so . This makes sense: we're discovering that the denominator should be larger than we thought, so previous contributions need to be scaled down.
When the new block has no scores exceeding our current maximum, . The correction factor becomes , leaving previous accumulations unchanged. We're simply adding to the sum without revising history.
Importantly, corrections compound correctly. If we process blocks A, then B, then C, the final answer is identical to processing them all at once. The mathematics guarantees exact equivalence, not an approximation.
Seeing It in Action
Let's trace through the online softmax algorithm on a concrete example. We'll process three blocks of random scores and watch the running statistics evolve:
def online_softmax_demo(blocks):
"""
Demonstrate online softmax computation used in FlashAttention.
Args:
blocks: List of arrays, each containing a block of scores
Returns:
Final softmax values and running statistics
"""
# Initialize
m_prev = -np.inf # Running maximum
l_prev = 0.0 # Running sum of exponentials
history = []
for i, block in enumerate(blocks):
# New maximum (for numerical stability)
m_new = max(m_prev, np.max(block))
# Correction factor for previous accumulations
correction = np.exp(m_prev - m_new)
# Update sum of exponentials
l_new = correction * l_prev + np.sum(np.exp(block - m_new))
history.append(
{
"block": i,
"block_max": np.max(block),
"m_prev": m_prev,
"m_new": m_new,
"correction": correction,
"l_new": l_new,
}
)
m_prev = m_new
l_prev = l_new
return m_prev, l_prev, history
# Example: three deterministic blocks of scores
blocks = [np.random.randn(4) for _ in range(3)]
m_final, l_final, history = online_softmax_demo(blocks)Online Softmax Computation: Processing attention scores in blocks... Block 0: Block max: 1.523 Running max: -inf → 1.523 Correction factor: 0.0000 Running sum: 1.9649 Block 1: Block max: 1.579 Running max: 1.523 → 1.579 Correction factor: 0.9454 Running sum: 3.6279 Block 2: Block max: 0.543 Running max: 1.579 → 1.579 Correction factor: 1.0000 Running sum: 4.3705 Verification: Standard sum: 4.3705 Online sum: 4.3705 Match: True
The output traces the algorithm's journey through three blocks. Notice how the running maximum updates whenever a block contains a larger value, triggering a correction factor less than 1. The running sum grows as we accumulate more exponentials. At the end, we verify that our online computation produces exactly the same sum as computing softmax over all scores at once.
The match confirms that online softmax is exact, not an approximation. It computes the same result as standard softmax, just in a different order that avoids storing all scores simultaneously.


Memory Complexity Reduction
We've now seen the two key ingredients of FlashAttention: tiling to process attention in SRAM-sized chunks, and online softmax to correctly combine partial results. Together, they enable exact attention computation without ever storing the matrix.
Let's inventory exactly what FlashAttention keeps in memory at any moment during computation:
| Component | Size | Purpose |
|---|---|---|
| Q block | Current query vectors being processed | |
| K block | Current key vectors for dot products | |
| V block | Current value vectors for weighted sum | |
| Output accumulator | Running weighted sum for current queries | |
| Statistics | Running and for each query | |
| Score tile | Attention scores for current block pair |
For typical settings with and , this totals about 25 KB, fitting comfortably in SRAM. The memory complexity drops from in standard attention to in FlashAttention. More precisely:
where:
- : the sequence length
- : the head dimension (typically 64-128)
- : the block size for queries (number of query rows processed together)
- : the block size for keys/values (number of key-value rows loaded at once)
- : memory for the output accumulator (one -dimensional vector per query)
- : memory for the current score tile being computed
Since , , and are constants determined by hardware constraints (not sequence length), the dominant term is , giving linear scaling with sequence length.
This is the payoff of the entire approach. Standard attention requires memory for the attention matrix, limiting sequence length to what fits in GPU memory. FlashAttention's memory means you can process arbitrarily long sequences, with memory growing only linearly. A 10x longer sequence needs only 10x more memory, not 100x.
Let's verify this with concrete numbers:
def compare_memory_complexity(
sequence_lengths, d=64, B_r=64, B_c=64, dtype_bytes=2
):
"""
Compare memory requirements of standard vs FlashAttention.
"""
results = []
for n in sequence_lengths:
# Standard attention: stores full n x n matrix
standard_mb = n * n * dtype_bytes / 1e6
# FlashAttention: only tile + output + stats
# Q block + K block + V block + output + stats + score tile
flash_elements = B_r * d + B_c * d + B_c * d + n * d + n * 2 + B_r * B_c
flash_mb = flash_elements * dtype_bytes / 1e6
results.append(
{
"n": n,
"standard_mb": standard_mb,
"flash_mb": flash_mb,
"ratio": standard_mb / flash_mb,
}
)
return results
seq_lengths = [512, 1024, 2048, 4096, 8192, 16384]
memory_comparison = compare_memory_complexity(seq_lengths)Attention Matrix Memory: Standard vs FlashAttention (fp16)
Seq Len Standard Flash Reduction
--------------------------------------------------
512 0.5M 0.10M 5x
1,024 2.1M 0.17M 12x
2,048 8.4M 0.30M 28x
4,096 33.6M 0.57M 59x
8,192 134.2M 1.11M 120x
16,384 536.9M 2.20M 245xThe table reveals the dramatic memory savings FlashAttention provides. At 512 tokens, standard attention requires only 0.5 MB for the attention matrix, making FlashAttention's overhead relatively larger. But as sequence length doubles, standard attention memory quadruples while FlashAttention grows linearly. By 16K tokens, standard attention consumes over 500 MB just for the attention matrix, while FlashAttention uses only about 2 MB, a reduction of over 250x. This difference determines whether a model fits in GPU memory at all.

