Worked examples — CUDA programming model basics
This page is the hands-on companion to CUDA programming model basics. The parent note built the machinery: blocks map to SMs, threads carry a global index, and memory has speed tiers. Here we use that machinery on every awkward case a real problem can throw at you.
Before anything, let us re-anchor the one formula we will lean on the whole page. Picture a long line of numbered lockers — that is your array. We chop the line into equal blocks (fixed-size chunks). Inside each block, threads are numbered from upward.

Read the figure first. The horizontal strip is your array; each little box is one thread's element, and the number inside each box is that thread's globalIdx. The cyan boxes are ordinary threads; the single amber box (rightmost, index ) is the thread we track in Example 1. Above the boxes, the white brackets group boxes into blocks — three brackets labelled block 0, block 1, block 2. Below each box the small white t0 … t3 tags are the seat number inside that block (threadIdx.x). Notice the pattern: seats restart at at every new bracket, while the big number keeps climbing — that climb is the global index.
Every symbol above now has a picture. Two more pieces of notation appear repeatedly below, so we define them now, before first use:
Now the matrix.
The scenario matrix
Every CUDA-indexing problem you will meet is one of these cells. The examples below are labelled with the cell they cover, and together they cover all of them.
| Cell | What makes it tricky | Covered by |
|---|---|---|
| A. Clean divide | is an exact multiple of block size — no leftovers | Ex 1 |
| B. Ragged tail | not divisible — extra idle threads appear | Ex 2 |
| C. Degenerate: tiny | smaller than one block, or , or | Ex 3 |
| D. Reverse map | Given a globalIdx, find which block & seat produced it |
Ex 4 |
| E. 2-D grid | Image / matrix — two index axes, row-major flattening | Ex 5 |
| F. Memory-cost word problem | Should data live in registers, shared, or global? | Ex 6 |
| G. Launch-config sizing | Choose block size; count idle threads and wasted warp lanes | Ex 7 |
| H. Exam twist: stride loop | More elements than threads — one thread handles many | Ex 8 |
| I. 3-D grid | Volumes / batches — three index axes, depth·height·width flatten | Ex 9 |
Cell A — the clean divide
Forecast: guess the index before reading on. (Hint: last block, last seat — should be the very last element.)
- Read the two pieces.
blockIdx.x = 2,blockDim.x = 4,threadIdx.x = 3. Why this step? The formula needs exactly these three numbers; gather them first so nothing is guessed later. - Threads before my block: .
Why this step? Two whole chunks of seats sit ahead of me; I must skip all . (In the figure, that is the two full brackets
block 0andblock 1.) - Add my seat: . Why this step? Seat inside my chunk lands me past my chunk's front door — the amber box in the figure.
- Boundary check
if (idx < N): is ? Yes. Why this step? Even in a clean divide we still test — good habit, and it protects the ragged cases later.
Answer: globalIdx , valid.
Verify: Total threads , exactly . The indices produced are — every element hit once, none twice, none skipped. is the largest, so the last thread hits the last element. Consistent.
Cell B — the ragged tail
Forecast: will come out even? No — so expect leftover idle threads. Guess how many.
- Ceiling division. We need at least enough threads for all elements: Why this step? blocks give threads; blocks give only , which would leave elements untouched. We must round up — that is exactly what the ceiling bracket (defined above) does.
- Why the trick? Adding
threadsPerBlock − 1before integer-dividing forces a round-up without floating-point math. Why this step? Integer division truncates down; the offset pushes any non-zero remainder over the next boundary. It answers the question "did anything spill past the last full block?" - Total threads launched: . Why this step? Blocks come in fixed sizes — you cannot launch a partial block, so you always over-provision.
- Idle threads: . These have
idx. Why this step? This is exactly whyif (idx < N)exists — without it, threads – read past the array end. See the mistakes callout in the parent note.
Answer: 4 blocks, 1024 threads launched, 24 idle.
Verify: ✓ and ✗, so is the smallest valid count. Idle , and . ✓
Cell C — degenerate & edge inputs
Forecast: is the sneaky one — how many blocks should you launch for nothing?
- (a) : block.
Why? Even one element needs one block to hold it; you cannot launch a fraction of a block. Threads launched , idle . Only thread passes
idx < 1. - (b) : blocks. Via the trick: (integer division). Why? Nothing to do → the ceiling formula returns blocks.
- (c) : block. Trick: . Why? Exactly one full block; the offset does not falsely bump us to because . This confirms the ceiling trick never over-rounds on exact multiples.
Answer: (a) 1 block, 255 idle; (b) 0 blocks (guard the launch), 0 idle; (c) 1 block, 0 idle.
Verify: (a) ✓. (b) ✓. (c) ✓, and the trick gives , not , so no phantom extra block.
Cell D — the reverse map
Forecast: which block? Roughly , so block maybe. Let's be exact.
- Recover the block by integer division (the floor bracket defined above — round down): Why this step? The formula was with . Dividing by and dropping the remainder strips off the seat and leaves the block — it inverts the multiplication.
- Recover the seat with the remainder (modulo): Why this step? The remainder is exactly "how far past the block's front door" — the seat we skipped over in step 1.
Answer: block 3, seat 45.
Verify: Rebuild the index forward: ✓. And , a legal seat. Both directions agree — division and modulo are the clean inverse of the index formula.
Cell E — the 2-D grid
Images and matrices need two axes. Picture a grid laid over a photo: threadIdx.x/blockIdx.x count columns (across), threadIdx.y/blockIdx.y count rows (down).

