Inside the model
how a transformer actually computes
One request, one GPU
what happens between a prompt and a token
Serving many requests
the machinery around the model
Across many GPUs
when one card is not enough
How an agent runs
the loop, and one real instance of it
What it carries
context and state across a long run
What enters the context
retrieval, memory, and reaching out
Building the harness
what a real implementation forces you to decide
The problem
sparse attention, and the machine it lands on
Making it fast
three versions, three different bottlenecks
Making it correct, then tuned
merging the pieces, and choosing the shape
From a neuron to a network
what a model computes, with real numbers
Learning from mistakes
how the numbers get better

Everything that happens between a prompt and a token

Fifteen mechanisms from the serving stack, each one a step-through walkthrough rather than a wall of text. Start anywhere on the map — every box opens its own page.

click any box to open it · flowing edges show which way work moves

THE REQUEST REACHES A GPUuser requesta prompt arrivesservice meshrouting + healthload balancerwhich replica is cheapest?prefix cacheseen this preamble?schedulerwhich requests run nowone replicaa GPU, or severalTWO PHASES, TWO BOTTLENECKSprefillread the whole promptdecodeone token at a timewhy they differcompute vs bandwidthKV cachebuilt, then readGQA / MQAshrink what decode readsPagedAttentionstore it in blocksINSIDE ONE FORWARD PASSself-attentionwho attends to whomFlashAttentionsame math, less trafficlinear attentiona summary, not every tokenWHEN ONE GPU IS NOT ENOUGHTP / PP / EPwhere to cutbubbleskeep stages fullcollectivesthe exchangetokens stream backthe user sees textAND IF THE CALLER IS AN AGENT, IT HAPPENS AGAINcontext growsevery call re-reads itcheckpointingsurviving a crashMCPreaching toolsRAG vs memorywhat enters the contextthe agent submits the next call

Inside the model

Self-attentionhow a word finds what it refers toFull vs linear attentionkeep every token, or keep a summaryFlashAttentionthe matrix that never gets built

One request, one GPU

Prefill vs decodeone request, two different machinesKV cachewhat it saves, and what it costsMHA, GQA and MQAshrinking what decode has to readPagedAttentionwhy KV memory is stored in blocks

Serving many requests

Continuous batchingrescheduling every decode stepPrefix cachingreuse across requests, not within oneService meshrouting between servicesLLM-aware load balancingwhy the busiest replica can be cheapestPrefill–decode disaggregationstopping the two phases fighting

Across many GPUs

Tensor, pipeline and expert parallelismthree ways to cut a modelPipeline bubbles and micro-batchingkeeping the stages fullGPU collectivesfour shapes of exchange

The loop that wraps the model

An agent is not one model call. Six pieces: the loop itself, one real instance of it, and the four problems the loop creates — context, state, reach, and what gets to enter the context.

click any box to open it · flowing edges show one turn of the loop

a goal arrives“fix the failing test”THE LOOP — IT RUNS DOZENS OF TIMESthe modelpicks an actionorchestratorpermissions, sandboxtool callshell, search, MCPobservationa log, a fileupdate stateand checkpointthe loopnot one model callWHAT THE LOOP MAKES HARDthe context keeps growingevery call re-reads the historythe state must survivecrashes, restarts, approval waitsit must reach systems you don’t ownMCP standardises that boundaryand something decides what entersRAG vs memoryTHE SAME LOOP, INSTANTIATED — A CODING AGENTsearchthe repolocalizewhat runshypothesiseroot causeeditsmallest changerun testsfind outa failed test is an observation — go back to localization with more informationpatch + evidencenot a plausible guessAND WHAT A REAL HARNESS FORCES YOU TO DECIDEthe prompt is the statefour roles, one rulecompactionthe sawtootherror policyrecoverable or terminalthree leverson the context budgetprogrammatic toolscode that composes them

How an agent runs

The agent loopwhat an agent actually isCoding agent architecturethe verification loop

What it carries

Agent context growthwhy long loops get expensiveAgent state and checkpointingsurviving the interruption

What enters the context

MCPstandardising how agents reach the worldRAG vs memorytwo retrievals, two questions

Building the harness

The prompt is the statefour roles, one structural ruleContext compactionthe sawtooth, and its caching billError policyrecoverable, or terminalThree levers on the contexttruncation, compaction, disclosureProgrammatic tool callingcode that composes the tools

One CUDA kernel, 600 microseconds to 57

NVIDIA MLSys 2026 — sparse attention on a B200. Seven mechanisms, in the order the bottleneck moved: what the kernel is handed, the machine it lands on, and the four versions that each answered a different limit.

click any box to open it · the bar chart is measured latency, lower is better

WHAT THE KERNEL IS HANDED100,000 tokensthe KV cacheLightning Indexerscores every positiontop-K = 2,048scattered indicessparse attention kernelQK → softmax → PVone output vectorper query tokenon a B200148 SMs · 228 KB eachTHE BOTTLENECK IT STARTS WITHthe 2,048 entries are scattered, not a slicesparse attention removes arithmetic and replaces it with irregular memory access — every optimisation below answers that8 blocks on 148 SMs≈ 5% of the GPU has work · a block never leaves its SMFOUR VERSIONS, FOUR DIFFERENT BOTTLENECKSv1 · WMMAmake the arithmetic fast~600 µsv2 · cp.asyncoverlap the gather~470 µsv3 · Split-Kfill the whole GPU~95 µsv4 · autotunemeasure, do not guess~57 µsbar height = measured latency, lower is betterAND THE STATE THAT MAKES V3’S SPLIT LEGALonline softmax mergewhat v3’s 256 blocks merge intosoftmax(A ∪ B) ≠ softmax(A) + softmax(B)so each block keeps sufficient statistics — (M, LSE, O) — instead of a normalised answer: the same state FlashAttention carries tile to tile

The problem

Sparse attention, as a kernelwhat you are handed, and why it is hardThe B200, and the one rule148 SMs, and a block that cannot move

Making it fast

WMMA and Tensor Coresv1 · make the arithmetic fastcp.async and double bufferingv2 · stop waiting for the gatherSplit-Kv3 · the change that actually mattered

Making it correct, then tuned

Merging the splitsthe state that makes Split-K legalAutotuning the split factorv4 · why a formula could not do it

From one neuron to a trained network

The foundations, with real numbers: one 2-2-1 network carried from the perceptron through the forward pass, the activation functions, backprop, autodiff, mini-batch SGD and the optimizer — every value checkable by hand. Dashed boxes are the roadmap still to write.

click a solid box to open it · dashed boxes are still to do

THE PATH SO FAR — ONE TINY NETWORK, CARRIED THROUGH SEVEN PAGES1 · a neurona line, from mistakes2 · a networkmatmul, bend, matmul3 · the bendReLU, GELU, softmax4 · the gradientchain rule, backwards5 · autodiffwhat backward() records6 · mini-batchesthe gradient you can afford7 · the updateGD → momentum → Adamthe same example on every page: x = (1.0, 0.5), y = 1 · forward gives L = 0.693 · backprop gives eight gradients · one step gives L = 0.599TRAINING WELL — TO DOInitialisationrandom weights at the right scaleTO DONormalisationBatchNorm · LayerNorm · RMSNormTO DORegularisationdropout · weight decayTO DOSchedules & batch sizewarm-up, cosine, clippingTO DOMixed precision & the loopbf16 · the real training loopTO DOTHE TRANSFORMER — TO DO, EXCEPT ATTENTIONTokenisation & embeddingsBPE · the embedding tableTO DOPositional encodingsinusoidal · RoPETO DOAttentionwritten — in InfrastructureThe transformer blockattention + MLP + residual + normTO DOPretraining objectivenext token · scaling lawsTO DOFine-tuningSFT · LoRA · RLHF / DPOTO DOCLASSIC ML, FOR INTERVIEWS — TO DOLinear & logistic regressionBias, variance, overfittingLoss functionsCNNs and RNNs

From a neuron to a network

The perceptrona neuron is a weighted vote, and a vote is a lineThe forward passweighted sums and bends, layer by layerActivation functionsthe bend, and the slope backprop multiplies by

Learning from mistakes

Backpropagationthe chain rule, run backwardsAutomatic differentiationwhat loss.backward() actually doesSGD and mini-batchesthe gradient you can affordOptimizersfrom a gradient to an update — GD, SGD, momentum, Adam

Next up — the roadmap

What the next batches will cover, in the order they should be learned. Each becomes a step-through page like the five above.

Training well

  1. InitialisationXavier / He, and why the scale of random weights decides whether signals survive depth
  2. NormalisationBatchNorm, LayerNorm, RMSNorm — keeping activations at a fixed scale so gradients stay usable
  3. Regularisationdropout, weight decay, early stopping — why a model that fits the training set can still be wrong
  4. Schedules & batch sizewarm-up, cosine decay, the batch-size–learning-rate rule, gradient clipping
  5. Mixed precision & the loopbf16, loss scaling, gradient accumulation — the training loop as it is actually written

The transformer

  1. Tokenisation & embeddingsBPE, the embedding table, why the vocabulary is a design choice
  2. Positional encodingsinusoidal, learned, RoPE — how a set of vectors learns to be a sequence
  3. Attentionalready written up in the Infrastructure area — revisit it from the forward-pass side
  4. The transformer blockattention + MLP, residual connections, pre-norm — one layer, then N of them
  5. Pretraining objectivenext-token prediction, perplexity, scaling laws
  6. Fine-tuningSFT, LoRA, RLHF / DPO — what changes after pretraining

Classic ML, for interviews

  1. Linear & logistic regressionthe perceptron with a proper loss; closed form vs gradient descent
  2. Bias, variance, overfittingtrain/val/test, cross-validation, what the learning curves say
  3. Loss functionsMSE, cross-entropy, softmax with many classes, hinge, contrastive
  4. CNNs and RNNsweight sharing, receptive fields, why sequences moved to attention
Inside the model

Self-attention

how a word finds what it refers to

Attention is the operation that lets one token pull information from another. Everything else in a transformer is scaffolding around it — and every efficiency technique later in these notes is a response to what it costs.

The problem

A token's meaning depends on other tokens, but a neural network processes a fixed-size vector per position. Nothing in the word hungry tells you who is hungry.

What it buys you

It lets every token ask a question and pull the answer from wherever it lives in the sequence — learned, not rule-based. This is what resolves pronouns, agreement and long-range reference.

How it works

Each token emits a Query, a Key and a Value. The query is compared against every key to produce scores; softmax turns those into weights; the output is the weighted sum of values. Q and K decide routing; V is the payload.

Self-attention — watching a word find what it refers toQ and K decide who attends to whom; V is what actually gets carried.
The cat that chased the mouse was hungry Query "who is this about?" Key Value 4% Key Value 55% Key Value 3% Key Value 11% Key Value 2% Key Value 13% Key Value 4% Key Value 8% new "hungry" attentionweight
1 / 1
Say this

Q and K decide who attends to whom; V is what gets carried. That split is why you can cache K and V, and why RoPE only touches Q and K.

Going deeper

A worked example — Resolving what a word refers to

“The cat that chased the mouse was hungry.” When the model processes hungry, that token emits a query meaning roughly who is this about? It scores highly against the key of cat and weakly against mouse, so the value it pulls in is mostly cat's.

Nothing told the model that cat is the subject. The match between one token's query and another's key is the entire mechanism — which is why attention is the part that resolves pronouns, agreement and long-range reference.

Why three projections instead of using the hidden state directly

If you compared raw hidden states, matching would collapse into plain similarity — a token could only attend to tokens that look like itself. Separate Q and K projections let the model learn an asymmetric relation: hungry can ask for a subject without resembling one.

Why divide by √d

Dot products of d-dimensional vectors grow with d. Without the scaling, scores get large, softmax saturates to nearly one-hot, and gradients through it vanish. The divisor keeps the logits in a range where softmax stays soft and trainable.

What the interviewer usually asks next

“Why is it quadratic?” Because every query meets every key — S² pairs, each a d-dimensional dot product. That single fact is the origin of FlashAttention, sparse attention, linear attention, and most of the long-context literature.

How it connects

  • FlashAttentionthe same operation, made IO-efficient
  • KV cachecaching K and V works because only they get reused
  • Full vs linear attentionwhat you give up by not storing every token
  • Prefill vs decodethe quadratic term lives in prefill

What it costs

  • Cost is quadratic in sequence lengthevery token is compared with every token, so doubling the context quadruples the attention work
  • Memory toothe score matrix is S×S, which is what FlashAttention refuses to materialise
Inside the model

Full vs linear attention

keep every token, or keep a summary

Two ways to store the past. Full attention keeps every token individually; linear attention folds them into one fixed-size state. The difference is a compression-versus-recall trade.

The problem

Full attention pays for every token pair and stores every past key and value, so both cost and memory grow with the length of the sequence.

What it buys you

Linear attention makes per-step cost constant and memory flat, so sequence length stops being the thing that limits you.

How it works

Reorder the computation around a feature map so the past collapses into a recurrent state. Each new key and value updates the state; each query reads from it. No explicit token-by-token score matrix ever forms.

Full vs linear attention — keep every token, or keep a summaryA compression-versus-recall trade, seen directly.
Full attention keeps every past key and value query memory Linear attention folds the past into one fixed-size state recurrent state size never changes K,V query memory
1 / 1
Say this

Full attention is lossless lookup at quadratic cost; linear attention is lossy lookup at constant cost.

Going deeper

A worked example — The question a fixed-size state cannot answer

“What was the variable named on line 200?” Full attention treats this as a lookup: there is a key for that token and the query can match it exactly, however far back it sits.

Linear attention has folded line 200 into a single fixed-size state along with everything else, so the name survives only as a blend. It will produce something plausible rather than something exact. Ask instead “what is this document about?” and the compressed summary answers perfectly well, at constant cost however long the document is.

Where the linear form comes from

Softmax attention cannot be reassociated because the softmax sits between Q·Kᵀ and V. Replace it with a feature map φ so scores become φ(q)·φ(k), and the computation reassociates: instead of (QKᵀ)V you compute Q(KᵀV). KᵀV is a fixed-size matrix independent of sequence length — that matrix is the recurrent state.

Why hybrids exist

Retrieval-shaped work (copying an exact token, following a reference) is what a compressed state does worst. Interleaving a few full-attention layers among many linear ones recovers that ability while keeping most of the cost saving — which is why most published “linear” models are not purely linear.

The honest comparison

Linear attention is not a strict improvement. It trades an exact, addressable memory for a lossy summary. If your workload is long-context retrieval, that trade is bad; if it is long-context summarisation, it is good.

How it connects

  • Self-attentionthe exact version this approximates
  • FlashAttentionexact and still quadratic — a different axis entirely
  • KV cachewhat linear attention replaces with a fixed-size state

What it costs

  • Precise recall is losta fixed-size state blurs specific tokens — what was the variable on line 200 is exactly what it is bad at
  • Broad questions still worka summary answers what is this document about perfectly well, at constant cost
  • Hence hybridsmostly linear layers with a few full-attention layers kept for the lookups that matter
Inside the model

FlashAttention

the matrix that never gets built

An IO-aware, exact attention implementation. It does not change the result and does not reduce the arithmetic — it removes memory traffic, which turns out to be where the time actually went.

The problem

Attention runs as several stages, and the huge S×S score and probability matrices get written to slow GPU memory between them — written, read, written, read. For a long sequence that matrix is larger than all the fast on-chip memory available.

What it buys you

Every one of those numbers is needed exactly once on its way to the output, so none of them ever has to exist in HBM. Removing those round trips is a large wall-clock win at zero accuracy cost.

How it works

Tile Q, K and V into blocks that fit in SRAM. Fuse score → softmax → weighted-sum into one kernel so each tile is consumed immediately. Use an online softmax that carries a running max and normaliser, so the result stays exact tile by tile.

FlashAttention — why the big matrix never gets builtStep through what actually moves between slow and fast memory.
HBM — large, but slow to reach On-chip SRAM — tiny, but fast QK, V output S × S scores every query × every key written out, read back, written, read again Q block K,V block score tile this block only running state max m— sum ℓ— output accumulator— O row block discarded — never written to HBM HBM traffic Q1Q2Q3Q4K,V 1K,V 2K,V 3K,V 4O1
1 / 1
Say this

GEMMs were already tiled — that was never the problem. FlashAttention tiles and fuses the whole attention operation, and online softmax is what makes that legal.

Going deeper

A worked example — The matrix that never needed to exist

For a long sequence, the score matrix of a single attention head can be far larger than all the fast on-chip memory available. The naive path writes that whole thing to slow memory, reads it back to apply softmax, writes the result out again, then reads it once more to multiply by V.

But every one of those numbers is needed exactly once, on its way to the output. FlashAttention loads a tile, folds it into a running total, and throws it away. Same arithmetic, same answer — the data simply never makes the round trip.

How online softmax stays exact

Softmax normally needs the whole row before it can divide by the sum. Instead, carry a running max m and running sum ℓ. When a new tile arrives with a larger max, rescale the accumulated output by exp(m_old − m_new) before adding the new contribution. The correction is exact, so the final result is bit-for-bit what the naive version produces.

Why recomputation can be the cheaper option

In the backward pass FlashAttention recomputes the attention tiles rather than storing them. That is more FLOPs but far less memory traffic — and since the kernel is bandwidth-bound, trading arithmetic for traffic is a win. It is the same reasoning as the forward pass, applied to gradients.

FA1 vs FA2 vs FA3

FlashAttention-1 introduced the tiled, fused, IO-aware formulation. FlashAttention-2 kept the idea and fixed how work is partitioned across warps and thread blocks, cutting non-matmul overhead. Later versions target newer hardware features. The idea has not changed since FA1 — the implementations got better at feeding the GPU.

How it connects

  • Self-attentionthe operation being optimised
  • Prefill vs decodebiggest effect on the compute-bound phase
  • Full vs linear attentionapproximate alternatives — a different axis

What it costs

  • No accuracy cost at allit is exact, which is the whole point
  • Still quadraticit removes IO, not arithmetic — saying otherwise fails the question
  • Extra FLOPs are sometimes traded for less trafficrecomputing in the backward pass can be cheaper than storing
One request, one GPU

Prefill vs decode

one request, two different machines

A single request has two phases that stress completely different parts of the hardware. Almost every serving technique targets one or the other, so naming the phase is the first step in any answer.

The problem

Prefill reads the whole prompt at once and is limited by compute. Decode emits one token at a time and is limited by memory bandwidth. Treating them as one workload optimises neither.

What it buys you

Separating them tells you which lever to pull: prefix caching and chunked prefill attack prefill; GQA, KV quantisation and larger batches attack decode.

How it works

Prefill processes every prompt token in parallel and builds the KV cache — big matmuls, hardware well fed. Decode then produces one token per step, doing almost no arithmetic but re-reading the weights and the whole cache each time.

