Transformer 解剖:从 Attention 到推理系统

第 14 章 两阶段推理:Prefill 与 Decode 的不同性格

作者 杨艺韬 · 3,936 字

第 14 章 两阶段推理:Prefill 与 Decode 的不同性格

第 1 章我们说,Transformer 在训练时全面胜过 RNN,但在推理时反而要花精力优化。这一章开启全书最厚的部分——推理系统——回答「模型训完之后,怎么把它高效跑给用户用」。

第一件要看清楚的事:LLM 推理不是一次「forward pass」,而是分成两个截然不同的阶段。如果你把它们当成一回事来调度,得到的会是一个利用率 5-10% 的推理引擎;理解了它们的差别,能把利用率推到 50% 以上。

读完这章你能:

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)。模型要计算每个位置的:

关键点:所有 N 个 token 同时进入计算——这和训练时的一次前向完全一样。Q、K、V 都是 (N,dmodel)(N, d_{\text{model}}) 的大矩阵,attention 是 (N,N)(N, N) 的全矩阵,FFN 一次处理 N 个 token。

计算量

所有这些计算都涉及大矩阵——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² 部分时,时间是 O(Nd2)O(N \cdot d^2);考虑 attention 后,是 O(Nd2+N2d)O(N \cdot d^2 + N^2 \cdot d)。短 prompt(N << d)下 FFN 主导(线性复杂度),长 prompt(N > d)下 attention 主导(二次)。

14.3 Decode 阶段:N 次「单 token forward」

Decode 阶段每次只生成 1 个 token。流程:

  1. 输入:上一步生成的最后 1 个 token(不是整个序列!)
  2. 嵌入它:变成一个 (1,dmodel)(1, d_{\text{model}}) 的向量
  3. 过每一层 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
  4. 得到输出 logits:1 个位置的概率分布
  5. 采样下一个 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["进入下一轮"]

计算量

注意计算量和 prompt 长度 N 无关(attention 那一项除外),主要是「单 token 经过整个模型」的开销。

显存读取

这是 Decode 阶段的关键瓶颈:每生成一个 token 都要把整个模型的权重从 HBM 拉一遍

H100 的 HBM 带宽是 3.35 TB/s。一个 70B 模型 BF16 是 140 GB——单次 forward 把权重全读一遍至少要 140/335042140 / 3350 \approx 42 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 矩阵形状 (N,d)(N, d) 大矩阵 (1,d)(1, d) 单向量
Attention 计算 N×NN \times N 矩阵 1×N1 \times N 单行
主要瓶颈 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 的时间。

TTFT=TPrefill+Tfirst decode\text{TTFT} = T_{\text{Prefill}} + T_{\text{first decode}}

主要由 Prefill 时间 决定。短 prompt(< 1K token)下 TTFT 可能只有几十 ms;长 prompt(128K)下 Prefill 可能要几秒钟。

TPOT (Time per Output Token)(或 ITL,Inter-Token Latency):连续生成 token 之间的时间间隔。

TPOT=TDecode (单步)\text{TPOT} = T_{\text{Decode}} \text{ (单步)}

主要由 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 →"]

用户体验上:

业界 SLO(服务级别目标)的常见值:

14.6 Batch 行为:为什么 Prefill 和 Decode 不能同 batch

服务化时为了提升吞吐,会把多个用户请求 batch 到一起。但 Prefill 和 Decode 在 batch 时表现完全不同。

Prefill 的 batch 行为

Decode 的 batch 行为

这就是为什么Decode 阶段 batch 的边际收益极高,Prefill 阶段 batch 的边际收益低

更进一步的问题:把 Prefill 和 Decode 混在同一个 batch 里会互相干扰

这是 LLM 推理服务化的核心痛点。下一节讲 continuous batching 怎么对付。

14.7 Continuous Batching:动态拼接

朴素的批处理(static batching):等够 64 个用户的 query 到了,一起 Prefill + 同步 Decode 64 步——所有人同时输出第 1 个 token、第 2 个 token、……、第 64 个 token。

