Turning electricity into tokens
A guest lecture (Dan Fu, UCSD / Together) on the other side of the model: serving it. Once you've trained a model, inference is the engine that turns GPUs into intelligence. The thesis: if you understand inference and the GPU kernels underneath, you can drive full-stack innovation — in routing, kernels, and even architectures.
Why inference is the piece
Scale drove the capability explosion (hundreds-of-millions of params in 2018 → multi-trillion today), and the transition is faster than expected — like Manhattan going from 130,000 working horses (1902) to cars outnumbering them by 1912. For LLMs that "1912 moment" was roughly last year. If GPUs are the new oil, inference is the engine that turns it into useful motion. Training is a one-time cost; inference is the repeated cost that actually serves users — and ML models are just DAGs of operations that inference engines/kernels map onto hardware.
What happens to a request
The pipeline
disaggregated
prefix sharing
Production traffic looks like neither training data nor a made-up workload. Each application has its own token-shape distribution — a coding agent sends tens of thousands of input tokens (the whole codebase) then a short or thinking output; summarizing a book looks different again; quick chat different still. Workloads are turn-based and agentic (tool calls, multiple turns, variable inter-turn gaps), each with its own SLA: interactive chat wants first token in <1s; batch jobs just want throughput. The session shape (tokens/turn, generation length, stickiness, gaps) defines the serving problem.
Prefill vs decode — two different beasts
After tokenizing and a cache lookup, the ML computation splits into two phases with opposite characteristics:
| Prefill | Decode | |
|---|---|---|
| What | encode the whole prompt (10K tokens in → 1 out) | generate one token at a time |
| Bottleneck | compute-bound (like training's forward pass) | memory-bandwidth-bound (load the whole model per token) |
| Frequency | once per prompt | once per generated token (many) |
Because they're so different, you disaggregate them onto different workers — even different chips: prefill on flop-heavy GPUs, decode on memory-bandwidth chips (NVIDIA buying Groq's LPUs; OpenAI's Cerebras partnership; SambaNova). The engine runs a loop: schedule → execute → sample, repeat. Continuous batching keeps it full — requests of varying length come in, finish, and are evicted dynamically, contending for both compute and KV-cache memory.
The KV cache is the heart of serving
Many users start with "hi ChatGPT," or paste the same book, or continue a conversation — so you cache activations and reuse them via prefix sharing (a radix tree finds which tokens you've seen). You want this cache as large as possible, so it spills down a memory hierarchy:
fastest, scarce
CPU speed matters now
the SSD/DRAM buying spree
This is exactly the 1970s/80s paging problem (run out of RAM → swap to disk). Eviction by LRU is within ~2× of optimal; better is to predict the future (opening an old chat strongly signals you'll ask about it → prefetch its KV cache to GPU). Splitting the model is its own choice — tensor parallelism (split each tensor across GPUs) or expert parallelism (MoE experts across GPUs) — and these choices set your bottlenecks. New NVL72 Grace-Blackwell racks (72 GPUs, fast interconnect) raise fresh questions: how to split a trillion-param model, and fault tolerance when one of 64 GPUs dies (the connectors are literally flimsy plastic).
Scale bugs & a simple win
At trillions of tokens/day, things that work at small scale break at 0.001% frequency: a subtly-wrong kernel turns logits to NaN → the model loops "hi hi hi"; a broken tool-call handler → doom loops of tens of thousands of tokens; an off-by-one read of uninitialized GPU memory → the model randomly emits a Chinese character then "decides" to think in Chinese (misblamed on quantization).
A two-line routing change: a fresh request (low cache-hit, thousands of new tokens to prefill) shouldn't run on the same GPUs as someone mid-conversation. Route cold/fresh requests to one prefill pool and warm requests to another → up to 40% faster serving. The field is very early — this is the kind of thing that'll look obvious in 10 years.
Megakernels and looped models
Megakernels — beat the per-op overhead
Decode runs the whole model to produce one token, turning a massively parallel GPU into a "glorified memory loader." Worse, we write one kernel per operation (norm, matmul, attention) — easy to program, but it injects downtime: kernel launch/teardown gaps, tail effects (waiting on the longest item), and gaps between kernels that add up across the model.
A megakernel fuses many ops into a single kernel — like FlashAttention's fusion, but far more aggressive — treating the GPU as a distributed system that schedules work with dependencies to maximize utilization. You can start a layer's weight load during the previous op, begin the KV-cache load while QKV+RoPE is still running, and load the O-projection weights before attention finishes. Result: 30–70% speedup on the attention kernel, whole layers fused, and ~72% of memory bandwidth on an H100 — near speed-of-light decode (built with the ThunderKittens library). The cost is "blood, sweat, and tears": a skilled engineer writes megakernels for ~2–3 models / 1 hardware / batch sizes 1–16 in a year — bump to batch 17 and start over.
Parcae — scaling by recurrence, stabilized
Beyond scaling params and data, can you scale a third axis? Parcae (UCSD) revisits looped transformers: run some blocks in a loop, keeping parameters constant but adding a FLOPs dial — more quality-per-parameter and more expressivity. The catch: naive loop transformers blow up if you nudge the learning rate (loss spikes, NaNs) — a sign something deeper is wrong.
# Look at the residual across loop steps (it changes little). Isolate the
# nonlinear block R; what's left is a linear system in matrices A, B:
h[t+1] ≈ A^t · (stuff) + … # dominated by powers of A
# spectral radius of A > 1 → explodes; prior choices were (marginally) unstable
# Parcae: A = negative-diagonal (decays), B = norm-constrained → spectral radius < 1 → stable
With this SSM-style reparameterization, training is stable even at learning rates that wrecked prior loop models, and Parcae beats both a prior loop transformer and a strong transformer baseline on perplexity and downstream quality. The scaling-law punchline: holding params fixed, as you scale data you should also scale recurrence (the curves go "down and to the right") — yet today's models have zero recurrence (the far-left of the curve), hinting we might be leaving quality on the table. And fewer parameters is an inference win: more room for KV cache, less cross-GPU communication, maybe even a recurrent-block megakernel.
Co-design notes (from Q&A)
Golden rules & takeaways
A guest perspective rounding out the course: how everything you've built gets served — and where the open research problems are.
References
Guest lecture by Dan Fu (UCSD / Together). Related works & tools:
- Yu et al. (2022) — Orca (continuous batching)
- Kwon et al. (2023) — vLLM / PagedAttention
- Zhong et al. (2024) — DistServe (prefill/decode disaggregation)
- ThunderKittens (Hazy Research) · Megakernels blog
- Dehghani et al. (2018) — Universal Transformers (looped)
- Geiping et al. (2025) — Scaling test-time compute via recurrent depth
- Together AI · UCSD Hao AI Lab (inference research)