Learn: The GPU Memory Hierarchy

What you're learning

Where every byte lives on a GPU, how fast you can reach it, and why moving data — not doing math — is what actually bounds most LLM kernels. By the end you should be able to look at a kernel, name which level of the hierarchy dominates its runtime, and explain why FlashAttention exists.

This lesson assumes 01-gpu-architecture-and-simt — SMs, warps, the SIMT execution model, and occupancy. We now zoom in on the thing those warps are constantly waiting on: memory.


1. Learning map

graph TD
    A["Lesson 01<br/>SMs, warps, occupancy"] --> B["The hierarchy<br/>registers → SMEM/L1 → L2 → HBM → host"]
    B --> C["Registers<br/>per-thread, ~1 cycle"]
    B --> D["Shared memory / L1<br/>per-SM scratchpad, banked"]
    B --> E["L2 cache<br/>GPU-wide"]
    B --> F["Global memory / HBM<br/>the bandwidth wall"]
    C --> C1["Register pressure<br/>→ spills → occupancy"]
    D --> D1["Bank conflicts<br/>→ serialization → padding"]
    F --> F1["Coalescing<br/>32 threads → sectors/cache lines"]
    C1 --> G["The memory wall<br/>FLOPs ≫ bandwidth growth"]
    D1 --> G
    F1 --> G
    G --> H["Most LLM kernels are<br/>memory-bound"]
    H --> I["→ Lesson 05: GEMM tiling"]
    H --> J["→ Lesson 06: FlashAttention"]
    H --> K["→ Lesson 03: Roofline"]

    style B fill:#44a,color:#fff
    style G fill:#a44,color:#fff
    style H fill:#4a4,color:#fff

2. Why this matters

Here is the single most important number in GPU performance engineering. An H100 SXM5 delivers roughly 990 TFLOP/s of dense BF16/FP16 tensor-core throughput and reads from HBM3 at roughly 3.35 TB/s.1 Divide them:

This is the ridge point of the roofline (lesson 03). It says: for every byte you pull from HBM, you must do ~300 floating-point operations just to keep the tensor cores fed. If your kernel does fewer FLOPs per byte than that, it is memory-bound — the tensor cores sit idle waiting for data, and buying more compute buys you nothing.

Most of the operations in a transformer — elementwise activations, LayerNorm, softmax, residual adds, and even attention at inference — fall well below 295 FLOP/byte. They are limited by how fast you can move bytes, not how fast you can multiply. So understanding where bytes live and how to move them cheaply is the whole game.

The one-sentence version

On modern accelerators, a FLOP is nearly free and a byte from HBM is expensive — in both time and energy. Performance engineering is mostly the art of not touching HBM.


3. The hierarchy, top to bottom

Every GPU has a memory hierarchy that trades capacity for speed: the fastest storage is tiny and private, the largest is slow and shared. All numbers below are for the H100 (Hopper, SXM5);12 the shape is the same across NVIDIA generations, only the constants move.

graph TD
    R["<b>Registers</b> — per thread<br/>~256 KB/SM (65,536 × 32-bit)<br/>latency ~1 cycle · BW ~tens of TB/s per SM"]
    S["<b>Shared memory / L1</b> — per SM<br/>up to 228 KB SMEM (256 KB unified L1+SMEM)<br/>latency ~20–30 cycles · BW ~19 TB/s aggregate"]
    L2["<b>L2 cache</b> — GPU-wide<br/>50 MB<br/>latency ~150–250 cycles · BW ~several TB/s"]
    H["<b>Global memory / HBM3</b> — GPU-wide<br/>80 GB<br/>latency ~450–800 cycles · BW ~3.35 TB/s"]
    P["<b>Host DRAM</b> — over PCIe / NVLink<br/>PCIe 5.0 ~64 GB/s · NVLink 4 ~900 GB/s<br/>latency ~µs (thousands of cycles)"]

    R -->|spill| S
    S --> L2
    L2 --> H
    H -->|H2D / D2H| P

    style R fill:#1a7,color:#fff
    style S fill:#2a6,color:#fff
    style L2 fill:#a83,color:#fff
    style H fill:#a44,color:#fff
    style P fill:#666,color:#fff
