6.2.14 · D3GPU Architecture

Worked examples — GPU memory bandwidth optimization

3,011 words14 min readBack to topic

This is the worked-examples companion to the parent topic. The parent gave you the tools: coalescing, transactions, efficiency , bank conflicts, and arithmetic intensity . Here we hit every case those tools can produce — perfect access, worst access, degenerate one-thread cases, the AoS scattering case, and the roofline boundary — with the numbers checked at the bottom.

Before we start, one plain-words refresher so no symbol is unearned:

Here is the bytes each thread wants (a float is ). The symbol means round up — you can't make half a fetch; even one leftover byte forces a whole extra transaction.


The scenario matrix

Every memory-access problem in this topic falls into one of these cells. The worked examples below are labelled with the cell they cover, and together they fill the whole table.

Cell Access pattern What's special about it Example
A Consecutive (stride 1) Best case, Ex 1
B Strided (stride ) Wasted bandwidth grows with Ex 2
C Extreme stride () Full serialization, floor Ex 2b (inside Ex 2)
D Degenerate: 1 active thread Warp with 31 threads masked off Ex 3
E Broadcast: all same address Zero-stride, special hardware path Ex 4
F AoS scatter (struct span ) Partial coalescing Ex 5
G Shared-memory bank conflict -way conflict → cycles Ex 6
H Word problem (real workload) Choose a layout, predict speedup Ex 7
I Roofline boundary (limiting case) Bandwidth- vs compute-bound crossover Ex 8
J Exam twist (misaligned base) Alignment offset forces extra transaction Ex 9

Prerequisites worth a click if a step feels fast: GPU-Warp-Scheduling, Cache-Hierarchy, CUDA-Shared-Memory, Roofline-Model.

Figure — GPU memory bandwidth optimization

The figure above is the whole matrix in pictures — each colored strip is one warp's 32 requests, and the black boxes are the 128-byte fetches that actually get hauled. Keep glancing back at it.


Ex 1 — Cell A: perfectly consecutive

Forecast: guess the transaction count before reading on. (One? Thirty-two?)

  1. Find the address span. Thread 0 is at offset , thread 31 at offset . Span bytes (the because thread 31 still wants its own 4 bytes). Why this step? Transactions depend only on how far apart the first and last bytes are, not on how many threads — the fetch carries a contiguous block.
  2. Round up to transactions. . Why this step? One 128-byte block covers the whole span exactly.
  3. Compute efficiency. Useful bytes . Hauled .

Verify: Look at strip A in the figure — the colored requests tile the single black box with no gaps and no overflow. Units: bytes/bytes = dimensionless, correct for an efficiency. ✓


Ex 2 — Cells B & C: strided access

Forecast: which one collapses all the way down to the 3.125% floor?

Part (a), :

  1. Span. Last thread at offset ; span bytes. Why? Doubling the stride doubles the address spread.
  2. Transactions. . Why round up here matters: 252 bytes needs the block plus the block — the data ends at byte 251, still inside the second block, so two transactions cover it (not three).
  3. Efficiency. .

Part (b), :

  1. Span. Offset of last thread ; span bytes.
  2. Transactions. . Why exactly 32? With stride floats, each thread lands in its own 128-byte block — no two share a fetch. This is the worst case = Cell C.
  3. Efficiency. .

Verify: matches the parent note's worst-case . In the figure, strip C's requests scatter one-per-box — 32 boxes, 31/32 of each wasted. Sanity: dropped monotonically as grew (100% → 50% → 3.1%), which is what "more spread = more waste" demands. ✓


Ex 3 — Cell D: degenerate, one active thread

Forecast: does one lonely thread still pay for a whole 128-byte fetch?

  1. Span. Only one active address, so span bytes. Why this step? Masked threads issue no request; the span is just the live one.
  2. Transactions. . Why it can't be zero: the block is quantized at 128 bytes — you cannot fetch fewer.
  3. Efficiency. Useful , hauled : .

Verify: Same 3.125% floor as the strided worst case — for the opposite reason. Cell C wastes because addresses spread out; Cell D wastes because the warp is nearly empty. This is why keeping warps full matters: an under-populated warp burns bandwidth just like a scattered one. ✓


Ex 4 — Cell E: broadcast (zero stride)

Forecast: naïvely you might fear 32 threads fighting over one word — is it a conflict?

  1. Global-memory span. All threads at offset 0, span bytes → transaction. Why? Every request is the same address, so the address span is just one word.
  2. Efficiency by the general formula. The DRAM had to deliver only 4 distinct useful bytes (one word), hauled inside one 128-byte block: Why "distinct" here? measures DRAM traffic waste, and DRAM only moved one word regardless of how many threads consumed it. So the fetch is cheap on trips (1 transaction) even though its DRAM efficiency is at the floor — those two facts are not in conflict.
  3. Shared-memory case. The parent's broadcasting exception fires: identical address → hardware broadcasts in 1 cycle, no bank conflict. Why? A conflict needs different addresses in the same bank. Same address is the one exception.

Verify: 1 transaction, (global), and 1 cycle (shared, broadcast). Contrast with Ex 6 where different addresses in one bank cost dearly. ✓


Ex 5 — Cell F: Array-of-Structures scatter

Forecast: it's not the full worst case — guess between 1 and 32.

  1. Span. Thread 's x is at offset . Last thread ; span bytes. Why? The wanted fields sit every 24 bytes — scattered, but not as far as stride-32.
  2. Transactions. . Why 6 and not 32? Six neighbouring blocks cover the 748-byte spread, so AoS partially coalesces.
  3. Efficiency. .

