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 max(12,20)=20. The bottom bar shows the diverged world where the same two paths run back-to-back, so the warp finishes at 12+20=32. The amber gap on the right is the pure divergence waste — exactly the quantity you compute as Tdiv−Tideal in the problems below.
Solution L1.1No 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).
Solution L2.1
Both paths are taken by at least one thread, so both count.
Tideal=max(15,25)=25 cyclesTdiv=15+25=40 cyclesEfficiency=4025=0.625=62.5%
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:
Tdiv=20+30+25+5=80 cyclesTideal=max(20,30,25,5)=30 cyclesEfficiency=8030=0.375=37.5%
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:
Tdiv=100+50=150 cycles,Tideal=max(100,50)=100Efficiency=150100=0.66=66.7%
The "weighted" guess 50+100/32≈53.1 cycles is wrong because the 100-cycle path is not shared across 32 threads — it costs its full length even for a single lane.
Solution L3.1Worst case — 4 distinct paths reached:Tdiv=10+14+8+12=44 cycles,Tideal=max(10,14,8,12)=14 cyclesEfficiency=4414≈0.318=31.8%Best arranged case — only p1 and p3 taken (e.g. B true whenever A, C true whenever not-A):
Tdiv=10+8=18 cycles,Tideal=max(10,8)=10 cyclesEfficiency=1810≈0.556=55.6%
Nesting depth did not set the cost — the number of distinct leaves actually reached did. See the tree figure.
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 max=20 iterations worth of issue slots for all 32 lanes, of which only ∑=320 are useful.
Utilisation=32×20320=640320=0.5=50%
Half of every iteration slot after the short lanes exit is wasted on masked lanes.
Recall
Solution L3.3EffX=120max(40,40,40)=12040=33.3%EffY=120max(10,10,10,10,10)=12010=8.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.
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, Tdiv=60=Tideal:
Efficiency=6060=100%
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.2First, where does the rule come from? Two ways to run a two-path conditional:
Branch (serialize): pay TA+TB — each path issued once, back-to-back.
Predicate (no branch): every lane runs both paths and masks the unwanted result, so it also pays TA+TB of work — but 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 2max(TA,TB): you do the bigger path's work twice (once real, once masked) with no branch machinery.
Set predication's charge below branching's charge:
branch chargeTA+TB>predication charge2max(TA,TB)⇒predicate is cheaper.
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 2max blows past the sum and the real branch wins).
Now apply it with TA=4, TB=3:
TA+TB=7,2max(TA,TB)=2×4=8.
Since 7<8, 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 (TA+TB>2max) 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 → Tdiv=Tideal, efficiency =100%for every warp.
The four warp-flavours cost 20, 30, 25, 5 respectively; the average per-warp time is
420+30+25+5=480=20 cycles/warp.
Compare: the mixed warp spent 80 cycles to do the same four kinds of work. Sorting bought a 4× speed-up here purely by killing divergence — the classic parallel-algorithm preprocessing trick, also a staple of CUDA optimization patterns.
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): Tdiv=30.
Warps 1, 2, 3 (each splits into 30 and 18, both taken): Tdiv=30+18=48 each.
Sum the issued cycles across all four warps:
Total issued=30+3×48=30+144=174 cycles.
The all-uniform ideal charges each warp only its longest single path once (max(30,18)=30), across four warps:
Tidealblock=4×30=120 cycles.Block efficiency=174120≈0.690=69.0%.
One clean warp cannot rescue three dirty ones — divergence is per-warp and the penalties simply add across the block.
Recall
Solution L5.2Option A — every warp splits, both paths taken: per-warp =40+10=50; four warps =4×50=200 cycles.
Option B — 2 warps uniform-heavy (40 each) + 2 warps uniform-light (10 each):
2×40+2×10=80+20=100 cycles.Speed-up=100200=2.0×.
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: 25+15=40 cycles.
Warp P: 40 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: 40+10=50 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? ::: Tdiv=∑(lengths of paths with≥1taker); Tideal=max(those lengths); efficiency =Tideal/Tdiv. Populations don't affect timing; uniform code before/after runs once.