LevelScopeCapacity (H100)LatencyBandwidthManaged by
Registersper-thread256 KB/SM (64K × 4B)3~1 cycle~tens of TB/s per SM4compiler
Shared mem / L1per-SMup to 228 KB SMEM (256 KB unified)5~20–30 cycles4~19 TB/s aggregate4programmer (SMEM) / HW (L1)
L2 cacheGPU-wide50 MB2~150–250 cycles4~several TB/shardware
Global / HBM3GPU-wide80 GB1~450–800 cycles43.35 TB/s1programmer (allocations)
Host DRAMsystem100s of GB~µsPCIe5 ~64 GB/s, NVLink4 ~900 GB/s1programmer (copies)

Note the two enormous cliffs. Going register → SMEM is ~20–30×. Going SMEM → HBM is another ~20–40× in latency and ~150× in bandwidth. The entire discipline of GPU kernel optimization is: pull data down the pyramid once, reuse it as many times as possible up top, and write it back once.

Latency is hidden, bandwidth is not

A single HBM load takes ~500 cycles, but the SIMT model (lesson 01) hides that latency by swapping in other ready warps while one warp stalls — this is what occupancy buys you. What occupancy cannot fix is bandwidth: if a kernel must move bytes through a 3.35 TB/s pipe, no amount of parallelism moves them faster. Latency is a scheduling problem; bandwidth is a physics problem.


4. Registers and register pressure

Registers are the only storage a thread reads/writes at full ALU speed with ~1-cycle latency. Each SM has a fixed register file — on H100, 65,536 32-bit registers per SM (256 KB).3 This file is partitioned across all resident threads, which is the direct link back to occupancy from lesson 01.

If a kernel needs registers per thread, the number of threads that can be resident on one SM is bounded by:

Two examples on H100 (max 2048 resident threads/SM):3

  • : threads — register file does not limit occupancy.
  • : threads — you’re capped at 50% occupancy by registers alone.
  • : threads — 25% occupancy.

When a kernel needs more registers than the compiler can allocate (either it hit the per-thread cap of 255,3 or you forced higher occupancy with __launch_bounds__), the excess variables spill to “local memory.”

"Local memory" is a lie — it's global memory

Despite the name, local memory is not close to the thread. It is a per-thread private region that physically lives in HBM (cached in L1/L2).3 A register spill therefore turns a ~1-cycle access into a potential ~500-cycle HBM round trip. A spilling inner loop can be catastrophically slow. Check ptxas -v (or Nsight Compute) for spill stores/spill loads — nonzero counts in a hot kernel are a red flag.

There is a genuine tension here: more registers per thread → more values kept in fast storage and fewer recomputes, but → lower occupancy → less latency hiding. The optimum is workload-dependent and is exactly the kind of thing you sweep experimentally.


5. Shared memory: the programmer-managed scratchpad

Shared memory (SMEM) is on-chip SRAM, physically the same silicon as L1 — on H100 they share a 256 KB block per SM that you split (e.g. 228 KB SMEM / 28 KB L1, configurable via cudaFuncAttributePreferredSharedMemoryCarveout).53 The crucial distinction:

  • L1 is hardware-managed cache — you don’t control what’s in it.
  • SMEM is a software-managed scratchpadyou explicitly stage data into it (__shared__ arrays) and it stays until you’re done. It is shared by all threads in a block, making it the primary vehicle for inter-thread cooperation and data reuse.

The canonical use: load a tile of data from HBM into SMEM once, then have all threads in the block read it many times from SMEM instead of hammering HBM. This is the heart of tiled GEMM (lesson 05).

Banks and bank conflicts

To sustain its huge bandwidth, SMEM is split into 32 banks, one per lane of a warp. Banks are 4 bytes (32 bits) wide and interleaved by word:3

