3.1.2 SIMT Deadlock and Stackless SIMT Architectures
The SIMT stack is elegant — and it has a failure mode serious
enough that NVIDIA redesigned divergence handling for Volta. The stack’s rule
is “finish the TOS path before anything else.” Combine that rule with a
spin lock and you get a program that is correct on any MIMD machine but
hangs on a stack-based GPU. ElTantawy and Aamodt named it
A: *mutex = 0;
B: while (!atomicCAS(mutex, 0, 1)); // spin until we take the lock
C: // critical section
atomicExch(mutex, 0); // release
atomicCAS reads mutex, compares with 0, and — atomically, serialized
across the warp’s threads — swaps in 1 if it matched, returning the old
value. So exactly one thread sees 0 (wins); the rest see 1 (spin). Watch
the stack paint itself into a corner:
Control-flow graph
SIMT stack (TOS on top)
Warp lanes over time
| A | B | B | B | B | |
|---|---|---|---|---|---|
| t0 | |||||
| t1 | |||||
| t2 | |||||
| t3 |
time →
event 1/5Line A initializes the shared variable mutex to 0 (lock free). All four threads execute together.
The winner waits at the reconvergence point, below the TOS; the spinners own the TOS and can never succeed until the winner releases — which it can’t, because it isn’t in the TOS active mask. Circular dependence, purely an artifact of the divergence mechanism.
Convergence barriers: divergence handling without a stack
NVIDIA’s fix (disclosed in a patent application and consistent with Volta’s
- Barrier Participation Mask — which threads of the warp take part in a given convergence barrier (several may exist at once, one per nesting level);
- Barrier State — which participants have already arrived;
- Thread State (per thread) — ready / blocked at a barrier / yielded (the escape hatch that lets other threads make progress in would-be-deadlock situations);
- Thread rPC (per inactive thread) — its next instruction;
- Thread Active — one bit per thread.
Two instructions drive it (Figs 3.7/3.8): ADD arms a barrier — every
thread active at that point sets its bit in the named barrier’s participation
mask; WAIT stops an arriving
The crucial difference from the stack: between ADD and WAIT, the scheduler may freely switch among warp splits. Divergence still splits the warp into subsets with a common PC — but no entry “owns” the warp anymore. Compare the book’s two timing figures on the same 8-thread warp:
if (threadIdx.x < 4) { A; B; }
else { X; Y; }
Z; // Volta: preceded by __syncwarp()
| X | Y | A | B | Z | |
|---|---|---|---|---|---|
| t0 | |||||
| t1 | |||||
| t2 | |||||
| t3 | |||||
| t4 | |||||
| t5 | |||||
| t6 | |||||
| t7 |
time →
The else-path executes first — the if-side lanes are masked off, idle.
| X | A | Y | B | Z | |
|---|---|---|---|---|---|
| t0 | |||||
| t1 | |||||
| t2 | |||||
| t3 | |||||
| t4 | |||||
| t5 | |||||
| t6 | |||||
| t7 |
time →
The else-split issues X…
Interleaving doesn’t raise SIMD efficiency — each issue still uses part of the datapath — but it lets one split’s memory stalls be covered by the other split’s work, and, decisively, it makes the spin lock finish:
| A | lock | C | lock | C | lock | C | lock | C | done | |
|---|---|---|---|---|---|---|---|---|---|---|
| t0 | ||||||||||
| t1 | ||||||||||
| t2 | ||||||||||
| t3 |
time →
mutex = 0; all four threads together.
3.1.3 Warp Scheduling
A core hosts many warps — in what order should they issue? (For now, assume each warp issues one instruction and can’t issue again until it completes; §3.2 relaxes this.)
With an “ideal” fixed-latency memory, you’d support just enough warps to hide that latency and schedule them round-robin (RR): every warp gets equal time; if warps × issue-time exceeds the memory latency, the execution units never idle. But warps aren’t free: each thread needs its own registers (that’s what makes zero-cost warp switching possible), so more warps = more register file = fewer cores per chip. The right number is a throughput-per-area balance, not “as many as possible.”
Real memory latency depends on locality — and locality is exactly where scheduling policy bites:
| Round-robin (equal progress) | Threads share data at similar execution points; DRAM-friendly access order | Warps arrive at the shared data together → on-chip cache hits; nearby addresses touched nearby in time → DRAM row locality |
| Repeat the same warp | Threads mostly touch disjoint data (complex per-thread data structures) | One warp runs long enough to keep ITS working set live in the caches (Rogers et al.) |
The deeper question — how scheduling interacts with the memory system — returns in force once Chapter 4 builds one, and in §5.1’s research survey.
Check yourself
1. Why does the SIMT stack deadlock on the spin-lock code (Fig 3.5)?
2. In the convergence-barrier mechanism, what does the WAIT instruction do?
3. Predict the schedule (use the two timelines): under Volta's Independent Thread Scheduling, statement X (else-split) has just issued and is waiting on memory. What can happen next that the stack-based design forbids?
4. How does the stackless mechanism resolve the spin lock (Fig 3.11)?
5. When does round-robin warp scheduling tend to WIN over repeatedly scheduling the same warp?