Prefill vs decode — one request, two different machinesWhy the first word is slow for a different reason than the rest.
Prefill — the whole prompt at once KV cache built Decode — one token at a time t1t2t3t4t5t6t7t8 every step re-reads the cache GPU doing useful work prefill decode What the user feels — —
1 / 1
Say this

Prefill is throughput-shaped and compute-bound; decode is latency-shaped and bandwidth-bound. Name the phase before naming the fix.

Going deeper

A worked example — Summarising a long document

You paste a long report and ask for a short summary. Prefill reads the entire report in one pass — a big, efficient piece of work, and what the user experiences as the wait before anything appears.

Decode then emits the summary one token at a time. Each step does almost no arithmetic but must re-read the model weights and the whole cache, so the pace of text appearing is set by memory speed. Flip the shape — a short prompt with a long answer — and the cost moves almost entirely into decode.

Arithmetic intensity is the real distinction

Prefill multiplies a big matrix by a big matrix: many FLOPs per byte moved, so the GPU is well fed. Decode multiplies a matrix by a single vector: the same weights are read to do a fraction of the work. That ratio — FLOPs per byte — is what makes one compute-bound and the other bandwidth-bound.

Why batching helps decode but not prefill

Batching decode turns vector-matrix products back into matrix-matrix products, amortising the weight read across many requests — the whole reason continuous batching exists. Prefill is already compute-saturated, so batching it adds little and can hurt latency.

Chunked prefill, and why schedulers care

A long prefill run in one piece blocks every in-flight decode step. Splitting it into chunks interleaved with decode keeps token streams smooth at a small throughput cost — the same problem prefill–decode disaggregation solves by moving the phases apart entirely.

How it connects

  • KV cachebuilt by prefill, read by decode
  • Prefill–decode disaggregationrunning the two phases on separate pools
  • Continuous batchingscheduling that mostly helps decode
  • Prefix cachingskipping prefill work entirely

What it costs

  • Prefill sets time-to-first-tokenthe wait before anything appears
  • Decode sets time-per-output-tokenthe pace the text arrives at
  • The balance moves with workloada long document with a short summary is nearly all prefill; a short question with a long essay is nearly all decode
One request, one GPU

KV cache

what it saves, and what it costs

The optimisation that makes autoregressive generation practical — and the single biggest consumer of GPU memory in a serving system. Almost everything in the serving section exists to manage it.

The problem

Generation is autoregressive, so a naive implementation pushes the entire prefix through every layer again for each new token, rebuilding keys and values identical to last time.

What it buys you

The past does not change, so its keys and values can be computed once and reused. Work per decode step goes from growing with the sequence to flat.

How it works

Each step computes Q, K and V for the new token only, appends its K and V to the cache, and attends across everything stored. Old queries are never reused — which is precisely why it is a K/V cache and not a Q cache.

The KV cache — what it saves, and what it costsWhy decoding reuses keys and values, and why memory is the thing that runs out.
KV cache — keys and values already computed K,V K,V K,V K,V K,V K,V K,V K,V K,V K,Vprompt tokens generated so far new Work per decode step without cache with cache GPU memory held by the cache GPU limit old queries never reused so Q is not cached
1 / 1
Say this

Only K and V are cached because old queries are never reused. The KV cache is why serving is a memory problem.

Going deeper

A worked example — Generating the five-hundredth token

Without a cache, producing token 500 means pushing the previous 499 tokens through every layer again, just to rebuild keys and values identical to last time. With a cache you compute one token's worth of work and read the rest.

The catch shows up with concurrency. One long conversation's cache is manageable; a hundred simultaneous long conversations can need more memory than the model weights themselves — and when the GPU fills, the server has to queue or evict, so the symptom users see is not slowness but rejected requests.

Reading the size formula

Memory ≈ 2 · L · S · Nkv · dhead · bytes. The 2 is K and V; L is layers; S is sequence length; Nkv is KV heads. Everything is linear, which is what makes the levers obvious: halve the KV heads (GQA) or halve the bytes (quantise to fp8/int8) and you halve the cache.

Why it is the capacity limit, not the compute limit

Weights are a fixed cost paid once. The KV cache is a per-request cost that grows with every token generated, so as concurrency rises it is the cache — not FLOPs — that fills the card first. This is why serving papers talk about memory almost exclusively.

What you can do when it does not fit

Four levers, in rough order of how often they are used: fewer KV heads (GQA/MQA), fewer bits per entry (quantisation), better allocation (PagedAttention), and less history to store at all (eviction, compaction, or a sliding window).

How it connects

  • Prefill vs decodeprefill builds it, decode reads it
  • MHA, GQA and MQAshrinking it by sharing KV heads
  • PagedAttentionstoring it without fragmentation
  • Prefix cachingreusing it across requests

What it costs

  • Trades compute for memoryand memory is the scarcer resource in serving
  • Grows with length × concurrencyone long conversation is fine; a hundred can exceed the model weights themselves
  • When it fills, requests are rejectednot slowed — which is why capacity, not FLOPs, usually caps a deployment
One request, one GPU

MHA, GQA and MQA

shrinking what decode has to read

Three arrangements of the same attention mechanism, differing only in how many query heads share a set of keys and values. It is a serving decision that got made inside the architecture.

The problem

Decode is bandwidth-bound and the KV cache is the thing being read every step — so the size of that cache directly sets how fast tokens come out and how many users fit on a card.

What it buys you

Sharing KV heads across groups of query heads cuts the cache substantially while keeping enough query heads for the model to stay expressive.

How it works

MHA gives every query head its own KV head. MQA gives all of them one. GQA groups them — a handful of KV heads shared across groups. A model with 32 query heads and 8 KV groups holds a quarter of MHA's cache.

MHA → GQA → MQA — shrinking what decode has to readThe only thing that changes is how many query heads share a set of keys and values.
Multi-Head Attention every query head keeps its own keys and valuesQ1Q2Q3Q4Q5Q6Q7Q8KVKVKVKVKVKVKVKVKV cache size 8 KV heads Concurrent users on the same GPU baseline
1 / 1
Say this

GQA sits at the knee of the curve — near-MHA quality at close to MQA memory.

Going deeper

A worked example — The same model, three cache sizes

Take a model with 32 query heads. Under MHA it stores 32 keys and values per token. Under MQA, one. Under GQA with 8 groups, eight — a quarter of MHA's cache.

That quarter is not an abstract saving: it is roughly four times as many concurrent users on the same GPU, or four times the context length for one user. MQA would save more still, but collapsing 32 heads onto a single shared key/value is where quality starts to show strain — which is why 8 is the sort of number real models pick.

What is actually lost

Query heads sharing a KV head can still weight it differently — their queries differ — but they can no longer attend to different things. Heads within a group are forced to look at the same set of keys, which removes some of the specialisation multi-head attention exists to provide.

It interacts with tensor parallelism

Under TP, heads are split across GPUs. With very few KV heads, either some GPUs hold no KV head at all or the KV must be replicated — so the KV-head count is not a free parameter; it is constrained by how you plan to shard.

Why it is decided at training time

Unlike batching or paging, this is architectural. You cannot convert an MHA checkpoint to GQA without retraining or at least a distillation-style conversion step, which is why it is chosen with serving economics already in mind.

How it connects

  • KV cachethe thing being shrunk
  • Prefill vs decodethe win lands on the decode side
  • GPU collectiveshead layout interacts with tensor parallelism

What it costs

  • Fewer KV heads means less representational freedomMQA can measurably hurt quality; GQA mostly does not
  • A quarter of the cache is roughly four times the concurrencyor four times the context for one user
  • It is baked in at training timeunlike most serving choices, you cannot change it later
One request, one GPU

PagedAttention

why KV memory is stored in blocks

Virtual memory, applied to the KV cache. It changes nothing about how attention is computed and everything about how many requests fit on a GPU.

The problem

Requests have unpredictable lengths. Reserving a contiguous worst-case region for each one wastes most of it, and when requests finish they leave holes that no new request fits into — classic fragmentation.

What it buys you

Removing reservation waste and fragmentation, so nearly all of the memory does real work. It also makes prefix sharing cheap, which is what prefix caching is built on.

How it works

Store KV in fixed-size blocks with a per-request block table, so a request's cache no longer has to be contiguous. It claims another block only when it needs one, and two requests sharing a prompt can point at the same blocks.

PagedAttention — why KV memory is stored in blocksReservation and fragmentation waste more memory than anything else in serving.
GPU memory for KV request A request B request C reserved, unused Memory actually put to work
1 / 1
Say this

PagedAttention is about where KV lives, not how attention is computed.

Going deeper

A worked example — You cannot know how long an answer will be

One request asks for a yes-or-no answer; the next asks for a long essay. Both arrive looking identical. Reserving the worst case for every request wastes most of the memory; reserving too little means a request dies halfway through.

Blocks dissolve the problem — each request claims another block only when it actually needs one. The bonus: two chat sessions that open with the same system prompt can point at the same blocks rather than each holding a copy.

The operating-system analogy, and where it stops

Block table ≈ page table; KV block ≈ page; the allocator ≈ the OS memory manager. The difference is that there is no swap — a GPU cannot page to disk cheaply — so when blocks run out the system must preempt a request and recompute its cache later, rather than transparently faulting it back in.

Why block size is a real tuning decision

Large blocks mean less bookkeeping but more waste in the final partly-filled block. Small blocks waste less but add indirection and kernel overhead. Typical values are a handful of tokens per block — small enough that internal fragmentation is negligible.

Copy-on-write makes branching cheap

Sharing blocks is what lets parallel samples from one prompt, or beam-search branches, share a prefix instead of duplicating it. When one branch diverges, only the block it writes to is copied.

How it connects

  • KV cachethe thing being stored
  • Prefix cachingblock sharing is what makes reuse cheap
  • Continuous batchingrequests join and leave freely because blocks are freed incrementally

What it costs

  • Needs a custom attention kernelone that can follow the indirection
  • Bookkeeping overheadblock tables and an allocator to maintain
  • Pure capacity winit makes nothing faster — it stops memory being wasted
Serving many requests

Continuous batching

rescheduling every decode step

Iteration-level scheduling. The insight is not a faster kernel but a better moment to make the decision.

The problem

Generation lengths vary wildly. A fixed batch runs until its slowest member finishes, so a batch of eight where seven finish early leaves seven slots idle while one long request runs.

What it buys you

Keeping the GPU saturated on variable-length work. Throughput rises substantially without any individual request getting faster.

How it works

Re-decide batch membership at every decode iteration. Finished requests are evicted immediately and waiting ones admitted in their place, so a slot never sits empty behind someone else's long answer.

Continuous batching — why one long request used to stall seven short onesThe same hardware, rescheduled every decode step.
Static batching the batch runs until its slowest member finishes slot 1reqslot 2reqslot 3reqslot 4reqslot 5reqslot 6reqslot 7reqslot 8req GPU work being done Waiting queuequeuedqueuedqueuedqueued
1 / 1
Say this

It is iteration-level scheduling rather than request-level. Nothing got faster — the hardware stopped waiting.

Going deeper

A worked example — One long request holding up seven short ones

A batch of eight requests starts together. Seven finish quickly; one is writing a long document and keeps going.

With a fixed batch, those seven slots sit empty until the long one finishes — the GPU doing a fraction of the work it could. With continuous batching, each slot is refilled from the queue the moment it frees up, so the long request stops being everyone else's problem.

What makes it possible at all

Decode steps are independent across requests: each one reads its own KV cache and produces one token. Nothing couples request A's step to request B's, so membership can change between iterations without breaking anything. Paged KV is what makes joining and leaving cheap in memory terms.

Admission control is the hard part

Adding a request needs KV capacity for its whole future, which you cannot know. Admit too eagerly and you run out mid-generation and must preempt someone; admit too cautiously and the GPU idles. Real schedulers reserve headroom and can preempt-and-recompute the newest request when they guess wrong.

What it does to your latency numbers

Throughput rises, but per-request latency becomes dependent on load. A p50 measured on an idle server tells you nothing about production. This is why serving benchmarks quote latency at a given throughput, not alone.

How it connects

  • Prefill vs decodeit is decode that gets scheduled this way
  • PagedAttentionblocks are freed incrementally, which is what lets requests leave mid-batch
  • LLM-aware load balancingcluster-level routing sits above this

What it costs

  • Scheduler complexityadmission, eviction and fairness all become real problems
  • Latency becomes contextuala request's speed now depends on what else is running
  • Contrast with dynamic batchingwhich only picks when to launch, then runs to completion
Serving many requests

Prefix caching

reuse across requests, not within one

Also called RadixAttention. The observation is simple: a token's KV depends only on the tokens before it, so an identical prefix always produces identical KV — no matter which request it belongs to.

The problem

Agents, chat threads and tool-heavy prompts resend nearly the same preamble every call. The same system prompt, tool definitions and document context get prefilled again and again.

What it buys you

Prefill cost and time-to-first-token collapse to the size of what is genuinely new, which for an agent on its twentieth step is a tiny fraction of the prompt.

How it works

Cached prefixes are indexed in a radix tree. A new request walks it to find the longest prefix already computed, reuses that KV, and prefills only the unmatched tail.

Prefix caching — why an agent stops re-reading its own preambleReuse across requests, and the one-character change that destroys it.
The prompt an agent sends on every single step system prompt tool definitions repository context step 20 identical on every one of the 20 calls the only new part Prefill work per step without caching with caching Cached prefixes, indexed as a tree system + tools + repo longest prefix already computed — reuse it as-is prefill this only
1 / 1
Say this

Cached tokens are still fully attended to. What is skipped is recomputing their K and V, not attending to them.

Going deeper

A worked example — A coding agent on its twentieth step

Every call this agent makes begins the same way: system prompt, tool definitions, repository context. Only the tail — the latest action and its result — is new. Without prefix caching, step 20 re-prefills that entire preamble from scratch, as it did on all nineteen previous steps.

The fragility is worth remembering: change one character near the start — inject a timestamp, reorder the tool list — and the shared prefix ends there, so the reuse mostly disappears. Keeping the volatile parts of a prompt at the end is a real engineering decision.

Why a radix tree rather than a hash map

A hash map can only answer “have I seen this exact prompt?” A radix tree over token sequences answers the question you actually have: “what is the longest prefix of this prompt that I have already computed?” Branches naturally share their common ancestors, so many conversations sharing one system prompt store it once.

It is not free memory-wise

Cached prefixes occupy the same KV memory that running requests need, so the cache needs an eviction policy — typically LRU with a bias toward keeping long, widely-shared prefixes, since those are the expensive ones to rebuild.

Where it changes system design

Because reuse depends on an exact match, prompt construction becomes a caching problem: stable instructions first, per-request content last, and no timestamps or randomised ordering near the top. It also gives the router a reason to send a request to a specific replica — cache affinity.

Compaction is its natural enemy

A prompt that only ever grows at the tail is a perfect cache workload — every request is a prefix of the next. Context compaction rewrites the middle, which invalidates everything after the edit. In one measured agent run the append-only baseline served 70% of prompt tokens from cache and the compacted run managed 57%. Both techniques are right; they just pull against each other, and raw token counts overstate what compaction actually saves.

How it connects

  • KV cachereuse within one request — this extends it across requests
  • PagedAttentionblock sharing makes the reuse cheap
  • LLM-aware load balancingcache affinity is a routing signal
  • Agent context growththe workload that makes this matter most

What it costs

  • Only exact prefix matches helpchange one character near the start and the reuse ends there
  • So put volatile content lasta timestamp at the top of a system prompt destroys the whole benefit
  • Costs cache memory and an eviction policythe tree is not free
Serving many requests

Service mesh

routing between services

The infrastructure layer that handles service-to-service communication. Worth knowing precisely because of where it stops.

The problem

Once a platform runs many services and many replicas, every one of them needs retries, timeouts, health checks, TLS and routing. Reimplementing that inside each application is duplicated, inconsistent, and impossible to change centrally.

What it buys you

Networking concerns move out of application code into infrastructure that can be configured in one place, so failover and routing policy change without touching any service.

How it works

A proxy — commonly Envoy — sits beside each service and handles its traffic: the data plane. A controller — commonly Istio — configures all those proxies centrally: the control plane.

Service mesh — routing between servicesWhere the network layer stops and the inference engine begins.
client API gateway service mesh Envoy proxy control plane Istio configures every proxy inference replica 1 healthy inference replica 2 healthy inference replica 3 healthy inside one replica: the engine scheduler which requests share the next decode step, which KV blocks to allocate
1 / 1
Say this

A service mesh routes between services; the inference scheduler decides execution inside one replica.

Going deeper

A worked example — Where the mesh stops and the engine starts

A request arrives at the gateway and the mesh picks one of three healthy inference replicas. That is the last decision the mesh makes.

Inside that replica, an entirely separate scheduler decides which requests share the next decode iteration, which KV blocks to allocate, and when to admit a queued request. The mesh has no visibility into any of it — which is exactly why LLM-aware routing has to be added on top.

Data plane vs control plane

The data plane (Envoy sidecars) carries actual traffic and must be fast. The control plane (Istio) never touches a request — it distributes configuration. If the control plane dies, existing proxies keep routing with their last known config; that split is the point of the design.

Why LLM traffic strains the usual assumptions

Mesh defaults assume short, uniform, idempotent requests. LLM calls are long-lived and streamed, so a default timeout can kill a legitimate generation, and a retry can duplicate expensive work. Timeouts, retry policy and streaming support all need revisiting.

What it gives you that is genuinely hard otherwise

Uniform mTLS between services, consistent retry and circuit-breaking, and per-service traffic telemetry — without shipping that logic in every application in every language you use.

How it connects

  • LLM-aware load balancingLLM-aware routing has to be added on top of this
  • Continuous batchingwhat happens once the request is inside a replica

What it costs

  • Another hop and another moving partlatency and operational surface both grow
  • It knows nothing about LLM costprompt length, KV pressure and cache locality are invisible to it
  • Different granularity entirelythe mesh routes requests; the engine scheduler decides tokens
Serving many requests

LLM-aware load balancing

why the busiest replica can be cheapest

Ordinary load balancing assumes requests cost about the same. For LLM serving that assumption is wrong in a way that flips the routing decision.

The problem

Two requests that look identical on arrival can differ by orders of magnitude in cost — prompt length, output length, prefill versus decode mix, KV pressure, and whether a replica already holds a reusable prefix.

What it buys you

Routing on expected serving cost rather than connection count, which can cut prefill work dramatically by sending a request where its cache already lives.

How it works

Score replicas on signals a web balancer never needed: queue depth, GPU and KV-cache pressure, prompt length, estimated output length, and above all prefix-cache affinity.

LLM-aware load balancing — why the busiest replica can be the cheapestRouting on expected serving cost instead of connection count.
new request 15K shared prefix + 100 new load balancer least connections replica A 5 active · 70% used holds the 15K prefix replica B 2 active · 40% used no useful cache Signals a router can actually seequeue depth / active requestsGPU + KV-cache pressureprompt length → prefill costestimated output lengthprefix-cache affinityExpected cost of serving it here replica A replica B
1 / 1
Say this