Read the figure first. The big square is a corner of the image. The faint white lines chop it into blocks (four across, four down). The cyan-shaded square is the one block at blockIdx = (2, 3) — its columns run (that is to ) and its rows run ( onward). Inside it, the amber square marker is our target pixel: seat (5, 7) inside that block lands at column , row . The amber label spells out both little index sums. Notice rows increase downward (image convention) — the y-axis is flipped for exactly that reason.
Forecast: row uses the .y numbers, column uses the .x numbers. Guess before computing.
- Column (x-axis): Why this step? Same 1-D logic as Example 1, applied along the horizontal axis only.
- Row (y-axis): Why this step? The vertical axis is a second, independent copy of the same formula.
- Flatten to 1-D (row-major):
Why this step? Memory is a straight line; a full row of pixels sits before each new row. Multiplying
row × widthskips all complete rows above, then+ colwalks across the current one. This is exactly the 1-D idea nested inside itself.
Answer: pixel (row 55, col 37), flat index 28197.
Verify: Bounds: ✓, ✓. Flatten range check: max legal flat index is , and ✓. Re-derive col from flat: ✓; row ✓.
Cell F — the memory-cost word problem
Forecast: re-reading from far-away global memory 8 times sounds wasteful. Guess the cycle counts.
- All-global cost: cycles. Why this step? If nothing is cached, every one of the 8 reads pays the full off-chip trip. See Memory Hierarchy for why global is ~ cycles (off-chip, ~cm of wire + controller).
- Shared-memory cost: one global load , then shared reads . Total cycles. Why this step? Pay the expensive trip once to stage the data on-chip, then re-read cheaply. Shared memory lives inside the SM.
- Speedup: . Why this step? This ratio is the reason shared-memory tiling exists — reuse amortises the one expensive load.
Answer: 2400 vs 340 cycles → ≈7.06× faster with shared memory.
Verify: Break-even reuse count solves . So any element read twice or more already favours shared memory — and we read it times, comfortably past break-even. ✓
Cell G — launch-config sizing
Forecast: divides evenly — tempting! But is even allowed (max is ), and is it a multiple of ? Guess where the hidden waste is.
- Option A — 256: . Threads . Idle (grid-level) . Why this step? threads is whole warps — no partial warp. Wasted warp lanes per block , so total wasted lanes .
- Option B — 1000: , so it is legal. exactly. Threads . Idle (grid-level) . Why this step? An exact divide means zero leftover threads at the grid level — no ragged tail. So far it looks perfect.
- Quantify the warp-level waste for Option B. Warps per block warps. Those warps physically occupy lanes, but only threads are real. Wasted lanes per block . Why this step? The hardware always schedules a full warp of lanes; the last warp of each block runs with live threads and dead lanes. Those dead lanes still consume a scheduling slot — pure waste.
- Total wasted lanes across the grid (Option B): wasted lanes. Why this step? Multiply per-block waste by the block count to see the full picture. Option A wastes idle threads total; Option B wastes warp lanes total — over more waste, hidden behind its "zero idle threads" headline.
Answer: A: 3907 blocks, 1 000 192 threads, 192 idle, 0 wasted lanes. B: 1000 blocks, 1 000 000 threads, 0 grid-idle, but 24 wasted lanes/block = 24 000 total — the worse choice.
Verify: A: and ✓ (smallest count); → waste ✓. B: exactly ✓, ✓ (legal); warps/block , waste/block ✓, total ✓.
Cell H — the exam twist (grid-stride loop)
Sometimes you launch fewer threads than elements on purpose (e.g. to fit a specific GPU). Each thread then loops, jumping forward by the total number of threads each time. That jump is the stride.

