6.2.11GPU Architecture

Warp divergence penalties

2,623 words12 min readdifficulty · medium2 backlinks

Overview

When threads in a warp take different execution paths through conditional code, the GPU must serialize those paths, destroying the parallel efficiency that makes GPUs powerful. This is warp divergence, and it can reduce effective throughput to 1/32nd of peak on NVIDIA GPUs.

Figure — Warp divergence penalties

GPUs use SIMT (Single Instruction Multiple Thread): one instruction decoder broadcasts to32 execution units. When threads diverge, the hardware must execute each branch serially, masking off inactive threads.

The Mechanism

How the Hardware Handles Divergence

Step-by-step execution model:

  1. All threads reach a branch together (same instruction pointer)
  2. Each thread evaluates condition → produces true/false
  3. Hardware creates execution mask: bitmask marking which threads go way
  4. Execute first path with mask A active (other threads idle, burn power)
  5. Push second path onto divergence stack
  6. Execute second path with mask B active (first group now idle)
  7. Reconverge when both paths meet at a common point
Tdiverged=i=1kTiT_{\text{diverged}} = \sum_{i=1}^{k} T_i

Where TiT_i is the execution time of path ii.

Why this formula? Without divergence, all32 threads execute in parallel: Tideal=max(T1,T2,,Tk)T_{\text{ideal}} = \max(T_1, T_2, \ldots, T_k). With divergence, we serialize: each path runs sequentially. The speedup loss is:

Efficiency=max(T1,T2,,Tk)i=1kTi\text{Efficiency} = \frac{\max(T_1, T_2, \ldots, T_k)}{\sum_{i=1}^{k} T_i}

Derivation for worst-case (each thread different):

If every thread takes a unique path through a conditional with nn instructions:

  • Ideal: 1 warp executes nn instructions in time ntcyclen \cdot t_{\text{cycle}}
  • Diverged: 32 sequential executions, each nn cycles → 32ntcycle32n \cdot t_{\text{cycle}}

Throughput reduction: 32×32\times slower

__global__ void divergent_kernel(int *data, int n) {
    int idx = threadIdx.x;
    if (idx < 16) {
        data[idx] = data[idx] * 2;      // Path A: 10 instructions
    } else {
        data[idx] = data[idx] + 100;    // Path B: 8 instructions  
    }
}

What happens inside one warp (threads 0-31):

  1. All 32 threads evaluate idx < 16
  2. Threads 0-15 → true, threads 16-31 → false
  3. Execute path A (multiply): threads 0-15 active, 16-31 masked off (idle) → 10 instruction cycles
  4. Execute path B (add): threads 16-31 active, 0-15 masked off → 8 instruction cycles
  5. Reconverge at kernel exit

Total cost: 10 + 8 = 18 instruction cycles

Without divergence (if all took same path): max(10, 8) = 10 cycles

Efficiency: 10/18 = 55.6%

Why this step? The hardware can't skip instructions for inactive threads in SIMT—it must decode and issue them, just with masked outputs. This is the serialization penalty.

__global__ void data_dependent(int *types, float *data) {
    int idx = threadIdx.x;
    int type = types[idx];  // Each thread loads different value
    
    if (type == 0) {
        data[idx] = sqrt(data[idx]);        // 20 cycles
    } else if (type == 1) {
        data[idx] = exp(data[idx]);         // 30 cycles
    } else if (type == 2) {
        data[idx] = log(data[idx]);         // 25 cycles
    } else {
        data[idx] = data[idx] * data[idx];  // 5 cycles
    }
}

Scenario: In one warp, 8 threads have type=0, 10 have type=1, 8 have type=2, 6 have type=3.

Execution:

  1. Path type=0: sqrt, 20 cycles,8 threads active
  2. Path type=1: exp, 30 cycles, 10 threads active
  3. Path type=2: log, 25 cycles, 8 threads active
  4. Path type=3: multiply, 5 cycles, 6 threads active

Total: 20 + 30 + 25 + 5 = 80 cycles

Ideal parallel: max(20,30,25,5) = 30 cycles

Efficiency: 30/80 = 37.5%

Why this matters: Data-dependent branching where each thread might take a different path is catastrophic for GPU efficiency. This is common in ray tracing (different materials), particle systems (different particle types), or sparse matrix operations.

__global__ void loop_divergent(int *counts, float *data) {
    int idx = threadIdx.x;
    for (int i = 0; i < counts[idx]; i++) {  // Each thread different count
        data[idx] += i * 0.5f;
    }
}

If counts[] = [5, 20, 3, 15 ...] across a warp:

  • Thread with count=3 finishes first, goes idle
  • Thread with count=20 keeps entire warp busy until it finishes
  • Effective iterations: max(counts)=20\max(\text{counts}) = 20
  • Utilization: counts32max(counts)\frac{\sum \text{counts}}{32 \cdot \max(\text{counts})}