The least-busy server is not always the cheapest server. Route on estimated total serving cost.

Going deeper

A worked example — When “least connections” picks wrong

Replica A: five active requests, 70% utilised — but it already caches the 15K-token prefix this request shares. Replica B: two active requests, 40% utilised, no useful cache.

Least-connections sends it to B, which must prefill all 15K tokens. The cache-aware router sends it to A, reuses the prefix, and prefills only the new tail. A is busier and still cheaper. The catch worth volunteering: if every request with that prefix routes to A, A becomes a hotspot.

Why queue length is a poor proxy

Two requests in a queue could be 100 tokens or 100K. Without prompt length, expected output length and KV pressure, a count tells you almost nothing about how long the queue will take to drain.

Estimating output length

Nobody knows how long a generation will be, so routers approximate: request type, prompt shape, historical averages per endpoint, or an explicit max_tokens. Getting it wrong is survivable — the router only needs to rank replicas, not predict precisely.

The tension to name out loud

Cache affinity pulls traffic toward one replica; load balancing pushes it apart. Production routers blend them — affinity as a strong preference, overridden once a replica's pressure crosses a threshold.

How it connects

  • Prefix cachingthe cache whose location drives the routing
  • Service meshthe layer this sits on top of
  • Prefill–decode disaggregationadds another routing dimension — which pool
  • KV cachethe pressure signal being measured

What it costs

  • Needs live telemetry from every replicaand a cost model that can be wrong
  • Affinity fights balancingrouting every shared-prefix request to one replica creates a hotspot
  • So real routers blend the twoand fall back once a replica saturates
Serving many requests

Prefill–decode disaggregation

stopping the two phases fighting

Take the prefill/decode distinction seriously enough to put them on different machines.

The problem

On a shared pool a long prefill occupies the GPU while it runs, so every other user who was mid-answer stops receiving tokens. Their stream visibly stutters.

What it buys you

Interference disappears, and each pool can be sized and tuned for its own bottleneck — compute on one side, memory bandwidth on the other — and scaled independently.

How it works

Prefill workers ingest the prompt and build the KV cache; that cache is transferred over the interconnect to a decode worker, which streams tokens out.

Prefill–decode disaggregation — stopping the two phases from fightingSeparate pools, and the KV transfer you pay for them.
Shared pool — prefill and decode on the same GPUs GPU pool 20K-token prefill other users' tokens paused decode steps prefill pool big GEMMs compute-bound scale for long prompts decode pool one token per step bandwidth-bound scale for long answers KV Where the interference goes chunked prefill same pool, prefill sliced and interleaved — no KV moves disaggregation separate pools — but the KV cache crosses the network
1 / 1
Say this

Disaggregation removes interference but pays a transfer; chunked prefill reduces interference and moves nothing.

Going deeper

A worked example — A 20K prompt and a 500-token answer

On a shared pool, that 20K-token prefill monopolises the GPU while it runs, and every other user's tokens pause.

Disaggregated: a prefill worker builds the KV cache, ships it to a decode worker, and that worker streams the 500 tokens while the prefill pool moves on to the next prompt. Nobody's stream stutters. The bill: a 20K-token KV cache is large, and it crossed the network to get there.

Do the transfer arithmetic before believing the design

The KV cache to move is roughly 2 · L · S · Nkv · dhead · bytes — for a long prompt this is gigabytes. On NVLink or InfiniBand that is milliseconds; over ordinary Ethernet it can exceed the prefill you were trying to protect. This is why the technique appears in large deployments and not small ones.

It lets the two pools differ in kind

Prefill wants raw compute; decode wants memory bandwidth and capacity. Once separated you can give them different GPU types, different batch policies, even different parallelism strategies — impossible when one pool does both.

Layer-wise streaming hides some of the cost

The transfer does not have to wait for the whole prefill to finish: each layer's KV can start moving as soon as it is computed, overlapping transfer with the remaining compute.

How it connects

  • Prefill vs decodethe distinction this is built on
  • KV cachethe thing that has to cross the network
  • LLM-aware load balancingrouting now has to pick a pool as well as a replica
  • GPU collectivesthe interconnect that decides whether this pays off

What it costs

  • The KV cache must cross the network on every requestpure overhead, and it is what decides whether this pays off
  • Compare with chunked prefillsame pool, prefill sliced and interleaved — less interference, nothing moves
  • So it is an interconnect questionfast links favour disaggregation; slow ones favour chunking
Across many GPUs

Tensor, pipeline and expert parallelism

three ways to cut a model

When a model does not fit on one GPU it has to be split. There are three places to cut, and each creates a different communication pattern — which is what actually decides the choice.

The problem

Weights and activations exceed a single GPU's memory, and every way of splitting introduces communication the single-GPU version never paid.

What it buys you

Models far larger than one device become trainable and servable, provided the communication pattern matches the hardware topology.

How it works

TP splits the matrices inside a layer — GPUs cooperate on one layer and exchange partial results constantly. PP gives each GPU a different block of layers — data crosses only at stage boundaries. EP puts different MoE experts on different GPUs and routes tokens to the ones they selected.

TP, PP and EP — three ways to cut a modelEach cut creates a different communication pattern, and that decides everything.
Tensor Parallelism split the matrices inside one layer GPU 1 shard GPU 2 shard GPU 3 shard GPU 4 shard
1 / 1
Say this

TP splits tensors, PP splits layers, EP splits experts — and the choice follows the hardware topology, not preference.

Going deeper

A worked example — Why TP stays inside the node

With TP=4, four GPUs split every layer's matrices and must combine partial results at each one — many times per forward pass. Put those four on NVLink inside one node and it works; stretch them across a slower network and communication dominates, making four GPUs slower than one would have been if the model fit.

With PP=4, GPU 0 holds layers 1–20, GPU 1 holds 21–40, and data crosses only at the three boundaries. That tolerates a slower link — which is why the common layout is TP within a node, PP across nodes.

Row-parallel and column-parallel, and why they pair

Splitting a weight matrix by columns lets each GPU compute an independent slice of the output with no communication. Splitting the next matrix by rows means each GPU consumes exactly the slice it already holds, and one AllReduce at the end combines the result. Transformer MLP and attention blocks are deliberately arranged as a column-parallel layer followed by a row-parallel one, so each block costs one collective rather than two.

What each strategy costs in memory

TP shards weights and activations, so it reduces per-GPU memory the most. PP shards weights by layer but each stage must hold activations for every in-flight micro-batch. EP shards only the expert weights — attention layers are still replicated or TP-sharded.

Why real systems combine all three

A large MoE model might run TP=8 inside a node, PP=4 across nodes, and EP across the whole cluster. The dimensions are orthogonal; the art is matching each one's communication pattern to the link that can carry it.

How it connects

  • GPU collectivesTP leans on AllReduce, EP on All-to-All
  • Pipeline bubbles and micro-batchingthe cost PP introduces
  • Prefill–decode disaggregationanother axis of splitting, at the request level

What it costs

  • TP communicates constantlyso it belongs on the fast links inside one node
  • PP communicates rarely but bubblesstages wait on each other
  • EP routing is data-dependenta popular expert becomes a straggler everyone waits for
Across many GPUs

Pipeline bubbles and micro-batching

keeping the stages full

Pipeline parallelism is sequential by construction, so its first version wastes most of the hardware. Micro-batching is the fix, and it has its own cost.

The problem

Stage 2 cannot start until stage 1 hands something over. With one batch in flight, exactly one stage is busy and the rest idle — the bubble.

What it buys you

Keeping several micro-batches in flight overlaps the stages, so after a short warm-up all of them are working on different micro-batches at once.

How it works

Split the batch into micro-batches fed in back to back. Gradients accumulate across them before a single optimizer step, so correctness is unchanged. Schedules like 1F1B interleave forward and backward passes to cut activation memory.

The pipeline bubble — and how micro-batches fill itWhy splitting by layer leaves most of your GPUs idle, until you keep more work in flight.
One batch through four stages stage 1 stage 2 stage 3 stage 4 time → Hardware doing useful work
1 / 1
Say this

PP creates the pipeline; micro-batches keep it full. You want many more micro-batches than stages.

Going deeper

A worked example — Four stages, one batch versus four micro-batches

One batch of 64 through four stages: stage 1 works while 2, 3 and 4 wait; then stage 2 works while the rest wait. Three quarters of the hardware is idle at any moment.

Split it into four micro-batches of 16 and after a short warm-up all four stages are busy on different micro-batches at once. Same total work, most of the idle time gone — but each stage is now doing a smaller, less efficient chunk, which is the trade you accept.

The bubble fraction, roughly

With p stages and m micro-batches, the pipeline spends about (p−1) steps filling and (p−1) draining out of roughly (m + p − 1) total — so the wasted fraction is approximately (p−1)/(m+p−1). Push m well above p and it shrinks; it never reaches zero.

What 1F1B is actually for

The naive schedule runs all forwards then all backwards, so every micro-batch's activations are held simultaneously — peak memory scales with m. One-forward-one-backward starts backward passes as soon as possible, so activations are freed early and peak memory scales with p instead. It is a memory optimisation, not a speed one.

Interleaved stages trade communication for a smaller bubble

Giving each GPU several non-contiguous chunks of layers instead of one contiguous block shortens the fill and drain, at the cost of more boundary crossings — worth it when the interconnect can absorb them.

How it connects

  • Tensor, pipeline and expert parallelismPP is what creates the bubble
  • GPU collectivesgradient synchronisation across the pipeline

What it costs

  • Smaller chunks are less efficient per GPUsmaller matrices use the hardware worse
  • More in-flight micro-batches means more stored activationswhich is what 1F1B claws back
  • Warm-up and drain never disappearthe bubble shrinks toward zero but never reaches it
Across many GPUs

GPU collectives

four shapes of exchange

Split a model across GPUs and they must constantly exchange tensors. Collectives are how that exchange is expressed so the library can use the whole topology at once.

The problem

Done naively — point to point, one pair at a time — communication becomes the bottleneck and the extra GPUs stop paying for themselves.

What it buys you

Topology-aware implementations that saturate NVLink, NVSwitch or the network, turning communication from the limiting factor into a manageable cost.

How it works

AllReduce: combine, everyone gets the full result. AllGather: collect shards, everyone ends with the whole. ReduceScatter: combine, each keeps one slice. All-to-All: everyone sends different data to everyone. Note that AllReduce = ReduceScatter + AllGather, which is how ring implementations are built.

GPU collectives — four shapes of exchangeAllReduce, AllGather, ReduceScatter, All-to-All — and which parallelism needs each.
AllReduce combine everyone’s tensor — everyone gets the whole result GPU 1 [1,2] [4,6] GPU 2 [1,2] [4,6] GPU 3 [1,2] [4,6] GPU 4 [1,2] [4,6] AllReduce
1 / 1
Say this

Match the collective to the parallelism: TP leans on AllReduce, EP leans on All-to-All.

Going deeper

A worked example — The four, concretely

Two GPUs holding [1,2] and [3,4]: AllReduce → both end with [4,6]. AllGather → both end with [1,2,3,4]. ReduceScatter → one keeps 4, the other keeps 6.

All-to-All is the odd one: each GPU sends a different chunk to each destination. In MoE, tokens scatter to whichever GPUs host their chosen experts, then a second All-to-All brings the results home — which is why MoE performance lives and dies on interconnect quality.

Why ring AllReduce is the standard implementation

A ring moves each shard around the GPUs in two passes — reduce-scatter then all-gather — so every GPU sends roughly 2(N−1)/N of the data regardless of how many GPUs there are. Bandwidth cost is near-constant in N; only latency grows. That is why AllReduce = ReduceScatter + AllGather is an implementation fact, not just an identity.

Latency-bound vs bandwidth-bound collectives

Small tensors are dominated by per-hop latency, so tree algorithms win. Large tensors are dominated by bandwidth, so ring algorithms win. NCCL picks per call based on size and topology — which is why a microbenchmark at one message size can badly mispredict real performance.

Overlap is where the real wins are

Communication that cannot be removed can often be hidden: launch the collective on a separate stream and keep computing while it runs. Gradient bucketing in data-parallel training exists precisely so that AllReduce for early layers overlaps with the backward pass of later ones.

How it connects

  • Tensor, pipeline and expert parallelismTP leans on AllReduce, EP on All-to-All
  • Pipeline bubbles and micro-batchingstage boundaries are communication points
  • Prefill–decode disaggregationthe interconnect that carries the KV transfer

What it costs

  • Collectives synchronisethe slowest GPU sets the pace and one straggler stalls everyone
  • Scaling is bounded by interconnect, not FLOPsthis is the systems point to make
  • Topology dictates placementwhich is why TP stays inside a node
How an agent runs

The agent loop

what an agent actually is

An agent is not one model call. It is a loop — the model picks an action, something executes it, an observation comes back, and round it goes. Nearly every hard problem in agent systems comes from the loop rather than the model.

The problem

A single model call can only answer from what it was given. It cannot look something up, run a test, check whether its answer was right, or take a second attempt informed by the first.

What it buys you

Wrapping the model in a loop with tools turns prediction into something closer to work: the agent can act on the world, see what happened, and revise. It is also what makes the system expensive, stateful and risky.

How it works

Each turn: the model reads the context and emits an action; the orchestrator checks permissions and runs it in a sandbox; a tool executes; an observation returns; state updates. Then it repeats — often dozens of times — until a stopping rule fires.

The agent loop — what an agent actually isA model in a loop with tools, state, and a reason to stop.
a goal arrives "fix the failing test" the model reads context, picks an action the orchestrator permissions, approval, sandbox the tool call shell, search, API, MCP server the observation often large — a log, a file update state context grows, checkpoint writtenthe loop not one model call What the loop makes hard context grows every call re-reads the history state must survive crash, restart, approval wait it must reach out tools, data, other systems what enters context external knowledge vs past experience
1 / 1
Say this

An agent is a model in a loop with tools, state and a stopping rule. The model reasons, the orchestrator controls, the tools act, and the observations are what make it expensive.

Going deeper

A worked example — Where each layer's responsibility actually sits

Ask an agent to fix a failing test. The model emits run_tests("payments"). It does not run anything — it produces a decision.

The orchestrator decides whether that is allowed, runs it in a sandbox, and enforces a timeout. This layer is yours to build; no model provides it. The observation comes back as 400 lines of stack trace — larger than the entire conversation so far. That asymmetry is why agent context behaves nothing like chat context.

The loop is where the engineering lives

Tool schemas, permission policy, sandboxing, retries, timeouts, budget limits, and the stopping rule are all code you write around the model. Improving the model rarely fixes a badly designed loop, which is why agent quality differs so much between products using the same model.

Stopping rules, and how they fail

Common conditions: the goal is verifiably met, a step budget is exhausted, no progress across N steps, or a human must approve. Each fails differently — verification is often impossible, budgets truncate mid-task, and no-progress detection needs a definition of progress. Most production agents combine several.

Why observations dominate the budget

In chat, both sides produce sentences. In an agent, one side produces sentences and the other produces files, logs and pages. Trimming, summarising or referencing observations rather than inlining them is usually the highest-leverage change available.

Hidden reasoning can quietly eat the context

Some providers attach a full chain of thought to every response. Storing the whole response object in history resends all of it forever. One measured chess run carried 204,027 characters of reasoning against 7,585 characters of actual board observations — games took 31 minutes instead of 3.5. Reasoning is per-turn scratch work, not conversation: keep visible text and tool calls, drop the rest.

Parallel tool calls are safe on reads, unsafe on writes

A model can emit several calls in one response, all chosen from the same picture of the world. If they only read, that is a free speed-up. If any of them writes, every call after the first was chosen from a world that no longer exists. The chess harness executes the first move and rejects the rest with an explanation, because the opponent has already replied.

How it connects

  • Coding agent architectureone real instance of this loop, end to end
  • Agent context growthwhat the observations do to your budget
  • Agent state and checkpointinghow the state survives an interruption
  • MCPhow the tool step reaches systems you do not own

What it costs

  • Observations dominate the contexta single test log or file can outweigh many turns of dialogue
  • The orchestrator is code you writepermissions, approval gates and sandboxing are not provided by the model
  • Termination is a design decisionan agent without a clear stopping rule either quits early or burns budget rediscovering dead ends
How an agent runs

Coding agent architecture

the verification loop

A coding agent is the clearest instance of the agent loop, because its environment can answer back. Tests turn a guess into a fact, which is what separates it from retrieval plus generation.

The problem

A real repository is orders of magnitude larger than any context window, so the agent cannot read it. And a plausible-looking patch is worth nothing — the only thing that counts is whether the change actually works without breaking anything else.

What it buys you

Retrieval brings in only the code that matters; the verification loop replaces confidence with evidence. The deliverable is a patch plus proof that it passes.

How it works

Search the repo — grep and symbol search for exact names, semantic search for intent, dependency paths for what calls what. Localize to the code on the real execution path. Hypothesise a root cause before editing. Make the smallest change that tests it. Run the tests. A failure is a new observation, so go back to localization with more information.

Coding agent architecture — the verification loopSearch, localize, hypothesise, edit, test, replan — until it is proven, not plausible.
The loop read the issue expected vs observed search the repo grep, symbols, vectors localize what actually runs hypothesise root cause first edit smallest change run tests find out, don’t guess tests failed — that is an observation, not a dead end patch diff + evidence the repository far larger than any context window retrieval keyword + symbol + semantic context only what matters tools, in a sandboxread · grep · symbol searchedit file · apply patchshell · tests · build · lintgit diff · git statusdocs · browser · MCP servers
1 / 1
Say this

Search → localize → hypothesise → edit → test → replan. RAG retrieves and generates; a coding agent retrieves, acts, observes and repeats — the interactive verification loop is the whole difference.

Going deeper

A worked example — Why hypothesis-first beats edit-first

“The payments test fails intermittently.”

Edit-first: change three things that look suspicious, re-run, still fails. You now know less than when you started — which of the three was wrong?

Hypothesis-first: state the claim — “the retry path shares a connection pool across threads” — then make the single smallest change that tests it. Now the result carries information either way: it confirms the root cause, or rules it out cleanly and you move to the next hypothesis.

Localization is the step that decides the outcome

Empirically, most failures on repository benchmarks are localization failures, not editing failures: the model could have written the right patch if it had been looking at the right file. This is why retrieval quality and dependency-aware search matter more than raw code-generation ability.

Intermittent failures break the loop's core assumption

The loop assumes the test result is a signal. A flaky test makes it noise — a passing run may mean nothing. Handling that means running repeatedly, seeding randomness, or isolating the nondeterminism before trusting any result at all.

What the sandbox is really protecting

