Transformer 解剖:从 Attention 到推理系统
第 14 章 两阶段推理:Prefill 与 Decode 的不同性格
第 14 章 两阶段推理:Prefill 与 Decode 的不同性格
第 1 章我们说,Transformer 在训练时全面胜过 RNN,但在推理时反而要花精力优化。这一章开启全书最厚的部分——推理系统——回答「模型训完之后,怎么把它高效跑给用户用」。
第一件要看清楚的事:LLM 推理不是一次「forward pass」,而是分成两个截然不同的阶段。如果你把它们当成一回事来调度,得到的会是一个利用率 5-10% 的推理引擎;理解了它们的差别,能把利用率推到 50% 以上。
读完这章你能:
- 解释 Prefill 和 Decode 阶段的输入、输出、计算量、显存模式各自是什么;
- 区分 compute-bound 和 memory-bound 两种瓶颈,并定位每阶段属于哪种;
- 解读 LLM 推理的两个核心指标 TTFT(Time to First Token)和 TPOT(Time per Output Token);
- 理解 continuous batching 为什么是 LLM 推理的核心调度技术;
- 看懂 PD 分离(Prefill-Decode Separation)这种新一代推理架构。
14.1 一次推理实际在做什么
我们用一个具体例子展开。假设用户发了一个 query:
"请用 100 字介绍一下 Transformer 的核心思想。"
模型要做两件事:
第一件:把整个 prompt(27 个汉字 ≈ 30 token)「读」一遍,理解它问的是什么。
第二件:根据理解一个一个生成回答 token——直到生成 100 个字(约 100 token)后停止。
flowchart LR Q["用户 query<br/>30 token"] --> PRE["Prefill<br/>一次性处理 30 token"] PRE --> KV1["KV Cache (30 token)"] KV1 --> D1["Decode<br/>生成 1 个 token"] D1 --> KV2["KV Cache (31 token)"] KV2 --> D2["Decode<br/>生成 1 个 token"] D2 --> KV3["KV Cache (32 token)"] KV3 --> DOTS["...重复 99 次..."] DOTS --> A["完整回答<br/>100 token"]
可以看到 prefill 跑一次(处理 30 个输入 token),decode 跑 100 次(每次生成 1 个新 token)。这不是简单的「跑 100 次 forward」——Prefill 和 Decode 的计算模式完全不同。
14.2 Prefill 阶段:一次大矩阵乘
Prefill 阶段的输入是 N 个 token(这里 N=30)。模型要计算每个位置的:
- token embedding
- 经过 L 层 Transformer Block(每层包含 attention 和 FFN)
- 输出每个位置的 KV(缓存到 KV Cache)和最后一个位置的 logits
关键点:所有 N 个 token 同时进入计算——这和训练时的一次前向完全一样。Q、K、V 都是 的大矩阵,attention 是 的全矩阵,FFN 一次处理 N 个 token。
计算量:
- QKV 投影: FLOPs(前面的 2 是矩阵乘的乘加 FLOPs)
- Attention(QK^T + softmax + AV): FLOPs
- FFN: FLOPs(SwiGLU 三次矩乘)
所有这些计算都涉及大矩阵——GPU 的 Tensor Core 完美适配,算力利用率(MFU)能到 50-70%。这是 Prefill 阶段的核心特征:compute-bound(计算密集型)。
flowchart LR PROMPT["30 token prompt"] --> EMB["embedding<br/>(30, 4096)"] EMB --> QKV["QKV 投影<br/>大矩乘"] QKV --> ATT["attention<br/>30x30 矩阵"] ATT --> FFN["FFN<br/>30 x 11008"] FFN --> OUT["输出 logits<br/>+ 写 KV Cache"] COMPUTE["GPU Tensor Core 利用率<br/>50-70%"]
显存写入:每层 Transformer Block 都把 K 和 V 写入 KV Cache。30 token × 80 层 × 8 KV head × 128 dim × 2(K/V)× 2 byte(FP16)≈ 9.8 MB——可以忽略。
Prefill 的「时间复杂度」:在不考虑 attention 的 N² 部分时,时间是 ;考虑 attention 后,是 。短 prompt(N << d)下 FFN 主导(线性复杂度),长 prompt(N > d)下 attention 主导(二次)。
14.3 Decode 阶段:N 次「单 token forward」
Decode 阶段每次只生成 1 个 token。流程:
- 输入:上一步生成的最后 1 个 token(不是整个序列!)
- 嵌入它:变成一个 的向量
- 过每一层 Transformer:
- QKV 投影:得到 1 个 query、1 个 key、1 个 value
- 新 K、V 拼接到 KV Cache(KV Cache 现在有 31 个 token)
- 新 query 对 KV Cache 中所有 31 个 K 做 attention(这一步是关键!)
- FFN 处理 1 个 token
- 得到输出 logits:1 个位置的概率分布
- 采样下一个 token
flowchart LR PREV["上一步的 token"] --> EMB1["embedding<br/>(1, 4096)"] EMB1 --> Q1["新 Q (1, 4096)"] EMB1 --> KV_NEW["新 K, V (1, 4096)"] KV_NEW --> KV_CACHE["KV Cache<br/>从 30 → 31 token"] Q1 --> ATT["新 Q 对所有 31 个 K<br/>(1×31 attention)"] KV_CACHE --> ATT ATT --> FFN["FFN<br/>处理 1 token"] FFN --> LOGIT["logits<br/>(1, V)"] LOGIT --> SAMPLE["采样 token"] SAMPLE --> NEXT["进入下一轮"]
计算量:
- QKV 投影: FLOPs(处理 1 个 token)
- Attention: FLOPs(1 个 query 对 N 个 keys)
- FFN: FLOPs
注意计算量和 prompt 长度 N 无关(attention 那一项除外),主要是「单 token 经过整个模型」的开销。
显存读取:
- 模型权重:所有 12 d² × L = ~140 GB(70B 模型)必须从 HBM 读到 SM
- KV Cache:从 31 个 token 的 KV 全部读出来做 attention
这是 Decode 阶段的关键瓶颈:每生成一个 token 都要把整个模型的权重从 HBM 拉一遍。
H100 的 HBM 带宽是 3.35 TB/s。一个 70B 模型 BF16 是 140 GB——单次 forward 把权重全读一遍至少要 ms。这就是 Llama-3-70B 在 H100 单卡上单 token 延迟的下界——和算力没关系,只受 HBM 带宽限制。
这种瓶颈叫 memory-bound(访存密集型):GPU 算力远没用满(实际 MFU 只有 5-15%),瓶颈在「权重从 HBM 搬到 SM 的速度」。
flowchart LR HBM["HBM 显存<br/>140 GB 权重"] -.->|"带宽 3.35 TB/s"| SM["GPU SM"] SM --> OP["计算<br/>only 1 token"] WAIT["GPU 算力大部分时间在等待数据"]
14.4 两阶段对比:截然不同的硬件画像
把两个阶段的关键指标放一起:
| 维度 | Prefill | Decode |
|---|---|---|
| 一次处理 token 数 | 整个 prompt(10s-1000s) | 1 个 |
| Q/K/V 矩阵形状 | 大矩阵 | 单向量 |
| Attention 计算 | 矩阵 | 单行 |
| 主要瓶颈 | Compute(GPU 算力) | Memory(HBM 带宽) |
| GPU MFU | 50-70% | 5-15% |
| 单次延迟 | 慢(与 N 成正比甚至 N²) | 快(约 30-100ms) |
| 重复次数 | 1 次 | N_output 次(生成几十到几百) |
| 主要写显存 | KV Cache 写入 | KV Cache 写入 + 权重读取 |
| Tensor Core 利用率 | 高 | 低 |
| Batch 友好度 | 高(单大 batch) | 中(continuous batching) |
这两阶段的「硬件画像」如此不同,以至于它们应该被看作两个独立的工作负载——不是「同一种工作的两个阶段」。
14.5 两个核心指标:TTFT 和 TPOT
LLM 推理服务化的核心指标只有两个:
TTFT (Time to First Token):用户发出 query 后,到收到第一个 token 的时间。
主要由 Prefill 时间 决定。短 prompt(< 1K token)下 TTFT 可能只有几十 ms;长 prompt(128K)下 Prefill 可能要几秒钟。
TPOT (Time per Output Token)(或 ITL,Inter-Token Latency):连续生成 token 之间的时间间隔。
主要由 Decode 单步时间 决定。一般在 20-100 ms。70B 模型单卡 BF16 大约 30-50 ms/token。
flowchart LR REQ[用户发送 query] --> PRE[Prefill<br/>处理 prompt] PRE --> FIRST[First Token] FIRST --> D2[Token 2] D2 --> D3[Token 3] D3 --> D4[...] TTFT["← TTFT →"] TPOT["← TPOT →"]
用户体验上:
- TTFT 决定「等多久能开始看到回答」——长 prompt 的应用(文档 QA)TTFT 是首要痛点
- TPOT 决定「读起来流不流畅」——短回答的对话场景 TPOT 不那么重要,但长回答(写文章)下 TPOT 直接影响用户感知
业界 SLO(服务级别目标)的常见值:
- TTFT < 1s(普通 chat)/ < 10s(长文档)
- TPOT < 50ms(流畅阅读)/ < 100ms(可接受)
14.6 Batch 行为:为什么 Prefill 和 Decode 不能同 batch
服务化时为了提升吞吐,会把多个用户请求 batch 到一起。但 Prefill 和 Decode 在 batch 时表现完全不同。
Prefill 的 batch 行为:
- 多个 prompt 一起 batch,每个 prompt 独立做 Prefill。
- 因为 Prefill 已经是 compute-bound 的,加 batch 不会显著增加 GPU 利用率(已经接近满)。
- 但 batch 会让 TTFT 上升——前一个 prompt 没 Prefill 完,后一个就要排队。
Decode 的 batch 行为:
- 多个用户的 Decode 步骤可以打包到一个 batch——每次都是「一堆用户各自生成自己的下一 token」。
- 因为 Decode 是 memory-bound、GPU 算力大量空闲,batch 几乎免费——把 batch 从 1 加到 32,TPOT 几乎不变(因为权重读一次给 32 个用户用),但吞吐 32 倍。
这就是为什么Decode 阶段 batch 的边际收益极高,Prefill 阶段 batch 的边际收益低。
更进一步的问题:把 Prefill 和 Decode 混在同一个 batch 里会互相干扰:
- Prefill 一个 batch 里有大量计算(N×N attention),跑完要几百 ms。
- 在这几百 ms 内,所有正在 Decode 的用户都得等——TPOT 飙到几百 ms(远超 SLO)。
这是 LLM 推理服务化的核心痛点。下一节讲 continuous batching 怎么对付。
14.7 Continuous Batching:动态拼接
朴素的批处理(static batching):等够 64 个用户的 query 到了,一起 Prefill + 同步 Decode 64 步——所有人同时输出第 1 个 token、第 2 个 token、……、第 64 个 token。
问题:
- 不同用户的 prompt 长度不一致——最长那个 prompt Prefill 时间最长,其他用户得等
- 不同用户的回答长度不一致——回答 100 token 的用户要等回答 10 token 的「白等」90 步
- 用户 1 完成后无法立刻让用户 65 接进来——必须等整 batch 完成才能换新 batch
Continuous Batching(也叫 in-flight batching,Yu et al., 2022 vLLM 团队提出)的核心想法:每生成一个 token,重新组装 batch;用户可以中途加入或退出。
flowchart TB
subgraph "Static Batching"
S1["t=0 Batch:<br/>U1, U2, U3 (同步 prefill)"]
S2["t=1 Batch:<br/>U1, U2, U3 (同步 decode)"]
S3["t=K Batch:<br/>U1 done, U2, U3"]
S4["t=K+1 等所有结束才<br/>能加 U4, U5"]
S1 --> S2 --> S3 --> S4
end
subgraph "Continuous Batching"
C1["t=0 Batch: U1 (prefill)"]
C2["t=1 Batch: U1 (decode), U2 (prefill)"]
C3["t=2 Batch: U1, U2 (decode)"]
C4["t=3 U1 done; Batch: U2, U3 (prefill)"]
C5["t=4 Batch: U2 (decode), U3 (prefill)"]
C1 --> C2 --> C3 --> C4 --> C5
end
Continuous batching 让推理引擎能在「任意时刻」组合「任意状态」的请求成 batch——某些是新进来的(要 Prefill)、某些是正在生成的(要 Decode)、某些是刚结束的(要释放显存)。
vLLM、SGLang、TensorRT-LLM 都用 continuous batching。这是 LLM 推理引擎和传统 ML 推理引擎(TensorFlow Serving、Triton)最核心的差异。
但 continuous batching 仍然不能完全解决 Prefill 干扰 Decode 的问题:当一个新用户加入做 Prefill 时,那一步的延迟会高(因为多了 Prefill 工作量)。一些场景下 TPOT 会有抖动。
14.8 Chunked Prefill:把长 Prefill 切片
针对「长 prompt 的 Prefill 拖慢所有 Decode」的问题,工程上的进一步优化是 Chunked Prefill(分块 Prefill)。
直觉:把一个长 prompt 的 Prefill 切成若干小块,每块小到能和 Decode 并行执行而不阻塞。
flowchart TB
subgraph "无 Chunked Prefill"
P1["长 Prefill 整段<br/>500ms 阻塞所有 decode"]
P1 --> D1["所有 decode 等"]
end
subgraph "有 Chunked Prefill"
PA["chunk 1<br/>50ms"]
DA["其他 decode 同时跑"]
PB["chunk 2<br/>50ms"]
DB["decode 继续"]
PA --> DA
DA --> PB
PB --> DB
end
vLLM 0.4+、SGLang、TensorRT-LLM 都支持 Chunked Prefill。代价是额外的调度复杂度,但对延迟敏感场景(chat、Agent)非常重要。
14.9 PD 分离:物理层面拆开
到 2024 年,工业界开始尝试把 Prefill 和 Decode 物理上跑在不同硬件上——叫 PD 分离(Prefill-Decode Disaggregation 或 Splitwise,Patel et al., 2024)。
直觉:既然 Prefill 是 compute-bound、Decode 是 memory-bound,不如让它们用不同硬件配置:
- Prefill 集群:用算力强的卡(H100、H200),算 GPU 优先
- Decode 集群:用 HBM 带宽优先的卡(H200、B200),或者更便宜的 A100、L40
请求来时先在 Prefill 集群完成 Prefill,把 KV Cache 通过 InfiniBand 传给 Decode 集群,由后者继续生成。
flowchart LR REQ[请求] --> PRE_NODE[Prefill 节点<br/>compute-optimized] PRE_NODE --> KV[KV Cache] KV -.IB 传输.-> DEC_NODE[Decode 节点<br/>memory-optimized] DEC_NODE --> RESP[逐 token 返回]
PD 分离的好处:
- 资源利用率高:Prefill 节点 GPU 满负载,Decode 节点 HBM 带宽满负载,两边都不浪费
- 延迟更可控:Prefill 不再干扰 Decode,TPOT 抖动消失
- 成本最优:Decode 集群可以用更便宜的卡
代价:
- KV Cache 传输开销——一份 KV Cache 可能几 GB,靠 InfiniBand 传输不能太慢
- 架构复杂——两套集群协作,调度更难
- 小集群不划算——只有大规模服务才能摊薄分离的开销
DeepSeek 在 V2 / V3 之后部署中重度使用 PD 分离。OpenAI、Anthropic、Google 据传也都用类似架构。这是 2024-2025 年 LLM 推理基础设施的最重要演化。
14.10 一些常见的推理优化要点
我们汇总一下两阶段相关的工程优化要点:
优化 Prefill:缩短 TTFT
- Prefix Caching:相同前缀(系统 prompt + 文档)复用 KV Cache,跳过 Prefill。OpenAI 默认开启,能省 50%+ 的输入成本。
- 大并发 / 大张量并行:用 TP(Tensor Parallelism)多卡分摊 Prefill 计算。
- Speculative Prefilling:用小模型做 Prefill 的近似,大模型做验证。
- 量化权重:INT4/FP8 让 GEMM 算得更快,Prefill 时间减半。
优化 Decode:缩短 TPOT、提升吞吐
- GQA / MLA:减少 KV head 数 / 用 latent KV,单次 attention 读 KV 量减少(直接降低 memory-bound 工作量)
- 量化权重:INT4 让权重读取量减 4 倍——70B 模型从 140GB 缩到 35GB,HBM 读取直接 4×
- Flash Attention:把 attention 的 HBM 访问从 O(N²) 降到 O(N)
- Continuous Batching + 大 batch:让权重读取在多个用户之间分摊
- Speculative Decoding:用小模型预测多个候选 token,大模型验证(第 17 章)
监控两个独立指标
服务化推理的 dashboard 必须分开监控 Prefill 和 Decode:
- TTFT P50 / P99——首 token 延迟
- TPOT P50 / P99——后续 token 延迟
- Throughput (Tokens/s)——吞吐
- GPU MFU——硬件利用率
- KV Cache occupancy——KV Cache 占用率
把它们放一起看,才能识别瓶颈在哪个阶段。
14.11 一个完整的推理时间分解
最后用一个具体例子收尾。设:
- 模型 Llama-3 70B(BF16,140 GB)
- 单 H100 80GB(一张放不下,假设 4 卡 TP)
- 用户 prompt 1024 token
- 生成 200 token 输出
Prefill 时间估算:
- 计算量: FLOPs
- H100 BF16 算力 989 TFLOPs,4 卡 TP 实际通信开销让有效算力 ~3 PF,按 50% MFU
- 时间 ms
实际 Prefill 1024 token 在 4 卡 H100 上大约 50-100 ms(含 launch、通信开销)。
Decode 时间估算:
- 每步主要瓶颈:读权重 140 GB / 4 卡 = 35 GB/卡,HBM 3.35 TB/s
- 时间 ms/卡
- 加 attention 读 KV、通信开销,实际约 30-40 ms/token
200 token 的 decode: s
总时间:
- TTFT ≈ 100 ms
- 200 token 输出耗时 ≈ 7s
- 总耗时 ≈ 7.1s
如果 batch=1,GPU 大部分时间在做 Decode 但 MFU 不到 10%——非常浪费。把 batch 加到 32,吞吐 32×,TPOT 几乎不变(因为还是 memory-bound,权重读一次摊给 32 用户)——这就是 continuous batching 的价值。
本章小结
- LLM 推理是两阶段:Prefill(处理 prompt,一次性算完)+ Decode(一个一个吐 token,重复 N 次)。
- 两阶段硬件画像截然不同:Prefill 是 compute-bound(GPU 算力满)、Decode 是 memory-bound(HBM 带宽满)。
- 两个核心指标:TTFT(Time to First Token,由 Prefill 决定)+ TPOT(Time per Output Token,由 Decode 决定)。
- 不能简单 batch:Prefill 和 Decode 的 batch 行为差异巨大;强行混 batch 会让 Decode 等 Prefill。
- Continuous Batching 是 LLM 推理引擎的核心创新——每步重新组装 batch,请求中途加入退出。
- Chunked Prefill 把长 Prefill 切片,避免阻塞 Decode。
- PD 分离把 Prefill 和 Decode 物理拆开跑在不同硬件——2024-2025 年的工业新架构。
- 优化两阶段需要不同手段:Prefill 靠 prefix caching、量化、多卡 TP;Decode 靠 GQA/MLA、Flash Attention、speculative decoding。
第 14 章把推理两阶段的「地理图」画完了。下一章我们 zoom in 到这两阶段都依赖的核心数据结构——KV Cache。我们要看清它怎么节省了几百倍计算、怎么变成显存杀手、PagedAttention 怎么解决它的碎片化问题。
延伸阅读
- Yu et al., Orca: A Distributed Serving System for Transformer-Based Generative Models, OSDI 2022——Continuous Batching 奠基。
- Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023——vLLM / PagedAttention 论文。
- Patel et al., Splitwise: Efficient Generative LLM Inference Using Phase Splitting, ISCA 2024——PD 分离论文。
- Agrawal et al., SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills, 2023——Chunked Prefill 论文。
- DistServe, DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving, OSDI 2024——继续推进 PD 分离。
- Anthropic 技术博客 Prompt Caching with Claude——Prefix Caching 工程实战。