CS336 · 第 10 课

推理优化

训练阶段追求稳定地消化大批量 token;推理阶段面对的是动态请求、自回归生成和持续增长的 KV cache。同一套模型权重没有改变,但 workload 改变了,因此性能瓶颈和优化目标也随之改变。

授课:Percy Liang 预计用时:80 分钟 难度:中等 → 困难

Spring 2026 官方来源:Lecture 10 trace · 课程日程

本页是中文学习导读;讲次主题、教师与材料版本以 Stanford 官方页面和讲义为准。


这一讲到底解决什么问题?

前面主要讨论如何高效训练模型;推理面对的是另一种 workload:请求到达时间不固定,生成必须逐 token 推进,KV cache 会随上下文增长,而且延迟与吞吐量往往互相牵制。

因此本讲不把“推理”视为一次更小的训练 forward,而是回答:prefill 和 decode 各在搬什么、算什么,系统怎样在用户延迟、总吞吐和显存容量之间做权衡?

30 秒知识地图

prefill / decode workload → arithmetic intensity → 权重与 KV traffic → 压缩数据(GQA / MLA / quantization)→ 减少必须执行的步骤(speculative decoding)→ 用 continuous batching 与 PagedAttention 管理动态请求。

Prerequisite refresher:三件事不补齐,就无法理解 KV cache

  1. Causal self-attention:位置 \(i\) 只能关注位置 \(1\ldots i\),不能看未来。
  2. Q/K/V:\(Q=XW_Q,\ K=XW_K,\ V=XW_V\);query 与 keys 算相关性,权重再对 values 加权求和。
  3. Autoregressive generation:生成一个 token 后把它追加到 prefix,再预测下一个;后一个 token 依赖更长的历史。

训练与推理:同一权重,不同对象生命周期

Training
tokens → forward → loss → backward → gradients → optimizer → updated weights
memory = weights + activations + gradients + optimizer states + buffers

Inference
prompt → prefill → KV cache → decode one token → append new K/V → repeat
memory = weights + KV cache + temporary activations
         (no gradients, no optimizer states, no backward)

推理不是“训练减掉 optimizer”这么简单:自回归请求新增了随 batch、层数和 context 增长的 KV cache,也新增了 TTFT、inter-token latency 与动态调度问题。

推理的应用场景

关键指标

指标定义重要性
TTFTTime-to-first-token用户等待时间
Latency秒/token生成速度
Throughputtokens/second系统容量

推理的算术强度分析

Prefill vs Generation

阶段并行形态更可靠的 mental model
Prefill一次处理 prompt 中的大量 token,容易形成较大的 GEMM并行度高,因而更容易进入 compute-bound 区域;是否真的 compute-bound 仍取决于 shape、batch、精度和 kernel
Decode MLP每个请求一次生成一个 token,但可把多个请求组成 concurrent batchbatching 提高权重 reuse 和 arithmetic intensity,因此并非永远 memory-bound
Decode attention新 query 必须读取随 context 增长的 KV cache当 \(T=1\) 时 arithmetic intensity 很低,通常强烈受 memory bandwidth 限制

“decode 是 memory-bound”只是入口,不是终点。需要把 MLP 的权重复用与 attention 的 KV cache traffic 分开分析,才能知道 batching 对哪里有效。

Deep Dive · MLP 与 attention 的 arithmetic-intensity 推导

MLP 层的算术强度

# MLP 前向传播(SwiGLU):
# 读 X (B×T×D), W_gate (D×F), W_up (D×F), W_down (F×D)
# 计算: U = X @ W_gate, G = X @ W_up, Y = Swish(G)*U @ W_down
# 写 Y (B×T×D)

FLOPs = 6 × B × T × D × F
Bytes = 4BTD + 6DF + 4BTD + 4BTD + 4BTF + 4BTD
      ≈ 4BTD + 4BTF + 6DF (忽略写入)

# 假设 B*T << D, F:
算术强度 ≈ B*T

# Prefill: B*T 很大 → compute-bound
# Generation: B*T = B(batch size)
#   B=1: 强度 = 1 → memory-bound
#   B=100: 强度 = 100 → 接近 compute-bound

Attention 层的算术强度

# Attention(FlashAttention 优化后):
# 读 Q (B×T×D), K (B×S×D), V (B×S×D)
# 计算: Q @ K^T, softmax, @ V
# 写 Y (B×T×D)

