6.2.7 · D4GPU Architecture

Exercises — Memory hierarchy (global, shared, registers)

3,834 words17 min readBack to topic

This page is a self-test. Each problem is stated cleanly, then its full worked solution is hidden inside a collapsible box. Try each one on paper first, then click to reveal.

The problems climb a ladder of difficulty:

  • L1 Recognition — can you name and place things?
  • L2 Application — can you plug numbers into one rule?
  • L3 Analysis — can you compare two options and reason about why?
  • L4 Synthesis — can you combine several rules into one design?
  • L5 Mastery — can you invent, estimate, and defend a full answer?

Everything here builds on the parent topic. If a term feels unfamiliar, the linked prerequisite notes are: GPU Thread Hierarchy, Warp Execution Model, CUDA Memory Types, Memory Latency Hiding, Cache Architecture, Memory Bandwidth Optimization, Matrix Multiplication Optimization, and Roofline Model.

Before we start, one picture to fix the whole mental model — the "distance ladder" of memory:

Figure — Memory hierarchy (global, shared, registers)

How to read this figure: each horizontal bar is one storage level, and the length of the bar grows with its latency (log-scaled, so the far-apart numbers fit). Read top to bottom: the green Registers bar is tiny (~1 cycle) and sits at the top — fastest, smallest, closest to the compute unit. Below it the yellow Shared Memory bar (~65 cycles), then the blue L2 Cache bar (~200 cycles), and finally the long red Global Memory bar (~600 cycles) at the bottom — slowest, largest, farthest away. The green arrow at the top and the red arrow at the bottom spell out the trend: climbing up the ladder gets you faster/smaller/closer storage, climbing down gets you slower/larger/farther storage. Every exercise on this page is a fight to keep hot data on a high (green) rung.


L1 — Recognition

Exercise 1.1

Order these four storage levels from fastest to slowest: L2 cache, registers, global memory, shared memory.

Recall Solution 1.1

What we do: read the latency column of the parent's summary table and sort ascending.

Level Latency (cycles)
Registers 1
Shared memory 30–100
L2 cache ~200
Global memory 400–800

Answer: registers → shared memory → L2 cache → global memory.

Why this order: speed comes from physical proximity. Registers sit inside the arithmetic unit; global memory is off-chip DRAM (VRAM) that light has to physically travel to.

Exercise 1.2

For each level, name its scope (per-thread, per-block, or per-device): registers, shared memory, global memory.

Recall Solution 1.2
  • Registers → per-thread (private; no other thread can read them).
  • Shared memory → per-block (every thread in the same block shares it).
  • Global memory → per-device (every thread in every block sees it).

Why: scope and speed go together. Private things are fast (no coordination); shared things need arbitration (banks); global things need a long trip. Widening the audience costs time.

Exercise 1.3

For each of the three levels — registers, shared memory, global memory — say who manages it (compiler-automatic or programmer-explicit). Then judge this student claim: "The compiler decides what goes in shared memory."

Recall Solution 1.3

