GPUs and SIMD Execution

A single instruction that adds two numbers wastes almost all of a modern chip's arithmetic hardware. Real workloads — image filters, neural network layers, physics simulations — apply the same operation to millions of data elements. This article answers a key question. How does hardware exploit that repetition instead of executing one scalar instruction at a time? The answer is data-level parallelism. It is expressed through two closely related but distinct mechanisms. These are SIMD (Single Instruction, Multiple Data) vector execution, and SIMT (Single Instruction, Multiple Threads) warp execution used by GPUs. Both amortize the cost of fetching and decoding one instruction across many arithmetic operations. This is why a GPU can deliver far more floating-point throughput per watt than a CPU core on the right kind of workload. It is also why frameworks like CUDA, OpenCL, and every major deep-learning library are built around this execution model. This matters because the last two decades of computing performance growth have come overwhelmingly from parallelism, not from faster clocks. Single-thread performance plateaued as power density limits ended frequency scaling. GPUs responded by trading large caches and aggressive out-of-order logic — the tools of single-thread performance — for thousands of simple lanes that stay busy on parallel data. Understanding SIMD/SIMT execution is key to understanding why GPUs dominate machine learning, scientific computing, and graphics. It also explains why writing efficient code for them requires thinking about data in bulk, rather than one value at a time. This article builds the concept from first principles. It shows how a vector instruction becomes many ALU operations. It explains how SIMT hardware maps that idea onto threads with masking for control flow. It covers how warp scheduling hides memory latency, and where the throughput and correctness tradeoffs actually lie. It connects to Hardware Multithreading and Simultaneous Multithreading, reuses the dependence concepts from Instruction-Level Parallelism and Dependences, and sets up The Roofline Model for reasoning about whether a kernel is compute-bound or memory-bound.

Key takeaways
  • SIMD applies one instruction to a fixed-width vector of data elements in lockstep, amortizing fetch/decode over many ALU ops.
  • SIMT (GPU model) groups scalar threads into warps/wavefronts that execute the same instruction on different data, using per-lane masking for divergent branches.
  • Warp schedulers hide memory and pipeline latency by switching among many resident warps every cycle, requiring no register spilling.
  • Throughput is maximized when all lanes are active and memory accesses are coalesced into few wide transactions.
  • Branch divergence and uncoalesced memory access are the two dominant sources of GPU inefficiency.
  • The model directly feeds into the Roofline Model: whether a kernel is bound by arithmetic throughput or memory bandwidth.
The core insight

GPUs turn many independent lanes of identical work into throughput by executing one instruction stream across thousands of data elements, hiding stalls by rapidly switching among ready warps instead of speeding up any single thread.

The idea

SIMD (Single Instruction, Multiple Data) is an architectural pattern in which one instruction specifies an operation — add, multiply, compare — that is applied simultaneously to multiple data elements packed into a wide register. A CPU vector unit with 256-bit registers holding eight 32-bit floats executes `vaddps` once and produces eight sums in one instruction issue. The parallelism is explicit at the instruction-set level: the compiler or programmer chooses the vector width, and the hardware replicates one ALU control signal across many datapaths.

SIMT (Single Instruction, Multiple Threads) is the GPU's variant of the same underlying idea, but expressed at the thread level rather than the register level. A programmer writes ordinary-looking scalar code — one thread computes one output pixel, one thread updates one array element — and the hardware groups threads into fixed-size groups called warps (NVIDIA terminology, 32 threads) or wavefronts (AMD, 64 threads). All threads in a warp share one instruction fetch/decode unit and execute the same instruction each cycle, but each thread carries its own registers and its own program counter conceptually, so control flow can diverge — handled by predication/masking rather than by refusing to compile. This is the crucial difference from pure SIMD: SIMT preserves a scalar programming model while the hardware underneath still executes in a SIMD-like lockstep across lanes. The other pillar of GPU throughput is latency hiding through massive multithreading, building directly on the register-file replication ideas from Hardware Multithreading and Simultaneous Multithreading. A GPU streaming multiprocessor (SM) holds the architectural state — registers, warp ID, program counter — for dozens of warps simultaneously (commonly 32–64 resident warps, i.e. 1024–2048 threads per SM). When one warp stalls on a memory load, the warp scheduler switches to another ready warp with zero-cycle overhead, because that warp's registers are already resident in a partition of a very large register file. This differs fundamentally from ILP techniques in Instruction-Level Parallelism and Dependences, which try to extract parallelism within one instruction stream via reordering and speculation; SIMT/GPU throughput comes from parallelism across thousands of independent instruction streams (threads), each individually simple and in-order.