FLOPs = 4 × B × S × T × D
Bytes = 4BSD + 4BTD + 4BTD ≈ 4BSD + 4BTD

算术强度 = S*T / (S + T)

# Prefill: T = S → 强度 = S/2 → 大 S 时 compute-bound
# Generation: T = 1 → 强度 ≈ 1 → memory-bound

# 在这个一阶模型里 B 同时缩放 FLOPs 与 KV bytes,因此不会像 MLP 那样
# 直接提高权重 reuse;真实实现仍受 paged layout、并行度和 kernel 影响。

核心洞察:

Reference · 理想化延迟与吞吐量核算示例
Llama 2 13B 在 H100 上的理论性能:

# Batch size = 1:
# 内存: 参数 26GB (bf16) + KV cache ~0.5GB
# 延迟: ~13 ms/token(受内存带宽限制)
# 吞吐量: ~77 tokens/s

# Batch size = 64:
# 内存: 26GB + 32GB KV cache
# 延迟: ~20 ms/token(更慢)
# 吞吐量: ~3200 tokens/s(更高!)

# 权衡: 大 batch → 高吞吐、高延迟
#        小 batch → 低延迟、低吞吐

这些数字只用于展示 batch 带来的方向性权衡,不应当作特定 serving engine、精度或软件版本的性能承诺。


KV Cache 压缩

先推导:为什么保存 K/V,而不是背定义

设已有 prefix token \(x_1,\ldots,x_n\)。在某一 attention layer,它们已经产生:

Q₁...Qₙ, K₁...Kₙ, V₁...Vₙ

生成新 token \(x_{n+1}\) 时,本层先计算 \(Q_{new},K_{new},V_{new}\)。新的 query 必须与所有允许看到的历史 keys 比较,并用 attention weights 读取历史 values:

\[ a=\operatorname{softmax}\left(\frac{Q_{new}[K_1;\ldots;K_n;K_{new}]^\top}{\sqrt{d_h}}\right),\qquad y_{new}=a[V_1;\ldots;V_n;V_{new}] \]

所以历史 \(K_1\ldots K_n\) 和 \(V_1\ldots V_n\) 在未来每一步还会被读取。若不保存,每生成一个 token 都要让整个 prefix 重新经过各层来重算它们。

为什么不 cache 历史 Q

past Q_i
→ 只在“token i 作为 query”那一次计算自己的 attention output
→ 未来 token 有自己的 Q_new,不再查询 Q_i
→ discard

past K_i / V_i
→ 每个未来 Q_new 都可能匹配 K_i 并读取 V_i
→ cache

KV cache 是由未来数据依赖推出来的,不是因为 K/V 比 Q 特殊或更容易存。

Prefill 与 decode 必须分开

阶段toy 场景做什么主要输出
Prefill一次输入 5000 prompt tokens并行处理整个 prompt;causal mask 保持语义每层 5000 个历史 K/V + 首个 next-token logits
Decode每轮新增 1 token只算新 token 的层输出,同时读取历史 KV新 logits,并向每层 cache 追加一个 K/V

Prefill 的较大 token 维通常更容易形成高效 GEMM,因而常更 compute-heavy;decode 尤其是长上下文 attention,每步计算一个小 query 却读取大量历史 KV,常更 bandwidth-sensitive。这些是 workload-dependent 判断,不是无条件定律。

从 shape 推 KV cache bytes

对 MHA,若每层有 \(n_{kv}\) 个 KV heads、每 head 维度 \(d_h\)、缓存 dtype 每元素 \(s\) bytes,则每个 sequence 的 cache:

\[ M_{KV}=2\times L\times S\times n_{kv}\times d_h\times s \]

其中 2 来自 K 与 V,\(L\) 是层数,\(S\) 是当前 cache token 数。若 batch 中每个请求各有 cache,再乘或逐请求求和;padding、block metadata 与 allocator overhead 另算。

toy MHA: n_kv=64, d_h=128, bf16 s=2 bytes
per token per layer = 2 × 64 × 128 × 2 = 32 KiB

S=100,000, L=80:
32 KiB × 100,000 × 80 ≈ 244 GiB(十进制约 262 GB)

