Transformer 解剖:从 Attention 到推理系统

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

作者 杨艺韬 · 4,999 字 · 发布于 · 更新于

第 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 都是 (N,dmodel)(N, d_{\text{model}}) 的大矩阵,attention 是 (N,N)(N, N) 的全矩阵,FFN 一次处理 N 个 token。

计算量:

  • QKV 投影 + 输出投影:2⋅N⋅dmodel2⋅42 \cdot N \cdot d_{\text{model}}^2 \cdot 4 FLOPs(前面的 2 是矩阵乘的乘加 FLOPs)
  • Attention(QK^T + softmax + AV):2⋅N2⋅dmodel+2⋅N2⋅dmodel2 \cdot N^2 \cdot d_{\text{model}} + 2 \cdot N^2 \cdot d_{\text{model}} FLOPs
  • FFN:N⋅dmodel⋅dffn⋅2⋅3N \cdot d_{\text{model}} \cdot d_{\text{ffn}} \cdot 2 \cdot 3 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² 部分时,时间是 O(N⋅d2)O(N \cdot d^2);考虑 attention 后,是 O(N⋅d2+N2⋅d)O(N \cdot d^2 + N^2 \cdot d)。短 prompt 下线性层(投影与 FFN)主导(线性复杂度);按上面的系数,每 token 线性层约 24d224d^2、attention 约 4Nd4Nd,要到 N 超过约 6d6d 时 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["进入下一轮"]

计算量:

  • QKV 投影 + 输出投影:2⋅1⋅dmodel2⋅42 \cdot 1 \cdot d_{\text{model}}^2 \cdot 4 FLOPs(处理 1 个 token)
  • Attention:2×(2⋅1⋅N⋅dmodel)2 \times (2 \cdot 1 \cdot N \cdot d_{\text{model}}) FLOPs(1 个 query 对 N 个 key 做 QK^T,再和 N 个 value 做 AV)
  • FFN:1⋅dmodel⋅dffn⋅2⋅31 \cdot d_{\text{model}} \cdot d_{\text{ffn}} \cdot 2 \cdot 3 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 把权重全读一遍至少要 140/3350≈42140 / 3350 \approx 42 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 矩阵形状 (N,d)(N, d) 大矩阵 (1,d)(1, d) 单向量
Attention 计算 N×NN \times N 矩阵 1×N1 \times N 单行
主要瓶颈 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 的时间。

TTFT≈T排队+TPrefill\text{TTFT} \approx T_{\text{排队}} + T_{\text{Prefill}}

第一个 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 之间的时间间隔。

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

主要由 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。

问题:

  1. 不同用户的 prompt 长度不一致——最长那个 prompt Prefill 时间最长,其他用户得等
  2. 不同用户的回答长度不一致——回答 10 token 的请求早早结束,它占的 batch 槽位却要陪回答 100 token 的请求「白等」90 步
  3. 用户 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 分离的好处:

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

代价:

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

DeepSeek-V3 技术报告(§3.4)明确写了预填充与解码分开部署:预填充最小部署单元 4 节点 32 卡,解码 40 节点 320 卡。闭源厂商的部署细节多未公开。这是 2024-2025 年 LLM 推理基础设施最重要的演化之一。

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

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

优化 Prefill:缩短 TTFT

  1. Prefix Caching:相同前缀(系统 prompt + 文档)复用 KV Cache,跳过这段前缀的 Prefill。OpenAI API 对受支持模型默认开启,命中缓存的输入 token 按折扣计费(官方文档称最高打一折)。
  2. 大并发 / 大张量并行:用 TP(Tensor Parallelism)多卡分摊 Prefill 计算。
  3. Speculative Prefill(Liu et al., arXiv:2502.02789):用轻量小模型估计 prompt 里各 token 的重要性,只把选中的 token(连同位置信息)送进大模型做 Prefill,属于有损近似。
  4. 量化激活 + 权重:FP8 / INT8 这种 W8A8 方案能让 GEMM 本身算得更快,Prefill 时间明显下降。注意 INT4 weight-only(W4A16)帮不上 Prefill——Prefill 是 compute-bound,反量化回 FP16 再算 GEMM 甚至可能比不量化更慢。它的收益全在 Decode 那一侧。

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

  1. GQA / MLA:减少 KV head 数 / 用 latent KV,单次 attention 读 KV 量减少(直接降低 memory-bound 工作量)
  2. 量化权重:INT4 让权重读取量减 4 倍——70B 模型从 140GB 缩到 35GB(不计量化 scale),读权重的时间理论上缩到 1/4
  3. Flash Attention:不物化 N×N 的注意力矩阵,把 attention 的 HBM 访问量压掉一个十倍量级的常数因子(第 18 章细讲);注意 Decode 时 query 只有一行、本来就没有 N×N 矩阵,这里受益的主要是长上下文下读 KV 的 attention 内核
  4. Continuous Batching + 大 batch:让权重读取在多个用户之间分摊
  5. 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 时间估算:

  • 计算量 ≈2⋅P⋅N⏟所有线性层+4⋅N2⋅d⋅L⏟attention 打分=2⋅70×109⋅1024+4⋅10242⋅8192⋅80≈1.43×1014+0.03×1014≈1.5×1014\approx \underbrace{2 \cdot P \cdot N}_{\text{所有线性层}} + \underbrace{4 \cdot N^2 \cdot d \cdot L}_{\text{attention 打分}} = 2 \cdot 70{\times}10^9 \cdot 1024 + 4 \cdot 1024^2 \cdot 8192 \cdot 80 \approx 1.43\times10^{14} + 0.03\times10^{14} \approx 1.5 \times 10^{14} FLOPs (线性层那一项按「每参数 2 FLOPs × 总参数量 PP」算最省事;写成 24d2NL24d^2NL 结果大致相当,但很容易漏掉层数 LL——漏了会把结果算小近两个数量级)
  • 4 卡 H100 SXM BF16 稠密峰值 4×9894 \times 989 TFLOPS ≈ 4.0 PFLOPS,假设 Prefill 能跑到 50% MFU,得有效算力 ≈ 2.0 PFLOPS
  • 时间 ≈1.5×1014/(2.0×1015)≈75\approx 1.5 \times 10^{14} / (2.0 \times 10^{15}) \approx 75 ms

这是没算 TP 通信开销的粗算,作为量级参考够用。

Decode 时间估算:

  • 每步主要瓶颈:读权重 140 GB / 4 卡 = 35 GB/卡,HBM 3.35 TB/s
  • 时间 ≈35/3350=10.4\approx 35 / 3350 = 10.4 ms/步(4 卡并行各读自己那 35 GB)
  • 实际还要加 attention 读 KV、TP 通信和调度开销,会明显高于这个下界;下面假设取 35 ms/token(示意取值,非实测)

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

总时间:

  • TTFT ≈ 75 ms(未计排队与 TP 通信,实际在 100 ms 上下)
  • 200 token 输出耗时 ≈ 7s
  • 总耗时 ≈ 7.1s

如果 batch=1,GPU 大部分时间在做 Decode 但 MFU 只有千分之一左右(每步约 1.4×10111.4\times10^{11} FLOPs 摊在 35 ms、4 卡上)——非常浪费。把 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 怎么解决它的碎片化问题。

延伸阅读

  • 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 工程实战。