The hardware can service one access per bank per cycle. So a warp’s 32 accesses complete in a single transaction if and only if they hit 32 distinct banks (or all hit the same address, which is a broadcast).36 If threads in a warp hit different addresses in the same bank, those accesses serialize into transactions — a -way bank conflict costing the cycles.

graph LR
    subgraph "No conflict: stride 1"
        T0["t0→bank0"]:::ok
        T1["t1→bank1"]:::ok
        T2["t2→bank2"]:::ok
        Tn["...→bank31"]:::ok
    end
    subgraph "2-way conflict: stride 2"
        A0["t0→bank0"]:::bad
        A1["t1→bank2"]:::bad
        A16["t16→bank0"]:::bad
    end
    classDef ok fill:#2a6,color:#fff
    classDef bad fill:#a44,color:#fff

The classic offender is a column access of a 2D SMEM array whose row width is a multiple of 32. Consider float tile[32][32];. Threads reading a column tile[i][c] for fixed c, i = 0..31 access addresses c, 32+c, 64+c, ... — all of which map to bank c mod 32 → a 32-way conflict, a 32× slowdown.

Column-major access into a 32-wide SMEM tile is a 32-way bank conflict

Any time consecutive threads step through SMEM with a stride that shares a common factor with 32, you serialize. Diagonal/transpose patterns are the usual culprits (matrix transpose kernels, attention score tiles).

The fix: padding. Declare the array one element wider than needed — float tile[32][33];. Now row starts at offset , so column accesses land on addresses whose banks are — all distinct. One wasted column of SMEM buys conflict-free column access. This padding trick is ubiquitous in transpose and GEMM kernels.


6. Global memory and coalescing

Global memory (HBM) is where your tensors live. Its latency is ~500 cycles and — decisively — its bandwidth is the scarcest resource on the chip. The hardware does not fetch individual bytes; it moves data in fixed-size sectors of 32 bytes (and 128-byte cache lines = 4 sectors).36 How your warp’s 32 threads map onto these sectors determines your effective bandwidth.

Coalescing is the hardware combining the 32 addresses a warp issues in one memory instruction into the minimum number of sector transactions.6

Coalesced (ideal): the 32 threads of a warp read 32 consecutive 4-byte words → 128 contiguous bytes → exactly one 128-byte cache line = 4 sectors, all fully used. 100% of fetched bytes are useful.

Strided: if thread reads base + 4 * stride * t, consecutive threads land in different sectors. With stride = 32, all 32 threads touch 32 different 128-byte lines. You fetch bytes to deliver the bytes you actually wanted:

That is a 32× effective-bandwidth loss from address pattern alone — the DRAM pins move at full 3.35 TB/s, but 97% of the bytes crossing them are thrown away.

graph TD
    subgraph "Coalesced — 1 transaction"
        direction LR
        C["warp reads addr 0,4,8,...,124"] --> CL["1× 128B cache line<br/>100% useful"]
    end
    subgraph "Strided (32) — 32 transactions"
        direction LR
        S["warp reads addr 0,128,256,..."] --> SL["32× 128B lines<br/>3% useful"]
    end
    style CL fill:#2a6,color:#fff
    style SL fill:#a44,color:#fff

Uncoalesced access is the #1 beginner GPU performance bug

Row-major arrays where consecutive threads map to consecutive rows (not columns), Array-of-Structs layouts, and gather/scatter with random indices all destroy coalescing. The fix is usually a data-layout change: Struct-of-Arrays instead of Array-of-Structs, transposing which index the thread ID maps to, or staging through SMEM so the global access pattern is coalesced even if the SMEM pattern isn’t. Always make threadIdx.x (the fastest-varying thread index) map to the fastest-varying (contiguous) memory dimension.


7. The memory wall

Here is the historical trend that makes all of the above matter more every year. Peak compute has grown far faster than memory bandwidth:

