The Physics of LLM Inference

From Transformer to Production Server

agent37.club

The Two Walls

"You download a 7B model…
CUDA out of memory."

14 GB model. 8 GB GPU. First wall.

"You rent a bigger GPU…
10 users hit at once."

50 tok/s single user. 10 concurrent → 30-second latency. Second wall.

The gap between "I downloaded a model" and "production inference service" is enormous.

What We’re Building

The full inference stack

API LayerOpenAI-compatible HTTP, streaming tokens
Tokenizer PoolCPU-bound tokenization in parallel workers
SchedulerBatching, memory management, preemption
Engine + KV CacheForward passes, CUDA graphs, paged memory
GPU KernelscuBLAS, FlashAttention, custom fused ops

mini-sglang: ~5,000 lines of Python. Small enough to understand, large enough to be useful.

Chapter 1

Transformer Mechanics

Attention, FFN, and the shapes that matter

Attention from Scratch

The Formula

Attention(Q, K, V) =
  softmax(QKT / √dk) ⋅ V

  • Q = what we’re looking for
  • K = what each position advertises
  • V = what each position actually contains
  • √dk = scaling factor (prevents softmax saturation)

Shape Trace

TensorShape
Input x(batch, seq_len, hidden_dim)
Q, K, V(batch, seq_len, head_dim)
QKT scores(batch, seq_len, seq_len)
Output(batch, seq_len, head_dim)
def attention(q, k, v):
    d_k = q.shape[-1]
    scores = torch.matmul(q, k.transpose(-2, -1))
    scores = scores / math.sqrt(d_k)
    weights = torch.softmax(scores, dim=-1)
    return torch.matmul(weights, v)

Multi-Head & Grouped-Query Attention

Multi-Head Attention

  • Split hidden_dim across heads: d_head = 4096 / 32 = 128
  • Each head learns different attention patterns
  • Reshape: (batch, seq, 4096) → (batch, 32, seq, 128)

GQA: The Memory Trick

  • 8 KV heads shared across 32 query heads
  • 4:1 ratio — 4 query heads per KV head
  • Uses repeat_interleave to expand K,V

KV Cache Memory Comparison

MethodKV HeadsCache (32K ctx)
Full MHA3242 GB
GQA85.2 GB
MQA10.65 GB
Full MHA
42 GB
GQA (8 heads)
5.2 GB

“That’s the difference between fitting on one GPU or needing two.”

The Feedforward Network

SwiGLU — 3 matrices instead of 2

# SwiGLU forward pass
def forward(self, x):
    gate = F.silu(self.w1(x))  # silu = x * sigmoid(x)
    up   = self.w3(x)
    return self.w2(gate * up)

# Shapes for 7B model:
#   x:      (batch, seq, 4096)
#   w1(x):  (batch, seq, 11008)  gate projection
#   w3(x):  (batch, seq, 11008)  up projection
#   w2(...): (batch, seq, 4096)  down projection

SiLU = x · sigmoid(x), also called Swish. Smooth, non-monotonic activation.

Parameter Count Per Layer

ComponentParamsShare
Attention (GQA)~38M22%
FFN (SwiGLU)~135M78%
Total per layer~173M100%

FFN has ~3.5x the parameters of attention.
Most people think attention is where the action is — but the FFN is where the knowledge lives.

32-layer model: 4.3B parameters in FFN alone

Full 7B Forward Pass

Putting it all together — parameter breakdown

ComponentShape / CountParams
Embedding151936 × 4096622M
Q proj (×32 layers)4096 × 409616.7M
K proj (GQA, 8 heads)4096 × 10244.2M
V proj (GQA, 8 heads)4096 × 10244.2M
O proj4096 × 409616.7M
FFN w1 (gate)4096 × 1100845M
FFN w3 (up)4096 × 1100845M
FFN w2 (down)11008 × 409645M
LM Head4096 × 151936622M
7.07B

total parameters

13.2 GB

at fp16 (2 bytes per param)

Structure

Embedding → 32 × [RMSNorm → Attention → + → RMSNorm → FFN → +] → Final Norm → LM Head

Chapter 2

The Generation Loop

Prefill, decode, and the KV cache

Autoregressive Generation

One token at a time — each depends on ALL previous tokens

The cat sat on the ← depends on all 4 previous
# The naive generation loop
def generate(model, prompt_ids, max_tokens):
    input_ids = prompt_ids.clone()
    for _ in range(max_tokens):
        logits = model(input_ids)        # re-run ENTIRE sequence
        next_tok = torch.argmax(logits[:, -1, :], dim=-1)
        input_ids = torch.cat([input_ids, next_tok.unsqueeze(-1)], dim=1)
    return input_ids