Why? The loop exit is a branch. On each iteration, some threads exit (condition false) while others continue. Hardware must keep executing iterations until the last thread finishes, with a shrinking active mask.

Common Patterns and Mitigation

Why it feels right: You think, "Only2 out of 32 threads take the else branch, so 93% are doing the same thing—shouldn't that be okay?"

Why it's wrong: The hardware executes both branches fully for the entire warp, regardless of how many threads take each path. Even if only 1 thread takes a different path, you pay the full cost of both paths.

Example:

if (idx == 0) {
    // 100 instructions - only thread 0
} else {
    // 50 instructions - threads 1-31
}

Cost: 100 + 50 = 150 cycles, not 50 + (100/32) = 53 cycles.

The fix: Understand that divergence is per-warp, and cost is sum of all paths, not weighted by thread count. Restructure to avoid branching altogether, or ensure divergence happens at warp boundaries.

Why it feels right: You think nested conditionals create more divergence points.

Why it's nuanced: Nested ifs within the same divergent path don't add extra serialization—they're part of one path's instruction count. But nested ifs that create orthogonal divergence patterns multiply the number of execution paths.

Example:

if (A) {
    if (B) { path1; } else { path2; }
} else {
    if (C) { path3; } else { path4; }
}

Worst case: 4 distinct paths if distribution is bad. Cost: sum of all path lengths.

The fix: Think in terms of "distinct execution paths" across the warp, not syntactic nesting depth.

Mitigation Strategies

  1. Align divergence with warp boundaries
// Bad: divergence within warp
if (idx % 2 == 0) { ... }
 
// Good: divergence at warp granularity
int warp_id = idx / 32;
if (warp_id % 2 == 0) { ... }  // Entire warps take same path

Why this works: If all threads in a warp take the same path, there's no divergence. Restructure data/computation so similar work groups by32s.

  1. Predication for short branches

Modern GPUs support predicated execution for small conditionals:

// Compiler may convert to predication (no branch):
result = (condition) ? a : b;  // Both computed, one selected per thread

When this helps: If both paths are<~5 instructions, predication avoids divergence overhead. Both sides execute, but results are masked. Only beneficial if TA+TBT_A + T_B (serialized) >2max(TA,TB)> 2 \cdot \max(T_A, T_B) (predicated).

  1. Sort/bucket data

Preprocess data so similar items are adjacent, aligned to warp size:

// Before kernel: sort particles by type in groups of 32
// Then: each warp processes uniform type, zero divergence
  1. Dynamic paralelism / warp-level intrinsics

Use __ballotsync, __shfl_sync to detect and handle divergence explicitly:

unsigned mask = __ballot_sync(0xFFFFFFFF, condition);
if (mask == 0xFFFFFFFF) {
    // All threads true: fast path
} else if (mask == 0) {
    // All threads false: fast path
} else {
    // Mixed: handle carefully
}

Performance Characteristics

For a warp split into kk groups executing paths of lengths I1,I2,,IkI_1, I_2, \ldots, I_k:

Instructions executed=i=1kIi\text{Instructions executed} = \sum_{i=1}^{k} I_i Instructions useful=maxiIi\text{Instructions useful} = \max_{i} I_i Wasted work ratio=IimaxIiIi\text{Wasted work ratio} = \frac{\sum I_i - \max I_i}{\sum I_i}

Binary split (50/50) with equal path lengths:50% waste.

Worst case (32-way split, equal lengths): 32II32I=96.875%\frac{32I - I}{32I} = 96.875\% waste.

Real-world measurements:

  • Well-structured code (warp-aligned divergence): <5% loss
  • Typical ray tracing (material branching): 30-50% loss
  • Pathological data-dependent (sparse matrices): 70-90% loss
Recall

Explain to a 12-year-old Imagine you're the teacher, and your class of 32 kids is doing a worksheet. You can only give ONE instruction at a time, and everyone must listen. The worksheet says "If you're a boy, solve problem A. If you're a girl, solve problem B."

You tell all the boys to work on problem A while the girls sit and wait. Then you tell all the girls to work on problem B while the boys sit and wait. Even though the kids could have worked in parallel, you had to serialize it because you can only give one instruction at a time.

That's warp divergence. The GPU can only say one thing to all 32 threads, so when they need to do different things, it has to go one at a time through each group. The more different groups, the more waiting everyone does.

The fix? Organize your class so all the boys are in one room and all the girls in another (warp-aligned divergence), or give worksheets that don't have "if you're a boy/girl" splits at all (branchless code).

