Warps are the fundamental unit of execution in NVIDIA GPUs, representing a group of threads that execute the same instruction in lockstep. Understanding warp scheduling is critical for optimizing GPU performance because the GPU scheduler operates at warp granularity, not individual threads.
Given a 3D thread block with dimensions (blockDim.x, blockDim.y, blockDim.z), threads are assigned to warps in row-major order:
Linear Thread Index=threadIdx.z×(blockDim.x×blockDim.y)+threadIdx.y×blockDim.x+threadIdx.x
Warp ID within Block=⌊32Linear Thread Index⌋
Why this matters: If blockDim.x = 64, threads0-31 form warp 0, threads 32-63 form warp 1. If blockDim.x = 17, threads 0-31 form warp 0, but only 17 threads are active—the other 15 slots are wasted. This is why block dimensions should be multiples of 32.
An SM can hold multiple thread blocks simultaneously (limited by registers, shared memory, and max blocks/SM). Each block contributes multiple warps. If an SM can hold 2048 threads and has 64 warps resident, the scheduler must decide which warp executes next.
Derivation of why zero-overhead switching works:
When warp W₁ stalls on memory, the scheduler immediately switches to warp W₂ in the same cycle. This is possible because:
Register files are partitioned: Each warp has dedicated register space (no context save/restore needed)
Program counters are per-warp: Switching warps = updating one pointer (1 cycle)
Instruction buffers are separate: Each warp's next instruction is pre-fetched
Latency hiding capacity:
If a memory operation takes L cycles and each warp performs one memory operation per W instructions, you need:
Nwarps needed=⌈WL⌉
Why? If L=400 cycles and W=10 instructions between memory ops, you need 400/10 = 40 warps to keep the SM busy while one warp waits.
Consider an if-else where nif threads take the if branch and nelse=32−nif take the else branch.
Without divergence (all threads take same path):
Tnormal=Tif
With divergence (assuming nif>0 and nelse>0):
Tdiverged=Tif+Telse
Worst case: Both branches have equal cost T and are equally populated:
Slowdown=TT+T=2×
Why this happens: The warp executes the if branch with a mask (disabling threads that took else), then executes the else branch with the opposite mask. Both paths run sequentially even if only1 thread needs each path.
Shared memory: e.g., 64 KB (configurable split with L1 cache)
Occupancy formula:
\frac{\text{Threads per Block} \times \text{Blocks per SM}}{\text{Max Threads per SM}},
\frac{\text{Blocks per SM}}{\text{Max Blocks per SM}},
\frac{\text{Warps per SM}}{\text{Max Warps per SM}}
\right)$$
Subject to:
\text{Registers per Block} \times \text{Blocks per SM} \leq \text{Total Registers}
Shared Memory per Block×Blocks per SM≤Total Shared Memory
If all warps need FP64, schedulers compete for limited FP64 units → underutilization of FP32 units.
Recall Explain to a 12-Year-Old
Imagine your teacher wants the class to solve math problems. Instead of calling on each student individually (slow!), she groups you into teams of 32. Each team gets the same problem, and everyone works on their copy at the same time. That's a warp!
The teacher (warp scheduler) walks around and checks which teams are ready. If Team A is stuck waiting for a book from the library (memory access), she immediately helps Team B instead. By the time she cycles through several teams, Team A's book has arrived, so no one wastes time waiting.
But here's the tricky part: if Team A's problem says "students with even IDs do addition, odd IDs do subtraction," the team has to split up. First, the even-ID students solve while odd-ID students sit idle. Then they switch. The team takes twice as long because they can't work together anymore. That's divergence!
The lesson: keep your whole warp team doing the same thing at the same time for maximum speed.
Cooperative-Groups - Modern warp-synchronous programming primitives
Register-Pressure - How register usage limits occupancy and warp count
#flashcards/hardware
What is a warp in GPU architecture? :: A group of 32 consecutive threads that execute the same instruction simultaneously in SIMT fashion. Warps are the fundamental scheduling unit in NVIDIA GPUs.
How is warp ID calculated from thread indices?
Linear thread index divided by 32 (floor division). Linear index = threadIdx.z * blockDim.x * blockDim.y + threadIdx.y * blockDim.x + threadIdx.x, then warpId = linearIdx / 32.
Why should thread block dimensions be multiples of 32?
To avoid wasted warp slots. If blockDim is not a multiple of 32, the last warp will have inactive threads that still consume resources but do no work.
What is warp divergence?
When threads within the same warp take different execution paths (different branches). The GPU must serialize the divergent paths, executing each with some threads masked off, reducing performance.
What is the worst-case performance penalty of warp divergence?
2× slowdown when both branches are equally costly and equally populated. The warp executes both branches sequentially instead of one path.
How does warp scheduling hide memory latency?
When one warp stalls on memory (100s of cycles), the scheduler immediately switches to another ready warp with zero overhead. With enough resident warps, the SM stays busy while stalled warps wait.
What is GPU occupancy?
The ratio of active warps to maximum warps per SM. Formula: Active Warps / Max Warps per SM. Higher occupancy provides more warps for latency hiding.
What factors limit occupancy?
Registers per thread, shared memory per block, threads per block, max blocks per SM, and max warps per SM. The most restrictive limit determines actual occupancy.
Why does warp context switching have zero overhead?
Register files are partitioned per warp, program counters are per-warp, and instructions are pre-fetched. Switching warps only updates a pointer, no context save/restore needed.
What is the difference between warp schedulers and cores?
Warp schedulers select which warp executes next. Cores (CUDA cores) actually execute instructions. Modern SMs have multiple schedulers (e.g., 4) to issue instructions from multiple warps per cycle.
How many warps are in a 256-thread block?
256 / 32 = 8 warps. Each block's threads are divided into groups of 32 consecutive threads.
What makes a warp eligible for scheduling?
The warp must be not stalled (no memory wait, dependency wait, or synchronization barrier) AND resources must be available (execution units, memory ports).
Why is threadIdx.x % 2 branching bad for performance?
It causes divergence in every warp because each warp contains both even (16) and odd (16) threadIdx values. Every warp must execute both branches serially.
What is the formula for warps needed to hide latency L?
Warps needed ≥ (Number of schedulers × L) / Instructions between stalls. For 4 schedulers and 400-cycle latency with 10 instructions between memory ops: 4 × 400 / 10 = 160 warps worth of work.
What is a warp-synchronous operation?
Operations that act across all threads in a warp simultaneously, like __shfl_sync() for data exchange or __ballot_sync() for voting, without requiring explicit synchronization.
Dekho, GPU mein jab hazaaron threads chalte hain, toh unhe individually manage karna bahut mehnga pad jaata. Isliye NVIDIA ne ek smart trick lagayi hai - 32 threads ko ek group mein daal diya jise warp kehte hain. Ye saare 32 threads ek hi instruction ko ek saath, lockstep mein execute karte hain. Socho jaise class mein 32 students ko ek hi math problem samjhaani ho - alag-alag 32 baar explain karna bekaar hai, toh teacher ek baar samjhaata hai aur sab saath follow karte hain. Bas yahi warp ka concept hai: ek baar instruction fetch aur decode hoti hai, lekin 32 parallel executions milti hain. Yeh control overhead ko saare threads mein baant deta hai, jisse GPU massive parallelism handle kar paata hai.
Ab yahan sabse important baat samajhne wali ye hai ki GPU scheduler warp ke level pe kaam karta hai, individual thread pe nahi. Matlab agar aap block dimensions ko 32 ke multiple mein nahi rakhte, toh waste ho jaata hai. Jaise agar aapka blockDim.x = 17 hai, toh warp toh phir bhi 32 threads ka banega, par sirf 17 active honge aur baaki 15 slots khaali barbaad ho jaayenge. Isiliye humesha block size 32 ka multiple rakhna chahiye - jaise (128,1,1) perfect hai kyunki 128/32 = exactly 4 full warps bante hain bina kisi waste ke.
Yeh cheez practically kyun matter karti hai? Kyunki CPU aur GPU ka philosophy alag hai - CPU ek single thread ko fast chalane pe focus karta hai (latency), jabki GPU bahut saare threads ko ek saath throughput ke liye optimize karta hai. Jab aap GPU code likhoge (CUDA vagera), toh agar aapne warp-friendly dimensions choose kiye, toh aapki performance kaafi better hogi. Warp waste kam karna, aur scheduler ko efficiently warps dena - yahi GPU optimization ka core funda hai. Isliye ye concept seekhna zaroori hai agar aapko serious GPU programming karni hai.