原始数字若把 KB、GB 与 KiB、GiB 混用会看似得到“256GB”。资源账本必须声明单位制;方向结论不变:长 context 与大 concurrent batch 会让 KV 成为主导显存对象。

三个反例

“不用 cache,显存就更省,所以一定更快。”显存少了,但每一步重算 prefix;生成 \(N\) 个 token 时会重复执行大量历史计算。

“cache 全部 Q/K/V 更完整。”历史 Q 没有未来消费者,只增加约 50% cache 元素而不避免重算。

“decode 永远 memory-bound。”大 batch 可显著提高 MLP 权重复用;短 context 或不同 kernel 下瓶颈可能变化。应把 decode MLP 与 decode attention 分开。

GQA(复习)

GQA 把 KV cache 缩小 n_heads / n_kv_heads 倍
Llama 2 70B: 64 Q heads, 8 KV heads → 8× 缩小

Multi-head Latent Attention (MLA)

DeepSeek-V2 的 MLA:
不存完整的 K, V 向量
存一个低秩压缩的 latent 向量 c (C=512 维)
需要时通过上投影恢复

KV cache: 512 + 64 (for RoPE) = 576 维
vs MHA: 64 × 128 × 2 = 16,384 维
压缩比: 28×!

Cross-layer Attention (CLA)

跨层共享 KV:
GQA 是共享 KV across heads
CLA 是共享 KV across layers

例如: 每 4 层共享一组 KV
KV cache 缩小 4×

Local Attention

部分层用 local attention(只关注附近 token)。若实现会淘汰窗口外 K/V,那么这些 local layers 的 cache 不再随完整历史 context 无限增长,而被 attention window size 上界约束;它仍然线性依赖 window size。

混合策略(character.ai):
- 每 6 层有 1 层 global attention
- 其余 5 层用 local attention
- 有效信息传播范围会随 window 与层数扩大,但不等于单层可直接访问完整历史


量化

为什么量化对推理有效?

许多逐 token decode workload 对权重与 KV 的 memory traffic 很敏感
减少表示大小 → 减少数据搬运 → 可能降低延迟并提高吞吐

量化级别:
fp32 (4 bytes) → bf16 (2 bytes) → fp8 (1 byte) → int4 (0.5 byte)

int4 相对 fp32 可把权重表示压缩到约 1/8;实际加速取决于 kernel、量化元数据、batch、瓶颈位置和硬件支持,不能直接等同于 8×。

Post-training Quantization (PTQ)

# 训练后量化: 不需要重新训练

# 简单量化(per-tensor):
scale = max(abs(x)) / 127  # int8
x_quantized = round(x / scale)

# 问题: outlier 会导致精度大幅下降
# LLM 中有 ~0.1% 的 outlier(值特别大的元素)

LLM.int8()

Dettmers et al. (2022):
1. 对大多数元素做 int8 量化
2. 对 outlier 保持 fp16
3. 矩阵乘法: outlier 用 fp16, 其余用 int8

效果: 几乎无损,但 15-23% 更慢(因为混合精度)

AWQ (Activation-aware Weight Quantization)

Lin et al. (2023) 的关键洞察:权重是否难以量化,不能只看权重自身;输入 activation 的分布会放大量化误差。AWQ 用一小批校准 activation 识别 salient channels,再搜索每个输入通道的缩放因子。

方法:
1. 统计 activation,定位对输出更敏感的输入通道
2. 对 activation 与 weight 做数学等价的逆向缩放:一边乘 \(s\),另一边除 \(s\)
3. 把缩放后的权重统一量化到低比特,而不是永久保留一小撮 FP16 权重

Implementation-specific:压缩率与端到端加速取决于 bit width、group size、kernel、batch 与硬件。论文中的单个速度数字不是所有 workload 的保证。

Supplement · 模型剪枝与蒸馏

模型剪枝与蒸馏

Pruning:删除低价值结构

剪枝试图移除对输出影响较小的权重、通道、attention heads 或整个层。非结构化稀疏只减少非零元素,若硬件/库不能跳过稀疏位置,wall-clock 不一定变快;结构化剪枝更容易映射到 dense kernel,但通常更伤精度。

Distillation:让更快的学生模仿教师

Spring 2026 官方 trace 给出的 recipe 是:先定义更快的 student 架构,再用原模型的输出/中间分布初始化或训练它,最后通过蒸馏修复精度。蒸馏可以与 GQA、local attention、较浅网络或更窄 hidden size 组合。