GPU (flagship)YearPeak FP16/BF16 TCHBM BWRidge (FLOP/byte)
P1002016~21 TFLOP/s (FP16)7720 GB/s~29
V1002017125 TFLOP/s8900 GB/s~139
A1002020312 TFLOP/s92.0 TB/s~156
H1002022990 TFLOP/s13.35 TB/s~295
B2002024/25~2250 TFLOP/s (dense)10~8 TB/s~280

Over P100→H100, compute grew ~47× while bandwidth grew ~4.6×. The ridge point marches right: each generation, a kernel must do more arithmetic per byte to stay compute-bound. Ops that were compute-bound on older hardware silently become memory-bound on newer hardware. This gap — compute scaling faster than the memory feeding it — is the memory wall, and it is why “just add more FLOPs” stopped being a strategy.

The corollary for LLM engineers

As accelerators get faster, a growing fraction of your workload becomes bandwidth-limited. The lever that keeps mattering is arithmetic intensity (FLOPs per byte moved) — and you raise it by reuse (tiling, fusion), not by faster math.

Data movement dominates energy, too

It’s not just time. The energy to move a word from HBM dwarfs the energy of the arithmetic you do with it. Rough, order-of-magnitude figures (process-dependent, but the ratios are the point):

OperationEnergy (approx.)11
FP16/BF16 multiply-add (on-chip)~1 pJ
Read from register / SMEM~1–10 pJ
Read from L2~20–100 pJ
Read a word from HBM/DRAM~several hundred pJ (100–1000× a FLOP)
Move a word host↔device over PCIe~nJ (1000s× a FLOP)

So an off-chip access costs 2–3 orders of magnitude more energy than the FLOP it feeds. At datacenter scale this sets the power bill and the thermal envelope: minimizing HBM traffic is simultaneously a speed, power, and cost optimization. This is the deep reason “IO-aware” is the design principle behind modern kernels.


8. GPU/LLM connection

Now tie it to transformers directly.

Attention is memory-bandwidth-bound. Standard attention computes , , . The score/probability matrices are in size. Naively materializing them means writing to HBM, reading it back for softmax, writing , reading it back for the matmul. For sequence length and head dim , you move bytes but do only FLOPs — arithmetic intensity (tens to ~128), far below the ~295 ridge. The kernel spends its life waiting on HBM, and the HBM traffic is also the memory-capacity bottleneck for long context.

FlashAttention (→ lesson 06) is the direct payoff of everything in this lesson. It never materializes or in HBM. Instead it tiles into blocks that fit in SMEM, computes attention block-by-block with an online softmax (running max + rescaling), and keeps the running output in registers/SMEM. HBM traffic drops from to where is the SMEM size — turning a memory-bound kernel into a compute-bound one and yielding large wall-clock speedups.12 It is the archetypal IO-aware / kernel-fusion algorithm: same math, dramatically less data movement.

Fusion in general. Elementwise ops (bias, GELU, dropout, residual add, LayerNorm) each have arithmetic intensity near 1 — pure bandwidth. Run them as separate kernels and each pays a full HBM read+write of the activation tensor. Fuse them into one kernel and the intermediate never leaves registers/SMEM: you read once, do all the math, write once. This is why torch.compile, Triton, and hand-written fused kernels win — they cut HBM round-trips, not FLOPs.

GEMM tiling (→ lesson 05) is where §5 and §6 combine. High-performance matmul (a) loads tiles of and from HBM into SMEM with fully coalesced global reads, (b) reuses each loaded element across an entire tile of the output (raising arithmetic intensity from to ), and (c) arranges the SMEM tiles (with padding) to be bank-conflict-free. Coalescing + SMEM tiling + conflict-free layout is literally the recipe that takes a matmul from a few percent of peak to 80–90%+.

The unifying mental model

Every major LLM kernel optimization — FlashAttention, fused elementwise, tiled GEMM, KV-cache layout, paged attention — is, at bottom, a scheme to do more work per byte pulled from HBM. Once you see the hierarchy, you see that they’re all the same idea.


9. Practice problems


10. Practice & resources