Naive cost: Token 1 processes p tokens. Token n processes p+n-1 tokens.
Total: np + n(n-1)/2 = O(n²) forward passes. “This is terrible.”

Prefill vs Decode — The Two Phases

THE fundamental insight of LLM inference

Prefill

TokensAll prompt tokens at once
ParallelismFull — all tokens known
OperationGEMM (matrix × matrix)
Bound byCompute
IntensityHigh — tensor cores stay busy

Builds the initial KV cache.

Decode

Tokens1 new token per step
ParallelismMinimal
OperationGEMV (matrix × vector)
Bound byMemory bandwidth
IntensityLow — GPU starving for data

Appends one K,V entry per step.

The KV Cache — The First Big Insight

“The central data structure of LLM inference”

Without Cache

Every decode step recomputes K,V for all previous tokens.

O(n²) per token generated

Step 1
p
Step 50
p+49
Step 100
p+99

With Cache

Store K,V once computed. Only compute for the new token.

O(n) per token generated

Step 1
1
Step 50
1
Step 100
1

Tradeoff: Exchange O(n²) redundant computation for O(n) memory usage. K,V at position i only depend on input at position i — once computed, they never change.

KV Cache Memory Math

Formula: 2 × layers × kv_heads × head_dim × 2 bytes × seq_len

Per Token

128 KB

Per Sequence

512 MB

Total (batch)

512 MB

of 80 GB H100 VRAM

Chapter 3

Compute Characteristics

GEMM, GEMV, and the roofline model

GEMM vs GEMV — Why It Matters

GEMM — Prefill

C = A @ B    (M,K) × (K,N)

FLOPs2 × M × N × K
Memory3n² elements (square case)
Intensity(2/3)n FLOPs/element
n=4096~1350 FLOPs/byte
StatusCOMPUTE-BOUND

GEMV — Decode

y = A @ x    (M,K) × (K,)

FLOPs2 × M × K
MemoryM×K + K + M elements
Intensity~0.5 FLOPs/byte
vs Ridge1200× below ridge point
StatusMEMORY-BOUND

This single distinction — GEMM vs GEMV — explains why prefill is fast and decode is slow. The entire performance story follows from here.

H100 SXM — The Numbers That Matter

2000

TFLOPS

fp16 tensor core (sparse)

3.35

TB/s

HBM3 memory bandwidth

~597

FLOPs/byte

ridge point (2000 / 3.35)

The ridge point is the dividing line. Below ~597 FLOPs/byte, your operation is memory-bound. Above it, compute-bound. Single-token decode sits at 0.5 FLOPs/byte — 1200× below the ridge.

GEMM BenchmarkAchieved TFLOPS
2048 × 2048617
4096 × 4096709
8192 × 8192686

The 700× Gap

Single-token decode for a 7B model on H100

700×

Memory wins over compute. The GPU is starving.

Memory-limited
13 GB / 2.6 TB/s = 5 ms
Compute-limited
0.007 ms

“The GPU loads the entire 13 GB model from memory for each token, but only does one row of computation. The GPU is a supercar stuck in a parking lot.”

The Roofline Model

One chart that explains everything about inference performance

Arithmetic Intensity (FLOPs/byte) — log scale Achievable TFLOPS — log scale 0.1 1 10 100 1000 10000 0.1 1 10 100 1000 10000 Memory BW = 3.35 TB/s Compute = 2000 TFLOPS Ridge ~597 GEMV B=1 0.08% util B=8 B=32 B=128 B=512 Prefill MEMORY-BOUND ZONE COMPUTE-BOUND Batch more!

Batch Size Is The Lever

Arithmetic intensity = B FLOPs/byte — directly proportional!

BatchTFLOPSTokens/secEfficiency
1 3.4 10,000 0.17%
8 27.0 80,000 1.35%
32 107.0 316,000 5.4%
128 430.0 1,270,000 21.5%
512 1,720.0 5,100,000 86.0%

You need batch ≈ 600 to saturate an H100. This is why production serving systems need continuous batching, complex scheduling, and memory management — maintaining large effective batch sizes is the entire game.

The Latency-Throughput Tradeoff

You can’t have both — choose your SLA

Batch Size Latency (ms/tok) Throughput (tok/s) 1 8 32 128 512 0.1ms 1.3ms 10K 5.1M Latency Throughput
BatchLatency/tokThroughputVibe
10.1 ms10K tok/sFast for one user
320.3 ms316K tok/sGood balance
1280.8 ms1.27M tok/sProduction sweet spot
5121.3 ms5.1M tok/sMax throughput

The fundamental tension: Each user wants low latency. The system wants high throughput. Production serving is the art of balancing both.

Chapter 4