Who manages each level:

  • Registers → compiler (automatic; the compiler assigns your local variables to registers without you asking).
  • Shared memory → programmer (explicit; you write __shared__ and fill it yourself — that is why it's called a "user-controlled cache").
  • Global memory → programmer (explicit; you allocate it and choose every read and write).

The claim is False. The compiler manages registers, not shared memory. Shared and global memory are both programmer-controlled; only registers are automatic.


L2 — Application

Exercise 2.1

An SM has registers. A kernel uses registers per thread. What is the maximum number of concurrent threads, ignoring other limits? Give both the idealized budget bound and the warp-rounded hardware value.

Recall Solution 2.1

What / Why: each thread carries away registers from the fixed pot , so we divide the pot by the per-thread cost and round down with the floor operator (you cannot run a fractional thread). Warp rounding: hardware allocates in chunks of 32, so Answer: 1365 threads by the plain-floor budget bound, 1344 threads once warp-granularity rounding is applied — the value the SM actually schedules.

Exercise 2.2

Using threads/SM, what is the occupancy for the kernel in 2.1? Report it both ways.

Recall Solution 2.2

Idealized budget bound: Warp-rounded (real hardware): Why occupancy matters: GPUs hide the 400–800 cycle global-memory wait by switching to another ready warp (see Memory Latency Hiding). Fewer resident threads = fewer warps to switch to = the SM sits idle waiting. The honest hardware number here is ~65.6%, so roughly two-thirds of the hiding power is available.

Exercise 2.3

Shared memory has banks, each bytes wide. Into which bank does byte address fall?

Recall Solution 2.3

What / Why: we first convert the byte address to a word index (divide by width , because banking is per 4-byte word), then take mod to see which bank cycles around to it. Answer: bank 1.


L3 — Analysis

Exercise 3.1

A warp of 32 threads reads an array of float (4 bytes). Segments are 128 bytes. Compare two access patterns and give the transaction count for each:

  • (a) thread reads address .
  • (b) thread reads address .

Then answer (c): what happens to case (a) if is not aligned to a 128-byte boundary?

Recall Solution 3.1

(a) Coalesced (and aligned). Assume is a multiple of 128 (aligned). Addresses span to , a total of bytes = exactly one 128-byte segment. (b) Strided. Each thread's address is 128 bytes apart, so every thread lands in its own segment. (c) Misaligned base — the hidden edge case. Suppose (not a multiple of 128). The 128 requested bytes now run from byte 64 to byte 191, straddling the boundary at 128. That touches two segments (0–127 and 128–255): So even a perfectly consecutive access pays double the transactions if its starting address is misaligned. The lesson: coalescing needs both a consecutive pattern and an aligned base — this is why CUDA allocators return 128/256-byte-aligned pointers.

Analysis: the aligned coalesced-vs-strided ratio is ; misalignment quietly turns your best case into a penalty. Same math, same 32 floats wanted — but pattern (b) drags 32 times more bytes across the bus, wasting 31/32 of every fetch. This is the single biggest lever in Memory Bandwidth Optimization.

Figure — Memory hierarchy (global, shared, registers)

How to read this figure: the top row of green dots shows the coalesced aligned case — 32 thread requests packed into a single shaded green 128-byte segment (one transaction). The middle row of yellow dots shows the misaligned case — the same consecutive requests straddle the boundary line and spill into a second segment (two transactions). The bottom row of red dots shows the strided case — each request lands in its own separate shaded red segment, so the warp needs many transactions. The horizontal axis is the byte address; the vertical dotted white lines mark 128-byte segment boundaries.

Exercise 3.2

Kernel A uses 32 registers/thread; kernel B uses 64 registers/thread. Both run on an SM with 65,536 registers, . Compute occupancy for each and explain the performance implication.

Recall Solution 3.2

Kernel A: , capped at → occupancy . Kernel B: → occupancy .

Analysis: doubling registers halved occupancy. Kernel B has only half as many warps to hide latency with. But — subtlety — B might still win if those extra registers eliminate memory traffic (fewer trips to global). This is the classic register-pressure vs. occupancy tradeoff: you never optimize occupancy in isolation, only alongside the memory traffic it buys you.

Exercise 3.3

Two warps access shared memory. In warp X, all 32 threads read word index (same address). In warp Y, thread reads word index . Give the serialization factor for each.

Recall Solution 3.3

Warp Y (thread → word ): words map to banks — all distinct → . No conflict, full speed.

Warp X (all → word 5): naïvely 32 threads hit one bank, so , suggesting . But shared memory has a broadcast rule: when many threads read the same address, hardware broadcasts it in one shot → .

Answer: both have , but for different reasons — Y because addresses spread across banks, X because identical addresses trigger broadcast. The dangerous case is in between: several threads hitting the same bank at different addresses (that truly serializes).


L4 — Synthesis

Exercise 4.1

You tile a matrix multiply with tiles (see Matrix Multiplication Optimization). Each float tile of A and of B is stored in shared memory. (a) How many bytes does one tile occupy? (b) How much shared memory does one block need for both tiles? (c) If the SM has 96 KB of shared memory, how many blocks fit (shared-memory limited only)?

Recall Solution 4.1

(a) floats bytes bytes = 1 KB per tile. (b) Two tiles (As and Bs): bytes = 2 KB per block. (c) blocks. Synthesis point: shared-memory footprint is a second occupancy limiter alongside registers. The true block count is the minimum across the register limit, the shared-memory limit, and the hardware block cap.

Exercise 4.2

For the transpose kernel, the parent declares __shared__ float tile[32][33]. (a) Why 33 columns instead of 32? (b) How many extra bytes does the padding cost per block?

Recall Solution 4.2

(a) With [32][32], column of row sits at word index . When threads read a column during transpose (varying , fixed ), their indices differ by 32 → for all all 32 hit the same bank → 32-way conflict. Padding to 33 makes column stride 33, and , so consecutive column elements land in 32 different banks → conflict-free. (b) Extra column = bytes per block. Synthesis point: a 128-byte "waste" buys a 32× speedup on the transpose step — the cheapest trade on this page.

Exercise 4.3

Redo the parent's tiled-vs-naive global-traffic count, but for matrices with tiles. (a) Naive global reads total. (b) Tiled global reads total. (c) Reduction factor, with explanation.

Recall Solution 4.3

, tile , tiles per dimension .

(a) Naive: each of output threads reads values from global. (b) Tiled: each block loads, per k-step, one A-tile + one B-tile elements, over k-steps reads per block. Number of blocks . (c) Reduction factor: Explanation: the reduction factor equals the tile edge length (here 16). Why exactly 16? In the naive version each output element re-reads every input value it needs straight from global; in the tiled version, once a tile is loaded into shared memory, all 16 threads sharing that row/column reuse it 16 times before it is evicted. Each loaded byte is therefore amortized over 16 uses, cutting global traffic by that same factor of 16.

Synthesis point: bigger tiles = more reuse (larger reduction factor), but bigger tiles cost more shared memory (Exercise 4.1) and more registers — the recurring three-way tension between reuse, occupancy, and capacity.


L5 — Mastery

Exercise 5.1

A GPU has peak compute TFLOP/s ( FLOP/s) and peak bandwidth GB/s ( B/s). (a) Find the ridge-point intensity . (b) A naive kernel does FLOP per bytes read (one multiply-add on two freshly-loaded floats). Is it memory-bound or compute-bound? (c) After tiling gives 16× reuse, what is the new intensity, and is it now compute-bound?

Recall Solution 5.1

(a) FLOP/byte. (b) Naive intensity FLOP/byte. Since , the kernel is far to the left of the ridge → memory-bound. It wastes almost all its compute waiting for data. (c) Tiling reuses each loaded byte 16×, so bytes-from-global drop 16× while FLOPs stay the same → intensity rises 16×: FLOP/byte. Still , so still memory-bound, but now runs faster on the memory roofline. Mastery point: tiling moves you rightward along the roofline. To cross the ridge to compute-bound you'd need even larger tiles / register blocking to push past 20.

Figure — Memory hierarchy (global, shared, registers)

How to read this figure: the axes are log–log — arithmetic intensity (FLOP/byte) across the bottom, attainable performance (FLOP/s) up the side. The white line is the roofline: a rising blue-dashed part on the left (memory-bound — performance limited by bandwidth ) and a flat yellow-dashed ceiling on the right (compute-bound — performance capped at ). They meet at the dotted vertical line, the ridge . The red dot (, naive) sits far left under the sloped part; the green dot (, tiled) has slid rightward toward the ridge but is still under the slope — still memory-bound, just faster.

Exercise 5.2

Design decision: you have a kernel bottlenecked by 100% occupancy but still slow. Profiling shows it is memory-bound with fully coalesced access. You have three levers: (i) increase registers to cache more per thread, (ii) add shared-memory tiling, (iii) increase block size. Which do you pick and why? Argue in 3–4 sentences.

Recall Solution 5.2

Pick (ii), shared-memory tiling. The kernel is memory-bound with already-coalesced access, so the bus is used efficiently but too much data is being fetched — the cure is reuse, not more coalescing. Lever (i) risks lowering occupancy (Exercise 3.2) and only helps if data is reused within a single thread; lever (iii) doesn't reduce total bytes moved at all. Tiling raises arithmetic intensity (Exercise 5.1), sliding you rightward on the roofline — the only lever here that attacks total global traffic, which is the actual bottleneck.

Exercise 5.3

Full estimation. A kernel launches 1,048,576 threads, each reading exactly one float (4 B) from global memory in a perfectly coalesced pattern. The GPU bandwidth is GB/s. (a) Total bytes moved. (b) Minimum time to move it (bandwidth-bound floor). (c) If instead the access were fully strided (32× more transactions), estimate the new time.

Recall Solution 5.3

(a) bytes MB of useful data. (b) (c) Strided access fetches 32× more bytes (whole segments for single floats), so effective transferred bytes and time scales up: . Mastery point: the coalesced floor is a hard physical limit — you cannot go faster than bandwidth allows. The 32× penalty is entirely self-inflicted by the access pattern, which is why coalescing is checked first in any optimization pass.