Hands-on exercises

  1. Coalesced vs. strided copy microbenchmark. Write two CUDA kernels that copy a large float array: one where thread reads in[t] (coalesced), one where it reads in[t * STRIDE]. Time both with CUDA events, compute achieved GB/s, and plot effective bandwidth vs. STRIDE ∈ {1,2,4,8,16,32}. You should see it fall off roughly as and floor near 1/32 of peak. Confirm the coalesced version approaches your GPU’s spec HBM bandwidth.
  2. Transpose kernel, with and without SMEM padding. Implement a matrix transpose that stages through a __shared__ float tile[32][32]. Measure it, then change the declaration to [32][33] and measure again. The padded version should be markedly faster — you just eliminated a 32-way bank conflict on the column write. Verify with Nsight Compute’s l1tex__data_bank_conflicts_pipe_lsu_mem_shared counter.
  3. Nsight Compute deep-dive. Profile any kernel with ncu --set full ./a.out. Read the Memory Workload Analysis section: check global load/store efficiency (%), dram__throughput vs. peak, L2 hit rate, and shared-memory bank-conflict counts. Then check ptxas -v output (or the Nsight “Launch Statistics”) for register count and any spill loads/stores. Learn to state, for a given kernel, whether it’s bound by HBM bandwidth, L2, compute, or occupancy.
  4. Register-pressure sweep. Take a compute kernel, add __launch_bounds__(256, N) for increasing N (forcing higher occupancy → fewer registers), and watch for the point where the compiler starts spilling (ptxas -v spill counts jump). Correlate with measured runtime — you’ll often find a non-monotonic sweet spot.

Real resources

  • PMPP — Programming Massively Parallel Processors, 4th ed. (Hwu, Kirk, El Hajj).13 The memory chapters: Ch. 5 (Memory architecture and data locality — tiling, SMEM), and the coalescing/memory-performance material in Ch. 6. The single best textbook treatment.
  • CUDA C++ Best Practices Guide — NVIDIA’s official doc; the Memory Optimizations chapter covers coalescing, SMEM/bank conflicts, and the L2/occupancy trade-offs with concrete guidance. https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/
  • GPU MODE lectures (formerly “CUDA MODE”) — community lecture series with excellent sessions on memory coalescing, shared memory, and profiling; pairs well with PMPP. https://github.com/gpu-mode/lectures
  • Nsight Compute documentation — how to read Memory Workload Analysis, the metric names (dram__bytes, l1tex__*, sm__*), and the roofline chart it generates. https://docs.nvidia.com/nsight-compute/
  • CUDA C++ Programming Guide — authoritative reference for the memory model, __shared__, bank definitions, and the L1/SMEM carveout API. https://docs.nvidia.com/cuda/cuda-c-programming-guide/
  • NVIDIA H100 architecture whitepaper — for the exact SM counts, register file, SMEM/L1 sizes, L2 size, and HBM3 bandwidth quoted above.

11. What’s next

Three threads, in order:

  1. 03-performance-modeling-roofline — we’ve been hand-waving “the ridge point”; the roofline model makes it rigorous. Given a kernel’s arithmetic intensity, predict whether it’s memory- or compute-bound before you write it, and read the roofline chart Nsight generates.
  2. 05-optimizing-gemm (upcoming) — apply coalescing + SMEM tiling + bank-conflict-free layouts to take a matmul from a few percent of peak to near-peak. The concrete synthesis of §5, §6.
  3. 06-memory-wall-and-flashattention (upcoming) — the flagship IO-aware kernel: why standard attention is HBM-bound and how tiling + online softmax fixes it.

And back to the foundation: 01-gpu-architecture-and-simt — occupancy and latency hiding, which this lesson’s register/bandwidth story depends on.


Reference topic: gpu-systems-for-llms | Filed: 2026-09-02


References