loss = alpha * cross_entropy(student_logits, labels) \
     + (1 - alpha) * kl_div(student_logits / T, teacher_logits / T) * T**2

# 关键权衡:教师生成数据有成本;student 的架构必须真的对应更低推理开销。

投机解码 (Speculative Decoding)

核心思想

关键洞察:验证比生成快。

给定一个序列,大模型可以一次性评估多个 token(像 prefill)。但生成一个 token 需要一次完整的前向传播。

投机解码:用小模型(draft)快速猜几个 token,然后用大模型一次性验证。

算法

投机解码 (Leviathan et al., 2023; Chen et al., 2023)

1. Draft 模型(小)按 \(q\) 生成 γ 个候选 token:\(d_1,d_2,\ldots,d_\gamma\)
2. Target 模型(大)一次并行算出这些位置的 \(p\)
3. 对每个候选抽一个随机数 \(u\sim U(0,1)\):若 \(u\le \min(1,p(d_i)/q(d_i))\),接受 \(d_i\)
4. 首次拒绝时,从归一化后的正差分布 \([p-q]_+\) 采样校正 token,并停止本轮验证
5. 若全部候选都接受,再从 target 的下一个位置采一个 token

为什么不能只写“\(p(d_i)>q(d_i)\) 就接受”?当 \(p<q\) 时仍应有 \(p/q\) 的接受概率;少了这个随机步骤与拒绝校正,最终序列就不再严格服从 target 分布。

Exact:规则正确实现时,输出分布与直接从 target 采样相同。Implementation-specific:是否加速取决于接受率、draft 成本、target 验证 shape 与系统实现。

为什么能加速?

不使用投机解码:
生成 γ 个 token → γ 次大模型前向 → γ × T_large

使用投机解码:
1 次 draft 前向 + 1 次大模型验证
→ γ × T_small + T_verify
→ 只有接受率足够高、draft 足够便宜且 target 能高效并行验证时才会加速

Benchmark-specific:论文与 serving engine 报告的倍数都绑定 target/draft 模型、硬件、batch、prompt/decode 长度、sampling 参数与实现;这里不把某个速度倍数当作普适结论。

Draft 模型的选择


动态批处理

问题:推理的 batch 是动态的

训练: 固定 batch, 固定长度
batch_size × sequence_length × hidden_dim

推理: 请求动态到达,长度不一
请求 1: 已生成 10 tokens
请求 2: 刚到达,prefill 中
请求 3: 已生成 100 tokens

朴素方案: 等所有请求完成再返回
→ 先到的请求被后到的拖慢

Continuous Batching(迭代级调度)

Yu et al. (2022) - Orca
不是等整个 batch 完成,而是每步都可以加入/移除请求

每个 decode step:
1. 检查是否有新请求到达 → 加入 batch
2. 检查是否有请求完成 → 移除
3. 对所有活跃请求做一步 decode

Paper-specific result:Orca 在论文的 GPT-3 175B、指定硬件与请求负载下,相对 NVIDIA FasterTransformer 报告了同等 latency 下最高 36.9× throughput。这个数字不能脱离 baseline、模型、arrival trace 与 latency target 当成 continuous batching 的普适倍数。

Selective Batching

问题: 不同请求长度不同,不能组成规则 tensor

解决方案:
- Attention: 每个序列单独处理(或 padding)
- 非 attention: 拼接所有序列 [total_tokens, hidden_dim]

这样非 attention 部分仍然高效


PagedAttention

问题:KV cache 内存碎片

朴素 KV cache 分配:
每个请求预留 max_seq_len 的空间

问题 1: 内部碎片
大多数请求远不到 max_seq_len
预留的空间浪费

问题 2: 外部碎片
不同请求的 KV cache 大小不一
内存中出现不连续的空洞

PagedAttention(操作系统启发)

Kwon et al. (2023) - vLLM

核心思想: 像操作系统管理内存页一样管理 KV cache

  1. 把 KV cache 分成固定大小的 block(如 16 tokens)
  2. 每个请求有一个 "页表"(block table)
  3. 按需分配 block,不需要连续

效果:
- 内存利用率从 ~40% 提升到 ~96%
- 支持更大的 batch size
- 支持共享前缀(system prompt)

