SIMT: One Instruction, Many Threads
GPUs get their raw computational throughput from massive parallelism achieved cheaply, and the key trick is that a single instruction fetch and decode can drive dozens of execution lanes simultaneously, since all threads in a warp are, at the hardware level, executing the identical instruction at the identical program counter at the identical moment, just against different data held in each lane's own registers. This is essentially SIMD, single instruction multiple data, but NVIDIA calls its variant SIMT to emphasize that from a programmer's perspective, each thread appears to have its own independent control flow, its own program counter and call stack in the abstract programming model, even though the underlying hardware imposes lockstep execution across the warp. This abstraction is what lets CUDA and similar programming models feel like ordinary multi-threaded code, you write a kernel as if describing one thread's work, and the hardware runs thousands of instances of it. The efficiency payoff is enormous: a single warp scheduler, one set of fetch and decode logic, one instruction cache access, drives 32 or 64 execution lanes, meaning the control overhead per unit of computation is a small fraction of what an equivalent number of independent CPU-style cores would require. But this efficiency is entirely predicated on all lanes within a warp actually wanting to execute the same instruction at the same time, which is precisely the assumption that conditional branches can violate.
What Happens When Threads Disagree
Consider a kernel with an if/else statement where the condition depends on each thread's own index or data, so some threads in a warp evaluate the condition true and others false, a situation called warp divergence or branch divergence. Because the hardware can only issue one instruction stream to the warp at any given cycle, it cannot simply have some lanes execute the if-branch instructions while other lanes simultaneously execute the else-branch instructions. Instead, the warp scheduler serializes the two paths: first it issues the if-branch's instructions to the entire warp, but with an active mask that disables, or predicates off, every lane whose thread actually wanted the else-branch, so those lanes execute the instructions as no-ops that do not modify any state. Then the warp executes the else-branch's instructions, this time with the mask inverted, active for exactly the lanes that were disabled before and disabled for the ones that were active. Only once both paths have been issued does the warp reconverge, meaning every lane becomes active again for whatever code follows the if/else. The critical consequence is that total execution time for the divergent region is roughly the sum of both branches' costs, not the maximum of the two as true parallel execution would achieve, because the hardware spends real cycles running masked-off, wasted lane-cycles during each phase.
Quantifying the Serialization Penalty
The performance cost of divergence scales with how evenly threads split across branches and how much work each branch contains. In the best case, all 32 threads in a warp agree on the same branch, there is no divergence at all, and the warp simply skips the other path's instructions entirely, paying zero extra cost. In the worst realistic case, exactly half the warp takes each path with roughly equal-cost branches, the warp now takes approximately twice as long to get through the conditional as a fully convergent warp would, since it must fully execute both the if-branch instructions, wasting 16 lanes' worth of cycles, and the else-branch instructions, wasting the other 16 lanes' worth. If the branches are very unequal in cost, say one path does a hundred operations and the other does one, the penalty is dominated by the expensive path regardless of how few threads take it, since the entire warp must wait through all hundred operations even if only a single lane needed them. This is why GPU programming guides consistently advise organizing data and thread-to-work mapping so that threads within the same warp tend to take the same branch together, a property sometimes called branch coherence, rather than scattering divergent conditions across arbitrary thread indices. Sorting or bucketing data by which branch it will trigger before launching a kernel is a common, effective real-world technique specifically to minimize this serialization cost.
Reconvergence and Nested Divergence
Handling a single if/else is conceptually straightforward, but real kernels often have nested conditionals, loops with data-dependent exit conditions, and function calls that themselves branch internally, all of which can create more complex divergence patterns that the hardware must track and eventually reconcile. GPUs maintain, conceptually, a reconvergence stack or similar structure that records, for each point where a warp diverges, which lanes went which way and where those paths are expected to merge back together, so the hardware can correctly restore the full active mask once every divergent path has completed. Loops present a particularly interesting case: if different threads in a warp need different numbers of iterations, perhaps because they are traversing linked lists of different lengths or converging on a solution at different rates, the entire warp must keep iterating until every single thread's individual loop condition is satisfied, with already-finished threads simply masked off for the remaining iterations, again wasting their lane-cycles. Newer GPU architectures, starting notably with NVIDIA's Volta generation, introduced independent thread scheduling, which allows finer-grained interleaving between divergent paths and can help with certain synchronization patterns between diverged threads, but it does not eliminate the fundamental throughput cost of divergence, executing two different instruction streams sequentially on the same set of execution lanes is inherently more expensive than executing one stream that all lanes agree on.
Designing Around Divergence in Practice
Because warp divergence is a hardware-level phenomenon invisible in a kernel's source code, correctness is never at risk, but performance can degrade sharply and silently, making it a favorite target of GPU profiling tools like NVIDIA Nsight Compute, which explicitly reports branch efficiency metrics showing what fraction of executed instructions were actually useful versus masked off as wasted lane-cycles. Common mitigation strategies include restructuring conditionals so the branch decision depends on warp-aligned boundaries, for instance branching on warp or block index rather than an arbitrary per-thread computed value, sorting or partitioning input data so that threads likely to take the same path are grouped into the same warps before kernel launch, and in some cases replacing a genuine branch with predicated, branchless arithmetic that computes both possible results and selects between them using a mask, trading some wasted computation for the elimination of divergence overhead entirely, a technique conceptually similar to branchless programming on CPUs but motivated by warp-wide execution rather than pipeline misprediction. Ray tracing and physically based rendering workloads are notorious for triggering heavy divergence, since neighboring rays or pixels frequently follow very different code paths, which is part of why GPU ray-tracing hardware includes dedicated units to handle exactly this kind of irregular control flow more gracefully. Recognizing divergence as a first-class performance concern, not just an implementation detail, is one of the clearest ways that GPU programming genuinely differs from writing parallel code for a CPU.
Frequently asked questions
What is a warp and how large is it?
A warp is a group of threads, 32 on NVIDIA hardware, that the GPU executes together in lockstep, issuing the same instruction to all threads in the group on each cycle. AMD's equivalent grouping is called a wavefront and is typically 64 threads.
Does branch divergence produce incorrect results?
No, divergence never affects correctness because masked-off lanes simply do not execute or write any state during the branch they did not take, and the hardware correctly reconverges all lanes afterward. The only effect is performance: the warp spends extra cycles executing both paths' instructions instead of just one.
How much slower is a fully divergent warp compared to a convergent one?
In the common case of a roughly even split with similarly costly branches, a warp can take close to twice as long to get through the conditional compared to a fully convergent warp taking the same branch. Highly unequal branch costs or deeply nested divergence can produce even larger slowdowns.
How can programmers reduce warp divergence?
Common techniques include structuring conditionals around warp-aligned boundaries rather than per-thread arbitrary values, sorting or grouping data so threads likely to take the same branch end up in the same warp, and using branchless, predicated arithmetic to avoid genuine control-flow divergence altogether. Profiling tools that report branch efficiency help identify which kernels are most affected.
Did NVIDIA's independent thread scheduling eliminate the divergence penalty?
No. Volta and later architectures introduced independent thread scheduling, which gives more flexibility in interleaving divergent paths and helps certain synchronization patterns between threads that have diverged, but the fundamental cost remains: executing two different instruction streams sequentially on shared lanes is inherently more expensive than one convergent stream.
Try it live
Everything above runs in your browser — open GPU Warp Scheduling & SIMD Branch Divergence and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.
▶ Open GPU Warp Scheduling & SIMD Branch Divergence simulation