Footnotes

  1. NVIDIA, NVIDIA H100 Tensor Core GPU Datasheet, 2023 — H100 SXM5: ~989–990 TFLOP/s BF16/FP16 dense tensor-core throughput, 80 GB HBM3 at 3.35 TB/s, NVLink 4 at 900 GB/s. https://resources.nvidia.com/en-us-gpu-resources/h100-datasheet-24306 2 3 4 5 6

  2. NVIDIA, NVIDIA H100 Tensor Core GPU Architecture whitepaper (GH100/Hopper), 2022 — 50 MB L2 cache on H100 SXM5, per-SM memory configuration, and full memory-subsystem description. https://resources.nvidia.com/en-us-tensor-core 2

  3. NVIDIA, CUDA C++ Programming Guide — memory model, __shared__, 32 shared-memory banks (4 B wide) and the bank-conflict rule, coalescing / 32-byte sectors, cudaFuncAttributePreferredSharedMemoryCarveout, “local memory” residing in device DRAM, plus the Compute Capability 9.0 appendix (65,536 registers/SM, 255 registers/thread, 2048 resident threads/SM). https://docs.nvidia.com/cuda/cuda-c-programming-guide/ 2 3 4 5 6 7 8 9

  4. W. Luo, R. Fan, Z. Li, D. Du, Q. Wang, X. Chu, “Benchmarking and Dissecting the NVIDIA Hopper GPU Architecture,” arXiv:2402.13499, 2024 — microbenchmarked register/SMEM/L2/global memory latencies and bandwidths on H100/H800. https://arxiv.org/abs/2402.13499 2 3 4 5

  5. M. Andersch et al., “NVIDIA Hopper Architecture In-Depth,” NVIDIA Technical Blog, 2022 — 256 KB combined L1/SMEM per SM with up to 228 KB configurable as shared memory. https://developer.nvidia.com/blog/nvidia-hopper-architecture-in-depth/ 2

  6. NVIDIA, CUDA C++ Best Practices Guide — “Memory Optimizations”: global-memory coalescing, shared-memory bank conflicts, and L2/occupancy trade-offs. https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/ 2 3

  7. NVIDIA, NVIDIA Tesla P100 whitepaper (GP100/Pascal), 2016 — ~21.2 TFLOP/s FP16, 720 GB/s HBM2. https://images.nvidia.com/content/pdf/tesla/whitepaper/pascal-architecture-whitepaper.pdf

  8. NVIDIA, NVIDIA Tesla V100 GPU Architecture whitepaper (GV100/Volta), 2017 — 125 TFLOP/s tensor-core FP16, 900 GB/s HBM2. https://images.nvidia.com/content/volta-architecture/pdf/volta-architecture-whitepaper.pdf

  9. NVIDIA, NVIDIA A100 Tensor Core GPU Architecture whitepaper (GA100/Ampere), 2020 — 312 TFLOP/s BF16/FP16 dense (624 with sparsity); A100 80 GB SXM at ~2.0 TB/s HBM2e. https://images.nvidia.com/aem-dam/en-zz/Solutions/data-center/nvidia-ampere-architecture-whitepaper.pdf

  10. NVIDIA, NVIDIA Blackwell Architecture Technical Brief, 2024 — B200: ~2.25 PFLOP/s FP16/BF16 dense (4.5 with 2:4 sparsity), ~8 TB/s HBM3e. https://resources.nvidia.com/en-us-blackwell-architecture

  11. M. Horowitz, “Computing’s Energy Problem (and what we can do about it),” IEEE International Solid-State Circuits Conference (ISSCC), 2014 — canonical per-operation energy figures showing DRAM access costs orders of magnitude more than an arithmetic op.

  12. T. Dao, D. Y. Fu, S. Ermon, A. Rudra, C. Ré, “FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness,” arXiv:2205.14135, 2022 — tiling + online softmax reduces attention HBM traffic from to . https://arxiv.org/abs/2205.14135

  13. W. Hwu, D. Kirk, I. El Hajj, Programming Massively Parallel Processors: A Hands-on Approach, 4th ed., Morgan Kaufmann, 2022 — Ch. 5 (memory architecture and data locality / tiling) and Ch. 6 (performance considerations, coalescing).