Read the figure first. The strip is a -element array, boxes numbered . Each box carries a small white t0…t7 tag showing which of the threads owns it — you can see the tags repeat because every thread jumps ahead by the stride of . The boxes owned by thread 3 are drawn in bright amber; the two curved amber arrows trace its journey . After , the next jump would land on , which is off the strip — so thread 3 stops. That is the loop condition i < N doing its job.
Forecast: thread 3 starts at element 3 and jumps by 8. Guess its list before computing.
- Compute the stride. . Why this step? The stride must equal the total thread count so the 8 threads tile the array without overlap — like 8 people counting off "1-8, 9-16, ..." repeatedly.
- March thread 3. Start ; then . Stop, because .
Why this step? The loop condition
i < Ncleanly halts each thread when it runs off the end — this replaces the singleif (idx < N)check with a loop that self-limits. - Collect the survivors. The values that passed
i < 20are . So thread 3 handles — three elements from one thread. Why this step? This is the whole point of the grid-stride loop: threads cover elements by each doing a few passes.
Answer: thread 3 → elements 3, 11, 19.
Verify: Coverage count: elements – (first pass, all 8 threads), – (second pass), – (third pass, only threads – reach). Total ✓, every index hit exactly once (no two threads share a start, and the stride equals the thread count so passes never collide). ✓
Cell I — the 3-D grid
Volumes (medical scans, physics grids) and batched tensors need a third axis: blockIdx.z / threadIdx.z count depth (which slice). It is the same one-axis formula, now stamped out three times, then flattened with a nested version of the row-major trick.
Forecast: three independent sums, then one three-term flatten. Guess first — it uses the .z numbers.
- x-coordinate: . Why this step? Horizontal axis, one copy of the base formula.
- y-coordinate: . Why this step? Vertical axis, independent second copy.
- z-coordinate (depth): . Why this step? Depth axis, independent third copy — this is the new piece 3-D adds.
- Flatten depth-major with : Why this step? Skip whole slices, then whole rows, then across.
Answer: , flat index 124236.
Verify: Bounds: all ✓. Max legal flat index , and ✓. Unflatten: ✓; ✓; ✓.
Recall Self-test
Clean vs ragged: what does the +threadsPerBlock-1 trick achieve? ::: It forces integer division to round up (ceiling ) so we never launch too few blocks, without floating-point math.
Reverse map: given globalIdx and blockDim, how do you recover blockIdx and threadIdx? ::: blockIdx globalIdx / blockDim (floor / integer divide), threadIdx globalIdx mod blockDim.
2-D flatten: why row × width + col? ::: Memory is 1-D; each full row of width pixels sits before the next, so row × width skips complete rows and + col walks the current one.
3-D flatten: what is the extra term and why? ::: — it skips whole 2-D slices before applying the 2-D formula.
What is a warp and why 32? ::: A warp is a hardware bundle of 32 threads run in lockstep; 32 is a fixed NVIDIA hardware width (one instruction feeds 32 lanes).
Shared vs global break-even: from how many reuses does shared memory win? ::: From 2 reuses ( gives ).
Why prefer 256 over 1000 threads/block even though 1000 divides evenly? ::: is a multiple of the warp size (0 wasted lanes); leaves dead lanes per block ( across the grid).
See also: GPU Architecture Overview · Parallel Algorithm Design · OpenCL vs CUDA · Deep Learning Frameworks