all_lessons / modern_gpu / lessons / 00 · feeding the core lesson 1 / 15

Part I · Why the classic model breaks

Feeding the tensor core

On a Blackwell B200 the math unit got so fast that supplying it with data is now the whole kernel. Everything Hopper and Blackwell added — TMA, TMEM, async barriers, warp specialization, clusters — is one forced move after another to stop starving a tensor core that can multiply faster than memory can feed it.

You already know the classic CUDA story from the gpu_kernels track: a thread block loads a tile of A and a tile of B into shared memory, calls __syncthreads(), every thread does its slice of the multiply with a tensor-core MMA (the WMMA / mma.sync path), syncs again, and loops over K. That kernel is correct. On an A100 it is even respectable. On a B200 it leaves roughly 99% of the tensor core idle, and this track is the story of why — and of the entirely different kernel shape that fixes it.

The engine of this whole track
The tensor core got so fast that feeding it became the entire kernel. Every lesson here derives one more mechanism as the forced move that removes one more thing starving the math unit. Read the title of any lesson as the answer to: "what was stopping the tensor core from running flat out, and what's the only way around it?"

The two numbers that set everything

A Blackwell B200 has two headline rates, and the entire paradigm falls out of their ratio:

Divide them. The roofline ridge — the arithmetic intensity at which a kernel stops being limited by memory and starts being limited by math — is:

ridge ≈ 2 × 10¹⁵ FLOP/s ÷ 8 × 10¹² B/s ≈ 250 FLOP / byte

Read that as a threshold. To break even with the tensor core, every byte you pull from HBM must be reused in about 250 floating-point operations. Touch a byte fewer times than that and you are memory-bound — the tensor core sits waiting. (These are round, order-of-magnitude ceilings; the exact figures wander with clock, power cap, and how you measure. The ratio, and the wall it implies, is what matters.)

From the MLC course · honesty note
Treat ≈ 2 PFLOP/s, ≈ 8 TB/s, and ridge ≈ 250 as headline ceilings, not datasheet guarantees. The point of this lesson is the shape of the problem — a math unit roughly 250× hungrier per byte than the prior generation of kernels was built for — not three exact constants.

For scale: a square matmul of size N has 2N³ FLOPs and moves ~3N² elements, so its arithmetic intensity is ≈ N/3 FLOP/byte (fp16). A 4096³ GEMM sits at ≈ 1365 — comfortably past the ridge, so it can be compute-bound. An RMSNorm or a GELU reuses each byte a handful of times — intensity ~1 — and lives permanently on the memory roof. The whole craft is getting the matmuls that deserve to be compute-bound to actually run compute-bound. The widget below lets you slide N and watch a point cross the ridge.

Interactive · the B200 roofline and the GEMM ladder

Log–log roofline. The sloped line is the memory roof (slope = 8 TB/s); the flat line is the compute roof (2 PFLOP/s); they meet at the ridge ≈ 250 FLOP/byte. Drag matmul size N to move the large-GEMM point (intensity ≈ N/3) and watch it climb the memory roof, hit the ridge, and saturate. The faint stepping markers are the optimization ladder this track builds — naive kernels live under the compute roof even when their math says they shouldn't, because they are starved, not slow.

B200 roofline · drag N, cross the ridge
x = arithmetic intensity (FLOP/byte), y = achieved TFLOP/s, both log-scale. A kernel can never sit above the roofline; the open question is whether it reaches it.
GEMM intensity (N/3)
regime
ladder step
vs naive

Why the synchronous kernel starves

The classic tiled GEMM runs its stages in series. One iteration of the K-loop looks like this, and each engine is idle while it waits on the one before:

loop over K tiles:
    load   A_tile, B_tile  from GMEM into SMEM   // memory path busy, tensor core IDLE
    __syncthreads()                              // everyone waits
    compute  acc += A_tile · B_tile  (mma.sync)  // tensor core busy, memory path IDLE
    __syncthreads()                              // everyone waits
store acc to GMEM                                // epilogue busy, everything else IDLE

