2 · Just enough GPU to read this code

This chapter explains the handful of hardware facts the kernels are built around. You don't need the whole CUDA programming guide: just how work is split into threads, how bytes reach those threads, and how a tensor core multiplies. Each idea is tied to the line of submission.py that depends on it.

ON THIS PAGE

2.1 The machine

H200 part number where it matters here
Streaming multiprocessors (SMs) 132 Each SM runs one or more thread blocks. A grid with fewer than 132 blocks leaves SMs idle.
Clock under load about 1.98 GHz So 1 µs ≈ 2,000 cycles.
Registers per SM 65,536 × 32 bit (256 KB) Our GEMVs keep weights, x values and sums in registers.
Shared memory per SM up to 228 KB Used only for the final reductions in our GEMVs.
L2 cache, shared by all SMs 50 MB Cold at the start of each timed call. x (at most 0.5 MB) is read from it many times.
HBM3e main memory 141 GB at 4.8 TB/s Every weight byte comes from here once per call.
Tensor-core peak (BF16, dense) 989 TFLOP/s Only matters for m = 128/256.

2.2 Grid, CTA, warp, lane

A kernel launch creates a grid of thread blocks. NVIDIA's documents also call a thread block a CTA (cooperative thread array); this guide uses "CTA" because the code does.

Each CTA is a group of threads that:

The threads of a CTA come in warps of 32. A warp executes one instruction at a time for all 32 of its threads ("lanes"). The SM keeps many warps resident and switches between them every cycle. When one warp is waiting for memory, another warp issues instructions. That is how a GPU hides memory latency: lots of independent warps, each with lots of loads in flight.

In our GEMV kernels:

CUDA concept in gemv_kernel / gemvz_kernel
grid (N/16, S): one CTA per block of 16 weight rows, times S slices of K
CTA NW warps (NW + 1 in gemvz), so NW × 32 threads
warp streams its own slice of K for the CTA's 16 rows
lane lane = tid & 31, split into g = lane >> 2 (0–7) and t = lane & 3 (0–3); see 2.5

For example, qkv_gate at m = 1 launches 2304/16 = 144 CTAs of 17 warps each.

2.3 The memory hierarchy

level size typical latency who sees it
registers up to 255 per thread 0 cycles one thread
shared memory / L1 up to 228 KB per SM about 30 cycles one CTA (shared memory), one SM (L1)
L2 50 MB about 200–300 cycles the whole GPU
HBM 141 GB about 0.5–1 µs under load the whole GPU

Our GEMVs read weights straight from HBM into registers. They never stage weights through shared memory, because every weight byte is used by exactly one warp, exactly once. Shared memory appears only at the end, to add up the warps' partial sums.

2.4 Feeding a memory-bound kernel

To keep 4.8 TB/s flowing, the GPU must have enough bytes "in the air" at every moment. This is Little's law: bytes in flight = bandwidth × latency.

4.8 TB/s × ~0.7 µs  ≈  3.4 MB in flight  ≈  25 KB per SM, all the time

A thread's load takes about a microsecond to come back. So each warp must issue several loads before it needs the first result, and each SM must host enough warps.

Three habits in our code serve this goal:

The load instruction itself is special:

ld.global.nc.L1::no_allocate.v4.u32
part meaning
global read from global memory (HBM, through L2)
nc non-coherent: the data is read-only during the kernel, so the fast read-only path can be used
L1::no_allocate don't keep it in L1: each weight byte is used once, so caching it would only evict useful data
v4.u32 four 32-bit words (16 bytes) per lane

2.5 Tensor cores and mma.sync.m16n8k16

A tensor core multiplies small matrix tiles in one instruction. The variant our GEMVs use is:

mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32   D = A · B + C
operand shape type registers per lane
A 16 × 16 (m × k) BF16 4 registers × 2 values = 8 values
B 16 × 8 (k × n) BF16 2 registers × 2 values = 4 values
C, D 16 × 8 (m × n) FP32 4 values

Together, the 32 lanes of a warp hold the three tiles. Each lane holds a few elements, called its fragment. That one warp instruction performs 16 × 8 × 16 = 2,048 multiply-adds. Which lane holds which element is fixed by the hardware, with lane = 4·g + t:

