FlashAttention Implementation › GPU Memory Hierarchy

Michael BrenndoerferUpdated June 30, 20255 min read
Language AI HandbookAll chapters
Part 17 of 67Efficient Attention
Chapter 125FlashAttention Implementation › GPU Memory Hierarchy
Reading section 2 of 10 · GPU Memory Hierarchy · 5 minSection 2 of 10 · 5 min

Book contents

Section 2 of 10 · 5 min read

FlashAttention Implementation › GPU Memory HierarchyOne section · Sections in this chapter2 / 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

Before understanding FlashAttention, you need to understand where bottlenecks occur on GPUs. Modern deep learning is often described as "compute-bound," but attention computation is "memory-bound." That classification determines whether arithmetic or memory traffic is the limiting factor.

Memory-Bound vs Compute-Bound

A computation is compute-bound when the limiting factor is the number of arithmetic operations the processor can perform. A computation is memory-bound when the limiting factor is how quickly data can be transferred to and from memory. Attention is memory-bound because we spend more time moving data than computing with it.

GPUs have a hierarchical memory structure with three main levels, each with different speeds and capacities:

In[3]:
Code
def gpu_memory_specs():
    """
    Approximate specifications for NVIDIA A100 GPU memory hierarchy.
    These numbers illustrate the dramatic differences between memory levels.
    """
    specs = {
        "HBM (High Bandwidth Memory)": {
            "capacity_gb": 80,
            "bandwidth_tb_s": 2.0,
            "latency_ns": 200,
        },
        "L2 Cache": {
            "capacity_gb": 0.040,  # 40 MB
            "bandwidth_tb_s": 4.0,
            "latency_ns": 50,
        },
        "SRAM (Registers + Shared Memory)": {
            "capacity_gb": 0.000192,  # 192 KB per SM, ~108 SMs
            "bandwidth_tb_s": 19.0,
            "latency_ns": 5,
        },
    }
    return specs


specs = gpu_memory_specs()
Out[4]:
Console
NVIDIA A100 GPU Memory Hierarchy:

Level                                        Capacity       Bandwidth      Latency
----------------------------------------------------------------------------------
HBM (High Bandwidth Memory)                     80 GB        2.0 TB/s       200 ns
L2 Cache                                      40.0 MB        4.0 TB/s        50 ns
SRAM (Registers + Shared Memory)               0.2 MB       19.0 TB/s         5 ns

The numbers reveal a fundamental tension. HBM (the main GPU memory where tensors live) has enormous capacity but relatively slow access. SRAM (registers and shared memory) is much faster but tiny. The speed difference is nearly 10x, while the capacity difference is over 400,000x. Standard attention computation repeatedly reads and writes to HBM, suffering the slow access speeds. FlashAttention restructures the computation to keep data in SRAM as long as possible.

Out[5]:
Visualization
Bar chart comparing GPU memory levels by bandwidth and capacity, showing SRAM with highest bandwidth but smallest capacity.
GPU memory hierarchy showing the trade-off between capacity and bandwidth. FlashAttention exploits this hierarchy by computing attention in small blocks that fit in SRAM, avoiding repeated HBM transfers.

Why Standard Attention is Memory-Bound

Consider what happens during standard attention computation:

  1. Load Q, K, V from HBM to compute S=QKTS = QK^T
  2. Write S back to HBM (this is an n×nn \times n matrix)
  3. Load S from HBM to compute softmax
  4. Write attention weights P back to HBM
  5. Load P and V from HBM to compute output
  6. Write output back to HBM

Each step involves transfers to and from the slow HBM. The n×nn \times n matrices S and P are particularly problematic: for a sequence of 4096 tokens with 32-bit floats, each matrix is 64 MB. With 32 attention heads, that's 2 GB just for intermediate storage, per layer, per batch element.

In[6]:
Code
def memory_transfers_standard_attention(n, d, dtype_bytes=4):
    """
    Calculate memory transferred in standard attention.

    Args:
        n: Sequence length
        d: Model dimension
        dtype_bytes: Bytes per element (4 for fp32, 2 for fp16)

    Returns:
        Dictionary with memory transfer statistics
    """
    # Input matrices Q, K, V: each n x d
    input_read = 3 * n * d * dtype_bytes

    # Score matrix S = QK^T: n x n, written then read for softmax
    score_write = n * n * dtype_bytes
    score_read = n * n * dtype_bytes

    # Attention weights P after softmax: n x n, written then read
    weights_write = n * n * dtype_bytes
    weights_read = n * n * dtype_bytes

    # V read again for P @ V
    v_read = n * d * dtype_bytes

    # Output: n x d
    output_write = n * d * dtype_bytes

    total = (
        input_read
        + score_write
        + score_read
        + weights_write
        + weights_read
        + v_read
        + output_write
    )

    return {
        "input_read_mb": input_read / 1e6,
        "intermediate_mb": (
            score_write + score_read + weights_write + weights_read
        )
        / 1e6,
        "output_mb": output_write / 1e6,
        "total_mb": total / 1e6,
    }


# Example: typical transformer settings
transfers = memory_transfers_standard_attention(n=4096, d=768, dtype_bytes=4)
Out[7]:
Console
Memory Transfers in Standard Attention (n=4096, d=768, fp32):

  Input Q, K, V:         37.7 MB
  Intermediate S, P:    268.4 MB
  Output:                12.6 MB
  Total transfers:      331.4 MB

The n×n intermediate matrices dominate memory traffic.

The intermediate matrices consume over 128 MB of memory transfers, dwarfing the input and output. This is where FlashAttention intervenes: by never materializing the full n×nn \times n matrices, it eliminates most of this memory traffic.