The main question of this lecture: we proved decode wastes bandwidth on paper. What must a kernel never write to HBM, and what must it never wait for, to actually reach the roofline?
Picture yourself profiling a 70B model on an H100 for the first time. You open the trace, and something is off: attention at long context takes seconds, yet your FLOP-counting from Lecture 1 says the math is milliseconds. Then you look at a decode step and find hundreds of tiny kernels separated by... nothing. Gaps. Idle silicon. Two leaks stand between your paper derivation and the metal: bytes that should never have left the chip, and time lost between kernels. This lecture is the hunt for both.
Lectures 1–6 decided what to compute and where; today is how one GPU actually computes it: which bytes live at which level of the memory hierarchy at which instant, and how hundreds of kernel launches are stitched into one decode step. Two kernel families carry almost all the load. The attention main line is FlashAttention → FlashInfer → FlashMLA → the sparse DSA chain; the MoE main line is grouped GEMM → DeepGEMM → DeepEP. Running through both is an execution-style evolution: manual launches → CUDA graphs → Mega MoE's communication–compute fusion. Every step is a response to a derivable number; deriving those numbers is our business today.
An H100 streaming multiprocessor (SM) presents you with a four-level hierarchy. Only two levels are under your exact control: registers and shared memory (SMEM). That is why kernel design is residency policy: which tiles are pinned in SMEM, which are streamed, and which are never materialized at all. Every byte crossing HBM costs \(1/\beta\) seconds; bytes that stay on chip are, to first order, free.
Three Hopper mechanisms changed what a good residency policy even looks like: TMA (async block-wise global↔shared copies; one thread issues, the hardware does the address arithmetic, think block-table lookup for paged KV, and signals an SMEM mbarrier); WGMMA (matrix multiply issued asynchronously per warpgroup, operands straight from SMEM); and thread-block clusters with distributed shared memory (blocks read each other's SMEM: a tile-exchange pool larger than 228 KB without touching L2/HBM). With copies and MMAs both asynchronous, the natural post-Hopper kernel is no longer "all warps do the same thing": producer warps run TMA for tile \(t+1\), consumer warpgroups run WGMMA on tile \(t\), epilogue warps drain tile \(t-1\), all handing off through mbarriers. This producer/consumer/epilogue skeleton is shared by FlashAttention-3, DeepGEMM, and Mega MoE. It keeps the tensor cores fed across the ~500-cycle HBM latency by decoupling "move bytes" from "multiply bytes". Two cousins to remember: Blackwell (SM100) adds tcgen05 with TMEM and native FP4 datapaths (they return below), and persistent kernels fix the grid at the SM count and pull tiles from a task queue: no re-launch cost, no wave quantization, and the freedom to interleave tiles of different subproblems onto free SMs.
Here is the easiest attention kernel you can write. If you have used eager PyTorch, you have written it: compute the full score matrix \(S = QK^\top\), softmax it into \(P\), multiply by \(V\). You can probably guess this is not the best way. But let us count exactly why not, because the number is spectacular. Assumptions: prefill of a length-\(L\) sequence, one layer, \(H_q = d_{model}/d_h\) query heads, \(n_{kv}\) KV heads, BF16 (\(b_{dtype} = 2\) B). We count per query head: GQA shares \(K, V\), but every query head owns its own \(S\) and \(P\).
The count, per query head
The QKᵀ GEMM reads \(Q, K\) (\(2Ld_h b\)) and writes \(S\) (\(L^2 b\)). Softmax reads \(S\), writes \(P\) (\(2L^2 b\)). The PV GEMM reads \(P\) (\(L^2 b\)), reads \(V\) and writes \(O\) (\(2Ld_h b\)). The quadratic term totals \(4L^2 b_{dtype}\) bytes per query head: four crossings of an \(L \times L\) matrix. Everything linear in \(L\) is negligible next to it. ∎
Summing over heads, traffic and arithmetic intensity of the naive implementation are \[ T_{naive} = H_q\left(4L^2 b_{dtype} + 4Ld_h b_{dtype}\right) \approx \frac{4L^2 d_{model}\, b_{dtype}}{d_h}, \qquad I_{naive} \approx \frac{d_h}{b_{dtype}} = \frac{128}{2} = 64\ \text{FLOP/byte}. \] (The FLOPs, for contrast, are \(\approx 4L^2 d_{model}\) per layer: Lecture 1's attention term.) And 64 sits far left of the H100 ridge \(\pi/\beta \approx 295\): attention executed naively is bandwidth-bound even at prefill sizes. The \(O(L^2)\) traffic, not the \(O(L^2)\) FLOPs, is the binding constraint. This is the single most important sentence about attention kernels.
torch.matmul + softmax in
eager mode), you are leaving a factor \(L/d_h \approx 10^3\) of HBM traffic, and up to ~10×
wall-clock at long context, on the table before any scheduling discussion begins.
Fine. Never write \(S, P\) to HBM. Just one small obstacle, and you may have spotted it: softmax needs the whole row before any output is final, because \(\mathrm{softmax}(s)_j = e^{s_j} / \sum_t e^{s_t}\) couples all KV positions through the denominator. Tiles are small; the row is long. How do you normalize a sum you haven't finished reading? Think first (is a running denominator even possible?), then read on.
The trick is online softmax: per query row, keep a running max \(m\) and running normalization \(\ell\), and correct past partial results by exponential rescaling whenever \(m\) grows. Process the score row \(s = qK^\top \in \mathbb{R}^L\) in blocks \(s^{(1)}, \dots, s^{(T)}\) of length \(B_c\) (with \(V\) blocked conformably); from \(m_0 = -\infty,\ \ell_0 = 0,\ O_0 = 0\): \[ m_i = \max\big(m_{i-1},\ \mathrm{rowmax}(s^{(i)})\big), \qquad \ell_i = \ell_{i-1}\, e^{m_{i-1} - m_i} + \sum_j e^{s^{(i)}_j - m_i}, \qquad O_i = O_{i-1}\, e^{m_{i-1} - m_i} + e^{s^{(i)} - m_i} V^{(i)}, \] with final output \(O_T / \ell_T\). This unnormalized form defers all division to the end (a normalized variant divides each step; the two are identical up to carrying \(1/\ell_i\) inside \(O\)).
The exactness theorem (and its one-line engine)
Claim: with \(s^{[i]}\) the concatenation of the first \(i\) blocks, \[ m_i = \max_j s^{[i]}_j, \qquad \ell_i = \sum_j e^{s^{[i]}_j - m_i}, \qquad O_i = \sum_j e^{s^{[i]}_j - m_i} v_j, \] hence \(O_T/\ell_T = \sum_j e^{s_j} v_j \big/ \sum_j e^{s_j} = \mathrm{softmax}(s)\,V\): exactly the full-row softmax. Proof sketch (induction on \(i\)). Base \(i=1\) is immediate. Step: the whole proof is the identity \(e^{s_j - m_{i-1}} \cdot e^{m_{i-1} - m_i} = e^{s_j - m_i}\). Substituting the hypothesis collapses \(\ell_i\) into the sum over all \(j \in [i]\), and the same algebra column-wise gives \(O_i\); at \(i = T\) the common factor \(e^{-m_T}\) cancels in the ratio. ∎ Numerically: max abs error ≈ \(4\times 10^{-16}\) in float64, both forms.
Three remarks to pocket. (1) Safety for free: \(m_T\) is the true row max, so every exponent is ≤ 0; the running max doubles as stabilization. (2) The factors \(e^{m_{i-1}-m_i}\) are the idea: when a later block raises the max, all previous contributions were denominated in too-large units and must be deflated. (3) Unlike top-k or windowed attention (Lecture 3), online softmax changes when operations happen, not which.
Tiling turns the recurrence into a traffic theorem: per KV block the kernel touches \(2 B_c d_h\) bytes of HBM (the \(K_j, V_j\) tile) and zero bytes for \(S_j, P_j\), which exist only in registers/SMEM. Summed: \(Q, K, V\) read once, \(O\) written once. Exactly the \(O(Ld)\) traffic we counted.
On Hopper, FA3 runs this loop on the warp-specialized pipeline: producer TMA warps stream the next \(K_j, V_j\) while consumers run WGMMA, and element-wise softmax overlaps the async MMA (the "GEMM–softmax overlap" pushing FA3 toward the compute ceiling; FA3 also adds FP8). The family history in one line: FA1 established online softmax + tiling; FA2 re-parallelized along the Q sequence (each block owns a Q tile and scans all KV, filling all 132 SMs even at long \(L\) and small batch); FA3 rebuilt the kernel for Hopper. Same mathematics every time. What evolves is the residency and overlap policy.
Now decode arrives and breaks the map. At decode \(q_{len} = 1\): exactly one query row per request, so FA2's "split along Q" yields only batch-size-many blocks, often far fewer than 132 SMs. You can probably guess where the parallelism must come from: the only long axis left is the KV length itself.
Split-KV (a.k.a. flash-decoding) divides the \(L\) KV positions into \(S\) segments; each segment runs the ordinary online-softmax recurrence in its own block and produces a partial triple \[ m_s = \max_{j \in s} s_j, \qquad \ell_s = \sum_{j \in s} e^{s_j - m_s}, \qquad O_s = \sum_{j \in s} e^{s_j - m_s} v_j. \] A small reduction kernel then merges the \(S\) partials. Let \(m^* = \max_s m_s\); every partial is deflated to the common scale exactly as in the online recurrence: \[ \ell^* = \sum_s \ell_s\, e^{m_s - m^*}, \qquad O^* = \frac{\sum_s \ell_s e^{m_s - m^*}\, O_s^{norm}}{\ell^*}, \qquad O_s^{norm} = O_s/\ell_s. \] Substitution collapses the merge into the global sum: the segments partition \([L]\), so this equals full-KV softmax by the same cancellation. Two equivalent forms of one correction: define each segment's log-sum-exp \(\mathrm{LSE}_s = m_s + \log \ell_s\); the merge weight of segment \(s\) becomes \(e^{\mathrm{LSE}_s - \mathrm{LSE}^*}\), a softmax over segment LSEs applied to partial outputs (verified numerically to \(6\times 10^{-16}\)).
A kernel alone is not a serving system. FlashAttention assumes dense contiguous Q/K/V and one shape per call; an engine needs FlashInfer's contract: native paged-KV support (the kernel indexes KV through Lecture 2's block table, no contiguous copy needed), ragged batches carried as metadata, a unified prefill/decode/append interface, JIT-compiled variants (head dim, RoPE, soft-capping, sliding window), and plan/run separation, where a host-side plan inspects the batch's length distribution and decides tile partitioning and load balance, keeping variable-length batches schedulable without device-side sync. Both vLLM and SGLang use FlashInfer as an attention backend: it is where Lecture 2's paging meets this lecture's tiling.
And then there is MLA, whose decode shape breaks standard kernel assumptions. Recall Lecture 2: in absorbed mode, per-head projections fold into \(Q\), so attention runs directly against the compressed cache: each token contributes one 576-element latent (512-dim latent + 64-dim RoPE key) shared by all 128 query heads. The decode GEMM is \[ \underbrace{O}_{128 \times 576} = \mathrm{softmax}\Big( \underbrace{Q_{lat}}_{128 \times 576}\, \underbrace{K_{lat}^{\top}}_{576 \times L}\Big)\, \underbrace{V_{lat}}_{L \times 576}, \] attention with 128 heads packed as GEMM rows against a single K-dim-576 cache. The naive treatment (128 independent heads) re-reads the same latent cache 128 times: 72 KB per token instead of 576 B. That forfeits exactly the bandwidth MLA was designed to buy. FlashMLA loads each block of latent KV from HBM once and lets all 128 packed heads consume it from on-chip memory.
TA says: burn this into your decode intuition: 90% MBU is what success looks like. A bandwidth-bound kernel that saturates the memory system has nothing left to give, and nobody needs FLOPs at decode. Also note the shape trick: head_dim 576 is 4.5× a standard head, so K-tile SMEM pressure is huge. FlashMLA absorbs it through aggressive tiling plus the producer/consumer pipeline. When an architecture changes the arithmetic (MLA's shared latent), the kernel must change its residency policy to match, or the architecture's savings evaporate in the memory system.
Last stop on the attention line: sparsity. FlashMLA additionally accepts an
indices tensor (batch, qlen, topk) and gathers the selected tokens from paged KV
directly into SMEM by index, materializing nothing intermediate. This is the execution
endpoint of DSA (Lecture 3). The full per-step chain in DeepSeek V3.2/V4 is three kernels:
Enough attention. The other half of the decode step is the MoE FFN. Under expert parallelism (Lecture 6) each GPU owns \(E_{local}\) experts and, every step, must run one GEMM per local expert. Two facts conspire against you. First, the per-expert token counts \(M_i\) are unequal (routing is data-dependent) and known only at runtime. Second, and this is the killer: at decode they are tiny. With \(B = 256\) requests and top-8 routing over 256 experts, the mean is \(M_i = 8\).
Why not one launch per expert? Two failures. Launch overhead multiplies by the expert count: \(E_{local} \times\) (Linear1 + SwiGLU + Linear2) at a few microseconds each (we will price a launch very soon). More fundamentally, tile quantization: tensor-core GEMMs compute in tiles of, say, 128 rows, so a GEMM with \(M_i = 8\) useful rows still pays for full tiles: \[ \eta_{tile} = \frac{\sum_i M_i}{\sum_i \lceil M_i / T_M \rceil\, T_M} = \frac{\text{useful FLOPs}}{\text{executed tile FLOPs}}, \qquad M_i = 8,\ T_M = 128 \Rightarrow \eta_{tile} = 6.25\%. \] The tensor cores execute 16× more work than is useful (random routing barely helps: a multinomial draw of 2048 assignments over 256 experts still gives \(\eta \approx 6.25\%\)). At prefill the same shape is fine: 16K tokens × top-8 over 256 experts gives \(M_i \approx 512 = 4 \times 128\), so \(\eta \approx 100\%\). The MoE GEMM efficiency problem is a decode-phase, small-\(M\) problem. That is why its solutions (persistent kernels, masked layouts, fusion) all target the decode path.
Answer
Useful rows: \(5+12+3+27+16+9+1+31 = 104\). Executed rows: \(16+16+16+32+16+16+16+32 = 160\). So \(\eta_{tile} = 104/160 = 65\%\). Better than 6.25%, because \(T_M\) is small, but a third of the tensor-core work is still padding. The hidden second waste: 8 separate launches also pay 7 extra launch costs and strand SMs at each expert's wave boundary. Both are gone under one grouped launch, before tile efficiency even enters the picture.
Grouped GEMM is the fix: one kernel schedules all subproblems \(\{(M_i, N, K)\}_{i=1}^{E_{local}}\). A routine builds a tile-level task list (every (expert, row-tile, col-tile) triple) and a persistent kernel pulls tiles from the queue. Tiles of different experts interleave freely across SMs, so small-\(M_i\) GEMMs no longer strand SMs at per-expert wave boundaries: the SM pool sees one long queue of same-shaped tile tasks. Tile quantization within each expert remains (pooling does not shrink tiles), but wave quantization and launch multiplicity are gone. One further design choice is a phase decision:
The library executing this is DeepGEMM: FP8/FP4/BF16 GEMMs, grouped GEMM in both layouts, the DSA indexer, and the Mega MoE kernels, all JIT-compiled. Two of its choices deserve a close look. Fine-grained FP8 scaling: naive FP8 keeps one scale per tensor, and with ~4 mantissa bits (E4M3) the dynamic range is poor; DeepGEMM gives every \(1 \times 128\) activation strip and every \(128 \times 128\) weight block its own scale, applied at dequantization, plus two-level accumulation: FP8 tensor-core accumulators are not precise over long \(K\) reductions, so partial sums are periodically promoted to higher precision on CUDA cores. Bolted-on FP8 with per-tensor scales never reaches usable accuracy: "just cast to FP8" is not a strategy for V3-class models. JIT as philosophy: no precompiled template zoo. Each concrete \((M, N, K)\), layout, and precision gets a specialized, cached kernel with tiles, unrolls, and pipeline depth matched to it. The cost is first-call compile latency: profile a JIT kernel cold and you measure the compiler, not the kernel. Published: ≈ 1550 TFLOPS FP8 on H800 (≈ 78% of the ≈ 1979 TFLOPS FP8 peak, a high MFU for a fine-grained-scaled GEMM), built on TMA + warp specialization on Hopper, tcgen05/TMEM + native FP4 on Blackwell.
Completing the MoE main line, DeepEP is the communication library behind Lecture 6's all-to-alls, in two phase-specific modes. Normal mode (prefill) aggregates tokens intra-node over NVLink (≈ 900 GB/s), then forwards cross-node over RDMA (≈ 50 GB/s per GPU), minimizing expensive inter-node bytes. Low-latency mode (decode) skips the aggregation hop and sends pure RDMA point-to-point, on the order of a hundred microseconds, with a hook mechanism: dispatch/combine return right after initiation, reception runs in the background, and overlap interleaves at micro-batch granularity (while micro-batch 1's tokens are in flight, micro-batch 2's experts compute). Everything is GPU-initiated RDMA (the IBGDA/NVSHMEM line: GPU threads ring the doorbells, no CPU proxy), and precision follows Lecture 6's contract: dispatch FP8, combine BF16.
So far we optimized inside kernels. Zoom out one level: a forward pass of a 60–80-layer model runs a dozen-plus kernels per layer, hundreds per step. In prefill, kernels run hundreds of microseconds and launch cost is noise; in decode, kernels run microseconds, and now the stuff between kernels is the leak. Let a decode step execute \(n_k\) kernels with total GPU execution time \(T_{exec}\), each launch costing \(t_{launch}\) of CPU submission/queueing plus \(t_{gap}\) of inter-kernel GPU idle. If launches don't fully overlap execution (small batches: the GPU drains faster than the CPU enqueues), \[ T_{step} = T_{exec} + n_k\,(t_{launch} + t_{gap}). \] Substitute a 70B-class model: \(80 \times \approx 12\) kernels per layer gives \(n_k \approx 960 \approx 10^3\), and with \(t_{launch} + t_{gap} \approx 2\text{–}5\,\mu s\): \[ n_k (t_{launch} + t_{gap}) \approx 1000 \times (2\text{–}5\,\mu s) = 2\text{–}5\ \text{ms}, \] against a small-batch decode step of ≈ 10 ms. 20–50% of the step is launch machinery, not math. No kernel-level optimization can reclaim it; the overhead lives between kernels.
CUDA graphs capture the whole step's kernel sequence (parameters, dependencies, stream structure) into a replayable object; each subsequent step is one graph launch, and both overhead terms die together. The catch is static shapes and addresses: inside a captured graph every buffer pointer and shape is frozen. Three engine mechanisms jointly satisfy the freeze:
But do not celebrate yet: graphs kill launch overhead; the kernel boundaries remain, and every boundary is a global sync. The expert GEMM cannot start until dispatch has fully finished, and combine cannot start until the GEMM finishes. During communication the SMs idle; during compute the network idles. DeepEP's hooks mitigate this at micro-batch granularity, but overlap can never cross a kernel boundary.
Mega MoE (in DeepGEMM) takes the logical endpoint: fuse the entire MoE layer (dispatch, Linear1, SwiGLU including re-quantization, Linear2, combine) into one kernel, via two enablers. SymmBuffer (symmetric memory): buffers with an identical address layout on every rank of an NVLink domain, so remote GPU memory is read and written with ordinary load/store instructions. Communication becomes a memory instruction inside the compute kernel. Warp specialization, generalized: dispatch warps pull this rank's tokens out of the symmetric buffer and track arrivals; MMA warps run the two GEMMs; epilogue warps do SwiGLU, re-quantization to FP8, and combine write-back, handing off at tile granularity, tokens joining the GEMM as soon as they arrive. No "all dispatch complete" sync point exists. (Precision: FP4 weights + FP8 activations, SM100 primarily, with an SM90 adaptation.)
| DeepEP (separate) | Mega MoE (fused) | |
|---|---|---|
| Communication | standalone comm kernel, GPU-initiated RDMA (IBGDA/NVSHMEM) | in-kernel NVLink load/store on symmetric memory |
| Scope | cross-node (RDMA) + intra-node | within the NVLink symmetric-memory domain |
| Overlap granularity | micro-batch (hooks) | tile |
| Synchronization | global sync at every kernel boundary | none; compute-on-arrival |
| Positioning | general-purpose; overlap via hooks | end-to-end optimal within its domain |
Which route wins is a hardware question: the fused route's scope is the NVLink-domain size. An 8-GPU domain covers only small EP degrees; NVL72 puts 72 GPUs into one symmetric-memory domain, so decode-scale EP fits entirely inside the fused route's scope. That is the hardware background of its current prominence. And the phase asymmetry returns: decode is IO-bound with a high launch/sync share, so fusion pays off most there; prefill's large GEMMs already run near the compute ceiling.
The 2026 frontier models changed the kernel workload itself: Kimi K3 and GLM-5.3-Flash make gated delta-rule linear attention (KDA) the dominant layer type, run routed experts in native MXFP4, and draft with block diffusion instead of autoregressive heads. KDA keeps a matrix-valued recurrent state \(S \in \mathbb{R}^{128 \times 128}\) per head (96 heads), updated as \(S_t = (I - \beta k k^{\top})\, \mathrm{Diag}(\alpha)\, S_{t-1} + \beta k v^{\top}\). There is no \(L \times L\) score matrix at all. The kernel problem inverts FlashAttention's: not "avoid materializing \(S, P\)" but "read, update, and write a per-head matrix state efficiently".
On the prefill side the recurrence goes chunkwise: a within-chunk parallel pass plus cross-chunk state passing (chunk size 64, 16-token secondary tiles), the state carried across chunk boundaries exactly like the \((m, \ell, O)\) triple. FlashKDA implements this in CUTLASS for SM90+ (the FLA library auto-dispatches to it). It is the first linear-attention kernel line with FlashAttention-class maturity. At decode, each KDA layer reads its full state, applies one rank-one-ish update, writes it back. Per layer per step: \[ 96\ \text{heads} \times 128 \times 128\ \text{elem} \times 2\ \text{B (BF16)} = \text{3.0 MiB} \quad \text{(fixed, independent of } L\text{)}, \] so K3's 69 KDA layers cost \(69 \times 3.0\) MiB = 207 MiB ≈ 0.2 GiB of state traffic per token-step. And the fused KDA decode kernel does it in one launch (short causal conv of kernel size 4, recurrent update, gated output RMSNorm; all states updated in place, no intermediate tensors, no per-op launches).
MXFP4 MoE in production. K3's routed experts ship native MXFP4 (QAT from SFT onward;
17.5 MB per expert). Executing them is not "FP8 with fewer bits": the SiTU-GLU activation must be
fused into the grouped-GEMM epilogue, and Blackwell's native FP4 datapath is what makes 4-bit
weights a bandwidth win rather than a dequantization tax. vLLM's recommended K3 backend
is deep_gemm_mega_moe (the fused kernel above, in FP4 production), with
flashinfer_trtllm for TP > 1.
TA says: two more 2026 numbers worth memorizing: kernel stories
wearing system clothes. DSpark (block-diffusion drafting, Lecture 8 teaser): its kernels
are MLA-native. Draft and target share the latent-cache layout, so verification reuses the
absorbed FlashMLA shapes. Measured: 118 → 370 tok/s per user on 16× GB300 NVL72 in vLLM
(3.14×); SGLang reports 113 → 423. And GLM-5.3-Flash's IndexPool: the DSA chain with
index_kpool = 4 pools 4 indexer keys into 1, shrinking the scorer's key traffic 4×
before top-2048 selection. (vLLM's sparse-MLA indexer kernels require Hopper+, since
gather-by-indices needs the async-copy machinery to hit bandwidth.) The moral of all three: sparse attention,
drafting, and quantization only become real when a kernel with the right residency
policy exists.
deep_gemm_mega_moe), not a
dtype flag: the 17.5 MB/expert figure converts into TPOT only if the GEMM consumes FP4
natively.
How to: read the starting point; read the question and think, for a minute, a day, a week... and only then open the answers. You are not supposed to reinvent several months of someone's research; each paper took its authors exactly that long. It is the habit of thinking that counts.
Existing solutions
Carry a running max \(m\) and running sum \(\ell\), and deflate all past partials by \(e^{m_{old} - m_{new}}\) when the max grows. Then \(S_j, P_j\) never need HBM at all. That is the online-softmax recurrence; tiled with the \(Q\)-outer/KV-inner loop it is FlashAttention (FA1), later re-parallelized along Q (FA2) and rebuilt around Hopper's async machinery (FA3). Notice the shape of the discovery: the math was old; the contribution was seeing that traffic, not FLOPs, was the constraint: a roofline argument made into a kernel.
Where this leads
Split the KV axis: each segment produces \((m_s, \ell_s, O_s)\), and a small reduction merges them with LSE-rescaled weights \(e^{\mathrm{LSE}_s - \mathrm{LSE}^*}\). This is exact, since the segments partition \([L]\). This is split-KV / flash-decoding inside a GPU, and the same merge across GPUs is Lecture 6's DCP. The 2026-era line adds the absorbed-cache twist (FlashMLA: 128 heads consume one shared 576-dim latent) and the sparse twist (DSA: gather only \(k\) selected tokens). It is still the same recurrence, with different machinery deciding which bytes arrive.
Existing designs
(i) Shrink the bubbles at the boundaries: DeepEP's low-latency hooks return after initiation, receive in the background, and overlap at micro-batch granularity. Works anywhere, even cross-node; overlap stays coarse. (ii) Erase the boundaries: Mega MoE fuses dispatch, both GEMMs, activation, and combine into one kernel over symmetric memory, tile-granularity hand-offs, no global sync. But this works only inside the NVLink domain. As domains grow to 72 GPUs, decode-scale EP fits entirely in route (ii); cross-node prefill keeps route (i). One more design exercise in the same spirit: your batch-size histogram has mass concentrated under 64 with a hard wall at 512. The \(w/(2\mathbb{E}[B])\) formula assumed a uniform batch distribution, so choose buckets from measured quantiles of the real histogram, dense where the mass is. The same quantile logic sizes the masked layout's capacity \(C\): every "static shape" mechanism converts a distribution question into a constant, so pick constants from measurement.
Enough theory. Now you be the reduction kernel. Below are two KV blocks, each with its local stats. Move the sliders and watch what happens to the old block when the new block's max exceeds it. Can you make the old block's merge weight drop below 0.1? Below 0.01? (Defaults are the first component of our worked example: \(m_1 {=} 2, \ell_1 {=} 8, O_1 {=} 4\) vs \(m^{(2)} {=} 3, \sum e^{s-3} {=} 6, \sum e^{s-3}v {=} 18\).)
Take Lab 7: Kernels (FlashAttention, Launch Overhead, CUDA Graphs) from the hands-on pack. Three experiments, one GPU:
F.scaled_dot_product_attention at \(L \in \{4K, 16K, 64K\}\) (warm up 20
iterations, time 100 with torch.cuda.Event) and convert to achieved GB/s. The
expected picture:
| \(L\) | naive traffic | naive time @ β | flash traffic | flash time @ β | ratio |
|---|---|---|---|---|---|
| 4K | 3.0 GiB | 0.96 ms | 128 MiB | 0.04 ms | 24× |
| 16K | 48 GiB | 15.4 ms | 512 MiB | 0.16 ms | 96× |
| 64K | 768 GiB | 246 ms | 2.0 GiB | 0.64 ms | 384× |
nsys profile -o decode_step vllm serve
... at \(B{=}1\), drive 200 single-stream decode requests, open the trace, count kernels
between two sampled tokens. Expect ~300+ per 8B decode step (32 layers × 8–12 kernels + glue); at
3–5 µs of CPU launch work each, ~1.0–1.7 ms of launch path against a ~5–12 ms step.--enforce-eager; compare step time and
CPU-side launch gaps. Expect eager to hurt far more at \(B{=}1\) than at \(B{=}64\): fat kernels
hide launch latency.Lab pitfalls, verbatim-worthy: no warm-up (first-call JIT/capture contaminates the trace);
timing with time.time() instead of CUDA events (async launches lie); profiling at
\(B{=}1\) only and generalizing to serving; counting host time as kernel time. The analysis
questions you will answer there: which kernels dominate decode step time (GEMMs, attention, or
elementwise), and does it match the \(2P/\beta\) floor?; what exactly do CUDA graphs trade (capture
cost, memory, batch-shape rigidity; why does vLLM capture only specific batch sizes)?; at what
batch size does --enforce-eager stop mattering, and why?; and where would
DeepGEMM-style JIT show up in your trace, and which benchmarking pitfall does that motivate?