A tile: 16 rows × 16 k-slots. Each cell shows the lane holding it; colour = g = lane >> 2.
0123456789101112131415
00011223300112233
14455667744556677
2889910101111889910101111
312121313141415151212131314141515
416161717181819191616171718181919
520202121222223232020212122222323
624242525262627272424252526262727
728282929303031312828292930303131
80011223300112233
94455667744556677
10889910101111889910101111
1112121313141415151212131314141515
1216161717181819191616171718181919
1320202121222223232020212122222323
1424242525262627272424252526262727
1528282929303031312828292930303131
Lane 5 (g=1, t=1) holds rows 1 and 9, k-slots 2, 3, 10, 11: eight BF16 values in four 32-bit registers.
B tile: 16 k-slots × 8 columns (tokens)
01234567
00481216202428
10481216202428
21591317212529
31591317212529
426101418222630
526101418222630
637111519232731
737111519232731
80481216202428
90481216202428
101591317212529
111591317212529
1226101418222630
1326101418222630
1437111519232731
1537111519232731
C/D accumulator: 16 rows × 8 columns (FP32)
01234567
000112233
144556677
2889910101111
31212131314141515
41616171718181919
52020212122222323
62424252526262727
72828292930303131
800112233
944556677
10889910101111
111212131314141515
121616171718181919
132020212122222323
142424252526262727
152828292930303131

How our GEMV uses it ("swap A and B").

Why a tensor core for a matrix-vector product? Instruction count. Without it, every 16 bytes of weights would need a dozen conversion and fused multiply-add instructions per lane. Those instructions would compete with the loads for issue slots. One mma per 16 bytes keeps the SM free to keep memory busy.

Hopper also has wgmma, a "warpgroup" MMA that takes tiles from shared memory and is much faster for large matrices. cuBLAS uses it at m = 128/256. Our shipped GEMVs use the older mma.sync, which is simpler and plenty for memory-bound work. Our own wgmma kernels (rounds 4, 6 and 7 in the report) never beat cuBLAS.

2.6 Reductions inside a CTA and across CTAs

The K dimension is long (up to 8192), so it is split among the warps of a CTA. Each warp ends with partial sums for the same 16 rows × tokens, which must be added up.

2.7 Templates: one compiled kernel per configuration

The kernels are C++ templates, such as gemv_kernel<NW, U, NB, DUAL, PIPE>, because:

The price is that every configuration is a separate compiled kernel. submission.py therefore lists exactly the combinations it uses (the C3(...) and Z3(...) lines, chapter 6). Asking for any other combination at run time raises "unsupported gemv config".

2.8 PTX, asm volatile and SASS

2.9 CUDA graphs and launch cost

Launching a kernel from Python costs several microseconds of CPU time. A CUDA graph records the launches once and replays them, so the judge measures GPU time only. Two consequences for the code:

  1. kernel_fn must not wait for the GPU or read GPU values on the CPU: no .item(), no printing tensors. The graph cannot contain that.
  2. Every kernel still pays roughly 1–2 µs of fixed cost inside the GPU: start-up, ramp-up, and the tail where the last CTAs finish. This is a big deal for the tiny router (floor 1.8 µs, ours 5.4 µs).
Try it · lab2_fragments.py — Lab 2: tensor-core fragments and the weight repack (_pack), emulated in numpy.
cd ~/projects/okc/muse_bf16_proj/learn/labs
python3 lab2_fragments.py
The file is already on your Mac at ~/projects/okc/muse_bf16_proj/learn/labs/lab2_fragments.py; you can also view or download it here.

Part 1 of the lab prints the three ownership maps above and checks an emulated mma against numpy. Part 2 belongs to chapter 5; you can run it now or later.

Check yourself

qkv_gate at m = 1 launches 144 CTAs on 132 SMs. What problem does that cause, and how does PIPE = 2 deal with it?

If only one CTA fit per SM, 12 CTAs would have to wait for a second "wave", almost doubling the time.

PIPE = 2 sets __launch_bounds__(threads, 2), which tells the compiler to use few enough registers that two CTAs fit on one SM. Then all 144 run at once.

Little's law check: compressed qkv_gate at m = 1 uses 16 streaming warps per CTA, and each lane keeps up to 2 groups × 48 bytes in flight. Is that enough per SM?

16 warps × 32 lanes × 96 bytes ≈ 49 KB per CTA, roughly double the ~25 KB per SM that Little's law asks for. So yes, with margin.

In the A-tile map, which elements does lane 0 hold, and why are they not contiguous?

Lane 0 holds rows 0 and 8, k-slots 0, 1, 8 and 9. The hardware spreads each lane over two rows and two k ranges.

If we loaded W in its natural row-major order, a lane's 8 values would come from 4 different places. Chapter 5 shows how relabelling k and repacking W turns them into one 16-byte load.