DAY 14 / PHASE 2 · SYSTEMS

Inference Optimization

Memory-Bound · Continuous Batching · KV Cache · Speculative Decoding

2026-05-28 · BigCat

推理优化的全部秘密,是喂饱一块被内存带宽饿着的 GPU。

// WHY THIS MATTERS

当你从「调 API」走向「自己 host 开源模型」——成本、延迟、吞吐瞬间变成你的工程问题。大多数人第一反应是「买更强的卡」,结果换了 H100 吞吐只涨 20%,钱白烧。原因是一个反直觉的事实:LLM 的逐 token 解码不是 compute-bound,是 memory-bound——GPU 的算力大半在空转,瓶颈是把模型权重从显存搬进计算单元的带宽。理解这一点,下游所有优化(continuous batching、KV cache 分页、speculative decoding、量化)就串成一条线:它们都在回答同一个问题——怎么用一次权重搬运,多算几个 token。这一期不讲「推理是什么」,讲资深工程师真正要做的决策:怎么定位瓶颈、四个杠杆的边际收益、各自的失败模式,以及 vLLM / TGI / TensorRT-LLM 怎么选。

// 01

Decode 是 memory-bound:continuous batching 是第一杠杆

论断:单请求推理在浪费 90%+ 的 GPU;提吞吐的第一步不是换卡,是 continuous batching。

背景与原理

推理分两段,瓶颈完全不同:prefill(处理 prompt)能并行算所有 token,是 compute-bound;decode(逐个生成)每步只算 1 个 token,却要把整个模型权重重新从 HBM 搬一遍——算术强度极低,GPU 的 Tensor Core 大半闲置,真正的瓶颈是显存带宽。这就是为什么 batch=1 时再贵的卡也跑不满。