GPU Architecture

Kernels, memory hierarchy, and profiling

GPU Architecture — The Hierarchy

Thread Hierarchy

Gridall thread blocks
Blocksrun on one SM each
Warps32 threads, lockstep SIMT
Threadsindividual execution units

H100: 132 SMs, each with CUDA cores + Tensor cores

Memory Hierarchy

LevelBandwidthSize (H100)
Registers~20 TB/s256 KB/SM
Shared Mem~15 TB/s228 KB/SM
L2 Cache~8 TB/s50 MB
HBM (Global)3.35 TB/s80 GB
Registers
20 TB/s
Shared
15 TB/s
L2
8 TB/s
HBM
3.35 TB/s

Classic optimization: load from HBM to shared memory once, access it many times.

Hello CUDA — Vector Addition

The simplest GPU kernel

// CUDA vector addition kernel
__global__ void vector_add(
    float* a, float* b,
    float* c, int n) {
  int idx = blockIdx.x * blockDim.x
           + threadIdx.x;
  if (idx < n) {
    c[idx] = a[idx] + b[idx];
  }
}

// Launch configuration
int block_size = 256;
int num_blocks = (n + 255) / 256;
vector_add<<<num_blocks, block_size>>>(
    d_a, d_b, d_c, n);

Thread Indexing

idx = blockIdx.x × blockDim.x + threadIdx.x

ConceptMeaning
__global__Callable from CPU, runs on GPU
blockIdx.xWhich block am I in?
blockDim.xThreads per block (256)
threadIdx.xMy index within the block
if (idx < n)Bounds check for edge blocks
<<<B, T>>>Launch config: blocks, threads

Block size 256 = 8 warps of 32 threads. (n + 255) / 256 = ceiling division for full coverage.

Profiling — Where Time Actually Goes

nsys profile -o output python inference.py

GEMM (matmul)
45.2%
FlashAttention
23.1%
Elementwise
12.4%
RMSNorm
8.2%
Other
11.1%
GEMM + Attention = 68.3%
# Profile your inference script
$ nsys profile -o output python inference.py
$ nsys stats output.nsys-rep

# Label regions with NVTX
from torch.cuda import nvtx
with nvtx.range("prefill"):
    output = model(input_ids)
with nvtx.range("decode"):
    for i in range(100):
        output = model(token)

The Lesson

“Profile before optimizing. Your intuition is probably wrong.”

Optimizing the 8% RMSNorm? Waste of effort.
Optimizing the 68% GEMM + Attention? That moves the needle.

Watch for: Gaps between kernels (launch overhead), cudaMemcpy stalls, sequential kernels that could be parallel.

Chapter 5
Kernel Optimization

Coalescing, shared memory, tensor cores, and Triton

Making every transistor count

Memory Coalescing — Rule #1

Adjacent threads must access adjacent memory

Coalesced Access
T0
T1
T2
T3
...
T31
↓ ↓ ↓ ↓      ↓
A[0]
A[1]
A[2]
A[3]
...
A[31]

1 transaction (128 bytes)

Strided Access (stride=2)
T0
T1
T2
T3
↓      ↓      ↓      ↓
A[0]
A[1]
A[2]
A[3]
A[4]
A[5]
A[6]
A[7]

Multiple transactions (wastes half)

Measured Bandwidth (H100 SXM)

Coalesced
2988 GB/s (89%)
Stride-2
778 GB/s (23%)

3.8x degradation

Just reading every other element drops bandwidth by 77%. The hardware is incredibly sensitive to access patterns.

Shared Memory — The Programmer's Cache

  • Load from HBM to shared memory once, access many times
  • Up to 164 KB per SM, 20-30 cycle latency vs 300 cycles for global
  • 32 banks — stride of 32 causes bank conflicts (serialization)

Bandwidth Comparison

Shared Mem
~15 TB/s
HBM
~3.35 TB/s

4.5x faster bandwidth, 10-15x lower latency

Tiled Reduction Pattern

__global__ void sum_reduce(
    float* data, float* result, int n) {
  __shared__ float smem[256];
  int idx = blockIdx.x * blockDim.x
            + threadIdx.x;
  smem[threadIdx.x] =
    (idx < n) ? data[idx] : 0.0f;
  __syncthreads();
  for (int s = blockDim.x/2; s > 0; s >>= 1) {
    if (threadIdx.x < s)
      smem[threadIdx.x] += smem[threadIdx.x+s];
    __syncthreads();
  }
  if (threadIdx.x == 0)
    atomicAdd(result, smem[0]);
}

Tensor Cores — The Matrix Engines

4x4 matrix multiply-accumulate in one instruction

D4x4
=
A4x4
x
B4x4
+
C4x4

