AnyLearn
All lessons
Programmingintermediate

CUDA Memory Hierarchy: Global, Shared, Constant, Local, Registers

The five memory spaces a CUDA kernel can see and why they have wildly different speeds. Global vs shared vs constant vs local vs registers, coalesced access, bank conflicts, and a cheat-sheet table you'll actually reference.

Not signed in — your progress and quiz score won't be saved.
Lesson progress1 / 9

Five memory spaces, very different speeds

On a GPU the word "memory" hides a hierarchy spanning ~3 orders of magnitude in latency. A kernel can see:

  • Registers — per-thread, on-chip, ~1 cycle.
  • Shared memory — per-block, on-chip, ~20-30 cycles.
  • L1/L2 cache — automatic, ~30/200 cycles.
  • Constant memory — read-only, broadcast-optimised, cached.
  • Global memory — device-wide VRAM, ~400-800 cycles.
  • Local memory — per-thread but physically in global memory.

The whole performance-tuning game on GPUs is: keep hot data in fast tiers, touch global memory as little as possible, and make the global accesses you can't avoid look the way the hardware likes.

Full lesson text

All 9 steps on one page — for reading, reference, and search.

Show

1. Five memory spaces, very different speeds

On a GPU the word "memory" hides a hierarchy spanning ~3 orders of magnitude in latency. A kernel can see:

  • Registers — per-thread, on-chip, ~1 cycle.
  • Shared memory — per-block, on-chip, ~20-30 cycles.
  • L1/L2 cache — automatic, ~30/200 cycles.
  • Constant memory — read-only, broadcast-optimised, cached.
  • Global memory — device-wide VRAM, ~400-800 cycles.
  • Local memory — per-thread but physically in global memory.

The whole performance-tuning game on GPUs is: keep hot data in fast tiers, touch global memory as little as possible, and make the global accesses you can't avoid look the way the hardware likes.

2. Global memory: the big slow ocean

Global memory is the GPU's VRAM — gigabytes large, accessible by every thread in every block, persists across kernel launches. It's also the slowest tier by a factor of ~20-40 over on-chip memory.

You allocate it from the host with cudaMalloc:

float* d_arr;
cudaMalloc(&d_arr, N * sizeof(float));
// ... kernels read/write d_arr ...
cudaFree(d_arr);

Every __global__ kernel argument that's a pointer typically points here. The catch: every uncached load is hundreds of cycles. A naive kernel that does one global load per arithmetic op will be memory-bound, sitting on its hands waiting for VRAM. The whole rest of this lesson is about avoiding that.

3. Coalesced access: the only rule that matters

The hardware reads global memory in 128-byte transactions aligned to 128-byte boundaries. If the 32 threads of a warp all access adjacent 4-byte words, the hardware serves them with one transaction. That's coalesced access.

If those 32 threads access scattered addresses (say, each one a different 128-byte page), the hardware issues 32 separate transactions — 32x more memory traffic for the same compute.

// Coalesced: thread i reads element i
a[blockIdx.x * blockDim.x + threadIdx.x];

// Strided: thread i reads element i * stride — bad
a[(blockIdx.x * blockDim.x + threadIdx.x) * stride];

Lay out your data so consecutive threads touch consecutive addresses. This is why GPU code prefers struct-of-arrays over array-of-structs.

4. Shared memory: the user-managed cache

Each Streaming Multiprocessor (SM) has a small pool of on-chip memory (~48-100 KB) that you can carve into shared memory for each block. Threads in the same block can read and write it; threads in different blocks can't see each other's.

__global__ void blockSum(float* in, float* out) {
  __shared__ float tile[256];
  int i = blockIdx.x * blockDim.x + threadIdx.x;
  tile[threadIdx.x] = in[i];   // pull from global
  __syncthreads();             // wait for all threads
  // ... now use tile[] as fast scratch ...
}

It's not a hardware cache — you decide what goes in. Used right, it cuts global memory traffic by 10-100x on stencils, matmul tiles, reductions. Used wrong, it just adds code and bank-conflict pain (see next step).

5. Bank conflicts (in one paragraph)

Shared memory is split into 32 banks (one per warp lane). If 32 threads in a warp each hit a different bank, you get full bandwidth. If two or more threads in a warp hit the same bank at different addresses, the hardware serialises them — an n-way bank conflict runs n times slower.

