6.2.6 · D4GPU Architecture

Exercises — Thread blocks and grids

3,543 words16 min readBack to topic

This page is a self-test ladder. Cover each solution, work the problem yourself, then reveal. Each level climbs one rung: L1 Recognition → L2 Application → L3 Analysis → L4 Synthesis → L5 Mastery. Everything here builds on Thread blocks and grids and touches CUDA Programming Model, Streaming Multiprocessors, Warp Execution, GPU Occupancy, Shared Memory, Memory Coalescing, and Parallel Algorithm Design.

Before we start, one picture to fix the vocabulary we will use in every problem.

Figure — Thread blocks and grids

Level 1 — Recognition

Exercise 1.1

A kernel launches with blockDim.x = 128. A thread reports blockIdx.x = 3 and threadIdx.x = 10. What is its globalID?

Recall Solution

WHAT: Plug into the one formula. WHY: the block index counts complete blocks before us (each holds 128 seats), then we add our seat. Answer: 394. Picture: we jumped over blocks 0, 1, 2 (that's elements) and landed 10 seats into block 3.

Exercise 1.2

Match each built-in variable to what it measures: threadIdx.x, blockDim.x, blockIdx.x, gridDim.x.

Recall Solution
  • threadIdx.x ::: position of the thread within its block (0 up to blockDim.x - 1).
  • blockDim.x ::: number of threads per block.
  • blockIdx.x ::: position of the block within the grid (0 up to gridDim.x - 1).
  • gridDim.x ::: number of blocks in the grid.

Memory hook: thread words end in "thread/block position"; dim words are sizes.

Exercise 1.3

True or false: two threads in different blocks can call __syncthreads() and wait for each other.

Recall Solution

False. __syncthreads() is a barrier inside one block only — see Thread Synchronization. Blocks are autonomous; there is no cross-block barrier inside a single kernel launch. Threads across blocks never wait for one another this way.


Level 2 — Application

Exercise 2.1

Vector addition of N = 1000 elements with threadsPerBlock = 256. How many blocks do you launch? How many total threads run, and how many are idle?

Recall Solution

WHAT: ceiling division, because we must cover every element even if the last block is partly empty. WHY ceiling: , and 3 blocks give only threads — 232 elements left uncovered. So round up to 4.

  • Total threads launched: .
  • Idle (guarded-out) threads: .

Those 24 threads have globalID in and the if (i < N) guard makes them do nothing.

Exercise 2.2

In that launch, a thread has blockIdx.x = 2, threadIdx.x = 200. Which element does it process, and is it guarded out?

Recall Solution

Since , the guard if (i < N) passes — it does process element 712.

Exercise 2.3

A 2D image is width = 512, height = 384. Block is 16 × 16. A thread has blockIdx.x = 5, threadIdx.x = 3 (columns) and blockIdx.y = 7, threadIdx.y = 9 (rows). Give col, row, and the flattened 1D memory index (row-major).

Recall Solution

WHAT: apply the same formula per axis, then flatten. WHY flatten: memory is a 1D line; row-major means "walk along a full row, then drop to the next" — see Memory Coalescing. Answer: col 83, row 121, memory index 62035.


Level 3 — Analysis

Exercise 3.1

An SM allows 2048 resident threads. You launch 256-thread blocks. Your kernel needs 12 KB of shared memory per block, and the SM has 96 KB. How many blocks fit, and what occupancy do you achieve?

Recall Solution

WHY occupancy is a minimum: a block can only become resident if every resource it needs is available at once — threads, registers, and shared memory. If the thread budget allows 8 blocks but the memory budget allows only 5, then only 5 can actually sit on the SM: the tightest resource is the real ceiling. So we compute each limit separately and take the smallest — see GPU Occupancy.

  • Thread limit: blocks.
  • Shared-memory limit: blocks.

WHY floor here: a block that cannot fit its whole memory footprint is simply not admitted — there is no such thing as a "0.7 of a block" resident — so any fractional part is discarded downward.

Both limits say 8, so 8 blocks fit resident threads 100% occupancy.

Exercise 3.2

Same SM (2048 threads, 96 KB). Now the kernel uses 20 KB shared memory per 256-thread block. Blocks resident? Occupancy?

Recall Solution

WHY the min again: occupancy is capped by whichever resource runs out first (see 3.1). Compute both, keep the smaller.

  • Thread limit: blocks.
  • Shared-memory limit: blocks. WHY floor: , but a 5th block would need KB KB — it does not fit, so we drop the fractional 0.8 and keep 4.

The binding constraint is the smaller one: blocks. The lesson: memory hunger — not thread count — cut occupancy in half.

Exercise 3.3

A 256-thread block: how many warps? First, a definition — inside a warp of 32 threads, each thread occupies a fixed lane, numbered 0 to 31 (a thread's lane is just threadIdx.x % 32, its column within the warp). Now suppose lanes 0–15 of each warp take the if branch and lanes 16–31 take the else. Roughly what fraction of execution slots are wasted to divergence inside the divergent region?

Recall Solution

WHAT: a warp is a fixed bundle of 32 threads running one instruction together (SIMT) — see Warp Execution. A lane is a thread's fixed slot inside that bundle, numbered 0–31; when the warp executes one instruction, all 32 lanes fire it together (unless some are masked off).

  • Warps per block: warps.
  • When a warp splits into two paths, the hardware runs the if path with lanes 16–31 masked off, then the else path with lanes 0–15 masked off. Each pass wastes the masked half — those lanes still consume a cycle doing nothing.
  • So each divergent warp spends roughly 2× the cycles; about 50% of the slots in the divergent region are idle mask-outs.

Fix in practice: arrange branches so a whole warp takes one path (branch on globalID / 32, not on the lane number), so no lane inside a warp is ever masked against another.


Level 4 — Synthesis

Exercise 4.1

You must add two vectors of length N = 1 000 000. Design the launch: pick a block size that is a multiple of 32 in the recommended 128–512 range, compute the block count, total threads, and idle threads. Justify the block size in one sentence.

Recall Solution

Choice: B = 256 — a multiple of 32 (whole warps, no wasted lanes), and in the 128–512 sweet spot that keeps enough blocks per SM to hide memory latency without starving occupancy. Check: , so 3906 is too few; 3907 is correct.

  • Total threads: .
  • Idle (guarded) threads: .

Launch: vecAdd<<<3907, 256>>>(...) with the if (i < N) guard.

Exercise 4.2

Multiply matrices where C is M × N with M = 1000, N = 500, using 16 × 16 blocks. Compute gridDim.x (columns) and gridDim.y (rows), the total thread count, and the fraction of threads that are guarded out.

Recall Solution

WHAT: cover columns along x, rows along y (convention: x = horizontal = columns) — see Parallel Algorithm Design.

  • Threads launched: .
  • Useful outputs: .
  • Guarded-out threads: .
  • Fraction wasted: .

Small waste is normal — it is the price of tiling a non-multiple grid.

The figure below draws exactly this: the mint region is the useful 500 × 1000 output, the dashed coral rectangle is the launched 512 × 1008 region, and the two lavender strips at the right and bottom edges are the extra columns and rows whose threads get guarded out. Notice the waste lives only in the last (partial) blocks along each edge — the interior is 100% useful.

Figure — Thread blocks and grids

Exercise 4.3

A 3D volume of size X = 64, Y = 64, Z = 64 is processed with 8 × 8 × 8 blocks. Compute gridDim.x, gridDim.y, gridDim.z, and the global (x, y, z) coordinates of the thread with blockIdx = (2, 3, 4) and threadIdx = (1, 5, 7).

Recall Solution

WHAT: the index rule applies independently on each of the three axes — nothing new, just x, y, z side by side. Answer: grid is blocks; the thread maps to voxel .

Flatten to a 1D memory index — and WHY this exact nesting: memory is one long line, so we must decide which coordinate walks fastest. Convention makes x the fastest-changing (neighbouring x are adjacent in memory, which helps Memory Coalescing), then y, then z slowest. So we build the address in layers: pick the z-slice (each slice holds voxels), then the y-row inside it (each row holds voxels), then step x along that row: WHY it works: is the row number counted from the very start of the volume, and multiplying by skips over all the full rows before us — then lands on our seat in the current row. It is the same "skip the complete groups before you, then add your offset" logic as the 1D global-index formula, nested three deep.


Level 5 — Mastery

Exercise 5.1

You have three candidate block sizes for a kernel that uses register + shared-memory budgets summarized as "blocks-that-fit-per-SM." The SM holds 2048 threads. For each block size the resource ceiling on blocks is given. Compute resident threads and occupancy, and pick the winner.

Block size Blocks allowed by resources
128 12
256 8
1024 2
Recall Solution

WHAT: resident threads , where thread-limit blocks .

128: thread limit ; resources allow 12; .

256: thread limit ; resources allow 8; .

1024: thread limit ; resources allow 2; .

Winner: the 256-thread block. Both 256 and 1024 reach 100% occupancy, but 256 is the safer choice — with 8 blocks resident, one stalled block still leaves 7 others for the scheduler to run and hide the latency; with 1024 you have only 2 blocks, so a single stall halves your latency-hiding capacity. The 128-thread block is eliminated because resources (not threads) cap it at 12 blocks only 75% occupancy. Pick 256.

Exercise 5.2

You have a GPU whose SMs each allow at most 8 resident blocks (a real hardware limit). A student writes: block 0 sets flag = 1 in global memory, and all other blocks spin while (flag == 0); waiting, inside one kernel launch. On a GPU with 4 SMs and a grid of 40 blocks, explain precisely why this can deadlock, and give two correct alternatives.

Recall Solution

The residency limit that makes this fail: each SM can hold at most 8 blocks at a time, and there are only 4 SMs, so at most of the 40 blocks can be resident simultaneously. The other 8 blocks must wait for a resident block to finish and free its slot before they can even start.

Why it deadlocks: blocks are scheduled onto SMs in arbitrary order. Suppose the 32 resident slots are all filled with blocks 1–32, every one of them spinning on flag. Spinning blocks never finish, so their slots never free up, so the not-yet-resident blocks — which might include block 0 — never get scheduled. If block 0 is among the waiting 8, flag is never set, so the resident blocks spin forever: a circular wait. There is no guarantee block 0 runs first; block indices imply no execution order.

Correct alternatives (from Thread Synchronization):

  1. Split into two kernel launches. The kernel boundary is an implicit global barrier — everything from launch 1 completes before launch 2 starts.
  2. Cooperative groups (CUDA 9+) grid-level sync, launched with cudaLaunchCooperativeKernel, which guarantees all blocks are co-resident so a grid barrier is legal.
  3. (Last resort) atomic operations on global memory for coordination — correct but slow, and still must avoid assuming any block runs first.

Exercise 5.3

Given a fixed problem of N = 4096 elements, you want every resident thread to be useful (no guarded-out waste) and the block size to be a warp multiple. List the block sizes in {64, 128, 256, 512} that divide N evenly, and for each give the exact block count.

Recall Solution

"No waste" means N is an exact multiple of the block size (remainder 0). Check each:

  • exactly → 64 blocks, no waste. ✅
  • exactly → 32 blocks, no waste. ✅
  • exactly → 16 blocks, no waste. ✅
  • exactly → 8 blocks, no waste. ✅

All four divide evenly (they are all powers of two dividing it), and all are warp multiples (each is a multiple of 32). So every listed size gives zero guarded-out threads. Among them, 256 is the usual pick for the occupancy/latency-hiding balance argued in Exercise 5.1.


Recall Quick self-check ladder
  • The universal 1D index formula :::
  • The same rule on each of three axes ::: apply independently to x, y, and z (e.g. )
  • Block count to cover N elements (round up) :::
  • Blocks that fit a fixed resource budget (round down) :::
  • Occupancy ::: resident threads ÷ SM max threads, capped by the minimum over all resource limits
  • Why blocks must not message each other in one kernel ::: arbitrary scheduling order + limited residency → deadlock risk