4.1.25 · D3Computer Architecture (Deep)

Worked examples — GPU architecture — SIMT, warps, CUDA model

3,616 words16 min readBack to topic

This page drills the whole GPU architecture — SIMT, warps, CUDA model toolbox through worked cases. Before any code: we treat the GPU as a machine that runs threads in fixed bundles of 32 (a warp), organized into blocks, organized into a grid. Every formula we use here was derived in the parent note — we now put it under stress.


Two tools we use constantly — define them first


The scenario matrix

Every case class this topic can throw at you, and which example covers it:

# Case class Concrete trigger Example
A Clean divisible launch is a multiple of block size Ex 1
B Partial last block (rounding up) not divisible by ; needs guard Ex 2
C Degenerate size or (one tiny block) Ex 3
D Non-multiple-of-32 block wasted lanes in the tail warp Ex 4
E Warp divergence (worst case) intra-warp if/else, both paths run Ex 5
F Zero divergence (warp-uniform) branch on threadIdx.x / 32 Ex 5
G Coalesced memory (best case) stride-1 access, efficiency Ex 6
H Scattered memory (worst case) stride- access, efficiency Ex 6
I Latency hiding limit how many warps to hide 400 cycles Ex 7
J Word problem (real world) shade pixels, choose launch Ex 8
K Exam twist (2D index) blockDim vs gridDim trap in 2D Ex 9

Prerequisite threads if any cell feels shaky: SIMD and Vector Instructions (AVX, SSE), Memory Coalescing and Bandwidth, CUDA Shared Memory and Tiling, Amdahl's and Gustafson's Laws.


Example 1 — Cell A: the clean divisible launch

Step 1 — Count the blocks. Why this step? divides evenly, so the ceiling (round-up) does nothing here — this is the baseline against which the messy cases (B, C) look different.

Step 2 — Total threads launched. Why this step? When is a multiple of , launched threads equal data elements exactly — no wasted threads, so the bounds guard if (i >= N) return; never fires.

Step 3 — The specific global index. Why this step? "Skip 3 whole blocks (), then step 5 in." This is the derived index formula, not a memorized one.

Verify: , so it is a valid element. Blocks threads matches , so every element is covered exactly once. Units: (blocks)(threads/block) + (threads) = threads. ✓


Example 2 — Cell B: the partial last block

Step 1 — Round up with the integer trick. Why this step? Plain integer division truncates down to , which would drop elements . Adding pushes the remainder past the next integer so truncation rounds up to 4 — this is precisely the identity defined above.

Step 2 — Threads launched. Why this step? We deliberately launch more threads than data. The surplus is .

Step 3 — The guard fires for exactly the surplus. The guard is if (i >= N) return;. Threads with have , so 24 threads exit early; the other do real work. Why this step? Guard on the bad condition (), because the statement after is the early exit — guarding on i < N would kill the good threads (see the parent's steel-manned mistake).

Verify: working threads . ✓ Every valid element handled exactly once; no valid index dropped, no invalid index writing out of bounds.


Example 3 — Cell C: degenerate sizes ( and )

Step 1 — . Why this step? truncates to , so the round-up trick correctly gives zero blocks for empty input — the kernel launches nothing, no thread runs, nothing crashes. This is the safe degenerate floor.

Step 2 — , count blocks. Why this step? truncates to : one block still holds all 256 lanes; you cannot launch "10 threads" as a fractional block — the block size is fixed at 256.

Step 3 — How many exit early for . Launched , valid , so threads hit i >= N and return. Why this step? This shows most of the block is wasted when — a hint that tiny problems belong on the CPU (relates to Amdahl's and Gustafson's Laws: not enough parallel work to amortize launch overhead).

Verify: (a) blocks anything threads for elements ✓. (b) working threads ✓.


Example 4 — Cell D: a block that isn't a multiple of 32

Figure — GPU architecture — SIMT, warps, CUDA model

Step 1 — Warps needed (round up to 32). Why this step? A warp is an indivisible bundle of 32 lanes, so a fractional warp count must round up — that is why we use ceiling. 100 threads need 4 warps because 3 warps hold only 96.

Step 2 — Lanes provisioned vs used. Why this step? The 4th warp holds threads (4 active) and (28 idle, masked lanes) — look at the amber ghosted lanes in the figure.

Step 3 — Waste fraction. Why this step? Roughly a fifth of the tail warp's issue slots are burned. Choosing (a multiple of 32) would waste zero lanes — the fix from the parent's "Why 32?" callout.

Verify: and , so 4 is the minimal warp count ✓. Waste ✓.


Example 5 — Cells E & F: divergence worst case vs zero divergence

Figure — GPU architecture — SIMT, warps, CUDA model

Step 1 — Fix the baseline metric: total cycles to finish all 64 lanes. Both layouts do the same amount of work (all 64 lanes run their path). The fair comparison is wall-clock cycles until the last lane is done, assuming the two warps run on the same scheduler back-to-back (worst case for both — no second scheduler). This makes E and F directly comparable. Why this step? Comparing "one warp's cost" is ambiguous; comparing time to finish the whole job is not. We now measure both layouts against this single clock.

Step 2 — Case E (divergent): both paths serialize inside each warp. There is only one instruction fetch unit per warp, so a warp holding both A and B lanes runs A (B-lanes masked) then B (A-lanes masked): Both warps here are identical (16A+16B each), so run back-to-back on one scheduler: Why this step? Divergence forces every warp to pay ; there are 2 warps, so .

Step 3 — Case F (uniform): each warp takes exactly one path, no masking waste. Warp 0 (all A) costs ; warp 1 (all B) costs . Run back-to-back on the same scheduler: Why this step? When all 32 lanes of a warp agree, that warp pays only its own path — no idle-masked path. The job is just the sum of the two single-path warps.

Step 4 — The speedup, on one honest metric. Why this step? Same total useful work, same hardware, same clock — the only difference is divergence. Reordering lanes so warps are uniform halves the time here. Divergence is per-warp, not per-thread: correctness never changes, only speed.

Verify: (divergent per warp) ✓; ✓; ✓; ratio ✓.


Example 6 — Cells G & H: coalesced vs scattered memory

Figure — GPU architecture — SIMT, warps, CUDA model

Step 1 — Bytes actually requested (same for both). Why this step? The numerator of efficiency is fixed — the useful bytes. Only the denominator (bytes transferred) changes with the access pattern.

Step 2 — Case G, stride 1. 32 consecutive floats B fall inside one 128-byte transaction. Why this step? Consecutive addresses coalesce into a single line — the best case (top row of the figure, one solid cyan line).

Step 3 — Case H, stride 32. Addresses are bytes apart — each of the 32 reads lands in a different 128-byte line: Why this step? Only 4 useful bytes are taken from each 128-byte line — the other 124 are hauled across the bus and discarded (scattered amber dots in the figure). This is the ~ slowdown.

Verify: ✓; ✓. See also Memory Coalescing and Bandwidth.


Example 7 — Cell I: how many warps hide the latency?

Step 1 — The hiding condition. While one warp waits cycles, the scheduler must fill those cycles with other warps' work. Each other warp supplies useful cycles before it too stalls. To keep every cycle busy: Why this step? This is why occupancy matters (parent's latency-hiding example): warps are "spare tires." We need enough of them that the ALU never idles.