128 FLOPs per cycle per core

Tensor Cores
~2000 TFLOPS (fp16)
CUDA Cores
~67 TFLOPS

30x faster for matrix math

Dimensions Must Be Multiples of 16

Dim 1024 120 TFLOPS
Dim 1025 80 TFLOPS

33% performance drop from a single extra element!

Takeaway: Pad your matrices to multiples of 16. Always.

Triton — GPU Kernels in Python

Block-based programming model with auto-tuning

  • Block-level programming (not per-thread like CUDA)
  • Auto-handles coalescing, shared memory, tensor cores
  • @triton.autotune searches for best block sizes
  • 573 TFLOPS on 4096x4096 matmul (83% of cuBLAS)

A 10-line Triton kernel gets 83% of cuBLAS. Decades of hardware evolution accessible through simple Python.

@triton.jit
def matmul_kernel(
    a_ptr, b_ptr, c_ptr, M, N, K,
    BLOCK_M: tl.constexpr,
    BLOCK_N: tl.constexpr,
    BLOCK_K: tl.constexpr):
  pid_m = tl.program_id(0)
  pid_n = tl.program_id(1)
  offs_m = pid_m*BLOCK_M + tl.arange(0, BLOCK_M)
  offs_n = pid_n*BLOCK_N + tl.arange(0, BLOCK_N)
  acc = tl.zeros((BLOCK_M, BLOCK_N),
                dtype=tl.float32)
  for k in range(0, K, BLOCK_K):
    a = tl.load(a_ptr + ...)
    b = tl.load(b_ptr + ...)
    acc += tl.dot(a, b) # Tensor cores!
  tl.store(c_ptr + ..., acc)

Chapter 6
FlashAttention

The Attention Bottleneck, Solved

The N2 matrix never needs to exist

The Attention Bottleneck

Standard attention materializes an N x N matrix

32 MB

per attention head at seq_len=4K
40962 x 2 bytes

32 GB

for all attention matrices
32 heads x 32 layers

Deeply Memory-Bound

Arithmetic intensity: only 32 FLOPs/byte vs H100 ridge point of 600 FLOPs/byte. That is 18.75x below the compute ceiling.

Attention Matrix Size Scaling

N=1,024 → 2 MB (fits in L2)
N=4,096 → 32 MB (does NOT fit in L2)
N=16,384 → 512 MB (repeated HBM trips)

GPU SRAM per SM: only ~200 KB

Online Softmax — The Key Trick

Single pass instead of three

Standard Softmax (3 passes):

Pass 1: m = max(x)  [full row read]
Pass 2: exp(xi - m) for all i  [full row read]
Pass 3: sum → normalize  [full row read]

Online Softmax (1 pass):

def online_softmax(x):
  m, d = float('-inf'), 0.0
  for xi in x:
    m_new = max(m, xi)
    # Correction factor!
    d = d * math.exp(m - m_new) \
      + math.exp(xi - m_new)
    m = m_new
  return [math.exp(xi-m)/d for xi in x]

Step-Through

Online Softmax Walkthrough
Input: [3, 1, 5, 2, 4]
m = -inf   d = 0.0
Press "Next" to process each value...

Tiled Attention — Never Materialize N2

Break Q into blocks, iterate over K/V blocks

Q rows x K columns (click Next to process)

       K block 0   K block 1   K block 2   K block 3
Q blk 0
B x B
B x B
B x B
B x B
Q blk 1
B x B
B x B
B x B
B x B
Q blk 2
B x B
B x B
B x B
B x B
Q blk 3
B x B
B x B
B x B
B x B

SRAM Budget Per Tile

Q block: B x d = 8 KB
K block: B x d = 8 KB
V block: B x d = 8 KB
Scores: B x B = 4 KB
Total: 28 KB

28 KB per tile in SRAM vs 32 MB for the full attention matrix. ~1,000x reduction.

(B=64, d=128, fp16)

FlashAttention — Results

MetricStandardFlashAttention
Memory O(N2) O(N)
Kernel Speed 1x 2-4x faster
BERT-large E2E baseline 15% faster
Max Seq Length ~8K (OOM) 65K+

15% E2E seems modest because attention is a fraction of total compute. The kernel-level speedup (2-4x) is what matters.

FlashInfer: Inference-Specialized

  • Variable sequence lengths (ragged tensors, no padding)
  • Paged KV cache support
  • Separate prefill vs decode kernels
BatchSDPAFlashInferSpeedup
B=10.02ms0.02ms1x
B=80.11ms0.03ms3.7x
B=320.41ms0.10ms4.1x

GQA: 32 heads / 8 KV heads, seq=2048

Chapter 7
Continuous Batching

