Transformer 解剖:从 Attention 到推理系统
第 14 章 两阶段推理:Prefill 与 Decode 的不同性格
第 1 章我们说,Transformer 在训练时全面胜过 RNN,但在推理时反而要花精力优化。这一章开启全专栏最厚的部分——推理系统——回答「模型训完之后,怎么把它高效跑给用户用」。
第一件要看清楚的事:LLM 推理不是一次「forward pass」,而是分成两个截然不同的阶段。如果你把它们当成一回事来调度,得到的会是一个 GPU 大部分时间在空转的推理引擎;理解了它们的差别,才知道利用率该从哪里往上推。
读完这章你能:
- 解释 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「读」一遍,理解它问的是什么。这句话本身在主流中文 tokenizer 下只有十几个 token,套上对话模板的角色标记、系统 prompt 后会更多;下面为了好算,按 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)可以做得很高——注意这是推理 Prefill 阶段的口径,和第 11 章稠密训练的 MFU、下一节 Decode 的 MFU 不是同一个东西。这是 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/>高"]
显存写入:每层 Transformer Block 都把 K 和 V 写入 KV Cache。以 70B 级模型(80 层、8 个 KV 头、head_dim 128)为例,30 token × 80 层 × 8 KV head × 128 dim × 2(K/V)× 2 byte(FP16)≈ 9.8 MB——可以忽略。
Prefill 的「时间复杂度」:在不考虑 attention 的 N² 部分时,时间是 ;考虑 attention 后,是 。短 prompt 下线性层(投影与 FFN)主导(线性复杂度);按上面的系数,每 token 线性层约 、attention 约 ,要到 N 超过约 时 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 个 key 做 QK^T,再和 N 个 value 做 AV)
- FFN: FLOPs
注意计算量和 prompt 长度 N 无关(attention 那一项除外),主要是「单 token 经过整个模型」的开销。
显存读取:
- 模型权重:全部约 70B 个参数(BF16 下约 140 GB)必须从 HBM 读到 SM
- KV Cache:从 31 个 token 的 KV 全部读出来做 attention
这是 Decode 阶段的关键瓶颈:每生成一个 token 都要把整个模型的权重从 HBM 拉一遍。
H100(SXM 版)的 HBM 带宽是 3.35 TB/s。一个 70B 模型 BF16 是 140 GB——单次 forward 把权重全读一遍至少要 ms。(140 GB 放不进单卡 80 GB,实际要 TP 到多卡;这里的 42 ms 是「把 140 GB 权重扫一遍」所需的单卡等效时间,多卡 TP 会按卡数分摊。)这就是单 token 延迟的下界——和算力没关系,只受 HBM 带宽限制。
这种瓶颈叫 memory-bound(访存密集型):GPU 算力远没用满,瓶颈在「权重从 HBM 搬到 SM 的速度」。用 roofline 的算术强度看:batch=1 时每读 1 个 BF16 权重(2 字节)只做 1 次乘加(2 FLOPs),强度约 1 FLOP/byte;而 H100 SXM 的拐点是 989 TFLOPS ÷ 3.35 TB/s ≈ 295 FLOP/byte。所以 推理 Decode 口径下 batch=1 的 MFU 上限只有峰值的千分之几;batch 加到 B,强度大致跟着涨到 B FLOP/byte,这也是下文 batch 能「白送」吞吐的原因。
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 | 高 | 低(batch=1 时仅千分之几) |
| 单次延迟 | 慢(与 N 成正比甚至 N²) | 单步短(下界 ≈ 权重字节数 ÷ HBM 带宽) |
| 重复次数 | 1 次 | N_output 次(生成几十到几百) |
| 主要显存流量 | 读一遍权重 + 写 KV Cache | 每步读全部权重和 KV Cache + 写新 token 的 KV |
| Tensor Core 利用率 | 高 | 低 |
| 加 batch 的收益 | 低(算力已接近用满) | 高(权重读取被分摊) |
这两阶段的「硬件画像」如此不同,以至于它们应该被看作两个独立的工作负载——不是「同一种工作的两个阶段」。
14.5 两个核心指标:TTFT 和 TPOT
LLM 推理服务化的核心指标只有两个:
TTFT (Time to First Token):用户发出 query 后,到收到第一个 token 的时间。
第一个 token 就从 Prefill 最后一个位置的 logits 采样出来,所以 TTFT 主要由 Prefill 时间 决定。短 prompt(< 1K token)下 TTFT 可能只有几十 ms;长 prompt(128K)下,按 §14.11 的方法粗算,70B 模型在 4 卡 H100 上的 Prefill 要二三十秒。
TPOT (Time per Output Token)(或 ITL,Inter-Token Latency):连续生成 token 之间的时间间隔。
主要由 Decode 单步时间 决定。70B 模型 BF16、4 卡 TP 下,只算读权重的理论下界约 10 ms/token(§14.11),实际还要加上读 KV、TP 通信和调度开销。
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 个用户用;前提是上下文不长,每个用户的 KV 读取量远小于权重),但吞吐 32 倍。
这就是为什么Decode 阶段 batch 的边际收益极高,Prefill 阶段 batch 的边际收益低。
更进一步的问题:把 Prefill 和 Decode 混在同一个 batch 里会互相干扰:
- 长 prompt 的 Prefill 计算量很大(线性层加 N×N attention),按 §14.11 的方法粗算,8K token 在 4 卡 H100 上就要几百 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 时间最长,其他用户得等
- 不同用户的回答长度不一致——回答 10 token 的请求早早结束,它占的 batch 槽位却要陪回答 100 token 的请求「白等」90 步
- 用户 1 完成后无法立刻让用户 65 接进来——必须等整 batch 完成才能换新 batch
Continuous Batching(也叫 in-flight batching)最早由 Orca(Yu et al., OSDI 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 的批处理)最核心的差异。
vLLM 的 V1 调度器把这件事推到了更彻底的形式:它不再区分 prefill 请求和 decode 请求,每一步只做一件事——给每个请求分配一点 token 预算,让它的 num_computed_tokens 往 num_tokens_with_spec 追。vllm-0.8.5/vllm/v1/core/sched/scheduler.py:138-147 那段注释写得很直白:这一套「通用到足以覆盖 chunked prefill、prefix caching、投机解码」。换句话说,下一节的 Chunked Prefill 在这种调度器里不是一个特例分支,而是同一个预算机制的自然结果。
但 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(自 v0.4.1 起)、SGLang、TensorRT-LLM 都支持 Chunked Prefill(vLLM 侧的开关是 enable_chunked_prefill,见 vllm-0.8.5/vllm/config.py:1871,V1 引擎则无条件开启,见《vLLM 推理内核深度解析》第 11 章;再长的 prompt 还会被 long_prefill_token_threshold 进一步限流,见 vllm-0.8.5/vllm/v1/core/sched/scheduler.py:181)。代价是额外的调度复杂度,但对延迟敏感场景(chat、Agent)非常重要。
14.9 PD 分离:物理层面拆开
到 2024 年,工业界开始尝试把 Prefill 和 Decode 物理上跑在不同硬件上——叫 PD 分离(Prefill-Decode Disaggregation),代表工作有 Splitwise(Patel et al., ISCA 2024)和 DistServe(Zhong et al., OSDI 2024)。
直觉:既然 Prefill 是 compute-bound、Decode 是 memory-bound,不如让它们用不同硬件配置:
- Prefill 集群:用算力强的卡(H100、H200),算 GPU 优先
- Decode 集群:用 HBM 带宽优先的卡(H200、B200),或者上一代但带宽仍然可观的 A100 80GB(注意别挑 L40S 这类带宽只有几百 GB/s 的算力卡——Decode 卡的是带宽,不是 TFLOPS)
请求来时先在 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-V3 技术报告(§3.4)明确写了预填充与解码分开部署:预填充最小部署单元 4 节点 32 卡,解码 40 节点 320 卡。闭源厂商的部署细节多未公开。这是 2024-2025 年 LLM 推理基础设施最重要的演化之一。
14.10 一些常见的推理优化要点
我们汇总一下两阶段相关的工程优化要点:
优化 Prefill:缩短 TTFT
- Prefix Caching:相同前缀(系统 prompt + 文档)复用 KV Cache,跳过这段前缀的 Prefill。OpenAI API 对受支持模型默认开启,命中缓存的输入 token 按折扣计费(官方文档称最高打一折)。
- 大并发 / 大张量并行:用 TP(Tensor Parallelism)多卡分摊 Prefill 计算。
- Speculative Prefill(Liu et al., arXiv:2502.02789):用轻量小模型估计 prompt 里各 token 的重要性,只把选中的 token(连同位置信息)送进大模型做 Prefill,属于有损近似。
- 量化激活 + 权重:FP8 / INT8 这种 W8A8 方案能让 GEMM 本身算得更快,Prefill 时间明显下降。注意 INT4 weight-only(W4A16)帮不上 Prefill——Prefill 是 compute-bound,反量化回 FP16 再算 GEMM 甚至可能比不量化更慢。它的收益全在 Decode 那一侧。
优化 Decode:缩短 TPOT、提升吞吐
- GQA / MLA:减少 KV head 数 / 用 latent KV,单次 attention 读 KV 量减少(直接降低 memory-bound 工作量)
- 量化权重:INT4 让权重读取量减 4 倍——70B 模型从 140GB 缩到 35GB(不计量化 scale),读权重的时间理论上缩到 1/4
- Flash Attention:不物化 N×N 的注意力矩阵,把 attention 的 HBM 访问量压掉一个十倍量级的常数因子(第 18 章细讲);注意 Decode 时 query 只有一行、本来就没有 N×N 矩阵,这里受益的主要是长上下文下读 KV 的 attention 内核
- 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 (线性层那一项按「每参数 2 FLOPs × 总参数量 」算最省事;写成 结果大致相当,但很容易漏掉层数 ——漏了会把结果算小近两个数量级)
- 4 卡 H100 SXM BF16 稠密峰值 TFLOPS ≈ 4.0 PFLOPS,假设 Prefill 能跑到 50% MFU,得有效算力 ≈ 2.0 PFLOPS
- 时间 ms
这是没算 TP 通信开销的粗算,作为量级参考够用。
Decode 时间估算:
- 每步主要瓶颈:读权重 140 GB / 4 卡 = 35 GB/卡,HBM 3.35 TB/s
- 时间 ms/步(4 卡并行各读自己那 35 GB)
- 实际还要加 attention 读 KV、TP 通信和调度开销,会明显高于这个下界;下面假设取 35 ms/token(示意取值,非实测)
200 token 的 decode: s
总时间:
- TTFT ≈ 75 ms(未计排队与 TP 通信,实际在 100 ms 上下)
- 200 token 输出耗时 ≈ 7s
- 总耗时 ≈ 7.1s
如果 batch=1,GPU 大部分时间在做 Decode 但 MFU 只有千分之一左右(每步约 FLOPs 摊在 35 ms、4 卡上)——非常浪费。把 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 论文。
- Zhong et al., DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving, OSDI 2024——继续推进 PD 分离。
- Anthropic 技术博客 Prompt Caching with Claude——Prefix Caching 工程实战。