The one-loop core has a blind spot: its scheduler knows only
each warp’s thread IDs and next PC. It cannot know whether the next
instruction depends on one still in flight — so a warp can’t issue again
until its previous instruction completes, and hiding latency takes lots of
warps. The fix is to fetch first, ask questions later: instructions land
in a per-warp instruction
bufferinstruction buffer (I-buffer)Per-warp staging storage after fetch/decode enabling a second scheduling loop and I-cache miss hiding.Glossary → after I-cache access, and a second scheduling loop picks
issue-ready instructions from it. (The I-buffer also hides instruction-cache
miss latency, teaming up with instruction MSHRsMSHRMiss-status holding register: tracks an outstanding cache miss so further misses to the same line merge instead of replaying to memory.Glossary →.)
Ready means no data hazards — and detecting those cheaply at GPU scale is
its own puzzle. Reservation stations need associative logic (area, energy:
no). A CDC-6600-style out-of-order scoreboardscoreboard (GPU)Coon et al. design: 3–4 entries/warp of pending destination registers; instructions carry a dependency bit-vector cleared on writeback, issue when all-zero.Glossary → is complex (no). Even the
humble single-bit-per-register in-order CPU scoreboard explodes here: up to
128 registers × 64 warps = 8,192 bits, and if every waiting warp probed
its operands each cycle you’d need up to 256 read ports. GPUs instead
use Coon et al.’s design — a handful of entries per warp and a bit-vector
per buffered instruction. Step through it:
The GPU scoreboard (Coon et al.)cycle 0 / 5
Step 1: A load issues; its destination is recorded
Warp w0 issues `ld.global r7, [r2]`. The scoreboard allocates an entry for w0 holding the pending destination: r7. Nothing stalls — issue continues.
w0 scoreboard entries: [ r7 ] [ -- ] [ -- ]
One tick = one scoreboard-relevant event (Coon et al. design: 3–4 entries per warp holding pending destination registers; instructions carry a dependency bit-vector).
So the two-loop core: loop 1 picks a warp with I-buffer space and
fetches; loop 2 picks a buffered instruction with a clear dependency
vector and issues it.
3.3 Three-Loop Approximation
To switch warps every cycle, every warp needs its registers live in
hardware — recent NVIDIA parts carry 256 KB of register file per core.
SRAM area scales with port count, so the register file is built from
single-ported banks that fake multi-portedness. The catch: two operands
in the same bank can’t be read in the same cycle — a bank conflictregister bank conflictTwo needed operands living in the same single-ported bank, serializing their reads.Glossary →. Watch the naive design (banks →
crossbar → per-instruction staging registers, Fig 3.12; naive layout where
every warp’s r0 sits in bank 0, Fig 3.13) struggle with just three
instructions:
filled = operand read (color = warp)⏎ = writeback (higher priority than reads)EU = instruction reaches execution unit
cycle 0Press play or step through the cycles.
One tick = one core cycle. Naive banked register file with the naive (unswizzled) layout of Fig 3.13 — every warp's r5 lives in bank 1. Timing of Fig 3.14.
Six cycles, three instructions, mostly idle banks. (The layouts, concretely:
naive puts every warp’s r0 in bank 0 —
Bank 0
Bank 1
Bank 2
Bank 3
naive (Fig 3.13)
w0:r0, w0:r4, w1:r0…
w0:r1, w0:r5, w1:r1…
w0:r2, w0:r6, w1:r2…
w0:r3, w0:r7, w1:r3…
swizzled (Fig 3.16)
w0:r0, w0:r4, w1:r3…
w0:r1, w0:r5, w1:r0…
w0:r2, w0:r6, w1:r1…
w0:r3, w0:r7, w1:r2…
— the swizzle rotates each warp’s mapping by its warp id.)
The industry answer is
the operand collectoroperand collectorCollector units buffering each instruction's source operands so an arbiter can overlap reads across banked register files; the third scheduling loop.Glossary → — the
third scheduling loop. Staging registers become collector units:
each issuing instruction is allocated one, with buffer space for all its
source operands.
single-ported banks
Bank 0
Bank 1
Bank 2
Bank 3
Arbitrator
crossbar
collector units (one per issuing instruction)
w0 · mad
v:1
r3
rdy:0
——
v:1
r7
rdy:0
——
v:1
r1
rdy:0
——
w3 · add
v:1
r2
rdy:1
8,3,…,0
v:1
r5
rdy:0
——
v:0
—
rdy:-
——
w1 · add
v:1
r2
rdy:1
0,0,…,0
v:1
r5
rdy:1
0,2,…,1
v:0
—
rdy:-
——
SIMD Execution Unit
The operand collector (after Fig. 3.15): each issuing instruction owns a collector unit whose
rows buffer one source operand each (valid bit, register, ready bit, value); the arbiter routes
bank reads through the crossbar into waiting rows. The naive design (Fig. 3.12) had plain
per-instruction staging registers here instead — one instruction collecting at a time.
Multiple instructions now collect operands concurrently,
so the arbiter sees requests from many instructions at once and can keep
several banks busy every cycle — bank conflicts don’t vanish; they get
overlapped.
Pair that with a smarter swizzled
layoutswizzled register layoutOffsetting each warp's registers across banks (w1:r0→bank1) so equal-progress warps hit different banks.Glossary → (Fig 3.16): warp w’s register r goes to bank (r + w) mod
4, so equal-progress warps (hello, round-robin scheduling) touch
different banks for the same register name — and one warp’s writeback
stops colliding with its neighbors’ reads:
filled = operand read (color = warp)⏎ = writeback (higher priority than reads)EU = instruction reaches execution unit
cycle 0Press play or step through the cycles.
One tick = one core cycle. Operand collector + swizzled layout (Fig 3.16: warp w's registers rotate w banks). Timing of Fig 3.17.
Same six cycles — four instructions serviced, banks working in parallel,
one instruction reaching the EU per cycle.
The WAR hazard nobody ordered
The collector imposes no order between instructions leaving it. If a
younger same-warp instruction writes a register an older, conflict-stalled
instruction hasn’t read yet — silent corruption (write-after-read).
Candidate fixes, in increasing cleverness (Mishkin et al.): force program
order out of the collector; a release-on-commit warpboard (one
instruction in flight per warp — up to ~2× slowdown); a release-on-read
warpboard (one collecting instruction per warp — ≤10% cost); or a
bloomboard — a small Bloom filter tracking outstanding reads (~few %).
And recall from §2.2: Maxwell’s control instructions carry a
“read dependency barrier” — NVIDIA’s production answer to the same hazard.
3.3.2 Instruction replay: structural hazards without stalls
What if an instruction hits a structural hazard — collector units exhausted,
or a memory access that must be split into multiple operations? A CPU would
stall younger instructions. In a GPU, distributing a stall signal across
a huge register file and deep graphics pipeline hurts the critical path (or
costs buffering), and a stalled instruction from one warp can block other
warps that wanted an unrelated resource. GPUs choose instruction replayinstruction replayRe-executing an instruction from the I-buffer on structural hazard instead of stalling the pipeline.Glossary → instead: keep the
instruction in the I-buffer until it’s known complete (or all its pieces
have executed), and simply re-issue it as needed. No speculation — GPUs
avoid that on principle — just retry as a throughput-friendly alternative to
stalling.
Check yourself
1. What does the two-loop design add, and why?
2. Why don't GPUs use a classic per-register scoreboard like an in-order CPU's?
3. Predict the timing (naive layout, Fig 3.14): w1's add needs r5 and r1, both in bank 1, and w0's writeback to r5 (bank 1) is pending. Why does w1 take three grid cycles (4–6) to finish reading?
4. What do collector units change, exactly?
5. The operand collector can violate correctness without extra care. How?