The Serving Revolution

Just check after every iteration

Static Batching — The Problem

8 requests, padded to the longest output

Static Batch: 8 Requests (max_len = 200)
Req 0: ##################################################______________________________________________________________________________________________________________________________________________________________ 50/200
Req 1: ####################################################################################################____________________________________________________________________________________________________ 100/200
Req 2: ###########################################################################_____________________________________________________________________________________________________________________________ 75/200
Req 3: ######################################################################################################################################################################################################## 200/200
Req 4: ##################################################______________________________________________________________________________________________________________________________________________________________ 50/200
Req 5: ######################################################################################################################__________________________________________________________________________________ 150/200
Req 6: ###########################################################################_____________________________________________________________________________________________________________________________ 75/200
Req 7: ######################################################################################################################################################################################################## 200/200
900

useful tokens

700

wasted (padding)

56%

efficiency

Nearly half your GPU is doing nothing useful. At $2-3/hr for an H100, that is $1-1.50/hr burned.

The Orca Insight — Iteration-Level Scheduling

After EACH forward pass: remove completed, add waiting

Static: wait for all to finish

Step 50: [R0 done] R1 R2 R3 R4 R5 R6 R7
... R0 sits idle for 150 more steps ...
Step 200: [all done] → return batch
New requests: waiting in queue...

Continuous: check every step

Step 50: [R0 done] → add R8
Step 75: [R2,R6 done] → add R9, R10
Step 100: [R1 done] → add R11
Batch always full! No wasted slots.

GPU Utilization

Static
56%
Continuous
95%+

36.9x Throughput Improvement

Orca vs FasterTransformer on GPT-3 175B. Not a marginal optimization — a paradigm shift.

OSDI 2022 — Yu et al.

Request Lifecycle

State machine + scheduler strategies

WAITING
↓
PREFILLING
↓
DECODING
↓
COMPLETED
↓
PREEMPTED
↑ back to WAITING

Scheduler Strategies

StrategyTradeoff
FCFS Fair but not optimal — long requests block short ones
SJF Min avg latency — requires length estimation
Priority Premium vs free tier, deadline-aware

Preemption

Swap KV cache to CPU when a high-priority request arrives. Resume later without recomputing.

Radix Cache — Reuse KV Across Requests

100 requests sharing system prompt? Compute KV once, not 100 times

Radix Tree (prefix → KV cache)
[root]
Press "Add Prompt" to build the tree...

Savings by Workload

Chat + sys prompt
20-40%
Multi-turn
50%+
Batch templates
70%+
def match_prefix(self, tokens):
  node, matched = self.root, 0
  for token in tokens:
    if token in node.children:
      node = node.children[token]
      matched += 1
    else:
      break
  return node, matched

PagedAttention — Virtual Memory for GPUs

Decades of OS research applied to GPU memory

Naive: Contiguous Allocation

Seq A
Seq B
free
Seq C
free
Seq D

Fragmented! New sequence cannot use scattered free blocks.

Paged: Fixed-Size Blocks

A.0
B.0
A.1
C.0
B.1
D.0
B.2
C.1

No fragmentation! Block table maps logical → physical.

Block Table

SequenceLogicalPhysical
Seq A0, 10, 2
Seq B0, 1, 21, 4, 6
Seq C0, 13, 7
Seq D05
  • No external fragmentation
  • Copy-on-write for beam search
  • Waste: at most 1 block per sequence

Just like OS virtual memory: logical-to-physical mapping, page tables, CoW. Same idea, new hardware.

Chapter 8
Advanced Scheduling

Production refinements for serving at scale

Chunked prefill, mixed batches, CUDA graphs, overlap

Chunked Prefill — Don't Block Everyone

A 32K prompt blocks ALL decode users for 3 seconds

Monolithic Prefill

GPU: |========== 32K PREFILL (3000ms) ==========|
Decode users: BLOCKED — waiting 3000ms...

Chunked Prefill (512-token chunks)

GPU: PDPDPDPDPD ... x64
Decode: runs between chunks! Max delay 47ms
P = prefill chunk, D = decode step
3000ms

monolithic decode delay

47ms

chunked max delay

64x Improvement

in worst-case decode latency. 32K prompt / 512 tokens per chunk = 64 iterations.

Mixed Batches — Prefill + Decode Together

Different operations, different bottlenecks

PrefillDecode
Tokens 512 per request 1 per request
Operation GEMM GEMV
Bottleneck Compute-bound Memory-bound

Naive Padding Waste

