6.2.11 · D4GPU Architecture

Exercises — Warp divergence penalties

3,255 words15 min readBack to topic

This page is a self-test ladder for Warp divergence penalties (6.2.11). Work top to bottom: each level lifts the demand — from spotting divergence, to computing its cost, to analysing nested/loop structure, to designing fixes, to mastering the trade-offs.

Cover each solution and try first. Reveal only after committing to an answer.

Before we start, two symbols we will reuse everywhere, defined from zero:

The reason we add and not average: in SIMT there is one instruction decoder feeding 32 lanes. It can point at only one path's instructions at a time. Threads on the other path are still "there" — they are just masked: their outputs are thrown away, but the cycles still tick. So a path that even one thread takes costs its full length. Keep that in mind — it is the trap behind half the exercises.

The picture below is the mental model for every exercise on this page. Study it before you start: the top bar shows the ideal world where two paths (A = 12 cycles, B = 20 cycles) overlap in parallel, so the warp finishes at . The bottom bar shows the diverged world where the same two paths run back-to-back, so the warp finishes at . The amber gap on the right is the pure divergence waste — exactly the quantity you compute as in the problems below.

Figure — Warp divergence penalties

Level 1 — Recognition

Recall

Solution L1.1 No divergence. Every thread index 0–31 satisfies < 32, so all 32 threads evaluate the condition to true and take path A together. The else path B is taken by nobody, so it is never issued. Since all threads share one path, there is nothing to serialize. Divergence needs disagreement inside one warp.

Recall

Solution L1.2 Line (c). The predicate idx % 2 == 0 is true for even lanes (0,2,…,30) and false for odd lanes (1,3,…,31). Within the same warp the 32 threads split — 16 true, 16 false — so the hardware must issue the body once with the even-mask and once (empty here, the implicit else) with the odd-mask. Lines (a), (b), (d) have no branch: every thread does the same op on its own address, which is uniform control flow (see coalescing, a separate concern from divergence).


Level 2 — Application

Recall

Solution L2.1 Both paths are taken by at least one thread, so both count. The thread count (8 vs 24) never entered the arithmetic — only whether a path is taken and how long it is.

Recall

Solution L2.2 All four paths have at least one taker, so all four serialize: This matches Example 2 in the parent note — data-dependent 4-way branching is brutal.

Recall

Solution L2.3 Two paths are taken (lane 0 → path1; lanes 1–31 → path2), so both count fully: The "weighted" guess cycles is wrong because the 100-cycle path is not shared across 32 threads — it costs its full length even for a single lane.


Level 3 — Analysis

Recall

Solution L3.1 Worst case — 4 distinct paths reached: Best arranged case — only p1 and p3 taken (e.g. B true whenever A, C true whenever not-A): Nesting depth did not set the cost — the number of distinct leaves actually reached did. See the tree figure.

Figure — Warp divergence penalties
Recall

Solution L3.2 The loop back-edge is a branch: on each iteration, lanes whose i has reached their counts[idx] drop out (masked), but the warp must keep running iterations until the max-count lane (20) finishes. So the warp executes iterations worth of issue slots for all 32 lanes, of which only are useful. Half of every iteration slot after the short lanes exit is wasted on masked lanes.

Recall

Solution L3.3 Warp Y is worse. Same total serialized time, but Y's ideal (best single path) is far shorter, so more of Y's time is pure divergence waste. Lesson: more, shorter, equally-populated paths are the most divergence-inefficient shape — the ideal shrinks while the sum stays put.


Level 4 — Synthesis

Recall

Solution L4.1 idx % 2 alternates every lane, guaranteeing a split. Replace with a warp-granular predicate:

int warp_id = idx / 32;
if (warp_id % 2 == 0) heavy(); else light();

Now all 32 lanes of a warp share the same warp_id, so a warp is entirely heavy or entirely light — one path each. For an all-heavy warp: only path taken is 60 cycles, : The total work is unchanged, but the divergence penalty is eliminated. This is the alignment strategy from the parent note, and it feeds directly into occupancy planning.

Recall

Solution L4.2 First, where does the rule come from? Two ways to run a two-path conditional:

  • Branch (serialize): pay — each path issued once, back-to-back.
  • Predicate (no branch): every lane runs both paths and masks the unwanted result, so it also pays of workbut it deletes the branch instruction, the mask-stack push/pop, and the reconvergence, which cost roughly one "extra max-path" worth of overhead. So predication's effective charge is about : you do the bigger path's work twice (once real, once masked) with no branch machinery. Set predication's charge below branching's charge: The inequality is exactly the crossover: it holds only when the two paths are short and balanced (so their sum exceeds twice the larger), and fails when one path dwarfs the other (then blows past the sum and the real branch wins).

Now apply it with , : Since , the serialized-branch sum is already less than the predication charge — this branch is so short and balanced that a real branch is the raw-cost equal-or-better choice, yet in practice compilers still predicate here because it removes the divergence stall and reconvergence entirely at essentially the same cycle count. For very lopsided long branches () you'd instead prefer the real branch so the big path isn't paid by lanes that don't need it. Related idea on CPUs: branch prediction, which GPUs lack.

Recall

Solution L4.3 Sorted, each warp is uniform → single path → , efficiency for every warp. The four warp-flavours cost 20, 30, 25, 5 respectively; the average per-warp time is Compare: the mixed warp spent 80 cycles to do the same four kinds of work. Sorting bought a speed-up here purely by killing divergence — the classic parallel-algorithm preprocessing trick, also a staple of CUDA optimization patterns.


Level 5 — Mastery

Recall

Solution L5.1 Price each warp with the two-number method. A warp that splits into two taken paths costs the sum; a uniform warp costs its single path.

  • Warp 0 (uniform, one 30-cycle path): .
  • Warps 1, 2, 3 (each splits into 30 and 18, both taken): each.

Sum the issued cycles across all four warps: The all-uniform ideal charges each warp only its longest single path once (), across four warps: One clean warp cannot rescue three dirty ones — divergence is per-warp and the penalties simply add across the block.

Recall

Solution L5.2 Option A — every warp splits, both paths taken: per-warp ; four warps cycles. Option B — 2 warps uniform-heavy ( each) + 2 warps uniform-light ( each): Sorting halved the issued cycles by making every warp internally uniform. This is the synthesis of everything above: recognise divergence (L1), price it (L2), analyse the split (L3), pick a fix (L4), and prove the win at scale (L5).

Recall

Solution L5.3 Divergent region for both: cycles.

  • Warp P: cycles total.
  • Warp Q: after the paths reconverge, all 32 lanes are active again and share one instruction stream, so the 10-cycle tail is issued once: cycles. The tail is uniform control flow — no split, no serialization — so it is added once, not summed per path. Reconvergence is exactly the point where the divergence stack pops and the warp goes back to full-width SIMT.

Recall

Self-test: the one formula to leave with How do you price any divergent region? ::: ; ; efficiency . Populations don't affect timing; uniform code before/after runs once.