6.2.6 · D5GPU Architecture

Question bank — Thread blocks and grids

1,510 words7 min readBack to topic

True or false — justify

A block always runs on exactly one SM
True. Every thread of a block shares that SM's on-chip shared memory and registers, so the block cannot be split across two SMs — otherwise the shared memory promise would break.
An SM runs exactly one block at a time
False. An SM holds as many blocks as its budget allows (threads, registers, shared memory), often 4–8 at once. Interleaving them is precisely how the SM hides memory latency.
Blocks within a grid execute in numerical order (block 0, then 1, then 2…)
False. The runtime schedules blocks in any order and may run them concurrently; block 5 can finish before block 1 even starts. The numbering is just an identity, not a timeline.
__syncthreads() synchronizes the whole grid
False. It is a barrier for threads within one block only. There is no built-in whole-grid barrier inside a single ordinary kernel launch (see Thread Synchronization).
Doubling the threads-per-block always doubles throughput
False. Larger blocks consume more of the SM's fixed budget, so fewer of them fit; occupancy can drop and hide less latency. Throughput often peaks around 128–256 threads.
Two threads in the same warp can take different branches of an if and both run at full speed
False. A warp issues one instruction for all 32 lanes; on divergence the hardware masks off lanes and runs both paths serially, so divergence costs cycles.
Threads in different blocks can call __syncthreads() to wait for each other
False. The barrier is block-scoped. Cross-block waiting inside one kernel risks deadlock because the blocks you're waiting on may not even be resident yet.
A grid can be 1D, 2D, or 3D purely for the programmer's convenience
True. The dimensionality only shapes the index arithmetic; hardware ultimately flattens everything. A 2D grid just makes image/matrix mapping read naturally.
Global thread ID is threadIdx + blockIdx
False. It's blockIdx * blockDim + threadIdx. You must skip over all the complete blocks before you (each of size blockDim) before adding your local offset.
If block size is not a multiple of 32, some warp lanes are wasted
True. Warps are always 32 lanes; a 40-thread block becomes 2 warps (64 lanes) with 24 idle lanes that still occupy scheduling slots.

Spot the error

int i = threadIdx.x; used as the global index for a multi-block launch
Error: this only gives the local position inside the block, so every block would re-index elements 0…blockDim-1 and clobber each other. Correct: blockIdx.x * blockDim.x + threadIdx.x.
Launching blocks = N / threadsPerBlock for N = 1000, B = 256
Error: integer division gives 3 blocks (768 threads) and drops elements 768–999. Use ceiling division (N + B - 1) / B → 4 blocks.
Removing the if (i < N) guard "because the math works out"
Error: the last block usually has extra threads whose global ID exceeds N; without the guard they read/write out-of-bounds memory. The guard is mandatory whenever N isn't an exact multiple of block size.
Block 0 sets flag[0] = 1 and other blocks spin on while(flag[0]==0) inside one kernel
Error: blocks aren't guaranteed to co-reside, so the spinning blocks may occupy every SM slot while block 0 never gets scheduled → deadlock. Use separate kernel launches or cooperative groups instead.
dim3 blocks((N)/16, (M)/16) for a 2D image tiled by 16×16 blocks
Error: plain division truncates the ragged edge tiles. Use (N+15)/16 and (M+15)/16 so partial border tiles still get a block (with in-kernel bounds checks).
Using blockIdx.x for the matrix row and blockIdx.y for the column
Error (by convention): x is the horizontal axis → column, y is vertical → row. Swapping them breaks coalesced access because adjacent threadIdx.x should map to adjacent columns.
Setting block size to 1000 threads on a GPU whose max is 1024 "for max parallelism"
Error in judgement: 1000 isn't a multiple of 32 (wastes lanes) and the huge block crowds out others, hurting occupancy. Pick a 32-multiple like 256.

Why questions

Why must blocks be independent?
So the runtime can distribute them across any number of SMs in any order — this is what lets identical code scale from a 4-SM laptop chip to a 100-SM datacenter GPU (the core promise of the CUDA Programming Model).
Why is there fast shared memory per block rather than per grid?
Only threads that are physically co-located on one SM can cheaply share an on-chip scratchpad; a grid spans many SMs, so grid-wide shared memory would require slow global-memory traffic instead.
Why prefer many small blocks over a few giant ones?
More independent blocks give the SM more work to swap in whenever one block stalls on memory, so latency stays hidden and the compute units stay busy.
Why must warp size influence block size?
The hardware issues instructions per 32-lane warp, so a block that isn't a multiple of 32 leaves dead lanes in its final warp, wasting scheduling capacity.
Why does 2D indexing flatten to row * width + col?
Memory is physically 1D and stored row-major, so the flattened index reconstructs the linear address; getting width (not height) into the formula is what keeps a row contiguous.
Why can't we just launch one enormous block covering the whole problem?
A block is hardware-capped (e.g., 1024 threads) and must fit on a single SM, so one block could never span a million-element array — the grid exists precisely to tile beyond one SM's reach.
Why does splitting into two kernel launches act as a global barrier?
A kernel launch doesn't complete until all its blocks finish, so the next launch is guaranteed to see all results — an implicit whole-grid synchronization without any in-kernel spinning.
Why is occupancy limited by shared memory, not just thread count?
An SM must satisfy every resource simultaneously; if each block hogs shared memory, the SM runs out of that budget before it runs out of thread slots, so the tighter constraint wins (see GPU Occupancy).

Edge cases

What happens when N is smaller than one block's thread count (N = 50, B = 256)?
You still launch 1 block; the if (i < N) guard idles the surplus 206 threads. Wasteful but correct — the guard is what keeps it safe.
What if N is an exact multiple of block size — is the guard still needed?
Not strictly for correctness in that specific launch, but keep it anyway: it costs almost nothing and protects you the moment N changes to a non-multiple.
What is the largest global ID a launch of G blocks × B threads can produce?
(G-1)*B + (B-1) = G*B - 1, i.e. total threads minus one. If that exceeds N-1, the extra threads are exactly the ones the guard must reject.
A block of 32 threads where 31 hit the if and 1 hits the else — cost?
Both branches execute serially for the single warp: the taken lanes run while the others are masked, then they swap. You pay for both paths even though only one lane needed the else.
If a grid launches more blocks than an SM can hold at once, does the launch fail?
No — extra blocks simply queue and are scheduled as earlier blocks retire and free resources. Resource limits cap concurrent residency, not the total number of blocks you may launch.
Zero-size launch: what if you compute blocks = 0?
The kernel does nothing (no thread runs), which is usually a bug from N = 0 slipping through — always ensure ceiling division and a nonzero problem size before launching.
Recall Quick self-test

The single deepest idea to carry away ::: globalID = blockIdx*blockDim + threadIdx, blocks are independent and unordered, and __syncthreads() only ever binds threads within one block.