On an A100 the math was slow enough that the load overlapped well in practice and this was fine. On a B200 the tcgen05 MMA finishes its slice of work so fast that the load it is waiting on dominates the clock — the tensor core spends most of its cycles parked, and the threads that should be issuing math are instead computing addresses and copying bytes, stealing the very issue slots the math needs. Worse, the accumulator for a big modern tile outgrew the register file entirely. The synchronous loop has three separate problems, and Blackwell added one feature to kill each:

The fix: turn the kernel into a supply chain

Stop running the stages in series. Run all three hardware paths at once, each working on a different tile, like a 3-stage pipeline in a factory. While the tensor core computes tile k, the TMA engine is already fetching tile k+1, and the epilogue is draining the result of tile k−1:

TMA engine → load
Fetches the next operand tile from HBM into shared memory. A dedicated copy engine, so no math warp touches it.
working on tile k+1
tensor core → compute
Multiplies the current tile straight out of SMEM, accumulating into TMEM. Never idle, never waits on a copy.
working on tile k
epilogue → store
Drains the previous result out of TMEM, casts it, writes it back to HBM via a TMA store.
working on tile k−1

That is the mental model for the entire track: the kernel is a supply chain, not a loop. The math unit is the bottleneck machine you must never let go hungry, and every other piece of hardware exists to keep its input queue full and its output queue drained. Once you commit to that, the rest of the modern feature set stops looking like a grab-bag of acronyms and starts looking inevitable: you need a way to run producer and consumer warps simultaneously (warp specialization, lesson 10), a way to pool several SMs so one operand load feeds more math (clusters, lesson 07), and a way to keep the chain running across output tiles without paying launch overhead (persistent kernels, lessons 09–10).

The payoff this track builds: a 744× ladder to cuBLAS

The capstone (lessons 08–10) optimizes one kernel — a 4096 × 4096 × 4096 fp16 GEMM on a B200 — across a ladder of steps. Each step removes exactly one thing starving the tensor core. The numbers are the destination of this whole track:

StepWhat it addsTimevs naive
1 · naivesmallest correct tiled kernel70 ms
3 · SMEM tilingstage operands through shared memory53.6 ms1.3×
4 · TMA asynchardware copy engine off the math path0.49 ms~142×
7 · warp specializationproducer + consumer warps run at once0.23 ms~309×
8 · 2-CTA clustertwo SMs cooperate on one 256×256 tile0.104 ms~676×
9 · multi-consumertwo consumers share one operand load0.094 ms~744×

Read the table as a story, not a leaderboard. The single biggest jump lands at ~142× over naive — 53.6 ms drops to 0.49 ms — and it is just adding the TMA copy engine. That one number is the thesis of the track in miniature: the math was never the problem; feeding it was. By step 8 the kernel is genuinely compute-bound (the tensor core is the bottleneck, exactly as it should be), and step 9's final 0.094 ms is cuBLAS parity — the same kernel NVIDIA's own library runs.

Mental stance for the whole track
The right question is never "is occupancy high?" — modern kernels deliberately run low occupancy. The right question is "are the active hardware units staying busy?" A kernel with 12% occupancy whose tensor core never stalls beats a kernel at 100% occupancy whose tensor core waits on every tile. We unlearn the occupancy reflex in lesson 01.

The hardware vocabulary you'll need

Two hierarchies recur in every later lesson. First, the execution hierarchy — who runs an instruction, from one lane up to the whole GPU. Hopper inserted a new level (the warpgroup) and a new scope above the SM (the cluster):

LevelSizeWhat it is
thread1 laneone scalar program counter
warp32 threadsthe SIMT unit; one instruction, 32 lanes
warpgroup128 threads (4 warps)new on Hopper; the unit a wgmma issues over
CTA (thread block)≤ 1024 threadsruns on exactly one SM; shares its SMEM
clusterseveral CTAsHopper+; co-scheduled CTAs across SMs that can read each other's SMEM
gridall CTAsthe whole kernel launch

Second, the memory spaces — where data can live, from slowest/biggest to fastest/smallest. Blackwell added a brand-new one (TMEM) purely to house the accumulator:

SpaceScopeCapacity (B200)Role
GMEM (HBM)whole GPU~180 GBoff-chip DRAM; where tensors start and end
SMEMper-CTA≤ 228 KB / SMon-chip scratchpad; staging area for operand tiles
TMEMper-CTA128 lanes × ≤ 512 colsBlackwell-only; the MMA accumulator's new home
registersper-threadup to 255 / threadthe fastest store; operand fragments live here on older gens

The three knobs every decision turns

Underneath all the acronyms, every design choice in this track is some combination of three recurring elements. Naming them now means later lessons can say "this is a dispatch decision" and you'll know exactly what kind of move it is:

scope Which threads run an op — one elected lane, a warp, a warpgroup, a whole cluster? A tcgen05.mma is issued by one thread; cta_sync spans a block.
layout Where the data lives and in what shape — which memory space, what stride, what swizzle. Operands have rigid layout requirements; getting them wrong reads scrambled bytes.
dispatch Which hardware path runs it — a thread copy, the TMA engine, the async MMA, the epilogue. The supply chain is entirely about putting each job on the right path.

scope layout dispatch

Two generations, one invariant

Everything here targets two NVIDIA architectures, and they share one invariant the tensor core has held since Volta: D = A · B + C. What changes each generation is how the multiply is issued, where the operands live, and where the accumulator lives — which is exactly the scope/layout/dispatch trio above.

Hopper · sm_90
Introduces the warpgroup and wgmma (a 128-thread MMA), clusters of cooperating CTAs, and TMA. The accumulator still lives in registers.
Blackwell · sm_100 / B200
The 5th-gen tensor core tcgen05: async, issued by one elected thread, accumulating into the new TMEM. Adds 2-CTA cooperative MMA, TMA multicast, and distributed shared memory across a cluster.

The 15 lessons, as one argument

The track is one chain of forced moves in four parts. Each part hands the next a bottleneck it can't solve, and the next part's mechanism is the only way out:

PartLessonsThe forced move
I · Forcing function00–02Establish the wall (feeding ≫ math), how to measure fed-vs-starved (roofline), and the layout algebra you need to reason about operands at all.
II · The five primitives03–07The five hardware features each forced by the synchronous kernel's failures: tensor-core generations, TMA, TMEM, mbarriers, clusters/CLC.
III · GEMM, tiled → SOTA08–10Assemble the primitives into the 744× ladder above — from the smallest correct tile to cuBLAS parity.
IV · Flash Attention 4 & the craft11–14The harder capstone (softmax wedged between two MMAs), then debugging warp-specialized kernels and the real CUTLASS/CuTe toolchain.
Takeaway
A Blackwell tensor core multiplies at ≈ 2 PFLOP/s while HBM feeds it at ≈ 8 TB/s, so the roofline ridge is ≈ 250 FLOP/byte: a matmul has to reuse every byte ~250 times just to break even. The classic synchronous tiled GEMM — load → sync → compute → sync → store, in series — leaves a B200 tensor core idle most of the clock, because the math finishes faster than the load it waits on. The fix is to rebuild the kernel as a 3-stage supply chain (TMA fetches k+1, the tensor core computes k, the epilogue drains k−1, all at once). Every modern feature — TMA, TMEM, mbarriers, warp specialization, clusters — is a forced move to remove one more thing starving the math unit, and the payoff is the GEMM ladder from 70 ms to 0.094 ms (~744×, cuBLAS parity). Three knobs recur throughout: scope · layout · dispatch.
Where this connects
This track is the advanced sequel to gpu_kernels — that track teaches the classic synchronous SIMT model (warps, coalescing, SMEM tiling, a WMMA-era tensor core) that this one supersedes; we assume it and never re-teach SIMT from scratch. triton_kernels is the higher-level DSL that hides much of this paradigm — and lesson 14 marks exactly where it can't reach (TMA, warp specialization, TMEM). ai_compilers is the companion view of how kernels like these get generated rather than hand-written. And cs336 shows where these GEMMs and attention kernels plug into the full LLM training and inference stack.

So how do we actually measure "fed" versus "starved", and tell which kernels even deserve to be compute-bound? That's the roofline as a working tool — and the async overlap that lets a real kernel reach it. Lesson 01.