Now the fix — Structure of Arrays — makes x[i] consecutive, back to Cell A (1 transaction, 100%).

  1. Whole-struct speedup. All 6 fields: AoS transactions; SoA .

Verify: matches the parent's AoS-vs-SoA numbers (, ). Strip F in the figure shows the 6 boxes with only one useful sliver each. See Parallel-Algorithm-Design for choosing SoA up front. ✓


Ex 6 — Cell G: shared-memory bank conflict

Forecast: is a column the best or worst thing to read?

  1. Bank of each address. Bank . Element tile[i][0] sits at word index (row-major, 32 columns per row). Why word index ? Row starts words in from row .
  2. Reduce mod 32. for every . Why this is the disaster: all 32 threads map to bank 0, different addresses → a 32-way conflict.
  3. Cycle cost. -way conflict cycles: here , so 32 cycles instead of 1.

The padding fix: declare float tile[32][33] (one dummy column). Now tile[i][0] is at word index , and — all distinct banks → 1 cycle. Why works: , so successive rows shift by exactly one bank, spreading the column across all 32.

Verify: Conflict-free result hits banks once each. Speedup cycles . See CUDA-Shared-Memory for the padding idiom. ✓


Ex 7 — Cell H: word problem, pick a layout

Forecast: which one lets the red-channel warp coalesce?

  1. Interleaved red access. Red of pixel is at byte . Span for 32 threads ; transactions . Why? Reds are 12 bytes apart — same scatter idea as Ex 5, just .
  2. Planar red access. Reds are contiguous, 4 bytes apart. Span ; transactions . Why? Planar puts all reds in one run → Cell A.
  3. Ratio. fewer transactions for the red pass; same for green and blue passes.

Answer: choose planar (SoA) for channel-parallel work. Verify: interleaved gives , planar ; ratio matches step 3. Units: transactions are dimensionless counts. ✓


Ex 8 — Cell I: the roofline boundary

Forecast: will tiling be enough to escape the memory wall?

  1. Count the work. A full multiply does multiply-adds, each counting as 2 FLOPs, so total FLOPs . Why this step? Arithmetic intensity = FLOPs ÷ bytes, so we need the FLOP numerator first. Each of the output entries is a dot product of length products.
  2. Count the DRAM bytes with tiling — build it up.
    • Sub-step 2a — reads per output element, un-tiled: each output entry needs a full row of and a full column of , i.e. element reads from DRAM.
    • Sub-step 2b — how tiling divides that: we sweep the shared dimension in chunks of , giving tile-steps. Within each step a tile is fetched from DRAM once and then reused by every thread in the block from fast shared memory. So the DRAM reads per output element drop from to . Why this step? Reuse is the whole point of tiling: one DRAM fetch feeds worth of arithmetic, so DRAM traffic shrinks by the factor .
    • Sub-step 2c — convert to bytes and multiply over all outputs: there are output elements, each a 4-byte float per read: Why this step? Now numerator (FLOPs) and denominator (bytes) are both in hand, so follows directly.
  3. Arithmetic intensity. The cancels — intensity depends only on tile size, not matrix size. For : FLOPs/byte.
  4. Machine balance. FLOPs/byte. Why this number? It's the FLOPs you must do per byte to keep the compute units fed exactly at bandwidth's pace. Below it → starved by memory.
  5. Compare. bandwidth-bound, but less traffic than the naive case (). Limiting behaviour: to cross into compute-bound you need (reached via register blocking, since 80×80 shared tiles won't fit).

Verify: , tiled (<20 → bandwidth), naive (≪20). Crossover . All consistent with Roofline-Model. ✓


Ex 9 — Cell J: the exam twist (misaligned base)

Forecast: Ex 1 gave 1 transaction — does a shifted start really cost you?

  1. Byte range touched. Thread 0 at byte 64, thread 31 at byte . Range . Why include 191? Thread 31 wants bytes 188–191.
  2. Which 128-blocks? Block 0 covers , block 1 covers . Our range straddles the boundary at 128. Why it matters: transactions are aligned to 128-byte block boundaries, not to your data.
  3. Count the transactions. The range touches block 0 (bytes 64–127) and block 1 (bytes 128–191), so 2 transactions are needed — even though the data is only 128 bytes wide. Why 2 and not 1? A single fetch can only be one aligned 128-byte block; our data spills across the seam, so no single aligned block contains it all.
  4. Efficiency. Useful bytes; hauled bytes.

Verify: Alignment halves efficiency here — a classic trap. The lesson: pad allocations so base is a multiple of 128. Same 128 useful bytes as Ex 1, but the fetches because we crossed the seam. Look at strip J in the figure — the colored run sits astride the black gridline. ✓


Recall Quick self-test

Stride-2 float read: how many transactions and what efficiency? ::: 2 transactions, 50% (span 252 → ⌈252/128⌉ = 2) Stride-32 float read: how many transactions? ::: 32 (each thread its own 128-byte block) A warp with only thread 0 active reads 1 float — efficiency? ::: 3.125% (4 useful / 128 hauled) All 32 threads read the same shared word — how many cycles? ::: 1 (broadcast exception) AoS with 24-byte struct, one field per thread — transactions? ::: 6 (span 748 → ⌈748/128⌉) Consecutive reads but base=64 — transactions? ::: 2 (range straddles the 128-byte boundary) Tiled matmul T_tile=32 on a 20:1 machine — bound by? ::: bandwidth (I=8 < I* = 20)