The agent runs shell commands it wrote itself against a repository it only partly understands. Scoped file access, no network by default, resource limits, and no credentials in the environment turn a destructive mistake into a failed step rather than an incident.

How SWE-bench actually grades

Each instance gives the repository before the fix, the issue text, and hidden tests. The grading is two-sided: FAIL_TO_PASS proves the bug is fixed, and PASS_TO_PASS proves nothing else broke. Only the pair is meaningful — the second is what catches an agent that made things green by deleting a test.

How it connects

  • The agent loopthe general shape this specialises
  • RAG vs memoryrepo retrieval is RAG applied to code
  • Agent state and checkpointinglong runs need progress that survives a crash
  • Agent context growthevery file read stays in the history

What it costs

  • Localization quality dominatesget the wrong files and every step after it is wasted
  • Large speculative edits make failure uninterpretablewhen tests still fail you cannot tell which part was wrong
  • Verification costs real timerunning the suite is slow, and running only the targeted test risks silent regressions
What it carries

Agent context growth

why long loops get expensive

An agent is a loop of model calls, each appending to a history that every later call re-reads. The cost behaviour surprises people because two quantities get conflated.

The problem

The history grows linearly in steps, but since call t re-reads everything before it, the total input processed across the run grows quadratically in the number of steps.

What it buys you

Separating context size (what one call sees) from cumulative work (what the run paid for) tells you which fix applies — and they are not interchangeable.

How it works

Two different levers. Caching avoids recomputing history that is still present — cost falls, the window still fills. Compaction summarises or drops old history — the bars actually come down.

Why a long agent loop gets expensiveThe history grows steadily; the bill grows faster.
What each call has to read 1 2 3 4 5 6 7 8 9 10 tokens read agent step what gets appended source files, test logs, browser pages, API output context sizethe height of the last bar cumulative workthe sum of every bar total paid over the run
1 / 1
Say this

Caching fixes cost; compaction fixes window pressure. Diagnose which one you have before prescribing.

Going deeper

A worked example — A thirty-step debugging agent

The agent reads a file, runs the tests, reads the failure log, edits, re-runs. Each step appends its observations — and a single test log or source file can dwarf everything said so far.

By step 30 the model is re-reading the output of steps 1 through 29 to decide one more action, and most of it is long dead: a file it already fixed, a log it already diagnosed. The cost per step keeps climbing even though the useful state — what is broken and what has been tried — stays small.

Where the quadratic comes from

If each step appends d tokens, call t carries about d·t tokens, so the total across T calls is d(1+2+…+T) = d·T(T+1)/2 — quadratic in steps, even though the history itself only grows linearly. Conflating those two quantities is the classic mistake.

Caching and compaction fix different problems

Caching removes the cost of re-reading history that is still present; the window fills at exactly the same rate. Compaction removes the history itself. If you are hitting a cost ceiling, cache. If you are hitting the context limit, only compaction helps.

Designing against drift

Compaction discards silently, and you find out several steps later. Practical mitigations: keep a structured summary the agent maintains deliberately rather than a free-text one, keep artifact references rather than artifact contents, and keep the failed approaches — re-trying a ruled-out fix is the most common drift symptom.

What the ratio actually looks like

In one measured coding run the agent spent 463,000 prompt tokens against 13,000 completion tokens — roughly 35 to 1. The bill is for re-reading, not for thinking, which is why every lever in this section targets the prompt rather than the generation.

How it connects

  • Prefix cachingthe caching half of the fix
  • KV cachewhat is being reused
  • Agent state and checkpointingstructured state as an alternative to carrying raw history
  • Full vs linear attentionan architectural answer to the same pressure

What it costs

  • Caching does not reduce the windowonly the cost of re-reading it
  • Compaction risks driftyou discard something that turns out to matter, and find out several steps later
  • Agent context is observation-dominatedone test log can outweigh many turns of dialogue
What it carries

Agent state and checkpointing

surviving the interruption

Long agent runs hit crashes, restarts, truncated context and human-approval waits. What you persist decides whether any of those means starting over.

The problem

If the only record is conversation history, resuming means re-deriving where the agent was from a transcript — and if that transcript was truncated, the information is simply gone.

What it buys you

Structured execution state makes resume mechanical: load the checkpoint, see which steps are done, carry on.

How it works

Checkpoint the goal and plan, completed and pending steps, key observations, artifacts touched, pending actions and approval state, and environment references. Snapshot saves state directly; replay rebuilds it from an event log; real systems combine both.

Agent state and checkpointing — surviving the interruptionWhy conversation history is not enough, and what must never be replayed.
Agent run step 1 step 2 step 3 step 4 step 5 step 6 crash conversation history the raw trace checkpoint — structured execution stategoal + plansteps done / pendingkey observationsartifacts touchedpending action + approvalenvironment refs memory knowledge worth keeping "this repo uses pnpm" checkpoint where to resume "step 7 pending" snapshot vs replay save state directly, or rebuild it from an event log irreversible actions charge, email, delete must not replay
1 / 1
Say this

Memory is knowledge worth keeping, a checkpoint is execution progress, conversation history is the raw trace.

Going deeper

A worked example — The payment that must not run twice

An agent charges a card, then crashes before recording the result. On resume it re-reads its plan: “step 7, charge the card — not marked complete.” With no durable action record it charges again.

With an idempotency key written before the call and reconciled after, the runtime can ask the payment provider whether that key already succeeded, and skip it. This is why a state machine matters: “we sent it and don't know the outcome” is a different state from “we never sent it”.

Snapshot, replay, or both

Pure replay rebuilds state by re-running an event log — exact, but slow and dangerous if events had side effects. Pure snapshots are fast but lose everything since the last one. Production systems take periodic snapshots and replay only the log since, which bounds both recovery time and loss.

Write the intent before the action

The durable record has to exist before the side effect, not after — otherwise the crash window between acting and recording is exactly the case you cannot recover from. Write “about to charge, key=abc”, then charge, then reconcile.

What belongs in a checkpoint and what does not

In: goal, plan, step status, pending action and approval state, artifact references, environment handles. Out: the full transcript, and anything reconstructible. A checkpoint that grows with the conversation has become another context problem.

Outcome metrics hide process failures

A run can reach a correct final state while a quarter of its steps produced no action at all. Grading only the outcome misses that entirely. Inspect trajectories for the behaviour you care about — steps with no tool call, repeated identical actions, retries after a permanent failure — and A/B the harness, not just the model, because the interface is as much of the system as the weights are.

How it connects

  • Agent context growthstructured state instead of unbounded history
  • MCPthe tool calls whose status must be tracked
  • RAG vs memorymemory is knowledge; a checkpoint is progress

What it costs

  • Costs writes and a schema to maintainand deciding what to persist is genuinely hard
  • Too little and resume is impossible; too much and it becomes another context problem
  • Irreversible actions are the hard parttrack action IDs and idempotency keys, or recovery sends the email twice
What enters the context

MCP

standardising how agents reach the world

The Model Context Protocol standardises the boundary between an agent runtime and external capabilities. It sits at a different layer from function calling, and that distinction is what gets tested.

The problem

Every agent that wants GitHub, a database or a file system writes its own integration, each with its own auth, schema and failure modes. N agents times M services means N×M integrations.

What it buys you

Wrapping each service once and teaching each agent the protocol once turns N×M into N+M, and makes servers substitutable between hosts.

How it works

A host contains an MCP client that speaks to an MCP server wrapping one external system. Servers expose Tools (actions), Resources (data) and Prompts (templates).

MCP — standardising how an agent reaches the outside worldWhy it is not function calling, and which layer each one lives at.
Without a standard: N agents × M servicesagent Aagent Bagent CGitHubdatabasefiles9 bespoke integrations — each with its own auth, schema and failuresWith MCP: N + Magent Aagent Bagent C MCP standardGitHub serverdatabase serverfiles serverany compliant host can use any compliant server the model decides which tool and what arguments function calling the orchestrator checks permissions, approval, sandboxing runtime control the MCP client → server discovers and reaches the capability MCP the external system GitHub, a database, a file system the real API
1 / 1
Say this

Function calling is the model deciding which tool and what arguments. MCP is how the runtime discovers and reaches it.

Going deeper

A worked example — The layers in one call

You ask an agent to file an issue. The model emits a function call: create_issue(repo, title, body) — that is function calling. The orchestrator checks permissions and approval, then routes it through the MCP client to a GitHub MCP server, which translates it into a real GitHub API call.

Swap GitHub for Linear and the model's half is unchanged — a different server handles it. That substitutability is the entire point. Note also that MCP bypasses nothing: permission and sandbox checks still belong to the orchestrator.

Tools, resources and prompts are not the same thing

A tool is an action with side effects the model may choose to call. A resource is addressable data the host can read into context without the model deciding anything. A prompt is a reusable template the server offers. Conflating tools and resources is the most common misreading of the protocol.

Discovery is what makes it a protocol

A host can ask a server what it offers at runtime and present those capabilities to the model, instead of having them hard-coded at build time. That is the difference between a standard and a convention — and it is what turns N×M integrations into N+M.

The security surface it creates

Every connected server widens what the agent can reach, and tool descriptions are text the model reads — so a malicious server can attempt to influence behaviour through them. Permission checks, approval gates and treating server-supplied text as untrusted data remain the host's responsibility.

How it connects

  • Agent state and checkpointingtool-call status is what has to survive a crash
  • RAG vs memoryMCP can expose retrieval — that is integration, not strategy

What it costs

  • Another layer and another process to run
  • A standard interface can hide capabilities a native API exposes
  • It widens what an agent can reacha security surface, which is why permission checks stay with the orchestrator
What enters the context

RAG vs memory

two retrievals, two questions

Both put retrieved text into the working context and both may sit on the same vector store. What differs is the semantics of what is stored — and how each one fails.

The problem

The working context is small and the candidates are vast: external corpora on one side, everything this agent has already learned and decided on the other.

What it buys you

Knowing which one you need. RAG supplies knowledge the agent never had; memory supplies conclusions it already reached.

How it works

RAG asks what external knowledge is relevant to this task? — retrieving from docs, code or databases by vector search, BM25 or hybrid. Memory asks what from earlier should persist? — decisions, preferences, failed hypotheses.

RAG vs memory — two retrievals, two questionsSame infrastructure, completely different semantics.
"fix the flaky test in payments" RAG what external knowledge is relevant? the payments module source the test framework docs Memory what from before should persist? "we tried the timeout fix — it failed" "this user wants root cause, not retries" docs · code · wikis · databases — exists independently of you decisions · preferences · failed attempts — exists only because of you working context what the model actually sees memory also needs a write policy — what was worth keeping?
1 / 1
Say this

RAG says what does the world know; memory says what did we learn. Same plumbing, different semantics.

Going deeper

A worked example — The same question, two retrievals

“Fix the flaky test in the payments module.”

RAG pulls the module's source, the test file, and the framework docs — external knowledge, equally available to anyone. Memory pulls “we tried bumping the timeout last week and it did not help” and “this user wants the root cause, not a retry” — things that exist only because of prior interaction.

Drop RAG and the agent lacks the code. Drop memory and it cheerfully re-tries the timeout fix it already ruled out.

Memory needs a write policy; RAG does not

Documents already exist — nobody decided they were worth writing for the agent's benefit. Memories must be chosen, which adds a whole problem RAG never has: what, when, and at what granularity to save, and when to overwrite something now false.

Why retrieval quality dominates both

The model can only use what enters the context. Chunk boundaries that split a function in half, or an embedding that matches on topic rather than intent, fail silently — the answer looks confident and is simply missing the relevant material. Hybrid retrieval (lexical + semantic) exists because each covers the other's blind spot.

Memory can compound its own errors

A wrong conclusion written once gets retrieved forever, and each retrieval makes it look better established. Practical defences: store what was observed rather than what was concluded, timestamp entries, and allow later evidence to supersede rather than accumulate.

Why production coding agents do not use a vector database for the transcript

Retrieval assumes the query resembles what you need. Much of what an agent must not forget fails that test: “I already tried X and it failed” has to be present regardless of what the next step is about, and nothing in that step’s text would retrieve it. Summaries also preserve order and causality, which retrieved fragments lose — and retrieval hands back the raw output you were trying to shed. Retrieval does belong in coding agents, but on the codebase, not the transcript.

How it connects

  • Agent state and checkpointingmemory is knowledge, a checkpoint is progress
  • MCPhow a retrieval capability gets connected
  • Agent context growthboth spend the same scarce context

What it costs

  • Both spend context on text that may not help
  • RAG is only as good as its chunking and index
  • Memory has a write problem RAG does notdocuments already exist; memories must be chosen — and a wrong one gets retrieved forever
Building the harness

The prompt is the state

four roles, one structural rule

A chat completion is stateless: text in, text out. Everything an agent knows on step 40 is in the message list you rebuild and resend on step 40 — so memory, persistence and continuity are all properties of that list, not of the model.

The problem

Because the whole transcript is resent every step, the context window becomes a hard ceiling on run length, and cost becomes the area under a rising curve — quadratic in steps rather than linear.

What it buys you

Getting the protocol right is what makes everything downstream possible: tool results that the model can act on, errors it can correct, and a history that compaction can safely rewrite.

How it works

Four roles with rules the API enforces. system carries standing instructions (exactly one, first), user poses the task, assistant carries the model's text and its tool calls, and tool carries one observation tagged with the tool_call_id it answers.

The prompt is the stateFour roles, one structural rule, and why you cannot cut history anywhere you like.
The message list, rebuilt and resent every step system standing instructions user the task assistant call_1 · execute(ls) tool call_1 · listing assistant call_2 · execute(sed) tool call_2 · 10k of source assistant call_3 · execute(pytest) tool call_3 · test output tool_call_id ties them together the rules the API enforces· exactly one system message, first· user poses the task before any assistant turn· every assistant tool call must be answered· each tool message carries the id it answers cut here 400 — request rejected call_1 was answered, but its assistant message is gone — the tool reply is an orphan cut here valid the cut lands on an assistant boundary, so no call is separated from its observation the model remembers nothing everything it knows at step 40 is in the list you hand it at step 40 one measured run 463,000 prompt tokens · 13,000 completion the bill is for re-reading, not for thinking
1 / 1
Say this

The prompt is the state, and the state is a budget. Nothing about an agent's memory lives in the model.

Going deeper

A worked example — Why a broken call still needs a reply