The classic offender is a column-major access into a 32×32 tile: every thread in a warp hits the same column → same bank → 32-way conflict. The textbook fix is padding the tile to 33 columns: __shared__ float tile[32][33];. The wasted column shifts each row by one bank, so column reads spread across all 32 banks again.

6. Constant memory: small, read-only, broadcast

Constant memory is a small (64 KB) device region declared at file scope:

__constant__ float kernel[16];   // host writes once, device reads many

__global__ void conv(/*...*/) {
  float w = kernel[0];          // hits constant cache
}

The host writes it once with cudaMemcpyToSymbol. Kernels read it. Why bother? When every thread in a warp reads the same address, the constant cache broadcasts the value in a single cycle — almost free. Perfect for filter weights, simulation parameters, lookup tables. Bad for anything where different threads need different elements — broadcast collapses to serial reads.

7. Registers and local memory (the trap)

Plain local variables in a kernel live in registers — the fastest storage on the chip. Each SM has a fixed register file (e.g. 65,536 32-bit registers); the compiler divides it among the threads resident on the SM.

The trap: if your kernel uses too many registers per thread, or you index into a local array with a runtime index, the compiler spills the array to local memory — which sounds local but is physically global memory. Latency jumps from 1 cycle to ~500.

__global__ void bad() {
  float tmp[64];       // probably spilled to global!
  int i = threadIdx.x % 64;
  tmp[i] = ...;        // dynamic index = no register allocation
}

Check nvcc --ptxas-options=-v to see register usage and any local memory spills.

8. Cheat sheet: scope, lifetime, latency

SpaceDeclaredScopeLifetimeLatencySize
Registerlocal varone threadkernel~1 cycletiny
Localimplicit spillone threadkernel~400 cyclesglobal-backed
Shared__shared__one blockkernel~20-30 cycles~48-100 KB / SM
Constant__constant__grid (read-only)program~1 cycle (broadcast)64 KB total
GlobalcudaMallocgrid + hostuntil cudaFree~400-800 cyclesgigabytes

The optimisation playbook flows directly from this table: keep hot data in registers, use shared memory as a manual cache for block-local reuse, keep global accesses coalesced and minimal.

9. Hot to cold: the memory hierarchy

Where each space lives, and roughly how fast.

flowchart TD
  Reg["Registers (per thread, ~1 cycle)"] --> Onchip["On-chip SM memory"]
  Shared["Shared memory (per block, ~25 cycles)"] --> Onchip
  L1["L1 cache"] --> Onchip
  Onchip --> L2["L2 cache (~200 cycles)"]
  L2 --> Global["Global VRAM (~500 cycles)"]
  Const["Constant cache (broadcast)"] --> Onchip
  Local["Local memory (spilled)"] --> Global

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why is shared memory dramatically faster than global memory?
    • It uses ECC RAM instead of GDDR
    • It's physically on the SM (on-chip) rather than in VRAM
    • The CUDA driver caches it more aggressively
    • It bypasses the warp scheduler
  2. 32 threads of a warp each load `a[threadIdx.x * 16]`. What happens?
    • One coalesced 128-byte transaction
    • 32 separate 128-byte transactions, ~32x more memory traffic
    • The compiler detects the stride and vectorises it
    • Bank conflict on shared memory
  3. When does constant memory shine?
    • When each thread reads a different element
    • When all threads in a warp read the same address (broadcast)
    • For large arrays that don't fit in shared memory
    • For atomic updates from many threads
  4. Your kernel declares `float buf[128]` and indexes it with `threadIdx.x % 128`. Why might this hurt performance?
    • Arrays aren't allowed in kernels
    • The dynamic index forces the compiler to spill `buf` to local memory (i.e. global)
    • It causes a bank conflict in shared memory
    • The modulo operation is illegal in CUDA
  5. Which fix avoids the classic 32-way bank conflict on a `__shared__ float tile[32][32]` accessed by column?
    • Use `__syncthreads()` before the column read
    • Pad to `__shared__ float tile[32][33]`
    • Move `tile` to constant memory
    • Use `volatile` on the declaration

Related lessons