FlashAttention Implementation › How FlashAttention Uses Tiling

Michael BrenndoerferUpdated June 30, 202519 min read
Language AI HandbookAll chapters
Part 17 of 67Efficient Attention
Chapter 125FlashAttention Implementation › How FlashAttention Uses Tiling
Reading section 4 of 10 · How FlashAttention Uses Tiling · 19 minSection 4 of 10 · 19 min

Book contents

Section 4 of 10 · 19 min read

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 n×nn \times n 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:

  1. Compute all 4096×4096=16.74096 \times 4096 = 16.7 million attention scores
  2. Store this entire matrix in memory
  3. Apply softmax across each row
  4. 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 BrB_r consecutive query positions (rows of Q)
  • Key-Value blocks: Groups of BcB_c 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 BrB_r and BcB_c are chosen specifically so that all active data fits in SRAM at once.

In[12]:
Code
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)
Out[13]:
Console
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
Out[14]:
Visualization
Grid showing attention matrix divided into blocks, with one tile highlighted as currently being processed in SRAM.
FlashAttention tiles the attention matrix into blocks. For each query block (row), the algorithm iterates through all key-value blocks (columns), computing partial attention scores. Only one tile (shaded) is in SRAM at any time.

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:

softmax(xi)=exi∑j=1nexj\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{n} e^{x_j}}

where:

  • xix_i: the ii-th attention score (the value we want to convert to a probability)
  • exie^{x_i}: the exponential of xix_i. This keeps all values become positive
  • ∑j=1nexj\sum_{j=1}^{n} e^{x_j}: the sum of exponentials across all nn scores, serving as the normalizing constant
  • nn: 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 ∑j=1nexj\sum_{j=1}^{n} e^{x_j}, the sum of exponentials across all nn 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, xx and yy. The full softmax over both is:

softmax([x,y])=[exex+ey,eyex+ey]\text{softmax}([x, y]) = \left[\frac{e^{x}}{e^x + e^y}, \frac{e^y}{e^x + e^y}\right]

Now suppose we process xx first, before yy is available. At that moment, xx is the only score we know, so its "local softmax" is ex/ex=1e^x / e^x = 1, a probability of 100%. This is wrong, of course, but we didn't have complete information.

When yy arrives, we can fix our mistake. The correct probability for xx should be ex/(ex+ey)e^x / (e^x + e^y). We can get this by multiplying our initial answer (1) by the correction factor:

correction=exex+ey\text{correction} = \frac{e^x}{e^x + e^y}

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:

  • mm: the running maximum score seen so far
  • ll: the running sum of exponentials (shifted by the maximum for numerical stability)

Why track the maximum? Exponentials grow quickly: e100e^{100} is extremely large while e−100e^{-100} 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 s\mathbf{s} arrives, FlashAttention updates these statistics:

mnew=max⁡(mold,max⁡(s))m_{\text{new}} = \max(m_{\text{old}}, \max(\mathbf{s})) lnew=emold−mnew⋅lold+∑iesi−mnewl_{\text{new}} = e^{m_{\text{old}} - m_{\text{new}}} \cdot l_{\text{old}} + \sum_i e^{s_i - m_{\text{new}}}

where:

  • moldm_{\text{old}}: the running maximum from previous blocks
  • mnewm_{\text{new}}: the updated maximum after seeing the current block
  • loldl_{\text{old}}: the running sum of exponentials from previous blocks
  • lnewl_{\text{new}}: the updated sum after incorporating the current block
  • s\mathbf{s}: the vector of attention scores in the current block
  • emold−mnewe^{m_{\text{old}} - m_{\text{new}}}: the correction factor that rescales previous accumulations

Why the Correction Factor Works

The correction factor emold−mnewe^{m_{\text{old}} - m_{\text{new}}} 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, mnew>moldm_{\text{new}} > m_{\text{old}}. The difference mold−mnewm_{\text{old}} - m_{\text{new}} is negative, so emold−mnew<1e^{m_{\text{old}} - m_{\text{new}}} < 1. 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, mnew=moldm_{\text{new}} = m_{\text{old}}. The correction factor becomes e0=1e^0 = 1, 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:

In[15]:
Code
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)
Out[16]:
Console
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 mm updates whenever a block contains a larger value, triggering a correction factor less than 1. The running sum ll 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.

Out[17]:
Visualization
Step-by-step diagram showing how online softmax corrects running statistics as each block is processed.
Online softmax maintains running statistics (maximum m and sum l) as blocks are processed. Previous accumulations are corrected when new blocks reveal larger values. This keeps exact results without storing all scores.
Out[18]:
Visualization
Two-panel plot showing convergence of running maximum and running sum statistics across 16 blocks.
Online softmax statistics converge as blocks are processed. The running maximum (m) increases when new blocks contain larger scores, while the running sum (ℓ) accumulates exponentials. The final values exactly match what standard softmax would compute.

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 n×nn \times n matrix.

Let's inventory exactly what FlashAttention keeps in memory at any moment during computation:

FlashAttention SRAM memory footprint. Only these components reside in fast memory at any moment.
ComponentSizePurpose
Q blockBr×dB_r \times dCurrent query vectors being processed
K blockBc×dB_c \times dCurrent key vectors for dot products
V blockBc×dB_c \times dCurrent value vectors for weighted sum
Output accumulatorBr×dB_r \times dRunning weighted sum for current queries
StatisticsBr×2B_r \times 2Running mm and ll for each query
Score tileBr×BcB_r \times B_cAttention scores for current block pair

For typical settings with Br=Bc=64B_r = B_c = 64 and d=64d = 64, this totals about 25 KB, fitting comfortably in SRAM. The memory complexity drops from O(n2)O(n^2) in standard attention to O(n)O(n) in FlashAttention. More precisely:

Mflash=O(n⋅d+Br⋅Bc)M_{\text{flash}} = O(n \cdot d + B_r \cdot B_c)

where:

  • nn: the sequence length
  • dd: the head dimension (typically 64-128)
  • BrB_r: the block size for queries (number of query rows processed together)
  • BcB_c: the block size for keys/values (number of key-value rows loaded at once)
  • n⋅dn \cdot d: memory for the output accumulator (one dd-dimensional vector per query)
  • Br⋅BcB_r \cdot B_c: memory for the current score tile being computed

Since BrB_r, BcB_c, and dd are constants determined by hardware constraints (not sequence length), the dominant term is n⋅dn \cdot d, giving linear scaling with sequence length.

This is the payoff of the entire approach. Standard attention requires O(n2)O(n^2) memory for the attention matrix, limiting sequence length to what fits in GPU memory. FlashAttention's O(n)O(n) 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:

In[19]:
Code
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)
Out[20]:
Console
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         245x

The 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.

Out[21]:
Visualization
Heatmap of attention scores with grid lines showing tile boundaries and one tile highlighted as currently in SRAM.
Attention scores shown as a heatmap with tile boundaries. FlashAttention computes one tile at a time (highlighted), never storing the full matrix. The causal pattern (lower triangle) shows positions can only attend to earlier positions.
Out[22]:
Visualization
Log-scale plot showing quadratic memory growth for standard attention versus linear growth for FlashAttention.
Memory usage comparison between standard attention and FlashAttention. Standard attention grows quadratically with sequence length, while FlashAttention grows linearly. The gap widens significantly at longer sequences.