Step 2 — Plug in. Why this step? Fewer than 51 leaves gap cycles where the SM stalls with no ready warp; more than 51 gives diminishing returns (parent's "more threads isn't always faster" mistake).

Step 3 — Sanity against SM capacity. Many SMs cap at 64 resident warps; , so this workload is hideable. If were , we'd need — latency only partly hidden. Why this step? Occupancy is bounded by hardware limits (registers, shared memory, max warps).

Verify: ✓, and ✓ so hideable; with : ✓ not fully hideable.


Example 8 — Cell J: real-world word problem

Step 1 — Blocks (round up). Why this step? ; ceiling rounds up to . , so 78 blocks would drop 16 pixels. 79 covers them.

Step 2 — Threads launched and surplus. Why this step? 112 threads hit if (i >= 10000) return;. Every pixel gets exactly one working thread.

Step 3 — Warps. Why this step? Because is a multiple of 32, no lane is wasted inside any block (contrast Ex 4). Only the tail block has idle threads, not tail warps full of idle lanes.

Verify: and ✓; surplus ✓; working ✓; total warps ✓.


Example 9 — Cell K: the 2D exam twist (blockDim vs gridDim)

Step 1 — Nail the definitions (the trap). blockDim.x = 16 = threads per block in x; gridDim.x = number of blocks in x . (Parent mistake: blockDim is NOT the block count.) Why this step? Every 2D index bug traces to swapping these. Units check: threads/block vs blocks — different units, so they can never be the same quantity.

Step 2 — Locate the block index and the in-block thread index, per axis. The block that owns coordinate is "how many whole blocks of width 16 fit below ", i.e. integer division; the in-block offset is the remainder: Here (the floor, round-down partner of ceiling) is exactly the truncating integer division we defined. Why this step? This is the 1D index formula run backwards, once per axis — block index is the quotient, thread index is the remainder.

Step 3 — Rebuild the coordinate as a self-check. Why this step? Confirms our quotient/remainder split reconstructs the original pixel — no off-by-one.

Step 4 — Flatten to one linear memory index (row-major). Row-major means "skip whole rows above me, then step across my row": Why this step? This is the address a coalescing check (Ex 6) would use. Because consecutive x gives consecutive i, a warp scanning across a row () is coalesced — a direct link back to Cell G.

Verify: , ; , ; rebuilt , ; flattened ✓.


Recall Quick self-test — cover the answers

What does equal, and why can it never be ? ::: — ceiling always rounds up on any fractional part Guard for , : how many threads exit early? ::: Warps for a block of 100 threads, and wasted lanes? ::: warps, idle lanes () End-to-end cost of divergent vs uniform layout (, , 64 lanes)? ::: vs cycles → Coalescing efficiency for stride-32 4-byte reads? ::: Warps needed to hide with cycles/warp? ::: Blocks to shade pixels with ? ::: 2D pixel , blocks: block index x? flat index? ::: ; flat