The model emits execute({"cmd": ) — malformed JSON. The tempting implementation raises, logs it, and moves on.

But the assistant message with that tool call is already in the history, and the API requires every call to be answered. The next request is now structurally invalid and fails before the model sees anything.

So the dispatcher returns a string: {"error": "Arguments were not valid JSON"}, carrying that call's id. The model reads it and fixes the call on the next step. Nothing raises.

The assistant message is a commitment

Once the model has emitted a tool call, the harness has no way to un-emit it. Every path out of the dispatcher — success, bad JSON, unknown tool, wrong argument type, non-zero exit — has to produce exactly one observation carrying that id. Treating it as a total function rather than one that can throw is the design.

Why cost is quadratic and not linear

Step t resends everything from steps 1..t−1. If each step adds d tokens, the sum across T steps is d·T(T+1)/2. Completion tokens grow linearly; prompt tokens grow as the square. That is why one measured run showed 463k prompt against 13k completion — a ratio of about 35 to 1.

Provider-specific fields need a deliberate decision

Some models attach hidden reasoning to every response. Storing the whole response object in history resends all of it forever. In one run that was 204,027 characters of reasoning against 7,585 characters of actual observations — the prompt was 96% scratch work. Keep visible text and tool calls; drop the rest.

How it connects

  • The agent loopthe loop this protocol serves
  • Context compactionwhy the cut must land on an assistant boundary
  • Error policywhy a broken call still needs an answer
  • Agent context growthwhat resending everything costs

What it costs

  • Every tool call must be answeredeven a malformed one — an unanswered call makes the next request invalid, which is why errors return as text rather than raising
  • History cannot be cut at an arbitrary indexslice between an assistant message and its tool reply and the observation is orphaned
  • Prompt tokens dominateone measured coding run spent 463k prompt tokens against 13k of completion — the bill is for re-reading, not thinking
Building the harness

Context compaction

the sawtooth, and its caching bill

Rewriting the old part of a transcript into a written summary so the prompt stops growing. The interesting part is not the mechanism — it is what the summary is instructed to preserve, and the hidden cost it creates.

The problem

A prompt that grows about 600 tokens a step and never comes down makes total cost quadratic, and eventually the context window ends the run outright.

What it buys you

Capping the prompt turns that area from quadratic into roughly linear. Measured on a real SWE-bench instance: half the tokens and a quarter of the peak prompt, with both runs still producing a patch that resolved the issue.

How it works

At a threshold, the harness makes a separate model call — its own system prompt, no tools — whose only job is to write working memory. That replaces the old prefix; system and task messages and the recent steps stay verbatim. The cut lands on an assistant boundary, and each new memory merges the previous one rather than stacking on it.

Context compaction — the sawtoothRewriting history to cap the prompt, and the caching bill it quietly creates.
Prompt size, step by step threshold ReAct step → a separate model call own system prompt, no tools reads the prefix as text writes working memory what the summary must preserve objective · constraints · files · commands edits · concrete results · blockers · next action and the approaches that failed same model, same task, 200-step budgetcompactedfull contexttotal tokens221,187475,642peak prompt5,97424,463prompt cached57%70%
1 / 1
Say this

Compaction is lossy by construction, so what the summary is told to preserve is the design. And name the caching tension unprompted — it is the difference between reading the docs and paying the bill.

Going deeper

A worked example — What five compactions actually bought

Same model, same prompts, same 200-step budget on django__django-15368. One run compacting at a 6,000-token threshold, one with compaction disabled.

Compacted: 44 steps, 5 compactions, 221,187 tokens, peak prompt 5,974. Full context: 39 steps, 475,642 tokens, peak prompt 24,463. Both produced a patch that resolved the instance.

Half the tokens and a quarter of the peak — while taking five more steps and paying for five extra summariser calls. The baseline climbs about 600 tokens per step and never comes down, so its total is the area under a rising line.

Merge the memory, never stack it

Each compaction is handed the previous working memory along with the new prefix, and told to drop what later steps made obsolete. A memory that only appends recreates the exact problem it was built to solve — it just grows more slowly.

Render the prefix as text before summarising

The summariser reads a readable transcript, not raw JSON message objects. It is a one-off rendering cost that buys a markedly better summary, because the model is being asked to read a conversation rather than parse a data structure.

Check the summariser's finish_reason

Two of five summaries in the measured run came back with finish_reason length — they hit the 1,200-token output cap mid-sentence and the tail was lost. Nothing failed, nothing logged an error, and the run still succeeded. Either raise the cap or keep more recent steps verbatim, but first make the truncation visible.

How it connects

  • The prompt is the statethe boundary rule that constrains where you can cut
  • Prefix cachingthe cache that compaction quietly invalidates
  • Agent context growththe problem it exists to solve
  • Three levers on the contextthe lever it represents, among three

What it costs

  • It fights prompt cachingan append-only prompt caches beautifully; rewriting near the front invalidates everything after it — cache hits fell from 70% to 57%, so the real saving is smaller than the token counts suggest
  • It is lossy by constructionwhat the summary is told to keep is the entire design — drop the failed approaches and the agent retries what it already ruled out
  • The summariser can be truncated silentlytwo of five summaries hit the output cap mid-sentence; the run still succeeded, which is what makes it dangerous
Building the harness

Error policy

recoverable, or terminal

Every failure an agent hits falls into one of two classes, and confusing them is expensive in both directions — too eager to recover gives an infinite loop, too eager to crash gives brittleness.

The problem

A harness that treats all failures as recoverable will retry a permanently broken environment forever. One did exactly that: 165 wasted steps and 17.1 million tokens after a sandbox hit its lifetime limit mid-run.

What it buys you

Separating the two classes turns that same failure into a single step, while still letting the model correct its own mistakes without human intervention.

How it works

Failures the model can fix — bad arguments, malformed JSON, an illegal move, a failing test — are formatted into the observation so it reads the error and corrects itself. Failures nothing can fix — the environment is gone — must terminate. On a transport error, probe whether the environment is alive before deciding which it is.

Error policy — recoverable, or terminalThe two classes of failure, and the 17-million-token lesson in confusing them.
Two classes of failure, and only two the model can fix this bad arguments · malformed JSON · unknown tool illegal move · failing test · non-zero exit → return it as an observation nothing can fix this the sandbox is gone · credentials revoked the environment no longer exists → terminate with a clear message A recoverable failure, handled correctly tokens burned
1 / 1
Say this

Graceful degradation without a termination condition is an expensive infinite loop. Every retry policy needs to separate what the model can fix from what nothing can fix.

Going deeper

A worked example — The 17-million-token retry loop

A chess game hit its 200-step limit having played only 22 moves. The other 165 steps produced nothing at all.

The sandbox had reached its 30-minute lifetime and been reclaimed mid-game. Every subsequent move returned a transport error — correctly formatted as a recoverable observation — and the model dutifully tried again, 165 times, at roughly 100k tokens of prompt each time.

The error policy had no notion of a failure the model cannot possibly fix. An illegal move is the model's problem; a dead environment is not.

Probe, then abort — and keep a backstop

On a transport failure, ask the environment whether it is still alive and abort with a clear message if it is not. Add a five-consecutive-failure backstop for the case the probe itself succeeds over a dead tunnel. Illegal moves never increment the counter, and any success resets it.

The two failure modes are symmetric

Too eager to recover gives an infinite loop billed by the token. Too eager to crash gives brittleness — a single flaky network call kills a run that would have succeeded on retry. Both are error-policy bugs; only the first one is expensive enough to notice immediately.

This is where checkpointing earns its keep

A deliberate abort is only cheap if the run can resume. Terminating on a dead sandbox and then starting the whole task over is not much better than retrying. The termination condition and the checkpoint are two halves of the same design.

How it connects

  • The agent loopwhere the policy is enforced
  • The prompt is the statewhy a failed call still returns a string
  • Agent state and checkpointingwhat has to survive a deliberate abort
  • Coding agent architecturefailed tests are the recoverable kind

What it costs

  • A health probe can itself lieso add a consecutive-failure backstop — five in a row aborts even if the probe says fine
  • Recoverable failures must not count toward itan illegal move is not evidence the world is broken, and any success resets the counter
  • Cost per retry is what makes it urgentat 100k tokens a retry, a policy gap stops being untidy and becomes a bill
Building the harness

Three levers on the context

truncation, compaction, disclosure

Three different things flood a context window, and each needs its own lever. Reaching for only one is why “just summarise the history” is an incomplete answer to how you run an agent for hours.

The problem

A single observation can swamp the prompt on its own; well-behaved observations still accumulate across forty steps; and the description of what the agent can do costs context in every request before any work happens.

What it buys you

Bounding all three keeps a long run inside the window and keeps capability count independent of prompt size.

How it works

Truncation caps one result — keeping the head and the tail, because the end of a stack trace is the part that identifies the failure. Compaction bounds their accumulation. Progressive disclosure advertises each capability as a name plus one line and loads the full instructions only when the model asks — a two-line catalogue entry against a 4,771-character body.

Three levers on the context budgetTruncation, compaction and progressive disclosure answer different questions.
Three levers, three different questions 1 · one observation is enormous a cat of a large file, a full stack trace, a browser page 120,000 characters in one tool result first 4,900 … elided … last 4,900 2 · many small observations pile up each one is reasonable; forty of them are not 3 · describing what the agent can do is itself expensive ten capabilities, each with a page of instructions catalogue — name + one line each two lines in every prompt the full body 4,771 characters — loaded only when asked for
1 / 1
Say this

Truncation bounds one observation, compaction bounds their accumulation, progressive disclosure bounds what you carry before work starts.

Going deeper

A worked example — A two-line catalogue against a 4,771-character body

A skill is a folder with a SKILL.md whose frontmatter gives a name and a description. Loading it splits the file in two.

The catalogue entry — two lines — goes into the system prompt on every request. The full body, 4,771 characters of strategy, is revealed only when the model calls invoke_skill.

Ten skills therefore cost ten lines of prompt, not ten documents. And with no skills loaded, the agent is never told about the submission protocol at all — which is exactly what the tests check.

Head-and-tail, not head

Truncating a 120,000-character result to its first 10,000 throws away the end of the stack trace — usually the line that names the failure. Keeping the first and last 4,900 with an elision notice between them costs the same tokens and keeps the part that identifies the problem.

The same reasoning applies to tool descriptions

Progressive disclosure is usually discussed for skills or documents, but a tool schema with a long description is the same cost in the same place. Anything carried in every request to describe a capability the agent might not use is a candidate for the split.

Which lever to reach for, diagnostically

If a single step blew up the prompt, you need truncation. If the prompt crept up over forty steps, you need compaction. If the prompt was already large before the agent did anything, you need progressive disclosure. The symptom tells you which.

How it connects

  • Context compactionthe middle lever, in detail
  • Agent context growththe pressure all three respond to
  • MCPtool descriptions have the same problem
  • The prompt is the statethe list all of this is trimming

What it costs

  • Truncation can cut the part that matteredhead-and-tail is a heuristic, not a guarantee
  • Progressive disclosure costs a round tripthe model must ask before it can act on the full instructions
  • They are complementary, not alternativesnaming all three is what separates a real answer from “summarise the history”
Building the harness

Programmatic tool calling

code that composes the tools

Instead of emitting one tool call per turn, the model writes a snippet that calls the tools as ordinary functions, and the harness runs it somewhere isolated. The unit of work stops being a round trip.

The problem

One call per response caps how much a single step can do — which caps how deep a search or how long a procedure the agent can execute at all. A two-ply search over twenty candidates is 21 round trips, each resending the whole transcript.

What it buys you

Collapsing those 21 round trips into one step. Loops, branching and search stop costing a model call each, so procedures too long to spell out as individual calls become routine.

How it works

The tools already exist as functions inside the sandbox. The model sends code — in the measured run, base64-encoded Python — that calls them in a loop, evaluates the results, and commits only the final choice as its last statement. The tools ran 21 times; the model was called once.

Programmatic tool callingWhen the model writes code that calls the tools, the unit of work stops being a round trip.
One tool call per round trip model simulate_move result model simulate_move result model simulate_move result model simulate_move result model simulate_move result model simulate_move result model simulate_move result whole prompt resent one step · run_pythonbest = Nonefor mv in board.legal_moves(): after = simulate_move(mv) for reply in after.legal_moves(): score = evaluate(simulate_move(reply)) … keep the worst case for this mv … if better(mv): best = mvplay_move(best) 21 round trips → 1 step the tools ran 21 times; the model was called once you are now running model-written code it belongs in the sandbox, beside the tools — never in the agent process
1 / 1
Say this

It changes the unit of work from a call to a procedure — the difference between an agent that takes twenty actions and one that runs an algorithm.

Going deeper

A worked example — Twenty-one round trips, or one step

A two-ply search over twenty candidate moves needs twenty simulate_move calls and one play_move. As individual tool calls that is 21 round trips, each a full model call resending the entire transcript.

As a snippet it is one step: the code loops over the candidates, calls simulate_move inside the sandbox where it is just a function, keeps the best line, and calls play_move exactly once as its last statement.

The tools still ran 21 times. The model was called once.

It raises the ceiling, not just the speed

The saving in round trips is the obvious part. The less obvious part is that procedures too long to spell out as individual calls become possible at all — a deeper search, a retry loop, a multi-stage transformation. The unit of work stops being the thing that limits the algorithm.

Re-read the world afterwards

A snippet may have changed live state several times before returning. The harness cannot assume its cached picture is still accurate, so after every snippet it re-reads the environment. Skipping this is exactly the stale-state bug, just at procedure scale.

The debuggability cost is real

A sequence of individual tool calls leaves a readable trail: you can see which call failed and what it returned. A failed snippet gives you one opaque observation. That is a genuine argument for keeping ordinary tool calls as the default and reaching for code only when the work is loop-shaped.

How it connects

  • The agent loopthe step this compresses
  • The prompt is the stateeach round trip resends everything, which is what makes them expensive
  • Coding agent architecturethe same idea, applied to a repository
  • Error policysnippet failures are recoverable, sandbox death is not

What it costs

  • You are executing model-written codeit belongs in the sandbox beside the tools, never in the agent process
  • The world may have movedafter a snippet runs, the harness must re-read state — the snippet may already have changed it
  • Harder to inspecta failed procedure gives you one opaque observation instead of a readable trail of calls
The problem

Sparse attention, as a kernel

what you are handed, and why it is hard

DeepSeek Sparse Attention picks the 2,048 most relevant historical positions with a lightweight indexer, then attends only to those. The indexer is someone else's problem. What lands on the kernel is an arbitrary list of indices — and that is where the difficulty starts.

The problem

Cutting 100,000 tokens down to 2,048 removes almost all the arithmetic, but the 2,048 survivors are scattered through the KV cache. The kernel needs KV[17], KV[831], KV[9201], KV[45002] — not a slice. Every gather can land on a different HBM address, so nothing streams.

What it buys you

Naming this correctly is the whole setup: sparse attention trades a compute problem for a memory-access problem. Once you have said that, every optimisation that follows has an obvious motive.

How it works

The kernel receives Q for T query tokens plus an index list per query. For each tile of indices it gathers the corresponding K and V into shared memory, runs QK on Tensor Cores, updates an online softmax, accumulates PV, and moves on. Gather does not make the source contiguous — it pulls the currently needed subset on chip, and pays the scatter again next tile.

What the kernel is actually givenTop-K selection happens upstream; the scattered indices are the problem it inherits.
full KV cache e.g. 100,000 tokens Lightning Indexer picks the top-K positions K = 2048 positions [17, 831, 9201, 45002, …] not 0…2047 — scattered our kernel QK → softmax → PV over those 2,048 entries the project scope What a gather actually does HBM stays scattered every one of the 2,048 reads can hit a different address no contiguous-load trick applies a small on-chip tile the needed subset, staged into shared memory, contiguous there now Tensor Cores can run normally the layout is not fixed only the working set is moved
1 / 1
Say this

Sparse attention does not make attention easy — it converts an arithmetic problem into an irregular-memory problem. Everything after that is a response to the scatter.

Going deeper

A worked example — What K = 2,048 does and does not mean

A common misreading: K = 2048 does not mean the context is 2,048 tokens long. It means each query receives 2,048 selected historical KV positions out of however many exist.

The context can be 100,000 tokens. The Lightning Indexer scores all of them and keeps 2,048. Our kernel then does attention over those — and the indices it gets look like [17, 831, 9201, 45002, 103, …], not KV[0:2048].

That difference — a set of indices rather than a slice — is the entire difficulty of the kernel.

Is a “query” the user's question?

No, and the confusion matters. A query here is an attention query vector for one token: the model projects a token's hidden state into a query and uses it to decide which past information should flow in. So T = 8 means the kernel is processing eight query tokens — not eight user requests. That is why the benchmark's tiny T is a hardware problem rather than a product decision.

Did the kernel choose the 2,048 tokens?

No. The kernel assumes the sparse indices already exist; selection happens upstream in the indexer. Saying this clearly scopes the work: everything we did is about performing attention over an arbitrary scattered index list efficiently, not about deciding which tokens are relevant.

Does gathering make the entries contiguous?

Only on chip, and only for the current tile. The source stays scattered in HBM forever — gather pulls the currently needed subset into a small shared-memory tile where the layout is regular enough for Tensor Cores, and then pays the scatter again for the next tile. Which is why hiding the gather (cp.async) and having more warps to hide it behind (Split-K) are both worth doing.

QK, weights, PV — the vocabulary

QKᵀ gives attention scores; softmax(QKᵀ) gives attention weights; softmax(QKᵀ)V is the attention output. The mental model worth carrying: QK decides where to look, PV collects the information.

How it connects

  • FlashAttentionthe same tile-and-fuse idea, here over a scattered index list
  • Full vs linear attentiona different way to avoid reading every token
  • The B200, and the one rulethe machine this has to run on
  • KV cachewhat the indices point into

What it costs

  • Fewer FLOPs, worse access patterndense attention streams; sparse attention random-accesses, and bandwidth utilisation collapses
  • Correctness is boundedthe competition required matching the BF16 reference within abs_err 0.1, which rules out the cheap low-precision shortcuts
  • The indexer is a separate costthe kernel is only fast in the context of a pipeline that already paid to choose the indices
The problem

The B200, and the one rule

148 SMs, and a block that cannot move

You cannot reason about this kernel without a hardware model. The one that matters here is small: a GPU is 148 independent SMs, a block is assigned to exactly one of them, and shared memory belongs to the SM.

The problem

Performance intuition usually stops at “the GPU is parallel”. That is not enough to explain why a perfectly good kernel ran at 5% utilisation — for that you need to know what the scheduler can and cannot do with the blocks you launch.

What it buys you

One fact does most of the work: a CUDA block is assigned to one SM and stays there. Its threads share one shared-memory allocation and one __syncthreads() domain, so the hardware cannot lend half a block to an idle neighbour. Launch 8 blocks on 148 SMs and 140 SMs idle — permanently.

How it works

HBM and L2 are GPU-wide. Each SM has warp schedulers, CUDA cores, Tensor Cores, a register file, and up to 228 KB of shared memory carved into a private region per resident block. A warp is 32 threads scheduled together; 128 threads is 4 warps, 256 is 8, 512 is 16. More resident warps means more chances that something is ready to issue while another warp waits on memory.

The B200, and the one rule that mattersHBM, 148 SMs, and why a block can never move between them.
NVIDIA B200 · Blackwell HBM — global memory, large and slow L2 cache — shared by every SM 148 streaming multiprocessors inside one SM warp schedulers pick which warp issues next CUDA cores scalar arithmetic Tensor Cores 16×16×16 matrix MMA registers private to each thread shared memory up to 228 KB · carved per block grid the whole launch→ block lands on ONE SM→ warp 32 threads, scheduled together→ thread one lane
1 / 1
Say this

A block never leaves its SM. That single sentence explains why the grid shape is a design decision and not an implementation detail.

The memory hierarchy, in one picture

Who owns which memorythe classic thread · block · grid diagram, with B200 numbers on it
EXECUTIONMEMORYhover any level to see what it ownsthreadone instruction streamthread blocke.g. 256 threads = 8 warpsblock 0block 1block 2block 3gridevery block of one launchblock 0placed on one SM — and stays therethread 0registersprivatethread 1registersprivatethread 255registersprivate…shared memoryseen by all 256 threads of block 0 · its slice of the SM’s 228 KBblock 1256threadssharedits ownblock N256threadssharedits own…L2 cache · 126 MB · shared by all 148 SMs · managed by hardware, never addressed directlyglobal memory · HBM3eup to 180 GB at ~8 TB/s · every thread of every block can read and write it · outlives the kernel

Read it as ownership: a thread owns its registers, a block owns its slice of shared memory, and the whole grid shares L2 and global memory. Each step down is slower, larger, and visible to more threads — which is why fast kernels stage data from global memory into shared memory once and reuse it from there. Block 0 cannot see block 1’s shared memory: ordinarily blocks only meet in global memory (thread block clusters are the exception — see “Going deeper”).

MemoryWho can see itLives as long asOn a B200Rough cost
Registersone threadthe thread64K × 32-bit per SM (256 KB), at most 255 per threadfastest — read directly as instruction operands
Local memoryone threadthe threadregister spills and large per-thread arrays, physically in global memoryas slow as global (cached in L1/L2) — “local” means private, not close
Shared memoryevery thread in one blockthe block228 KB per SM, up to 227 KB for one block; shares a 256 KB pool with L1on-chip, tens of cycles
L2 cacheevery SM on the GPUmanaged by hardware126 MBa few hundred cycles (~150 ns measured)
Global memory (HBM3e)every thread of every block, and the host via copiesuntil freed — survives across kernel launchesup to 180 GB, ~8 TB/sthe slowest — what every optimisation tries to touch less

Sizes from NVIDIA’s Blackwell Tuning Guide (compute capability 10.0) and the DGX B200 datasheet; L2 latency from Chips and Cheese’s B200 measurements. Cycle counts are orders of magnitude, not specs.

Going deeper

A worked example — Eight blocks on a hundred and forty-eight processors

The benchmark launches with T = 8 query tokens and one block per token. The scheduler places those eight blocks on eight SMs. There are 148.

Nothing is wrong with the code. The GPU is not confused. It simply has no mechanism to help — a block cannot be divided and migrated, so the remaining 140 SMs have no work to take.

8 / 148 ≈ 5%. Every microsecond of optimisation inside the block is being applied to 5% of the machine.

Why a block cannot span SMs

The threads in a block need a single shared-memory allocation and a single __syncthreads() barrier domain. Both are SM-local resources. Splitting a block across two SMs would mean shared memory that spans two physical memories and a barrier across two schedulers — which is precisely the cost the block abstraction exists to avoid. The indivisibility is the price of shared memory being fast.

Shared memory is per-SM, not per-GPU

Each SM has its own shared-memory resource (up to 228 KB on B200), and CUDA carves a private region of it for every resident block. Ordinarily blocks never see each other's (clusters, below, are the one exception). The practical consequence shows up everywhere in this project: more shared memory per block means fewer blocks resident per SM, which means fewer warps available to hide a stall.

Why more warps hide memory latency

A warp that issues a scattered load and misses cache stalls for hundreds of cycles. If another warp is resident and ready, the scheduler issues from that one instead and the execution units stay busy. With regular, coalesced access this matters less; with a gather from arbitrary HBM addresses it matters a lot — which is why the final split kernel doubled its warp count.

The exception: thread block clusters

Since Hopper, blocks can be grouped into a cluster that the hardware guarantees to schedule together on neighbouring SMs. Blocks in a cluster can read and write each other's shared memory directly — distributed shared memory — instead of round-tripping through global memory. On B200 the portable cluster size is 8 blocks, with 16 available as a non-portable option. It refines the picture rather than overturning it: each block still lives on exactly one SM; the cluster just lets neighbours reach across.

Local memory is not local

When a thread needs more registers than it is allowed (at most 255), or indexes a per-thread array dynamically, the compiler spills to “local memory” — which is private to the thread but physically lives in global memory, cached in L1/L2. A spill-heavy kernel quietly turns its fastest storage into its slowest. nvcc -Xptxas -v reports spill bytes; it is worth checking whenever a kernel is mysteriously slow.

Occupancy is a means, not a metric

It is tempting to treat high occupancy as the goal. It is not — it is one way to buy latency hiding. A kernel that is genuinely compute-bound gains nothing from more resident warps, and a kernel that spends shared memory on deeper buffering may be better off with fewer. The question is always which resource is actually idle.

How it connects

  • Split-Kthe optimisation that exists because a block cannot span SMs
  • cp.async and double bufferingshared memory is the resource it spends
  • Sparse attention, as a kernelthe workload landing on this machine
  • Tensor, pipeline and expert parallelismthe same question one level up — where to cut the work

What it costs

  • Shared memory buys occupancy or buys buffering, not bothmore per block means fewer resident blocks per SM
  • Occupancy is not the goalit is a means to latency hiding; a kernel that is compute-bound gains nothing from more warps
  • The rule is a constraint, not a bugindivisible blocks are what make shared memory and __syncthreads() cheap in the first place
Making it fast

WMMA and Tensor Cores

v1 · make the arithmetic fast

The first version moved QK and PV off the scalar CUDA cores and onto the Tensor Cores through WMMA. It was a real speedup, and it did not touch the actual bottleneck — which is the more interesting half of the story.

The problem

Attention is two matrix multiplies with a softmax between them. Written as scalar fused multiply-adds, the hardware spends its issue slots and register bandwidth on bookkeeping rather than on arithmetic.

What it buys you

Tensor Cores are fixed-function hardware for exactly one shape of work: a small matrix multiply-accumulate. One instruction does a whole tile, so the arithmetic stops being the thing you are paying for.

How it works

WMMA — Warp Matrix Multiply Accumulate — lets one warp cooperatively issue an MMA. Here: m16n16k16 tiles in BF16, used for both QK and PV, with FP32 accumulation so the running sums stay stable. Low precision in, higher precision in the accumulator, which is the standard shape.

WMMA — moving the math onto Tensor CoresThe right optimization for the wrong bottleneck, and what that taught.
Scalar arithmetic on CUDA cores a0*b0 + c0a1*b1 + c1a2*b2 + c2a3*b3 + c3a4*b4 + c4a5*b5 + c5one multiply-add per instruction, per lane thousands of instructions per tile WMMA · Warp Matrix Multiply-Accumulate16×16×16 one warp-level instruction on dedicated silicon a whole 16×16×16 MMA at once How the kernel tiles it D = 512 compressed-KV dimension → 32 WMMA tiles of 16 D_pe = 64 positional dimension → 4 tiles both matmuls move onto Tensor Cores QK: Q @ Kᵀ + Q_pe @ K_peᵀ PV: P @ V BF16 in, FP32 accumulate fewer bits moved, far higher Tensor Core throughput accumulating wide keeps the numerics stable but it solved the wrong bottleneck arithmetic got faster; the scattered gather did not v1 · WMMA only · ~600 µs
1 / 1
Say this

Tensor Cores made the arithmetic fast. That mattered, but the kernel was never arithmetic-bound — which is exactly why v1 was still 600 microseconds.

Going deeper

A worked example — One instruction instead of a loop

A 16×16×16 matrix multiply-accumulate written with scalar arithmetic is thousands of fused multiply-adds, each costing an issue slot and register-file traffic.

WMMA m16n16k16 BF16 hands that whole tile to dedicated hardware: one cooperative instruction per warp, operands in BF16, accumulation in FP32.

We used it for both matmuls in attention — QK and PV — which is what made the arithmetic stop being the expensive part. And then the kernel was still 600 microseconds, because the arithmetic was never the expensive part.

Why BF16 in, FP32 out

Fewer bits per number means less data movement and much higher Tensor Core throughput, but accumulating thousands of products in low precision loses bits fast. The standard shape — low-precision multiply, higher-precision accumulate — keeps the throughput while holding the running sum stable. It is the same reason FlashAttention accumulates its softmax state in FP32.

The correctness bound is what limits precision tricks

The competition required matching the BF16 reference within abs_err under 0.1. That is a hard wall: it rules out FP8 shortcuts and approximate softmaxes. Worth mentioning unprompted — it turns “we used BF16” from a buzzword into a decision made inside a constraint.

WMMA is not FlashAttention

They solve different bottlenecks and get conflated constantly. WMMA maps matrix math onto Tensor Cores. FlashAttention-style tiling stops intermediate score and probability matrices from being written to HBM at all. You can have either without the other; this kernel has both, for different reasons.

Why fixing the wrong bottleneck first was still right

It is easy to call v1 wasted effort. It was not — until the arithmetic was fast, you could not see that memory was the limit, and the measurement that showed 600 µs after a large compute speedup is exactly the evidence that pointed at the gather. Optimisation is mostly a sequence of measurements that relocate the bottleneck.

How it connects

  • The B200, and the one rulethe units this targets
  • cp.async and double bufferingthe next bottleneck, once compute is fast
  • FlashAttentiona separate idea often confused with this one
  • Sparse attention, as a kernelthe operation being accelerated

What it costs

  • It fixed the wrong bottleneck firstv1 still ran ~600 µs, because the scattered gather was untouched
  • Precision is a budget, not a free levermatching the BF16 reference within abs_err 0.1 is what ruled out more aggressive quantisation
  • WMMA is not FlashAttentionone maps math onto hardware, the other removes memory round-trips — conflating them is a common interview tell
Making it fast

cp.async and double buffering

v2 · stop waiting for the gather

With the arithmetic fast, the kernel spent its time waiting for scattered KV entries to arrive. Double buffering does not move fewer bytes — it stops the Tensor Cores from standing still while the bytes move.

The problem

One shared-memory buffer forces a strict alternation: load a tile, compute on it, load the next, compute on that. The DMA path and the Tensor Cores take turns, and each is idle while the other works.

What it buys you

They are different hardware, so they can run at the same time. Two buffers let the gather for tile B be issued before tile A has been consumed, and the stage cost drops from the sum of the two to roughly the larger of the two.

How it works

cp.async issues an asynchronous global→shared copy that does not block the issuing warp. With buffer A and buffer B, the loop computes on one while filling the other, then swaps. Serially, T = T_load + T_compute; pipelined, T ≈ max(T_load, T_compute).

cp.async and double bufferingTurning load-plus-compute into load-or-compute, and why that was not enough.
Synchronous: load, then compute, then load load (HBM gather)compute (Tensor Cores)time → wall clock two shared-memory buffers buffer A buffer B tensor cores read one while the copy engine fills the other cp.async.cg.shared.global the async copy engine runs independently of the SM pipeline so a gather in flight does not stall the math .cg bypasses L1 — scattered reads would only thrash it — and goes to L2 the arithmetic of overlap serial: T = T_load + T_compute pipelined: T ≈ max(T_load, T_compute) the data still moves — the waiting is what disappears you can only hide as much as you have to hide it behind load = 10 · compute = 2 → hides 2, leaves 8 memory is still the bottleneck afterwards v1 ~600 µs → v2 ~470 µs · real, but not the breakthrough
1 / 1
Say this

Double buffering hides latency; it does not remove traffic. The 130 microseconds it bought are exactly the amount of load that happened to fit behind the compute.

Going deeper

A worked example — Ten units becomes five

With one buffer, the loop alternates: load A, compute A, load B, compute B. Four tiles at one unit each costs eight units, and one of the two pieces of hardware is idle the whole time.

With two buffers, the load for tile B is issued while tile A is being consumed. The loads occupy one track, the computes occupy another, and the total is the length of the longer track plus one stage of fill — five units instead of eight.

Same bytes. Same FLOPs. The difference is that the DMA engine and the Tensor Cores are different hardware, and they were taking turns for no reason.

Why the win was 130 microseconds and not 300

In steady state the stage cost is max(T_load, T_compute), not the average. If the gather costs 10 and the compute costs 2, you hide 2 and the other 8 are still on the critical path. The measured 600 → 470 µs says the gather was much larger than the compute — which is the diagnostic that pointed at the next optimisation.

Why not three or four buffers

Deeper pipelining is possible, but each stage costs shared memory per block, and shared memory caps how many blocks stay resident per SM. Past a point you are spending occupancy — which is itself a latency-hiding mechanism — to buy more overlap. The two levers compete for the same budget.

cp.async is asynchronous, which is the whole trick

An ordinary global load occupies the issuing warp until it returns. cp.async issues a global→shared copy that the warp does not wait on, so the warp can continue issuing MMA work and synchronise on the copy later. Without the async form, two buffers would buy you nothing — the warp would still be blocked.

When pipelining is the wrong lever entirely

If the kernel is saturating memory bandwidth, overlap does not help — there is no idle transfer capacity to fill, and the answer is to move fewer bytes. Here the problem was latency on scattered accesses rather than raw bandwidth, which is why hiding it worked at all.

How it connects

  • WMMA and Tensor Coresthe version this builds on
  • The B200, and the one ruleshared memory is the budget being spent
  • Split-Kthe bottleneck this could not reach
  • FlashAttentiontiling and fusion, the same family of idea

What it costs

  • Bounded by the larger sideif the load is 10 and the compute is 2, you hide 2 — 600 µs to 470 µs, not 600 to 300
  • More buffers cost occupancyeach extra stage is more shared memory per block, so fewer blocks stay resident — you buy latency hiding with the occupancy that was doing the hiding
  • It does not reduce traffic at allif you are actually bandwidth-limited, pipelining is the wrong lever
Making it fast

Split-K

v3 · the change that actually mattered

The benchmark ran eight query tokens on a 148-SM GPU. One block per query means eight blocks, which means eight SMs. No amount of making a block faster fixes a grid that small — you have to change the decomposition.

The problem

With T = 8 and one block per query token, the launch produced 8 blocks. A block cannot span SMs, so about 5% of the GPU had work and the other 140 SMs sat idle for the whole kernel.

What it buys you

Splitting each query's 2,048 entries across S blocks turns 8 blocks into T × S blocks. At S = 32 that is 256 blocks — enough independent work that every SM has something to run, and a queue behind it. This was the single largest jump in the project: 470 µs → 95 µs.

How it works

Each query's index list is cut into S chunks of 2048 / S entries. Each block processes one chunk and writes a partial online-softmax state — (M, LSE, O) — to scratch. A second reduce kernel merges the S partials per query into the final output.

Split-K — filling a 148-SM chip with eight tokensThe decomposition change that took the kernel from 470 µs to 95 µs.
One block per query token 148 SMs on the B200 SM utilisation T = 8 tokens · 1 block each each block walks all 2,048 selected KV entries grid = 8 blocks 140 SMs have nothing to do split the K-loop into S = 32 chunks 2,048 / 32 = 64 KV entries per block grid = T × S = 8 × 32 = 256 blocks enough independent work to fill the chip each block produces a partial online-softmax state (M, LSE, O) — local max, local normaliser, local weighted V — written to a scratch buffer a second reduce kernel merges the S partials per token
1 / 1
Say this

Split-K is a change of parallel decomposition, not a memory trick. The question it answers is ‘how do I get enough independent blocks to fill 148 SMs’, and the answer costs a reduction.

Going deeper

A worked example — Cutting the index list, not the matrix

Before: one block per query token owns all 2,048 selected KV entries and tiles through them. T = 8 gives 8 blocks.

After: each query's 2,048 entries are cut into S = 32 chunks of 64. Each chunk gets its own block. T × S = 8 × 32 = 256 blocks.

Every block does less work, more blocks exist, and the grid finally exceeds the SM count. 470 µs → 95 µs — the largest single step in the project, and it came from changing the shape of the launch rather than the contents of the kernel.

The misreading to avoid: it is not about fitting

Split-K is often explained as “the working set doesn't fit in shared memory, so split it”. That is not what is happening here. A single block could already stream all 2,048 entries with tiling and double buffering — nothing overflowed. The purpose is to expose more independent block-level parallelism. Getting this right is the difference between reciting the name and understanding the decision.

It adds work and still wins

Split-K creates S partial states per query, writes them to global scratch, and requires a second kernel to read them back and merge. Total work goes up. Wall-clock latency goes down, because far more of the machine is computing simultaneously. Latency and throughput are different quantities and this is the cleanest example of them diverging.

Which is why S has an optimum

More splits means more parallelism and more reduction overhead. At small S the GPU is underfilled; at large S the reduce kernel dominates and each block's tile is too small to amortise its own setup. Somewhere in between is the minimum, and where it sits depends on the workload — hence v4.

The same instinct appears at cluster scale

“The work does not divide into enough independent pieces to fill the hardware” is the same complaint as a pipeline bubble or an unbalanced tensor-parallel shard. The fix is always to re-cut the problem along a different axis and pay a combination cost. Here the axis is the KV index list and the cost is an online-softmax reduction.

How it connects

  • The B200, and the one rulethe indivisible-block rule that makes this necessary
  • Merging the splitswhat makes splitting mathematically legal
  • Autotuning the split factorchoosing S, which turns out not to be a formula
  • Tensor, pipeline and expert parallelismthe same instinct at cluster scale — re-cut the work to fit the machine

What it costs

  • It adds work rather than removing itS partial states per query, scratch traffic to write them, and a whole second kernel to read them
  • Wall-clock still fallsbecause far more of the machine computes at once — latency and total work are different quantities
  • So S must be tuned, not maximisedpast some point the reduction overhead eats the parallelism gain
  • It is not about fitting in shared memoryone block could already tile through 2,048 entries — saying otherwise is the most common misreading of Split-K
Making it correct, then tuned

Merging the splits

the state that makes Split-K legal

Once 32 blocks each see a slice of the same query's scores, something has to reassemble one softmax out of them. You cannot normalise the slices independently — and the fix is the same running state FlashAttention uses.

The problem

Softmax is not additive over a partition. Chunk A with scores [10, 9] and chunk B with scores [1, 0] both normalise to roughly [0.73, 0.27], even though globally B should contribute almost nothing. Local normalisation destroys exactly the information that says so.

What it buys you

Keeping sufficient statistics instead of a normalised answer lets independently-computed chunks be merged into the exact global result — not an approximation.

How it works

Each block emits mᵢ = max(scores), lᵢ = Σ exp(s − mᵢ) and oᵢ = Σ exp(s − mᵢ)·V. The reduce kernel takes m = max mᵢ, rescales every partial onto it with exp(mᵢ − m), sums, and divides: O = o / l. Exact, and order-independent.

Online softmax — what makes Split-K legalWhy you cannot softmax the pieces, and the three numbers that let you merge them anyway.
Two chunks of the same row of scores chunk A scores = [10, 9] local softmax → [0.73, 0.27] chunk B scores = [1, 0] local softmax → [0.73, 0.27] both chunks normalised to the same weights — but globally they are not equal scores of 1 and 0 should contribute almost nothing next to 10 and 9 softmax(A ∪ B) ≠ softmax(A) + softmax(B) So each chunk keeps sufficient statistics instead M the local maximum score mᵢ = max(scoresᵢ) LSE the local normaliser lᵢ = ∑ exp(score − mᵢ) O the local weighted V sum oᵢ = ∑ exp(score − mᵢ)·V the reduce kernel merges them m = maxᵢ mᵢ l = ∑ᵢ exp(mᵢ − m)·lᵢ o = ∑ᵢ exp(mᵢ − m)·oᵢ O = o / l rescale to a common maximum, then combine the same idea as Flash- Attention never hold all scores at once exact not an approxi- mation bitwise the right answer
1 / 1
Say this

Online softmax is what makes it legal to look at attention scores in pieces — whether the pieces are tiles inside one block, or blocks spread across the GPU.

Going deeper

A worked example — Why you cannot average two softmaxes

Chunk A has scores [10, 9]. Chunk B has scores [1, 0]. Normalise each on its own and both give roughly [0.73, 0.27].

But globally, a score of 1 next to a score of 10 should contribute essentially nothing. Adding the local softmaxes gives chunk B the same total weight as chunk A — a completely different answer from the true attention.

The information that would have said so — how big were your scores compared to everyone else's — was thrown away by the local normalisation. So you keep it instead: the maximum, the un-normalised sum, and the un-normalised output.

The three numbers, and why they are sufficient

mᵢ is the chunk's maximum score, lᵢ = Σ exp(s − mᵢ) is its un-normalised denominator, and oᵢ = Σ exp(s − mᵢ)V its un-normalised output. Rescaling by exp(mᵢ − m) onto a common maximum m converts any chunk's statistics into the global frame, so the merge is exact and does not depend on the order chunks arrive in.

Subtracting the max is not cosmetic

It is what keeps exp from overflowing. Attention scores can be large; exp(score) in FP32 overflows well before the scores become unusual. Carrying a running max and exponentiating only differences is the numerically stable form — the same reason single-kernel softmax implementations do a max pass first.

The same state, two different granularities

FlashAttention carries (m, l, o) from tile to tile inside one block so the S×S score matrix never reaches HBM. Split-K carries the identical state from block to block so 32 processors can reconstruct one softmax. Recognising that these are the same trick at different scales is a good thing to say out loud.

Exactness is a requirement here, not a preference

The correctness bar was matching the BF16 reference within abs_err 0.1. An approximate merge — averaging, or dropping small chunks — would have failed outright. Split-K is only available as an optimisation because an exact merge exists.

How it connects

  • Split-Kthe decomposition this makes possible
  • FlashAttentionthe same state, carried tile to tile instead of block to block
  • Self-attentionthe softmax being reconstructed
  • Autotuning the split factorthe kernel that runs this, and got faster in v4

What it costs

  • It is exact, which is the pointan approximate merge would have failed the abs_err 0.1 correctness bar outright
  • But it is not freeS partial states per query must be written to and read from global scratch
  • The reduction scales with Smore splits means more to merge — the other half of why S has an optimum
Making it correct, then tuned

Autotuning the split factor

v4 · why a formula could not do it

Once the shape of the kernel was right, the remaining question was what value of S to use. v3 guessed with a heuristic. v4 stopped guessing and measured — and the measurements did not agree with the heuristic.

The problem

v3 chose S ≈ 2·SMs / T, aiming for roughly 256 blocks on the theory that block count is what fills the GPU. But two configurations can produce the same grid and completely different per-block work: T=8, S=32 is 64 KV per block; T=32, S=8 is 256 KV per block.

What it buys you

Benchmarking the candidates picks the configuration that is actually fastest for this workload, and the winner moves between workloads. Together with the warp changes: 95 µs → 57 µs.

How it works

Sweep the valid powers of two that divide TOPK / BLOCK_K, benchmark each, keep the fastest. Alongside it, two warp-configuration fixes: the reduce kernel went from 4 warps covering 16 heads in four serial passes to 16 warps, one per head, in one pass; the split kernel went from 4 warps to 8, so a warp stalled on a scattered read is covered by another.

Autotuning the split factorWhy a formula that predicts block count cannot predict performance.
S — how many chunks each query’s 2,048 entries are split into S = 1 2048 / block S = 2 1024 S = 4 512 S = 8 256 S = 16 128 S = 32 64 S = 64 32 v3 used one heuristic S ≈ 2 · SMs / T aim for roughly 256 blocks and hope that the block count is what matters but block count is not the whole story T=8, S=32 → 64 KV/block T=32, S=8 → 256 KV/block both are 256 blocks · completely different work per block changing S changes far more than the grid size memory traffic per block · execution time per block · register pressure shared-memory pressure · reduction cost · scheduling behaviour no single formula captures all six at once Two more changes in the same version reduce kernel: one warp per head v3 · 4 warps iterating 16 heads → 4 serial passes v4 · 16 warps (512 threads) → one pass split kernel: 4 warps → 8 warps more resident warps per block (128 → 256 threads) so a warp stalled on a scattered read is covered by another
1 / 1
Say this

Block count alone does not determine performance. Changing S changes memory traffic, execution time, register and shared-memory pressure, and reduction cost all at once — no single formula captures six things, so you measure.

Going deeper

A worked example — Two configurations, same grid, different machine

v3 picked S with a heuristic: S ≈ 2·SMs / T, aiming for roughly 256 blocks. The reasoning is that block count is what fills the GPU.

But T = 8, S = 32 gives 64 KV entries per block, and T = 32, S = 8 gives 256. Both are 256 blocks. They differ in memory traffic per block, execution time per block, register pressure, shared-memory pressure, reduction cost and scheduling behaviour.

No single formula captures six things at once — so v4 sweeps the valid powers of two that divide TOPK / BLOCK_K, benchmarks each, and keeps the winner. It is frequently not where the heuristic pointed.

The two warp changes that came with it

The reduce kernel ran 4 warps over 16 heads — four serial passes. v4 uses 16 warps (512 threads), one per head, in a single pass. The split kernel went from 4 warps (128 threads) to 8 (256), so more independent warps are resident to cover a stalled scattered read. Neither is clever; both are about not leaving units idle.

Autotuning finds the winner without explaining it

A sweep tells you which configuration is fastest, not why. That is fine for shipping and unsatisfying for understanding — the honest answer in an interview is that the measurement decides and the profiler explains. If you can say which resource the winning configuration stopped wasting, you are ahead of the sweep.

Where autotuning stops being practical

It costs compilation and benchmarking time, which is acceptable offline and awkward when shapes arrive at runtime. The usual production compromise is a cached table keyed by shape, with a heuristic as the fallback for anything unseen — which is roughly what v3 was, used correctly.

Measured beats predicted, repeatedly

It is the same lesson as least-outstanding-requests beating round-robin in a load balancer: a model of the system is a simplification, and where the simplification is wrong the measurement is not. Reaching for a formula is the right instinct; refusing to check it against the hardware is not.

How it connects

  • Split-Kthe parameter being tuned
  • Merging the splitswhat the reduce kernel is computing
  • The B200, and the one rulewarps and occupancy, the thing being traded
  • LLM-aware load balancingthe same idea — measured behaviour beats a predictive formula

What it costs

  • Tuning time is real timea sweep costs compilation and benchmarking, which is fine offline and awkward if shapes change at runtime
  • It is workload-specific by constructionthe tuned value is a fact about one shape, not a law
  • It explains, but does not predictautotuning finds the winner without telling you why — profiling is still what turns a number into understanding
From a neuron to a network

The perceptron

a neuron is a weighted vote, and a vote is a line

The smallest possible learner: one neuron, one line. Everything in deep learning is this unit, repeated and stacked — and the one thing it cannot do, XOR, is the reason networks have hidden layers at all.

The problem

You have labelled points and want a rule that classifies a new one. The rule has to be learned from the data, not written by hand, and it has to be something a machine can adjust.

What it buys you

A weighted sum plus a threshold draws a line through the input space, and a one-line update rule moves that line toward every mistake until none remain — guaranteed, as long as a separating line exists.

How it works

Compute s = w·x + b and fire if s > 0. On a mistake with true label t, set w ← w + t·x and b ← b + t: adding a point to w pulls the line toward it, subtracting pushes it away. A second layer of neurons bends the space so that XOR, impossible for one line, becomes separable.

The perceptron — a neuron is a weighted vote, and a vote is a lineOne neuron learns a line from its mistakes; XOR shows why you need a hidden layer.
x₁x₂class +1class −1(0,0)(1,1)(1,0)(0,1)(0,0) ← (0,0)(1,0) ← (1,0) and (0,1)(2,1) ← (1,1)h₁ − 2h₂ = 0.5h₁h₂x₁x₂w₁w₂Σ+ bstep: s > 0 ?±1s = w₁x₁ + w₂x₂ + bx₁x₂h₁h₂outh₁ = ReLU(x₁ + x₂)h₂ = ReLU(x₁ + x₂ − 1)h₁ − 2h₂ > 0.5 ?one hidden layer of two neurons
1 / 1
Say this

A neuron draws a line. A layer bends the space so the next line works. Depth is repeated bending.

Going deeper

A worked example — One update, by hand

Start with w = (0, 1), b = −3 — the line x₂ = 3. The point (2, 3) has label +1 but s = 0·2 + 1·3 − 3 = 0, which is not > 0, so it is misclassified.

Apply the rule: w ← (0, 1) + (+1)·(2, 3) = (2, 4), b ← −3 + 1 = −2. Now s = 2·2 + 4·3 − 2 = 14 > 0: the point is on the right side. The line rotated toward the point because adding x to w makes w·x larger for that x in particular.

Six updates later, w = (1.4, 1.4), b = −5 separates all eight points. Different update orders give different final lines — the theorem promises a separator, not the best one.

Why XOR needs a hidden layer, in one sentence

A single neuron's decision boundary is the set where w·x + b = 0 — a line — and XOR's two classes sit on opposite corners of a square, so no line puts both +1 corners on one side without one −1 corner. Two hidden ReLUs, h₁ = ReLU(x₁ + x₂) and h₂ = ReLU(x₁ + x₂ − 1), map the four corners to (0,0), (1,0), (1,0), (2,1), which are separable.

From the perceptron rule to gradient descent

The rule w ← w + t·x is gradient descent on the loss max(0, −t·(w·x + b)) with learning rate 1: the gradient of that loss on a mistake is exactly −t·x. Swap the step output for a sigmoid and the hinge for cross-entropy and you have logistic regression — same neuron, differentiable loss, and the gradient now carries how wrong, not just whether.

What the interviewer usually asks next

“Why do we need nonlinear activations?” Because a stack of linear layers is one linear layer: W₂(W₁x + b₁) + b₂ = (W₂W₁)x + b′. The activation between them is the only thing that lets depth draw anything but a line.

The classic pictures

The TensorFlow Playground lets you watch a tiny network bend the plane on exactly these datasets, XOR included, live in the browser. Rosenblatt’s 1958 paper and Minsky & Papert’s Perceptrons (1969) are the historical bookends; the Wikipedia article has the convergence theorem and the original learning rule.

How it connects

  • The forward passthe same neuron, with real numbers, stacked into layers
  • Backpropagationthe update rule generalised: gradients instead of ±x
  • Self-attentionthe same matmul-and-bend inside a transformer

What it costs

  • Only linear boundariesone neuron is one line; XOR needs a hidden layer
  • Converges to a line, not the best linethe margin is whatever the update order produced — logistic regression and SVMs fix this
  • No notion of confidencea step output is ±1; replacing it with a sigmoid gives a probability, which is what makes a loss differentiable
From a neuron to a network

The forward pass

weighted sums and bends, layer by layer

What a network actually computes when you call it. Two inputs, two hidden neurons, one output, every number by hand — and the same two lines are exactly what a trillion-parameter model does at scale.

The problem

A single neuron can only draw a line. To represent anything curved, the outputs of neurons have to become the inputs of other neurons — and something has to stop the stack from collapsing back into one line.

What it buys you

Alternating a linear map (matmul + bias) with a nonlinearity (ReLU, sigmoid) gives a composition that can represent essentially any function — and whose every intermediate value can be stored for the backward pass.

How it works

Layer 1: z = W₁x + b₁, then h = ReLU(z). Layer 2: u = w₂·h + b₂, then ŷ = σ(u), a probability. The loss L = −log ŷ (cross-entropy, y = 1) turns how wrong into one number. With x = (1.0, 0.5): z = (0.5, 1.0), h = (0.5, 1.0), u = 0, ŷ = 0.5, L = 0.693.

The forward pass — weighted sums and bends, layer by layerEvery number computed by hand on a 2-2-1 network: z = Wx + b, h = ReLU(z), ŷ = σ(u), L = −log ŷ.
0.70.3−0.61.00.4−0.8b +0.1b +0.2b +0.61.0x₁0.5x₂h₁h₂ŷ = σ(u)loss L (y = 1)ReLU(z) = max(0, z)0.51.0σ(u) = 1/(1+e⁻ᵘ)0.5without the bendW₂(W₁x + b₁) + b₂= (W₂W₁) x + b′one linear map — depth bought nothingthe same two lines at real scaleH = ReLU(X W₁ + b₁) X: [batch × d_in] W₁: [d_in × d_hidden]ŷ = σ(H w₂ + b₂) e.g. batch 4096, d 4096 → one big matmul per layer
1 / 1
Say this

Forward = matmul, bend, matmul, bend. Store everything — backward is about to need it.

Going deeper

A worked example — The numbers, end to end

x = (1.0, 0.5). W₁ = [[0.7, −0.6], [0.3, 1.0]], b₁ = (0.1, 0.2), w₂ = (0.4, −0.8), b₂ = 0.6.

z₁ = 0.7·1.0 + (−0.6)·0.5 + 0.1 = 0.5 · z₂ = 0.3·1.0 + 1.0·0.5 + 0.2 = 1.0 · h = ReLU(z) = (0.5, 1.0).

u = 0.4·0.5 + (−0.8)·1.0 + 0.6 = 0.0 · ŷ = σ(0) = 0.5 · with y = 1, L = −log 0.5 = 0.693.

The network is exactly undecided and the loss is ln 2 — the loss of a coin flip. Every page in this group reuses these numbers.

Which activation, and why ReLU won

Sigmoid and tanh saturate: for large |z| their slope is nearly zero, so gradients vanish through deep stacks. ReLU's slope is exactly 1 wherever it is on, so signals pass through unchanged, and it costs one comparison. Its flaw is units that are off for every input (dead ReLUs); GELU and SiLU, used in transformers, smooth the corner so there is always some slope.

Why cross-entropy and not squared error for a probability

With a sigmoid output and squared error, the gradient at the logit is (ŷ − y)·ŷ(1 − ŷ), which goes to zero exactly when the network is confidently wrong (ŷ near 0 or 1) — the worst time to stop learning. With cross-entropy the σ′ factor cancels and the gradient is just ŷ − y: largest when most wrong.

The classic pictures

3Blue1Brown’s neural-network series (chapter 1) animates exactly this forward pass on handwritten digits. Stanford’s CS231n notes draw the layer-as-matrix picture used here.

Shapes at real scale

For a batch of B examples, X is B × d_in and the layer computes X W + b with W of size d_in × d_out — one matmul of B·d_in·d_out multiply-adds. A transformer's MLP has d_in = 4096, d_out = 16384; with B = 4096 tokens that is 2.7 × 10¹¹ multiply-adds in one line. The forward pass is the reason the infrastructure notes exist.

How it connects

  • The perceptronone of these neurons, learning on its own
  • Activation functionsthe bend, and why ReLU
  • Backpropagationthe same numbers, run backwards
  • Self-attentiona transformer layer is this plus attention
  • Prefill vs decodeinference is nothing but forward passes

What it costs

  • Every intermediate is kept for trainingz, h, u, ŷ — activation memory, the reason training needs several times the memory of inference
  • Cost is dominated by matmulbatch × d_in × d_out per layer — which is what GPUs are built for
  • The bend is not optionalwithout it depth buys nothing: W₂(W₁x) is one linear map
From a neuron to a network

Activation functions

the bend, and the slope backprop multiplies by

Step, sigmoid, tanh, ReLU, GELU, softmax. Each one is a different answer to the same two questions: how does it bend the signal going forward, and what does it multiply the gradient by going back? The second question decided the history of deep learning.

The problem

A network needs a nonlinearity between layers or depth means nothing — but backprop multiplies one slope per layer, so a function whose slope is small or zero starves every layer before it of gradient.

What it buys you

ReLU and its smooth relatives keep the slope at 1 wherever the unit is active, so gradients survive depth; sigmoid and softmax are kept for the output, where a bounded probability is the point.

How it works

Sigmoid squashes to (0, 1), slope at most 0.25. tanh to (−1, 1), slope at most 1; both saturate. ReLU = max(0, z): slope 1 when on, 0 when off. GELU/SiLU smooth the corner, used in transformers. Softmax turns a vector of logits into a distribution; with cross-entropy its gradient at the logits is p − onehot(y).

Activation functions — the bend, and the slope backprop multiplies byStep, sigmoid, tanh, ReLU, GELU and softmax: what each one does to the signal going forward and to the gradient going back.
-2-112123-1z (the weighted sum)a = f(z)stepsigmoidtanhReLUleaky ReLUGELUthe slope f′(z) — what backprop multiplies by0.251slope 0 everywhere (and undefined at 0)a gradient crossing 10 layers at typical |z| ≈ 1sigmoidslope 0.20× 0.20 × … × 0.20≈ 1e−7tanhslope 0.42× 0.42 × … × 0.42≈ 2e−4ReLU (on)slope 1.00× 1 × … × 1= 1ReLU (off)slope 0× 0= 0the first layers of a deep sigmoid network receive almost no signalsoftmax: an activation for a whole vector of outputsclass logit z e^z p = e^z / Σcat 2.0 7.389 0.659dog 1.0 2.718 0.242bird 0.1 1.105 0.099Σ e^z = 7.389 + 2.718 + 1.105 = 11.212 Σ p = 1.000exponentiate (so every value is positive), then divide by the total:a probability distribution over classes. The biggest logit wins,but not absolutely — “soft” max. Sigmoid is the two-class special case.paired with cross-entropy, the gradient at the logits is p − onehot(y):y = cat: (0.659−1, 0.242, 0.099) = (−0.341, 0.242, 0.099)every LLM ends in exactly this: softmax over the vocabulary.which bend, wherewhere activationhidden layers, MLPs / CNNsReLUcheap, slope 1, no saturationtransformer MLP blocksGELU / SiLU (SwiGLU)smooth ReLU; GPT, BERT: GELU; Llama: SiLUoutput, binary classificationsigmoidone probabilityoutput, many classessoftmaxa distribution over classesoutput, regressionnone (identity)any real numbergates in LSTMs / GRUssigmoid, tanhbounded, 0–1 for gatesthe rule: hidden layers want a slope that stays near 1 so gradients survive;the output layer wants whatever shape the loss expects.
1 / 1
Say this

Pick the hidden activation for its slope and the output activation for its range. ReLU/GELU inside, softmax at the end — that is every LLM.

Going deeper

A worked example — Ten layers, three activations

Backprop multiplies one slope per layer. At a typical pre-activation |z| ≈ 1: sigmoid′(1) = 0.20, tanh′(1) = 0.42, ReLU′(1) = 1.

Across ten layers the gradient reaching the first layer is scaled by 0.20¹⁰ ≈ 10⁻⁷ (sigmoid), 0.42¹⁰ ≈ 2 × 10⁻⁴ (tanh), or 1 (ReLU, if every unit on the path is on). With a learning rate of 0.01, the first layer of the sigmoid network moves by 10⁻⁹ per step: it is frozen.

Softmax, by hand: logits (2.0, 1.0, 0.1) → e^z = (7.389, 2.718, 1.105), sum 11.212 → p = (0.659, 0.242, 0.099). With the true class first, the gradient at the logits is p − onehot = (−0.341, 0.242, 0.099): push the right logit up, the others down, in proportion to how much probability they stole.

Why ReLU, and then why GELU

ReLU won in 2012 because its slope is 1 and it costs a comparison; the price is dead units and a kink at zero. GELU (z·Φ(z)) and SiLU (z·σ(z)) are smooth, never have slope exactly zero, and in practice give slightly better transformers at the same cost — GPT-2/3 and BERT use GELU, Llama uses SiLU inside a gated unit (SwiGLU).

Why the output layer is different

Hidden activations are chosen for their slope. The output activation is chosen to match the loss: sigmoid + binary cross-entropy for one probability, softmax + cross-entropy for a distribution, identity + squared error for a regression. In each matched pair the gradient at the logits collapses to prediction − target — the same ‘gradient = error’ every time.

The classic pictures

The table on Wikipedia’s activation-function page plots every function with its derivative. Stanford’s CS231n notes (neural networks part 1) walk through sigmoid, tanh, ReLU and leaky ReLU with the same ‘what does it do to the gradient’ framing used here. The GELU paper is Hendrycks & Gimpel, arXiv:1606.08415.

How it connects

  • The forward passwhere the bend sits in the computation
  • Backpropagationwhy the slope is what matters
  • The perceptronthe step function, and why it cannot be trained
  • Self-attentionsoftmax is also what attention uses to turn scores into weights

What it costs

  • Saturating functions vanishten sigmoid layers multiply a gradient by ~10⁻⁷; early layers never learn
  • ReLU units can diez < 0 for every input means slope 0 forever; leaky ReLU, GELU and a sane learning rate prevent it
  • The output is chosen by the losssigmoid for one probability, softmax for a distribution, identity for a number — not by taste
Learning from mistakes

Backpropagation

the chain rule, run backwards

How a network finds out which way each of its weights should move. It is the chain rule applied once per node, in reverse, reusing stored forward values — nothing more exotic than that, and worth doing by hand exactly once.

The problem

Lowering the loss means knowing ∂L/∂w for every weight, and a weight deep in the network affects L only through everything after it. Deriving each gradient separately would repeat the same work millions of times.

What it buys you

One backward sweep. The gradient at the output is just the error ŷ − y; each layer multiplies the incoming signal by its local derivative and hands it back. Every ∂L/∂w falls out as upstream signal × the input that weight multiplies.

How it works

Start at the end: ∂L/∂u = ŷ − y = −0.5. Output weights: ∂L/∂w₂ = −0.5·h = (−0.25, −0.5). Back through the weights: ∂L/∂h = −0.5·w₂ = (−0.2, 0.4). Through ReLU: multiply by 1 where z > 0. First-layer weights: ∂L/∂W₁ = ∂L/∂z ⊗ x. Eight gradients, one pass.

Backpropagation — the chain rule, run backwardsThe same 2-2-1 network; every gradient derived by hand from ∂L/∂u = ŷ − y.
0.70.3−0.61.00.4−0.8b +0.1b +0.2b +0.61.0x₁0.5x₂h₁h₂ŷ = σ(u)loss L (y = 1)∂w: −0.2∂w: 0.4∂w: −0.1∂w: 0.2∂w: −0.25∂w: −0.5the chain rule, one path
1 / 1
Say this

Gradient of a weight = upstream signal × the input it multiplies. Going backwards through a weight means multiplying by that weight.

Going deeper

A worked example — Every gradient, derived

Stored from the forward pass: x = (1.0, 0.5), z = h = (0.5, 1.0), u = 0, ŷ = 0.5, y = 1.

Output. ∂L/∂u = ŷ − y = −0.5. ∂L/∂w₂ = (∂L/∂u)·h = (−0.25, −0.5). ∂L/∂b₂ = −0.5.

Back through w₂. ∂L/∂h = (∂L/∂u)·w₂ = −0.5·(0.4, −0.8) = (−0.2, +0.4).

Through ReLU. Both z > 0, so ∂L/∂z = (−0.2, 0.4) unchanged.

First layer. ∂L/∂W₁ = ∂L/∂z ⊗ x = [[−0.2·1.0, −0.2·0.5], [0.4·1.0, 0.4·0.5]] = [[−0.2, −0.1], [0.4, 0.2]]. ∂L/∂b₁ = (−0.2, 0.4).

One chain checked by hand: ∂L/∂w₁₁ = (∂L/∂u)(∂u/∂h₁)(∂h₁/∂z₁)(∂z₁/∂w₁₁) = (−0.5)(0.4)(1)(1.0) = −0.2. ✓

Why ŷ − y, and why that matters

∂L/∂ŷ = −1/ŷ and ∂ŷ/∂u = ŷ(1 − ŷ); multiply and the ŷ cancels, leaving ŷ − y. The same cancellation happens for softmax + cross-entropy in a classifier with many classes: the gradient at the logits is simply (probabilities − one-hot). That is why those two are always paired, and why the error signal at the output of a classifier is literally the error.

Where the 2× comes from

For a layer h = W a, backward needs two products: ∂L/∂W = (∂L/∂h) aᵀ to update the weights and ∂L/∂a = Wᵀ (∂L/∂h) to keep going. Each is the same size as the forward matmul, so backward ≈ 2× forward, and a training step ≈ 3× a forward pass — the figure behind every FLOPs estimate for training.

The classic pictures

3Blue1Brown’s Backpropagation calculus draws the chain-rule tree for a one-neuron-per-layer network, the same shape as the chain panel here. Karpathy’s micrograd (and its two-hour video) builds backprop in 100 lines of Python on a graph just like the one on the autodiff page.

Vanishing and exploding gradients

Each layer multiplies the signal by its weight matrix and by the activation's slope. Across 50 layers that product either shrinks toward zero (sigmoid, small weights) or grows without bound. Residual connections add an identity path so the product always has a 1 in it; LayerNorm keeps the scale of activations fixed; careful initialisation starts the products near 1.

How it connects

  • The forward passthe values this reuses
  • Automatic differentiationthe same sweep, done by the framework
  • Optimizerswhat the gradients are for
  • FlashAttentionrecompute in backward instead of storing — the same trade

What it costs

  • About twice the forward costtwo matmuls per layer going back — one for ∂W, one for ∂h — so training ≈ 3× forward FLOPs
  • Needs the forward activationsor recomputes them: activation checkpointing
  • Dead units learn nothinga ReLU that was off passes zero gradient, so its weights get no signal from that example
  • Signals shrink or blow up with depthvanishing and exploding gradients — the reason residual connections and normalisation exist
Learning from mistakes

Automatic differentiation

what loss.backward() actually does

Nobody writes backprop by hand. The framework records the forward computation as a graph, and one call walks it backwards. Knowing what it records — and what it has to keep in memory — is the difference between using PyTorch and understanding it.

The problem

A modern model is thousands of operations whose derivatives would be impossible to derive and maintain by hand, and the gradient is needed with respect to every one of billions of parameters.

What it buys you

Each operation knows only its own local derivative. Recording operations as they run (define-by-run) and visiting them in reverse order assembles the full gradient from local pieces — one sweep for all parameters, at about twice the cost of the forward pass.

How it works

Forward: every op appends a node holding its grad_fn and saves the tensors its derivative needs. loss.backward() seeds 1 at the loss and visits nodes in reverse topological order; each multiplies the incoming gradient by its local derivative (a vector–Jacobian product) and passes the result to its inputs. Whatever reaches a leaf accumulates into .grad.

Automatic differentiation — what loss.backward() actually doesThe same network as a computational graph: local derivatives, walked in reverse, landing on .grad.
W₁x + b₁ReLUw₂·h + b₂σ−log ŷx(1.0, 0.5)z(0.5, 1.0)(−0.2, 0.4)h(0.5, 1.0)(−0.2, 0.4)u0.0−0.5ŷ0.5−2.0L0.6931W₁, b₁[[−0.2, −0.1], [0.4, 0.2]] (−0.2, 0.4)w₂, b₂(−0.25, −0.5) −0.5y = 1x: no grad neededx = torch.tensor([1.0, 0.5])W1 = torch.tensor([[0.7,-0.6],[0.3,1.0]], requires_grad=True)h = torch.relu(W1 @ x + b1) # b1, w2, b2 likewiseyhat = torch.sigmoid(w2 @ h + b2)loss = -torch.log(yhat) # y = 1loss.backward()W1.grad # tensor([[-0.2, -0.1], [0.4, 0.2]])
1 / 1
Say this

Each node knows only its local derivative. Reverse mode multiplies them from the loss backwards, so one sweep gives every parameter's gradient.

Going deeper

A worked example — Reading the graph the framework built

h = torch.relu(W1 @ x + b1) creates two nodes (a matrix–vector product, then an add) and a ReLU node; yhat = torch.sigmoid(w2 @ h + b2) adds three more; loss = -torch.log(yhat) two more. Each node stores a grad_fn and the tensors its derivative needs — ReLU keeps the mask (z > 0), sigmoid keeps ŷ, every matmul keeps its input.

loss.backward() walks them in reverse. At −log: incoming 1, local −1/ŷ = −2.0. At sigmoid: −2.0 × ŷ(1 − ŷ) = −0.5 — the framework never used the shortcut ŷ − y, it multiplied the two local pieces. At the second linear node it produces three outputs at once: a gradient for h (−0.5·w₂) that continues, and gradients for w₂ and b₂ that stop at those leaves.

Afterwards W1.grad is [[−0.2, −0.1], [0.4, 0.2]] — the hand-derived answer.

Forward mode vs reverse mode

Both use the same local derivatives. Forward mode pushes a perturbation of one input through the graph (a Jacobian–vector product) and gets the sensitivity of every output to that one input; reverse mode pulls a weight on one output back through the graph (a vector–Jacobian product) and gets the sensitivity of that one output to every input. Training has one output (the loss) and billions of inputs, so reverse mode wins by a factor of billions.

What is actually in memory during training

Weights, their gradients, the optimizer state (two more copies for Adam), and the saved activations — which for a transformer are every layer's input, attention scores and MLP intermediate for every token in the batch. The activations usually dominate at long sequence lengths. Activation checkpointing keeps only the layer boundaries and recomputes the rest during backward, trading ~30% more compute for a large memory cut.

The classic pictures

PyTorch’s Autograd mechanics note is the authoritative description of what gets saved and when; Baydin et al., Automatic differentiation in machine learning: a survey, has the canonical forward-vs-reverse mode tables. Karpathy’s micrograd is the smallest working implementation worth reading end to end.

Define-by-run vs a static graph

PyTorch records the graph fresh on every forward call, so Python control flow (if, loops, early exit) just works, at the cost of re-recording each time. Static-graph systems (TensorFlow 1, JAX's jit, torch.compile) capture the graph once and can optimise it — fuse kernels, plan memory — but need the structure to be fixed. Modern practice is eager for development and compiled for the training run.

How it connects

  • Backpropagationthe same arithmetic, by hand
  • Optimizersthe next line: optimizer.step()
  • The forward passwhat the recorded graph computes
  • Agent context growthanother memory-that-grows-with-length story

What it costs

  • Activation memoryevery saved tensor lives until backward consumes it — batch × sequence × depth of them
  • Reverse mode pays off for scalar outputsone output, many inputs is exactly training; forward mode would need one sweep per parameter
  • Gradients accumulatewhich is why you call zero_grad(), and how a tensor used twice gets the sum of both paths
Learning from mistakes

SGD and mini-batches

the gradient you can afford

The exact gradient is an average over the whole dataset, which no one can afford per step. SGD takes the average over a handful of examples instead: a noisy estimate, thirty times cheaper, and the noise turns out to be a feature. Every large model is trained this way.

The problem

One full-batch gradient step costs a pass over the data. On real datasets that is hours per step, and most of the pass is spent confirming what the first few hundred examples already said.

What it buys you

A mini-batch gradient is an unbiased estimate of the true one, with noise ∝ 1/√B, at cost B instead of N. Many cheap noisy steps beat one exact step by a mile for the same compute — and the noise seems to steer training toward flatter minima that generalise better.

How it works

Shuffle the data each epoch; take it in batches of B; one step per batch, so N/B steps per epoch. Batch size sets the noise and the cost; the learning rate scales up with the batch (the linear scaling rule) up to a critical size, with warm-up. Because the noise never vanishes, η is decayed over training so the walk settles.

SGD and mini-batches — the gradient you can affordOne parameter, 1,000 examples: why a noisy gradient on 32 of them beats the exact gradient on all of them.
N = 1,000 examples xᵢ — fit one number w to minimise the average ½(w − xᵢ)²0123456xa random mini-batch, B = 32μ = 3.03 (the answer)w = 0.0full gradient: w − μ = −3.03 → step η·3.03 = 0.91batch gradient: w − mean(batch) = −2.94 → step 0.88loss vs examples processed — the same compute, two walkers01 epoch2312345best possible: ½σ² = 0.49SGD, B = 32: 31 steps per epochfull-batch GD: 1 step per epochnever settles: jitter ≈ ησ/√Bwhy a little noise can help: sharp vs flat minimasharp: lower, but one noisy step away from the wallflat: a little higher, and noise cannot push you outa widely held view with real evidence behind it, not a theorem — phrase it carefully in an interviewgradient noise ∝ 1/√B (σ = 1.0 here)0.99B=10.35B=80.18B=320.09B=1280.03B=1000
1 / 1
Say this

SGD is gradient descent with a cheap, noisy gradient. B sets the noise, η scales with B, and the noise is why learning-rate schedules exist.

Going deeper

A worked example — One parameter, 1,000 examples

Fit w to 1,000 numbers drawn around μ = 3.03 (σ ≈ 1), loss = average of ½(w − xᵢ)². The exact gradient is w − μ. From w = 0 with η = 0.3, full-batch GD takes one step per pass over the data: w = 0.91 after one epoch, 1.99 after three.

A random batch of 32 has mean 2.94, so its gradient is −2.94 instead of −3.03: nearly the same direction at 1/31 of the cost. Thirty-one such steps per epoch put w ≈ 3.0 before the first epoch is a tenth over. The noise per step is σ/√B ≈ 0.18; near the minimum w jitters by about ησ/√B ≈ ±0.07 and never fully settles — the reason to decay η.

Batch size, learning rate and the critical batch

Doubling B halves the gradient variance, so the same per-epoch progress survives doubling η too — the linear scaling rule (Goyal et al., arXiv:1706.02677, trained ImageNet in an hour on 8k-image batches with it, plus a warm-up). It stops working past the critical batch size, where the gradient is already accurate and extra examples per step buy nothing; beyond that only more steps help.

Sharp vs flat minima

Keskar et al. (arXiv:1609.04836) observed that large-batch training lands in sharper minima and generalises worse, and attributed it to the missing noise. The picture is widely used and well supported but still argued about — later work showed the gap can mostly be closed by scaling η and training longer. In an interview, say ‘tends to’, not ‘always’.

The classic pictures

The loss-landscape renders everyone has seen — the spiky ResNet-56 surface that skip connections smooth out — are from Li et al., Visualizing the Loss Landscape of Neural Nets. The optimizer-race animations (SGD, momentum, NAG, Adagrad, Adadelta, RMSprop racing through a saddle and a valley) are Alec Radford’s, reproduced in CS231n notes part 3, which also covers batch size, learning-rate schedules and gradient checks. Sebastian Ruder’s overview of gradient descent optimization algorithms is the standard written tour.

How it connects

  • Optimizersmomentum and Adam are built on this gradient
  • Backpropagationhow one example's gradient is computed
  • Continuous batchingthe same word, a different meaning in serving
  • Tensor, pipeline and expert parallelismdata parallelism = the batch split across GPUs, gradients averaged

What it costs

  • Noise never fully goes awaythe jitter near the minimum is ∝ ησ/√B, which is why schedules exist
  • Batch size is a hardware decision tooa bigger batch is a bigger matmul; GPUs want it large, generalisation sometimes wants it smaller
  • Shuffling mattersun-shuffled batches give correlated, biased gradient estimates
Learning from mistakes

Optimizers

from a gradient to an update — GD, SGD, momentum, Adam

The gradient says which way is downhill; the optimizer decides how far to step, and in which direction once momentum and scaling get involved. Most of what makes large-model training work or fail lives here, in one hyperparameter and two running averages.

The problem

Plain gradient descent follows the raw gradient with one learning rate for every parameter. On a surface that is steep one way and flat another — which is what real loss surfaces look like — it zigzags across the steep direction and crawls along the flat one, and a learning rate that is safe for one is wrong for the other.

What it buys you

Momentum averages out the zigzag. Adam normalises each parameter's step by the typical size of its own gradient, so steep and flat directions move at the same pace. A schedule changes the learning rate over time so early steps are safe and late ones settle.

How it works

GD: w ← w − η∇L. SGD: the same with a mini-batch gradient. Momentum: v ← βv + ∇L, w ← w − ηv. Adam: running means of the gradient (m) and of its square (v); step = η·m̂/√v̂. AdamW: weight decay added as its own term. Warm η up from zero, then decay it.

Optimizers — from a gradient to an update, and why plain descent is not enoughOne step on the tiny network, then the same valley walked by GD, SGD, momentum and Adam.
minimumw₁ (shallow: ∂f/∂w₁ = w₁)w₂ (steep: ∂f/∂w₂ = 10 w₂)start (−9, 1.4)−∇f: mostly down the steep wallone step of gradient descent on the 2-2-1 network, η = 0.1parametervaluegradient−η·gradientnew valuew₁₁0.7−0.2+0.020.72w₁₂−0.6−0.1+0.01−0.59w₂₁0.30.4−0.040.26w₂₂1.00.2−0.020.98b₁(0.1, 0.2)(−0.2, 0.4)(+0.02, −0.04)(0.12, 0.16)w₂ (out)(0.4, −0.8)(−0.25, −0.5)(+0.025, +0.05)(0.425, −0.75)b₂0.6−0.5+0.050.650.6930.320loss over 8 steps on this exampleGD, η = 0.15η = 0.21: divergesη = 0.03: crawlsSGD (noisy)momentum, β = 0.8Adam, η = 0.8learning rate over trainingwarm-upcosine decay
1 / 1
Say this

GD is the idea, SGD is what you can afford, momentum fixes the zigzag, Adam fixes the scale, and AdamW with warm-up and cosine decay is the default.

Going deeper

A worked example — The same valley, five walkers

f(w) = ½(w₁² + 10w₂²), starting at (−9, 1.4). The gradient is (w₁, 10w₂): ten times steeper in w₂.

GD, η = 0.15. In w₂ each step multiplies by 1 − 1.5 = −0.5: it overshoots and flips sign. In w₁ each step multiplies by 0.85: a crawl. After 12 steps w₁ = −1.3.

η = 0.21. The w₂ multiplier is −1.1, magnitude above 1: the walk diverges, f goes from 1 to 39. η = 0.03 is stable but reaches only w₁ = −6.2.

Momentum, β = 0.8, η = 0.04. Alternating w₂ steps cancel in the velocity; the consistent w₁ steps accumulate. Reaches f = 0.15 — smoother and faster.

Adam, η = 0.8. Each coordinate's step is normalised by its own gradient scale, so w₁ (gradient 9) and w₂ (gradient 14) move about the same distance per step. The steepness difference stops mattering.

Why Adam's step is roughly ±η

m is a running mean of g and v a running mean of g², so m/√v is g divided by its own typical magnitude: about ±1 while the gradient keeps its sign, smaller when it oscillates. The step is therefore about η per parameter whatever the gradient's scale — which is what lets one η serve embeddings, attention and MLP weights whose gradients differ by orders of magnitude.

AdamW, and why decay is decoupled

L2 regularisation adds λw to the gradient — but Adam then divides that term by √v, so heavily-updated parameters are barely regularised. AdamW instead subtracts ηλw directly from the weight, outside the adaptive scaling. It is the default for transformer training; typical λ is 0.1, far larger than the 1e-4 people used with SGD.

The classic pictures

Alec Radford’s optimizer animations in CS231n notes part 3 are the race through a valley and over a saddle that this page’s plot imitates. Adam is Kingma & Ba, arXiv:1412.6980; AdamW is Loshchilov & Hutter, arXiv:1711.05101; Ruder’s overview walks the whole ladder with the same pictures.

Warm-up and cosine decay

Adam's v̂ is a poor estimate for the first few hundred steps and the initial weights are random, so a full-size η early on can wreck the run; warming η up linearly from zero avoids it. Decaying η afterwards (cosine to ~10% of peak is the common choice) lets the walk settle into the minimum instead of bouncing around it. Batch size interacts: larger batches tolerate larger η, roughly proportionally up to a point.

How it connects

  • SGD and mini-batchesthe noisy gradient every optimizer here consumes
  • Backpropagationwhere the gradient comes from
  • Automatic differentiation.grad is what optimizer.step() reads
  • The forward passthe loss being descended
  • Tensor, pipeline and expert parallelismoptimizer state is what sharded training has to split

What it costs

  • η is the hyperparametertoo large diverges, too small stalls — tune it first
  • Adam keeps two extra copies of every parameterm and v: optimizer state is 2× the model, the largest piece of training memory
  • Noise is a featuremini-batch noise is what makes large-scale training affordable, and it seems to help generalisation