Connections

  • 6.2.8-SIMT-execution-model - Fundamental execution model that causes divergence
  • 6.2.9-Warp-scheduling - How scheduler handles divergent warps
  • 6.2.10-Memory-coalescing - Memory efficiency also requires uniform access patterns
  • 8.4.2-Branch-prediction - Why CPU branch prediction doesn't apply to GPUs
  • 9.3.5-Parallel-algorithmsdesign - Designing algorithms to avoid divergence
  • 6.2.12-Occupancy-optimization - Divergence reduces effective occupancy
  • 10.1.3-CUDA-optimization-patterns - Practical patterns for divergence-free code

#flashcards/hardware

What is a warp in GPU architecture? :: A group of 32 threads (NVIDIA) or 64 threads (AMD wavefront) that execute the same instruction simultaneously in lockstep. They share one instruction pointer and are the fundamental unit of GPU execution.

What is warp divergence?
When threads within a single warp take different execution paths through conditional code (if/else, loops), forcing the GPU to serialize execution of each path, with inactive threads masked off but still consuming time.
How does the GPU handle warp divergence mechanically?
The hardware creates execution masks for each branch path, executes the first path with some threads active (others idle), pushes remaining paths onto a divergence stack, executes each path serially, then reconverges when all paths complete.
What is the performance cost formula for divergence with k paths?
T_diverged = sum of T_i for all k paths, versus T_ideal = max(T_i) in parallel. Efficiency = max(T_i) / sum(T_i). Worst case with32 distinct paths: 96.875% wasted work.
Why doesn't "only 1 thread diverges" mean low cost?
Because the hardware must execute both branch paths fully for the entire warp duration, regardless of how many threads take each path. Cost is sum of path lengths, not weighted by thread count.
What is the key strategy to minimize divergence?
Align divergence with warp boundaries (32-thread groups). Ensure all threads in a warp take the same path by organizing data/work so similar items are grouped in multiples of 32.
When does predicated execution help with divergence?
For short branches (<~5 instructions per path) where computing both sides and selecting results per-thread is cheaper than serializing the branches. Compiler converts to maskable instructions with no actual branch.
What is the worst-case divergence scenario?
Data-dependent branching where each thread in a warp takes a completely different execution path (e.g., 32 different material types in ray tracing, random loop counts). This serializes all32 paths: 32x slowdown.
How do loops cause divergence?
Loop exit conditions are branches. Each iteration, some threads may exit (condition false) while others continue, creating divergence. The warp must execute iterations until the last thread finishes, with shrinking active masks.
What is __ballot_sync used for in divergence handling?
Returns a bitmask showing which threads in a warp passed condition. Allows detection of uniform (all true/false) versus divergent cases, enabling fast-path optimizations when all threads agree.

Concept Map

broadcasts to

all share

forces

threads differ

hardware tracks with

drives

masks off

causes

paths meet at

wastes power and

SIMT execution model

Warp - 32 threads

Shared instruction pointer

Threads hit conditional

Warp divergence

Active mask stack

Serialized path execution

Inactive threads idle

Throughput loss up to 32x

Reconvergence point

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Warp divergence GPU performance ka sabse bada dushman hai. Socho ki ek instructor hai jo 32 students ko ek sath padhata hai, lekin wo sirf EK instruction de sakta hai ek time pe. Agar worksheet mein likha ho "agar tum ladke ho to question A karo, agar ladki ho to question B karo," to kya hoga? Instructor pehle sabhi ladkon ko A karayega jabki ladkiyan wait karengi (idle), phir sabhi ladkiyon ko B karayega jabki ladke wait karenge. Time double ho gaya, kyunki sab parallel nahi kar sakte the.

Yahi warp divergence hai. GPU ka hardware design SIMT hai—Single Instruction Multiple Threads—matlab ek instruction decoder 32 execution units ko broadcast karta hai. Jab threads different paths lete hain (if-else, loops), hardware ko path ko serially execute karna padta hai, inactive threads ko mask karke. Agar teen different paths hain, to teno ka time add ho jayega, parallel benefit khatam. Example: agar 16 threadsek path lete hain (10 cycles) aur 16 dosra path (8 cycles), to total cost 18 cycles, jabki parallel hota to sirf 10 cycles lagte (maximum).

Worst case tab hota hai jab har thread ko alag path chahiye—jaise ray tracing mein alag materials, ya sparse matricesein data-dependent operations. Tab 32 threads ko alag paths serially execute karne padte hain: 32x slowdown! Optimization ka golden rule: apna data organize karo taki similar kaam wale threadsek warp mein ayein (32 ka group). Agar sab threads ek warp mein same path le rahe hain, to zero divergence, full speed. Ya phir code likhte waqt hi branching avoid karo—predication use karo chhote conditionals ke liye. GPU programming mein "threads ko sync mein rakhna" performance ka asli mantra hai.

Go deeper — visual, from zero

Test yourself — GPU Architecture

Connections