Prefill: [512 tokens ################]
Decode1: [1___padding x 511___]
Decode2: [1___padding x 511___]
Efficiency: (512+1+1)/(512*3) = 33%

Smart: Split Attention, Share FFN

Embedding
All tokens together
↓
Prefill Attn
512 tokens, GEMM
Decode Attn
1 token, GEMV
↓ merge ↓
FFN (shared)
Per-token, no batch dimension issues
↓
LM Head
All tokens together

CUDA Graphs — 300 Launches → 1

Record once, replay with a single launch

Kernels per decode step: ~300
Launch overhead per kernel: ~5 us
Total launch overhead: 1.5 ms

Overhead as % of Total Time

Batch 1
30%
Batch 4
~15%
Batch 8
~10%
Batch 32
7.5%

Pre-capture graphs for: 1, 2, 4, 8, 16, 32. Pad to nearest captured size if needed.

Without CUDA Graph

CPU: launch launch launch ... x300
GPU:   k1    k2    k3  ...
Total: 6.5ms (batch 1)

With CUDA Graph

CPU: replay()
GPU: k1 k2 k3 ... k300 (autonomous)
Total: 5.1ms (batch 1) — 1.27x faster
g = torch.cuda.CUDAGraph()
with torch.cuda.graph(g):
  output = model.decode_step(
    input, kv_cache)
# Inference loop
for token in range(max_tokens):
  g.replay()  # single launch!

Overlap Scheduling — GPU Never Waits

CPU prepares step N+1 while GPU runs step N

Sequential: GPU idles during scheduling

GPU:
Compute (10ms)
idle
Compute (10ms)
idle
CPU:
sched
sched

Throughput loss: 2/(10+2) = 17%

Overlapped: CPU and GPU work in parallel

GPU:
Compute N (10ms)
Compute N+1 (10ms)
Compute N+2
CPU:
N+1
N+2
N+3

No idle gaps! Two threads with lock + join synchronization.

The Optimization Stack

How it all connects, bottom to top

L4: Advanced Scheduling chunked prefill, mixed batches, CUDA graphs, overlap
L3: Continuous Batching system-level scheduling — 36.9x throughput
L2: FlashAttention apply kernel tricks to the biggest bottleneck
L1: Kernel Optimization coalescing, shared memory, tensor cores
L0: GPU Hardware H100: 3.35 TB/s bandwidth, 2000 TFLOPS compute

Each layer builds on the one below. Ch 5 teaches HOW to make kernels fast. Ch 6 applies that to attention. Ch 7 moves to system-level scheduling. Ch 8 adds production refinements.

Numbers Worth Memorizing

The key metrics from Chapters 5-8

MetricValue
H100 HBM bandwidth 3.35 TB/s
H100 fp16 tensor compute ~2000 TFLOPS
Ridge point (memory-bound vs compute-bound) ~600 FLOPs/byte
GEMV arithmetic intensity (decode) 0.5 FLOPs/byte
Decode latency (7B, batch=1) ~5 ms
KV cache per token (7B, GQA) 128 KB
Shared memory bandwidth ~15 TB/s (4.5x HBM)
Static batching efficiency ~56%
Continuous batching utilization 95%+
FlashAttention memory complexity O(N) vs O(N2)

Key Terminology

Quick reference for Chapters 5-8

TermDefinition
CoalescingCombining contiguous memory accesses into fewer transactions
Bank ConflictTwo threads hitting the same shared memory bank → serialization
Tensor Core4x4 matrix multiply-accumulate hardware unit
RooflinePerformance vs arithmetic intensity — shows memory/compute ceilings
TritonPython-based GPU kernel language, block programming model
Online SoftmaxSingle-pass softmax with running max correction
FlashAttentionTiled attention that never materializes N2 matrix
TermDefinition
FlashInferInference-specialized attention (variable lengths, paged KV)
Continuous BatchingAdd/remove requests after every iteration
PagedAttentionBlock-based KV cache allocation — no fragmentation
Radix CachePrefix tree for reusing KV cache across shared prefixes
Chunked PrefillProcess long prompts in pieces to avoid blocking decode
Mixed BatchBatch containing both prefill and decode tokens
CUDA GraphPre-recorded GPU operations replayed with single launch

Mixture of Experts

More Parameters, Same Compute

Chapter 9

MoE Architecture

How it works

  • Router selects top_k experts per token
  • Only activated experts run — sparse computation
  • Output = weighted sum of selected expert outputs
  • Router is just a linear projection: d_model → num_experts

Qwen3-30B-A3B

30B total params
3B active per token
8 experts, top_k=2 — "A3B" = Active 3B

Architecture Flow

Token Input x
Router — Linear(4096 → 8)
Top-k Selection (k=2)
Expert FFN × 2 (parallel)
Weighted Combine → output
# Router selects 2 of 8 experts
router_logits = Linear(x)  # 4096 → 8
weights, indices = topk(logits, 2)
output = 0.7 * expert_3(x) + 0.3 * expert_5(x)

The Economics of MoE

Same quality. Fraction of the compute.

Dense 70B MoE 30B-A3B DeepSeek-V3
Total params 70B 30B 671B
Active per token 70B (ALL) ~3B 37B
Memory (fp16) 140 GB 60 GB ~1.3 TB
Compute/token 100% ~10% ~5.5%

Training Cost Comparison

Llama 3
$100M+ (30.8M GPU hrs)
DeepSeek-V3
$5.5M (2.78M GPU hrs)

18x cheaper to train. MoE + FP8 = radical efficiency.

MoE Inference Challenges

More capacity, more complexity

Expert Caching (LRU)

GPU VRAM is a cache over all experts. With 8 experts and room for 4, LRU eviction decides which stay. Same concept as CPU caches applied to neural networks.

Load Balancing

If all tokens route to 2 of 8 experts, 6 sit idle — 75% wasted capacity. Training uses auxiliary losses; inference lives with what the model learned.

GPU Memory Paradox

Must fit all experts in memory even though only k are active per token. MoE 30B needs 60 GB for weights but only 6 GB are "hot" each step.

Expert Parallelism

Distribute experts across GPUs. Token routing crosses GPU boundaries — requires all-to-all communication. DeepSeek-V3 uses 256 experts across many GPUs.

Grouped GEMM

Sort tokens by expert, process contiguous chunks. Batch 1: 0.25 tok/expert (terrible). Batch 256: 64 tok/expert (efficient).

Building a Production
LLM Server

From GPU Kernel to HTTP Response

Chapter 10

The Architecture — 6 Components

HTTP Server (FastAPI + uvicorn) async, 1 worker
Tokenizer Pool + Request Manager ProcessPoolExecutor, 4-8 workers
Scheduler continuous batching, priority queue
Engine (dedicated thread) get_batch() → forward() → emit tokens
KV Cache Manager paged allocation, block_size=16

One uvicorn worker. The GPU is the bottleneck, not the CPU.
Async event loop handles thousands of concurrent connections on a single thread.

The Critical Bridge

Sync Engine Thread ↔ Async Event Loop

Engine Thread (sync, blocking)
def _engine_loop(self):
    while self.running:
        batch = self.scheduler.get_batch()
        if batch is None:
            time.sleep(0.001)
            continue
        outputs = self.model.forward(batch)
        for req_id, token in outputs:
            self.emit_token(req_id, token)
HTTP Handler (async, event loop)
async def get_tokens(self, req_id):
    queue = self.queues[req_id]
    while True:
        token = await queue.get()
        if token is None:
            break
        yield token

loop.call_soon_threadsafe(queue.put_nowait, token)

THE most important line in the entire server. Bridges two worlds.

Streaming & Benchmarking

SSE Streaming Format

// Server-Sent Events over HTTP
data: {"choices": [{"delta": {"content": "Hello"}}]}

data: {"choices": [{"delta": {"content": " world"}}]}

data: {"choices": [{"delta": {}, "finish_reason": "stop"}]}

data: [DONE]

SSE over HTTP — no WebSockets needed. Works everywhere, reconnects gracefully.

Key Metrics (7B on H100)

Metricp50p99
Throughput4,000-6,000 tok/s
TTFT30-60 ms100-250 ms
ITL5-10 ms15-25 ms

p99 vs mean: Mean TTFT of 30ms sounds great. But if p99 is 500ms, 1 in 100 users waits half a second.

Percentiles are the metrics that matter for user experience.

Speculative Decoding

Trade cheap compute for expensive memory reads

1. Draft Phase (fast, small model)
# Small model proposes 4 tokens
for _ in range(4):
    token = draft_model.decode_step()
    speculated.append(token)

Draft model is small & fast.
4 decode steps, cheap memory reads.

2. Verify Phase (one pass, big model)
# Target model verifies ALL 4 at once
# Same cost as 1 decode step!
logits = target_model.forward(speculated)
# Rejection sampling: exact distribution
ratio = target_p / draft_p
accept if random() < ratio

Verification is like prefill —
multiple tokens, one memory read.

Correct guesses save everything.
4 tokens for the cost of ~1

Wrong guesses cost nothing.
Just discard and sample normally

Expected speedup: 2-3x
Output distribution is EXACT

The State of
LLM Inference in 2026

What has changed since the book was written

2026 Update

FlashAttention-3 & New Hardware

FlashAttention-3 (NeurIPS 2024)

  • Warp-specialized async: producer warps (data movement) + consumer warps (compute) overlap
  • Interleaved matmul & softmax — both run simultaneously
  • FP8 path with block quantization — 2.6x more accurate than naive FP8

740 TFLOPS on H100 (75% util)
1.2 PFLOPS with FP8 path

Hardware Generations

H100B200GB200 NVL72
Inference vs H200 baseline 4x 30x
Native FP4 No NVFP4 NVFP4
Cost/M tokens $1.56 — $0.10
FP4 PFLOPS N/A 10 —

AMD MI350

Native FP4 + FP6 support. 4x over MI300X. A credible alternative for memory-bound decode.

Serving Frameworks Evolution

The 2026 landscape

NVIDIA Dynamo — datacenter-scale orchestration Rust + Python, NIXL transfers
vLLM V1 — torch.compile by default KVConnector, 218+ models, disaggregated P/D
SGLang — RadixAttention 85-95% cache hit, 5-6x throughput
TensorRT-LLM — Blackwell-optimized FP4 GEMM, attention DP, fastest on B200
LMCache — distributed KV cache layer GPU → CPU → disk → Redis → remote

Key shift: torch.compile replacing hand-written kernels (vLLM V1). Radix-tree KV reuse as a new caching paradigm (SGLang). Datacenter orchestration layer above engines (Dynamo).

Speculative Decoding Matures

From research to production (2025-2026)

EAGLE-3 (NeurIPS 2025)

2-6x speedup
Production-ready. Supported in vLLM + SGLang.

SpecBundle (Dec 2025)

Open-source EAGLE-3 draft models for targets from 8B to 1T parameters. Up to 4x speedup with SGLang.

Apple ReDrafter

2.8x on H100, 2.3x on Apple Silicon
RNN draft model + dynamic tree attention

Production Adoption

  • Google Search — spec decoding powers AI Overviews, same quality, faster
  • Vertex AI — EAGLE-3 integration for production inference
  • Red Hat / vLLM — "Speculators" library, standardized spec decode

The Caveat

Performance is fragile and variable. At higher batch sizes, verification overhead negates gains. Best for latency-sensitive interactive scenarios: chat, code completion, real-time assistants.

Disaggregated Prefill/Decode

The dominant production pattern of 2026

Prefill Workers

GPU utilization90-95%
BottleneckCompute-bound
Ops/byte200-400
ParallelismTensor Parallelism

KV cache transfer via NVLink / RDMA

↓↓↓

Decode Workers

GPU utilization20-40%
BottleneckMemory-bound
Ops/byte60-80
ParallelismData Parallelism

Why Disaggregate?

Prefill and decode have radically different compute profiles. Different hardware for different regimes.

Results

Throughput
6.4x improvement
Latency variance
20x reduction

Production Deployments

  • Meta, LinkedIn, Mistral, HuggingFace via vLLM
  • NVIDIA Dynamo + NIXL for KV transfer
  • Emerging: intra-GPU disaggregation (Nexus)

This follows directly from the roofline. Prefill sits on the compute-bound side; decode sits on the memory-bound side. Different hardware for different regimes.

The 2026 Production Stack

The full modern inference architecture

Request Router / Load Balancer LLM-aware routing
NVIDIA Dynamo / Orchestrator dynamic GPU scheduling, disagg P/D
Prefill Workers (TP) ↔ Decode Workers (DP) vLLM V1 / SGLang / TensorRT-LLM
LMCache / NIXL KV Transfer Layer GPU → CPU → disk → Redis → remote
Model: FP8 (Hopper) / NVFP4 (Blackwell) FlashAttention-3 + EAGLE-3 spec decode

Attention
FlashAttention-3
740 TFLOPS (FP16)

Speculation
EAGLE-3 via SpecBundle
2-6x latency reduction

Precision
NVFP4 native on Blackwell
3.5x memory reduction

Everything Connects

The roofline model (Chapter 3) explains it ALL

The Roofline Map
COMPUTE ────── ridge ────── MEMORY
BOUND                           BOUND

Prefill ........|
                |........Decode
Big batch ......|
                |......Small batch
  • Disaggregation: prefill is compute-bound, decode is memory-bound — different hardware for each
  • Batch size: moves you along the roofline — bigger batches push toward compute-bound
  • Speculative decoding: decode has spare compute, use it to verify draft tokens for free
  • FlashAttention: attention was memory-bound — IO-aware tiling makes it compute-bound
  • MoE: sparse compute per token, but same memory bandwidth cost — stays efficient on the roofline

One model, everything follows. Every optimization in this book is explained by understanding where you sit on the roofline and moving to a better spot.

Not magic.
Engineering constrained by physics.

The Physics of LLM Inference — Elliot Arledge

agent37.club

github.com/Infatoshi/physics-llm-inference