Copy-on-Write

多个请求共享 system prompt 的 KV cache
当某个请求需要修改时,才复制

类似操作系统的 fork()

Serving engines:把算法变成动态系统

系统官方 trace 强调的定位
vLLMPagedAttention 与通用高吞吐 serving 的常用默认选择
SGLangRadixAttention / prefix reuse,适合 agentic 与共享前缀工作负载
TensorRT-LLMNVIDIA GPU 上高度优化的 kernel、量化和多 GPU runtime
llama.cppCPU / 本地推理与紧凑量化生态

引擎名称不是性能保证。最终结果取决于模型结构、量化格式、batch 到达过程、prefix reuse、GPU 型号和调度策略,必须在真实请求分布上测 TTFT、吞吐与尾延迟。


Reference · 超越 Transformer 的其他生成架构

超越 Transformer

State Space Models

Mamba, Jamba, etc.
用 O(1) 状态替代 O(n) KV cache
推理时不需要存储整个 KV cache

但: 不擅长 associative recall
混合架构(Transformer + SSM)是当前趋势

Diffusion Models for Text

非自回归生成
所有 token 同时生成,多步 refinement

优势: 并行生成,速度快
劣势: 文本质量目前不如自回归


总结

推理优化的四个方向

方向技术效果
减少 KV cacheGQA, MLA, CLA, local2-32× 内存节省
减少模型成本量化、结构化剪枝、蒸馏降低权重流量与计算量;收益需实测
投机解码draft model2-3× 加速
系统优化continuous batching, paged attention减少空槽与 KV 碎片;收益依 engine、请求分布与 latency 目标实测

核心原则:


知识检查

问题 1:为什么长上下文的逐 token decode attention 通常特别 bandwidth-sensitive?

问题 2:投机解码为什么能加速推理?

问题 3:PagedAttention 解决了什么问题?

问题 4:Continuous batching 的核心思想是什么?

问题 5:AWQ 量化的核心思想是什么?

Concept Check · 未来 token 为什么需要历史 K/V,却不需要历史 Q?

答案:新 token 用自己的 \(Q_{new}\) 查询所有历史 \(K_i\),再读取对应 \(V_i\)。历史 \(Q_i\) 只在 token \(i\) 产生自身 attention output 时消费过一次,未来没有数据依赖。

若回答“因为 Q 更小/更大”,说明你在用存储大小解释机制;真正原因是张量的未来消费者不同。

Concept Check · 5000-token prompt 的 prefill 是不是生成了 5000 个输出 token?

答案:不是。它并行计算 5000 个 prompt positions 的层表示并建立 KV cache,最后用末位置 logits 开始采样第一个新 token。Prompt tokens 是输入,不是响应输出。

若答“是”,说明你把并行处理输入 positions 与 autoregressive 输出步骤混在了一起。

Concept Check · GQA 把 KV heads 从 64 减到 8,哪些对象不变?

答案:在其他条件不变时,query heads 与模型输出宽度可以不变;每 token 每 layer 的 K/V 元素约缩到 1/8。质量、kernel 效率与实际显存收益仍需实测。

若认为整个模型 FLOPs/参数/显存都自动缩到 1/8,说明你把局部 KV 结构变化错误扩展到了所有对象。


你现在应该能够解释

通往第 11 课:训练最优与部署最优不是同一个目标

Scaling law 若只优化 training loss,可能选出部署时权重/KV 太大、延迟太高的模型。下一课会把 inference cost 加进外推与决策,并区分拟合区间内证据和区间外假设。


如果只能记住 5 件事

  1. Prefill 与 decode 是不同 workload,不能用同一个“compute-bound / memory-bound”标签概括全部推理。
  2. 较大的 \(B\times T\) 让 prefill 更容易形成高效 GEMM;decode MLP 也能通过 batching 提高权重复用。
  3. Decode attention 必须持续读取随 context 增长的 KV cache,因此通常特别受 memory bandwidth 限制。
  4. GQA / MLA、量化和 speculative decoding 分别减少 KV、权重表示或串行生成步骤。
  5. Continuous batching 与 PagedAttention 解决动态请求和 KV 内存管理;最终应同时测 TTFT、inter-token latency、吞吐与尾延迟。

延伸阅读

Reference · 论文与系统实现