问题:

  1. 不同用户的 prompt 长度不一致——最长那个 prompt Prefill 时间最长,其他用户得等
  2. 不同用户的回答长度不一致——回答 100 token 的用户要等回答 10 token 的「白等」90 步
  3. 用户 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 集群完成 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 分离的好处:

  1. 资源利用率高:Prefill 节点 GPU 满负载,Decode 节点 HBM 带宽满负载,两边都不浪费
  2. 延迟更可控:Prefill 不再干扰 Decode,TPOT 抖动消失
  3. 成本最优:Decode 集群可以用更便宜的卡

代价:

  1. KV Cache 传输开销——一份 KV Cache 可能几 GB,靠 InfiniBand 传输不能太慢
  2. 架构复杂——两套集群协作,调度更难
  3. 小集群不划算——只有大规模服务才能摊薄分离的开销

DeepSeek 在 V2 / V3 之后部署中重度使用 PD 分离。OpenAI、Anthropic、Google 据传也都用类似架构。这是 2024-2025 年 LLM 推理基础设施的最重要演化。

14.10 一些常见的推理优化要点

我们汇总一下两阶段相关的工程优化要点:

优化 Prefill:缩短 TTFT

  1. Prefix Caching:相同前缀(系统 prompt + 文档)复用 KV Cache,跳过 Prefill。OpenAI 默认开启,能省 50%+ 的输入成本。
  2. 大并发 / 大张量并行:用 TP(Tensor Parallelism)多卡分摊 Prefill 计算。
  3. Speculative Prefilling:用小模型做 Prefill 的近似,大模型做验证。
  4. 量化权重:INT4/FP8 让 GEMM 算得更快,Prefill 时间减半。

优化 Decode:缩短 TPOT、提升吞吐

  1. GQA / MLA:减少 KV head 数 / 用 latent KV,单次 attention 读 KV 量减少(直接降低 memory-bound 工作量)
  2. 量化权重:INT4 让权重读取量减 4 倍——70B 模型从 140GB 缩到 35GB,HBM 读取直接 4×
  3. Flash Attention:把 attention 的 HBM 访问从 O(N²) 降到 O(N)
  4. Continuous Batching + 大 batch:让权重读取在多个用户之间分摊
  5. Speculative Decoding:用小模型预测多个候选 token,大模型验证(第 17 章)

监控两个独立指标

服务化推理的 dashboard 必须分开监控 Prefill 和 Decode:

把它们放一起看,才能识别瓶颈在哪个阶段。

14.11 一个完整的推理时间分解

最后用一个具体例子收尾。设:

Prefill 时间估算

实际 Prefill 1024 token 在 4 卡 H100 上大约 50-100 ms(含 launch、通信开销)。

Decode 时间估算

200 token 的 decode:200×357200 \times 35 \approx 7 s

总时间

如果 batch=1,GPU 大部分时间在做 Decode 但 MFU 不到 10%——非常浪费。把 batch 加到 32,吞吐 32×,TPOT 几乎不变(因为还是 memory-bound,权重读一次摊给 32 用户)——这就是 continuous batching 的价值。

本章小结

  1. LLM 推理是两阶段:Prefill(处理 prompt,一次性算完)+ Decode(一个一个吐 token,重复 N 次)。
  2. 两阶段硬件画像截然不同:Prefill 是 compute-bound(GPU 算力满)、Decode 是 memory-bound(HBM 带宽满)。
  3. 两个核心指标:TTFT(Time to First Token,由 Prefill 决定)+ TPOT(Time per Output Token,由 Decode 决定)。
  4. 不能简单 batch:Prefill 和 Decode 的 batch 行为差异巨大;强行混 batch 会让 Decode 等 Prefill。
  5. Continuous Batching 是 LLM 推理引擎的核心创新——每步重新组装 batch,请求中途加入退出。
  6. Chunked Prefill 把长 Prefill 切片,避免阻塞 Decode。
  7. PD 分离把 Prefill 和 Decode 物理拆开跑在不同硬件——2024-2025 年的工业新架构。
  8. 优化两阶段需要不同手段:Prefill 靠 prefix caching、量化、多卡 TP;Decode 靠 GQA/MLA、Flash Attention、speculative decoding。

第 14 章把推理两阶段的「地理图」画完了。下一章我们 zoom in 到这两阶段都依赖的核心数据结构——KV Cache。我们要看清它怎么节省了几百倍计算、怎么变成显存杀手、PagedAttention 怎么解决它的碎片化问题。

延伸阅读