解法是把多个请求的同一 decode step 拼成一个 batch:权重搬一次,同时为 N 个请求各算一个 token,吞吐近似线性放大。但传统 static batching 有致命缺陷——它等整个 batch 里最长的序列生成完才能释放,短请求被长请求拖死,GPU 利用率随生成进度暴跌。Orca(OSDI'22)提出 iteration-level scheduling(即 continuous batching):每个 decode step 结束就动态踢出已完成的、塞入排队的新请求,GPU 永不空转。Anyscale 实测对比 HF 朴素实现达到 23× 吞吐。这是单一改动里收益最大的。

Static batching(等最长序列) Continuous batching(逐 step 调度) step→ 1 2 3 4 5 6 step→ 1 2 3 4 5 6 R1 [██████]................ R1 [██████]R5[████.... R2 [████].................. R2 [████]R6[██████.... R3 [██████████████]........ R3 [██████████████].... R4 [██].................... R4 [██]R7[████]R8[██.. ↑ R1/R2/R4 早完成 ↑ 一空出就立刻补新请求 GPU 却空转等 R3 GPU 利用率拉满

实战示例

# vLLM 默认就是 continuous batching,一行起服务
vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --max-num-seqs 256          # 并发请求上限,决定 batch 能多大

# 诊断你到底是不是 memory-bound / under-batched:
# 压测时盯 nvidia-smi —— 若 GPU-Util 高但 SM 算力占用低、显存带宽打满
# 且吞吐随 batch 几乎线性上涨 → 你被内存带宽卡住,加 batch 就赚
nvidia-smi dmon -s u        # 看 sm% 与 mem% 的差距

记住分工:throughput(吞吐)靠加 batch,latency(延迟)靠减 batch——这是一对零和。先想清你优化的是哪个。

失败模式:(1)低 QPS 场景硬上大 batch——没那么多并发请求可拼,batch 永远填不满,收益约等于零。(2)只看吞吐忽视 TTFT(首 token 延迟):batch 越大,新请求排队越久,交互式应用体感反而变差。throughput 与 latency 必须分开定指标。
进阶资源 · Orca(continuous batching 起源), usenix.org · OSDI'22 · Anyscale How continuous batching enables 23x throughput, anyscale.com/blog
// 02

KV cache 才是内存霸主:PagedAttention + prefix caching

论断:限制 batch size 的往往不是模型权重,是 KV cache;把它当虚拟内存管(分页 + 前缀复用)能直接翻倍吞吐。

背景与原理

continuous batching 想拉大 batch,但 batch 越大、序列越长,KV cache 吃的显存越多——长上下文下它能超过权重本身。大小估算(标准公式,非引用具体数字):

KV_bytes ≈ 2 × n_layers × n_kv_heads × head_dim
           × seq_len × batch × dtype_bytes
# 「2」= K 和 V 两份;一条 8K 上下文的请求,KV 可达几百 MB

传统做法给每个请求连续预留「最大长度」的显存,但请求实际长度不一,导致 60-80% 显存被碎片和过度预留浪费——能放下的 batch 被白白压小。PagedAttention(vLLM 的核心,SOSP'23)照搬操作系统的虚拟内存:把 KV cache 切成固定大小的 block(页),非连续存储、按需分配,浪费降到 4% 以下,等价于 batch 翻倍、吞吐 2-4×。

第二招是 prefix caching:多个请求若共享相同前缀(system prompt、few-shot 示例、长文档),那段前缀的 KV 只算一次、跨请求复用。这正是 Anthropic prompt caching 在服务端的原理——你把稳定内容放前面、变动内容放后面,命中缓存就省掉重复 prefill。

实战示例

# vLLM 开 prefix caching + 量化 KV cache(再省一半显存)
vllm serve  \
  --enable-prefix-caching \    # 相同前缀的 KV 自动复用
  --kv-cache-dtype fp8         # KV 用 fp8,显存腰斩、吞吐再涨

# prompt 结构:稳定前缀在前,变动在后 —— 才能命中前缀缓存
[system + 工具定义 + few-shot]   ← 跨请求逐字节相同 → 缓存命中
[用户本轮输入]                   ← 每次不同 → 只 prefill 这一小段
失败模式:(1)prefix caching 只在前缀逐字节相同时命中——在 system prompt 里塞了时间戳 / 随机 ID / 动态拼接顺序错乱,命中率瞬间归零。(2)fp8 KV cache 在长上下文 + 高精度任务(数学、长链推理)会掉点,上线前必须用自己的 eval 测,别只看吞吐数字。
进阶资源 · vLLM PagedAttention 论文, arXiv:2309.06180 · Anthropic Prompt Caching 文档, docs.claude.com/.../prompt-caching
// 03

Speculative decoding:小模型偷跑,吞吐翻倍且输出不变

论断:既然 decode 是 memory-bound,一次搬权重多验几个 token 几乎免费——draft-then-verify 是无损 2-3× 加速。

背景与原理

关键洞察接着 §1:decode 时 GPU 算力本来就闲着,那不如一次 forward 多算几个 token 的概率。Speculative decoding(Leviathan et al., ICML'23)让一个便宜的 draft 模型先快速猜出 k 个候选 token,再让大模型一次并行验证这 k 个——接受最长的正确前缀,在第一个分歧点重采。draft 猜得准就一步前进多个 token,猜错也只是回退到正常速度。

最反直觉的一点:通过 rejection sampling,最终输出分布与原模型严格一致——这是无损加速,不是近似。加速比取决于 acceptance rate(draft 猜中率)与 draft/target 的成本比。Medusa(arXiv 2401.10774)更进一步:不用单独 draft 模型,给原模型加几个轻量 decoding head + tree attention 自己猜自己,省掉维护两个模型的麻烦,实测 2.3-3.6×。

┌─ draft 模型猜 k 个 ─┐ prompt ──▶ "the cat sat on" ──▶ draft: [" the"," mat"," and"," ran"] │ (便宜、快) ▼ 大模型一次并行验证这 4 个候选 ──▶ 接受 " the"," mat" ✓✓ 第 3 个分歧 ✗ → 重采 " ." 结果:一次大模型 forward 前进了 3 个 token(而非 1 个) 分布与原模型完全相同(rejection sampling 保证无损)

实战示例

# vLLM:用小模型当 draft(必须同 tokenizer,小 10-20×)
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --speculative-model meta-llama/Llama-3.2-1B-Instruct \
  --num-speculative-tokens 5     # 一次猜 5 个,太多反而拖慢

# 无现成 draft 模型时,用 n-gram 投机(零额外模型,靠上文重复)
  --speculative-model "[ngram]" --ngram-prompt-lookup-max 4
失败模式:(1)高 batch 下收益消失——batch 大时 GPU 已经 compute-bound(§1 的空闲算力被填满了),没有余力做投机验证,反而因 draft 开销变慢。Speculative decoding 是低并发 / 重交互场景的杠杆。(2)draft 与 target 分布差太大 → acceptance 低 → 净亏。代码、结构化输出 acceptance 高(套路强),自由创作低。
进阶资源 · Leviathan et al. Fast Inference via Speculative Decoding, arXiv:2211.17192 · Medusa(多 decoding head), arXiv:2401.10774
// 04

选引擎与量化:vLLM / TGI / TensorRT-LLM 的工程权衡

论断:没有「最快引擎」,只有匹配你 workload + 运维约束的引擎;量化是第二大杠杆,但有暗坑。

背景与原理

三大开源引擎都实现了 §1-§3 的核心优化,差别在峰值性能 vs 部署灵活 vs 上手成本这三角的取舍:

第二大杠杆是量化:把权重从 fp16 压到 int4/fp8。因为 decode 是 memory-bound(§1),权重更小 = 搬运更快 = decode 直接加速,同时省显存能放大 batch。weight-only int4(AWQ / GPTQ)显存腰斩还提速;fp8(权重+激活)需 Hopper 及以上架构,精度损失更小。

实战示例

# 决策速记
快速验证 / 自建 / 迭代频繁     → vLLM
HF 生态 / 要稳定生产运维       → TGI
NVIDIA 卡榨干性能 / 模型稳定   → TensorRT-LLM

# 量化选型
显存紧、要塞下大模型           → AWQ int4(weight-only)
Hopper+、要精度也要速度        → fp8(权重 + KV cache)
vllm serve  --quantization awq   # 一个 flag 开
失败模式:(1)TensorRT-LLM 编译产物锁定 GPU 架构 + 配置——换卡、换 batch/seq 上限都要重编,CI/CD 复杂度陡增,别为了 10% 吞吐过早上它。(2)int4 量化在推理 / 数学任务掉点明显大于闲聊,必须跑自己的 eval;只比吞吐不比质量是最常见的自欺。

// 综合实战 · 给开源模型搭一个会自我度量的推理服务

把四点串成一个周末项目:用 vLLM host 一个 8B 模型,逐个叠优化、量边际收益——这是把「我知道」变成「我做过」的唯一办法。

  1. 定指标,分开测TTFT(首 token 延迟)、TPOT(每 token 延迟)、throughput(总 tokens/s)。用 vLLM 自带 benchmark_serving.py 压测,别用单条请求测吞吐。
  2. 基线:默认 continuous batching,扫 --max-num-seqs 从 16 到 256,画吞吐-延迟曲线,找到你的「knee」(拐点)。
  3. 加 §2:开 --enable-prefix-caching,构造共享 system prompt 的请求流,看命中后 TTFT 掉多少。
  4. 加 §3:低并发场景挂一个 1B draft 模型,量 acceptance rate 与 TPOT 变化——再把并发拉高,亲眼看着投机收益消失(验证 §3 失败模式)。
  5. 加 §4:换 AWQ int4 版本,对比显存占用、能放大多少 batch、吞吐涨多少——再用 10 道有标准答案的题测质量是否掉点

做完这一圈,你对任何「我们推理快 N 倍」的宣传都会条件反射地追问:测的是 TTFT 还是吞吐?什么并发?质量掉了没?——而不是被一个孤立的吞吐数字带跑。

// ENGLISH GLOSSARY

Prefill
处理输入 prompt 的阶段,可并行算全部 token,compute-bound。
Decode
逐个生成输出 token 的阶段,每步重搬全部权重,memory-bound。
Memory-bound
瓶颈在显存带宽而非算力——LLM 解码的本质特征,下游优化的总纲。
Continuous Batching
iteration-level scheduling:每个 decode step 动态增删请求,GPU 不空转。
KV Cache
缓存历史 token 的 Key/Value,避免重算;长上下文下吃显存的主力。
PagedAttention
把 KV cache 当 OS 虚拟内存分页管理,消除碎片,vLLM 核心。
Prefix Caching
跨请求复用相同前缀的 KV;prompt caching 的服务端原理。
Speculative Decoding
draft 模型猜、target 模型并行验证,无损 2-3× 加速。
TTFT / TPOT
Time-To-First-Token / Time-Per-Output-Token,交互式延迟的两个核心指标。
Weight-only Quantization
只量化权重(如 AWQ int4),省显存且加速 memory-bound 的 decode。

// 深入思考

continuous batching 提吞吐、speculative decoding 也提吞吐,为什么不能简单叠加?
因为二者抢的是同一份资源:空闲算力。Continuous batching 用大 batch 把闲置算力填满,此时系统已逼近 compute-bound;speculative decoding 恰恰需要「有空闲算力来并行验证投机 token」才划算。batch 一大,验证 token 没地方算,draft 开销纯亏。所以经验法则:高并发服务 靠 batching,低并发/单用户交互(如本地 coding agent)靠 speculative。生产系统常按当前负载动态切换,而非同时全开。
为什么 prompt caching 能省钱省延迟,但只对「前缀」有效,不能缓存中间或结尾的相同片段?
因为 attention 是因果的(causal):每个 token 的 KV 表示依赖它之前所有 token。前缀相同 → 那段 KV 的计算上下文完全一致 → 可直接复用。但若相同片段出现在中间,它前面的 token 不同,KV 会被不同的上文「染色」,无法复用。这就是为什么工程上要把稳定内容(system / 工具定义 / few-shot)严格放最前、变动内容放最后——这是 prefix caching 命中率的物理约束,不是约定俗成。
speculative decoding 号称「无损」——输出分布与原模型完全一致。这怎么可能不牺牲质量来换速度?
关键在 rejection sampling 的数学构造:draft 提议分布 q,target 真实分布 p。对每个投机 token,以 min(1, p/q) 概率接受;拒绝时从修正分布 (p−q)⁺ 重采。可以证明最终采样严格服从 p。所以加速来自把串行的多步压缩成一次并行 forward,而非近似。draft 猜得越准(q 越接近 p)接受越多、越快——但即使 draft 是个胡猜的模型,输出质量也不变,只是退化到原速。速度是赌注,质量是保底。
都说「decode 是 memory-bound」,那 Mixture-of-Experts(MoE)模型为什么能在不增加激活算力的前提下做大?这和 memory-bound 矛盾吗?
不矛盾,反而强化了它。MoE 每个 token 只激活一小部分专家,激活的 FLOPs 不随总参数线性涨——但所有专家的权重仍要驻留显存、且被搬运的那部分也走 HBM。于是 MoE 把瓶颈进一步推向「内存容量 + 带宽 + 专家路由的不规则访存」。这就是为什么 MoE 推理的核心难题是显存与专家并行(expert parallelism)/ 调度,而不是算力。memory-bound 的视角恰好解释了 MoE 的工程重心在哪。
如果只能给一个团队一句推理优化建议,且他们 QPS 很低(内部工具,个位数并发),你会说什么?
「别碰 batching,先上 speculative decoding + 量化。」低 QPS 下 continuous batching 没请求可拼,吞吐优化几乎无用;用户痛的是单次延迟。此时 GPU 算力大量空闲,正是 speculative decoding 的最佳土壤——直接砍 TPOT。再叠 weight-only int4 量化,既加速 memory-bound 的 decode,又让小卡也能跑。把昂贵的多卡 batching 方案留给真正高并发的生产服务——用错杠杆比不优化更浪费钱。

// 延伸阅读