How it works

Step 1 — Kernel launch and the thread hierarchy. A programmer writes a kernel: a function executed once per logical thread. Threads are organized into a hierarchy — threads within a block (CUDA) or workgroup (OpenCL), blocks within a grid. The hardware's job is to map this hierarchy onto physical lanes.

Step 2 — Warp formation. The runtime partitions each thread block into fixed-size warps (32 consecutive thread IDs on NVIDIA hardware). Warp formation is purely index-based: thread IDs 0–31 form warp 0, 32–63 form warp 1, and so on. This is a static, cheap operation — no dynamic load balancing at this stage.

Step 3 — Instruction issue across a warp. Each cycle (or over a few cycles depending on the SM's SIMD width relative to warp size), the warp scheduler picks one ready warp and issues its next instruction to all 32 lanes simultaneously. Each lane executes the instruction on its own registers and its own slice of memory addresses. This is functionally SIMD execution, but the "vector width" (32) is fixed by hardware and hidden behind the scalar-thread abstraction.

Step 4 — Divergence handling via masking. When threads in a warp take different branches (e.g., `if (x[i] > 0)`), the hardware cannot execute two instruction streams at once. Instead it uses an active mask: it executes the "taken" path with the mask disabling threads that shouldn't participate, then executes the "not-taken" path with the complementary mask, then reconverges. This means divergent branches cost the sum of both paths' latencies, not the max — the central throughput penalty of SIMT.

Step 5 — Memory access coalescing. When a warp executes a load/store, the memory system checks whether the 32 per-thread addresses fall into a small number of contiguous cache-line-sized segments. If thread `i` accesses address `base + i*4` (i.e., consecutive floats), the 32 requests coalesce into one or two wide memory transactions. If addresses are scattered (e.g., a strided or random-access pattern), the hardware must issue up to 32 separate transactions, collapsing throughput by an order of magnitude.

Step 6 — Latency hiding via warp scheduling. While a warp waits on a long-latency memory operation, the scheduler issues instructions from other resident warps. Because each SM holds many warps' worth of register state simultaneously (no swapping, no spilling — this is the direct generalization of the fine-grained/simultaneous multithreading register-duplication idea), the scheduler can fully hide memory latency as long as enough independent warps are resident and ready. ```pseudocode // ===== Software view: one thread per output element ===== kernel VectorAdd(A[], B[], C[], N): i = blockIdx * blockDim + threadIdx // global thread id if i < N: C[i] = A[i] + B[i] // ===== Hardware view: SIMT execution of one warp ===== // WARP_SIZE = 32 (fixed by hardware) function ExecuteWarp(warp, PC): activeMask = warp.mask // bit set = thread participates instr = Fetch(PC) if instr is BRANCH(cond): // evaluate cond per-lane -> two disjoint sub-masks takenMask = activeMask AND EvalPerLane(cond, true) notTakenMask = activeMask AND EvalPerLane(cond, false) if takenMask != 0: Push(divergenceStack, (reconvergePC, notTakenMask)) ExecutePath(takenPC, takenMask) if notTakenMask != 0 and takenMask == 0: ExecutePath(notTakenPC, notTakenMask) // reconvergence happens when execution reaches reconvergePC else: for lane in 0..31: if activeMask has bit(lane): RegisterFile[lane].apply(instr) // one ALU op per active lane AdvancePC(warp) // ===== Warp scheduler: picks a ready warp each cycle ===== function SchedulerCycle(residentWarps): ready = { w in residentWarps : w.notStalled() } if ready is empty: return IDLE // all warps waiting on memory/sync w = SelectByPolicy(ready) // e.g., greedy-then-oldest, round-robin IssueNextInstruction(w) ``` The `SelectByPolicy` step is where real hardware differs across vendors and generations — common policies include round-robin and "greedy-then-oldest," chosen to balance fairness against exploiting instruction-cache/data locality across consecutive issues from the same warp.

Figure
SIMT execution: warps, lanes, and divergence via masking
Streaming Multiprocessor (SM): one warp of 32 threads, 8 lanes shown Warp Scheduler — issues ONE instruction per cycle to all active lanes of the selected warp Fetch / Decode (shared) Lane0 Lane1 Lane2 Lane3 Lane4 Lane5 Lane6 Lane7 ALU ALU masked ALU masked ALU masked ALU regs[i]regs[i]regs[i]regs[i] regs[i]regs[i]regs[i]regs[i] if (x[i] > 0) then A else B — a divergent branch inside one warp Pass 1 (mask = threads where x[i]>0): lanes 0,1,3,5,7 execute path A; lanes 2,4,6 are masked off (idle, correct). Pass 2 (mask = threads where x[i]≤0): lanes 2,4,6 execute path B; lanes 0,1,3,5,7 are masked off. Reconvergence: both masks merge; all 8 lanes proceed together on the next shared instruction. Cost: latency(A) + latency(B), even though only one path is "real work" per lane — this is the divergence penalty. Coalesced access: lane i reads base+4i (consecutive) → 1–2 wide memory transactions for the whole warp. Strided/random access: lane i reads scattered addresses → up to 32 separate transactions, ~32x bandwidth loss.

Complexity & correctness

Throughput model. Let a GPU have SS streaming multiprocessors, each capable of issuing one warp instruction (32 lanes) per cycle at clock rate ff. Peak throughput in operations per second is

Tpeak=S×32×f×(ALU ops per instruction issue slot)T_{\text{peak}} = S \times 32 \times f \times (\text{ALU ops per instruction issue slot})

This is a direct multiplicative gain over a scalar core: one instruction fetch/decode amortizes over 32 ALU operations, so the control overhead per useful operation drops by a factor of 32 relative to a scalar pipeline. This is the same amortization principle as SIMD vector units in CPUs, just realized at thread-group granularity.

Why masking is correct. The key invariant is: at every point in the divergence-and-reconvergence protocol, exactly one control-flow path is active per lane, and every lane executes precisely the sequence of instructions that its own scalar program order would dictate. Formally, if thread tt's serial execution trace is the sequence of instructions I1t,I2t,…I_1^t, I_2^t, \dots, SIMT execution guarantees each IktI_k^t is executed with thread tt's correct register state, in order, regardless of what other lanes in the warp are doing. The mask mechanism enforces this by having a lane execute an instruction only if its mask bit is set; masked-off lanes neither read nor write architectural state (registers, memory) for that instruction. Because divergent paths are executed serially (one full pass per distinct control path, not interleaved instruction-by-instruction), there is no possibility of one lane's speculative execution corrupting another's state — this is what makes SIMT semantically equivalent to independent scalar threads, at a real cost in time (the union of path lengths) but zero cost in correctness.

Cost of divergence. If a warp's 32 threads split into kk mutually exclusive groups with path lengths ℓ1,…,ℓk\ell_1, \ldots, \ell_k (in instructions), the warp's total issue-cycle cost is ∑j=1kℓj\sum_{j=1}^k \ell_j, versus max⁡jℓj\max_j \ell_j in an idealized world with independent per-thread control. In the worst case (all 32 threads on distinct paths, or two paths of nearly equal weight), throughput on that warp degrades toward 1/321/32 of peak — the single biggest correctness-preserving inefficiency in the model.

Latency hiding — why it works and its limit. Suppose a memory operation has latency LL cycles and the SM issues one instruction per cycle when at least one warp is ready. If WW warps are resident and each warp's compute-to-memory ratio leaves it needing to wait LL cycles after issuing a memory request, the scheduler can fully hide that latency as long as

W≥Lcycles of independent work per warp between memory requestsW \geq \frac{L}{\text{cycles of independent work per warp between memory requests}}

Occupancy — the fraction of the SM's maximum warp slots that are actually resident, limited by register file size, shared memory, and thread-block size — determines whether WW is large enough. This is the GPU generalization of the same latency-hiding argument used for fine-grained multithreading in Hardware Multithreading and Simultaneous Multithreading: more independent contexts convert stall cycles into useful issue cycles, up to the point where the hardware runs out of resident warps or the memory system itself saturates.

Space complexity. The register file must hold live state for every resident warp simultaneously — no spill-to-memory context switch exists in the fast path. For WW resident warps of 32 threads each using rr registers per thread, the SM needs 32⋅W⋅r32 \cdot W \cdot r physical registers. This is why register usage per thread directly limits occupancy: a kernel using more registers per thread supports fewer resident warps, at a fixed register file size, trading per-thread performance against latency-hiding capacity — a genuine and unavoidable tradeoff, not a bug.

Worked example

Consider a warp of 8 threads (shrunk from 32 for readability) executing: ``` i = threadIdx if (x[i] > 0): y[i] = sqrt(x[i]) // path A, 4 cycles else: y[i] = 0 // path B, 1 cycle z[i] = y[i] + 1 // reconverged instruction ``` Input data: `x = [4, -1, 9, -3, 0, 16, -2, 1]`

lane (i)x[i]branch outcomepath taken
04trueA
1-1falseB
29trueA
3-3falseB
40falseB
516trueA
6-2falseB
71trueA

Execution trace:

cycleactive mask (lanes executing)operation
1{0,2,5,7}path A, cycle 1 of 4 (start `sqrt`)
2{0,2,5,7}path A, cycle 2 of 4
3{0,2,5,7}path A, cycle 3 of 4
4{0,2,5,7}path A, cycle 4 of 4 (`y[i] = sqrt(x[i])` completes)
5{1,3,4,6}path B, cycle 1 of 1 (`y[i] = 0`)
6{0,1,2,3,4,5,6,7}reconverged: `z[i] = y[i] + 1` for all lanes

Total: 6 cycles for the warp. An idealized machine with independent per-thread control (impossible in real SIMT hardware, but useful as a bound) would need only max⁡(4,1)+1=5\max(4,1) + 1 = 5 cycles if the two groups could execute concurrently on separate hardware. The extra cycle is exactly the divergence penalty: 4+14 + 1 (serial execution of both paths) instead of max⁡(4,1)\max(4,1). Now compare to the non-divergent case where all 8 threads have `x[i] > 0`: all take path A, mask is always full, and the warp finishes in 4+1=54 + 1 = 5 cycles with zero wasted lane-cycles. The divergent case wastes 4×44 \times 4 (path A idles 4 lanes for 4 cycles) +1×4+ 1 \times 4 (path B idles 4 lanes for 1 cycle) =20= 20 lane-cycles of masked-off, non-productive slots out of 8×6=488 \times 6 = 48 total lane-cycles issued — a 42% utilization loss purely from this one branch, even though the final result is identical and correct in both cases.

Common pitfalls & traps

PitfallWhy it hurtsFix
Assuming warps execute like fully independent threadsDivergent branches serialize; masked lanes still consume issue cyclesRestructure conditionals to align with warp boundaries, or use predication for short bodies
Strided or scattered memory access per laneDestroys coalescing; each lane may trigger a separate transactionReorganize data layout (array-of-structs → struct-of-arrays) so consecutive lanes touch consecutive addresses
Using too many registers per threadReduces resident warp count (occupancy), hurting latency hidingBound register usage or use launch-bounds hints; profile occupancy, not just per-thread speed
Treating occupancy as "always maximize it"Higher occupancy does not help if the kernel is already compute-bound; extra warps can even hurt cache localityMeasure whether the kernel is latency-bound before chasing occupancy
Ignoring reconvergence points across nested branches/loopsCompiler-inserted reconvergence can serialize more paths than expected, especially with early `return`/`break` inside divergent codeKeep divergent control flow shallow; avoid early exits inside warp-divergent regions when possible
Assuming SIMD (CPU vector) and SIMT (GPU) are identicalThey differ in whether divergent control flow is even expressible; SIMD often requires software masking by the programmer/compilerRecognize SIMT gives a scalar illusion; SIMD vector ISAs generally do not

Pioneers & history

GPUs evolved from fixed-function graphics pipelines into general-purpose SIMT parallel processors as vector-array-style supercomputer ideas were re-adapted to commodity silicon. The intellectual lineage runs from early vector supercomputers, whose designers first grappled with amortizing control overhead over wide data operations, through to the CUDA-era architects who formalized SIMT as a distinct execution model bridging vector hardware and scalar programmability.

Origins · 1 minSeymour Cray and how this was discovered
SC
Seymour Cray
1925–1996 · Vector supercomputing

Cray's designs, culminating in the Cray-1 (1976), pioneered vector processing: one instruction operating on long vector registers, pipelined across many functional-unit stages. This established the core amortization idea — one instruction, many data elements — that GPUs later reincarnated at a much larger and cheaper scale using thousands of simple cores instead of a handful of deeply pipelined vector units.

History
1976
— Cray-1 vector supercomputer demonstrates pipelined SIMD vector execution at scale.
1999
— NVIDIA GeForce 256 marketed as the first "GPU," with fixed-function graphics pipelines.
2006
— NVIDIA introduces CUDA and the unified shader/SIMT programming model on the Tesla architecture.
2007
— CUDA C compiler and toolkit released, exposing warps and SIMT execution to general-purpose programmers.
2016
onward — GPU SIMT architectures become the dominant substrate for deep learning training via massively parallel matrix and tensor operations.

Where this matters

Every major deep learning framework — training transformers, convolutional networks, diffusion models — depends on SIMT execution to turn matrix multiplications into thousands of parallel warp-scheduled threads; tensor cores extend the same lockstep idea to small fixed-size matrix tiles. Scientific computing (fluid dynamics, molecular dynamics, weather simulation) uses CUDA/OpenCL/SYCL kernels structured explicitly around avoiding divergence and maximizing coalesced access. Real-time graphics rendering, the GPU's original domain, still relies on SIMT to shade millions of pixels or vertices with identical shader programs per frame. Cryptocurrency mining, video encoding/decoding, and databases with GPU-accelerated joins and aggregations all exploit the same warp/lane model. Understanding this execution model is also the prerequisite for reasoning about whether a given kernel is arithmetic-bound or bandwidth-bound — the question answered systematically by The Roofline Model, and for understanding how GPU cores relate architecturally to the multithreaded CPU cores discussed in Hardware Multithreading and Simultaneous Multithreading.

Ask the tutor: How does warp scheduling policy (e.g., round-robin vs. greedy-then-oldest) change latency-hiding behavior under different occupancy levels?

Is SIMD the same thing as SIMT?

No. SIMD is an instruction-set-level mechanism: one instruction explicitly operates on a fixed-width vector register, and control flow divergence must be handled by the programmer or compiler via software masking, since there is no per-lane program counter abstraction. SIMT is a GPU execution model where the programming model looks like ordinary independent scalar threads, and the hardware transparently executes them in lockstep across warps, handling divergence automatically via active masks and a reconvergence mechanism. SIMT can be seen as SIMD execution hidden behind a scalar illusion, which is why GPU code often reads like normal sequential C/C++ per thread despite running on genuinely SIMD hardware underneath.

Why does branch divergence hurt performance if it doesn't affect correctness?

Masking guarantees every lane computes exactly what its own scalar trace requires, so results are always correct. The performance cost arises because a warp's issue unit executes one instruction stream per cycle; when threads disagree on control flow, the hardware must execute each distinct path serially with the other lanes masked off and idle. The warp's total time becomes the sum of all divergent path lengths rather than the maximum, and any masked-off lane during a given pass contributes zero useful work that cycle. This inefficiency is invisible to correctness tests, which is precisely what makes it dangerous in practice — programs "work" while running far below peak throughput.

How many warps can be resident on an SM, and why does it matter?

The limit is set by the SM's fixed resources: the register file, shared memory/scratchpad capacity, and a maximum thread-block/warp count per SM (architecture-dependent, commonly up to 32–64 resident warps or 1024–2048 threads). A kernel using more registers or shared memory per thread reduces how many warps can be resident concurrently, which is called reduced occupancy. Occupancy matters because latency hiding depends on having enough independent, ready warps to fill the SM's issue slots whenever some warps are stalled on memory; too few resident warps means the SM idles during long-latency operations instead of switching to other work.

What is memory coalescing and why is it central to GPU performance?

Coalescing is the hardware's ability to merge the individual memory requests issued by the 32 threads of a warp into a small number of wide transactions when those addresses are contiguous or follow a regular pattern. Because off-chip memory bandwidth is a scarce, shared resource, one 128-byte coalesced transaction serving 32 threads is vastly more efficient than 32 separate small transactions. When access patterns are scattered (e.g., due to poor data layout or pointer-chasing structures), the hardware must issue many separate transactions per warp, and effective bandwidth can drop by an order of magnitude even though the arithmetic work is unchanged — often turning a compute-bound kernel into a severely memory-bound one.

Why don't GPUs just use out-of-order execution and branch prediction like high-performance CPUs?

CPUs invest heavily in single-thread speed: deep out-of-order pipelines, large branch predictors, and speculative execution, because they typically run few threads with complex, unpredictable control flow, drawing on the techniques discussed in Instruction-Level Parallelism and Dependences. GPUs instead assume workloads with abundant, largely independent, simple parallel work; they use simple in-order pipelines per lane and spend their transistor and power budget on replicating ALUs and register files across many lanes and many resident warps. This trades single-thread latency for aggregate throughput, which is the right tradeoff when the workload naturally decomposes into thousands of similar, mostly-regular tasks, as is common in graphics, dense linear algebra, and deep learning.

How does occupancy relate to actual performance, and is higher occupancy always better?

Occupancy measures the fraction of an SM's maximum warp capacity that is actually resident and available to the scheduler. Higher occupancy generally helps when a kernel is latency-bound, since more resident warps give the scheduler more options to hide stalls behind useful work. However, once occupancy is high enough to fully hide latency, further increasing it yields no additional benefit and can even hurt performance by reducing each thread's share of cache and register resources, worsening data locality. The right target is "enough occupancy to hide the kernel's actual latency," not maximum occupancy — a nuance easy to miss when tuning naively.

What connects SIMT execution to the Roofline Model?

The Roofline Model classifies a kernel's performance ceiling by comparing its arithmetic intensity (operations per byte moved from memory) against the machine's peak compute throughput and peak memory bandwidth. SIMT execution determines the achievable peak compute throughput (how many warps' worth of ALU operations can actually be issued per second, accounting for divergence losses), while coalescing efficiency determines the achievable memory bandwidth. A kernel with heavy divergence or poor coalescing effectively operates below the theoretical roofline in both dimensions, which is why understanding warp execution is a prerequisite to correctly placing a kernel on, and reasoning about, its roofline plot.

Before this

Where this leads