大模型知识体系 第 20 章 🎮 交互版(含动画演示与测验)
← FlashAttention 与 Triton:为什么 Attention 还能更快🏠 回到交互版首页训练生命周期:Pretrain 与 SFT →
你正在浏览静态阅读版(无 JavaScript 也可阅读,便于搜索与 AI 抓取)。交互演示与答题功能请前往 交互版。
第 20 章 · 第四部分 · 服务 (Serve)

LLM Inference Systems:从 Prefill 到 vLLM / SGLang

Prefill/Decode、TTFT/ITL、Continuous Batching、PagedAttention、Prefix Cache、投机解码、Serving 指标模拟器。

对应课件:LLM Systems(第二批新增)

本章定位:第二批最重要的章节之一。目标不是「会用 vLLM」,而是回答:为什么需要 serving system? 学完你应该能解释:prefill/decode 为什么是两个性能阶段、continuous batching 与 PagedAttention 各自解决什么问题。

📘 代码类型约定(Build / Systems 章节统一)
  • 【Runnable】可直接运行(关键逻辑已在本课程验证脚本中实测)
  • 【Runnable on CUDA】需要 GPU(当前验证环境无 CUDA,标注 NOT EXECUTED)
  • 【Skeleton】工程骨架:逻辑完整,需要自备环境

20.1 先从一张图开始:Serving Pipeline

先懂直觉

一次「对话」在服务端要经过:排队 → 切词 → 调度 → 预填充(prefill)→ 逐字解码(decode)→ 采样 → 返回。Serving system 的全部工作,就是把这条流水线上的每个环节同时服务很多请求,并且在延迟与吞吐之间做取舍。

Request
  ↓  Tokenizer          文本 → token ids
  ↓  Scheduler          排队 / 组批 / 分配 KV 空间
  ↓  Prefill            一次处理完整 prompt(大矩阵计算)
  ↓  KV Cache           每层缓存 K/V
  ↓  Decode             每次生成 1 个 token(循环)
  ↓  Sampler            temperature / top-p → 选出 token
  ↓  Detokenizer        拼回文本
Response
🎮 交互:点击每个阶段看它在做什么交互演示 · 静态阅读版

20.2 Prefill vs Decode:两个完全不同的性能阶段 ★

先懂直觉

Prefill:把整段 prompt 一次性喂进模型——序列长、矩阵大、并行度高,是「计算密集型」的大 GEMM。 Decode:每次只生成一个 token——序列长度是 1,但每一步都要读取全部历史 KV,是「带宽密集 + 串行依赖」。 两者对硬件的需求完全不同,现代 serving 的核心挑战就是把它们高效地混合调度。

Prefill Decode
输入 完整 prompt(S 个 token) 1 个新 token(+ 历史 KV)
主要矩阵形状 [S,d]×[d,⋅][S, d] \times [d, \cdot](大) [1,d]×[d,⋅][1, d] \times [d, \cdot](瘦长)
并行度 高(序列维并行) 低(每步串行依赖)
瓶颈 对典型大模型、足够长 prompt 与合适 batch,往往更接近 compute-bound;具体仍取决于 shape、kernel 与硬件 常见 LLM decode 往往受权重/KV 读取与内存带宽影响显著;具体瓶颈随 batch、并行方式与 kernel 变化
典型指标 TTFT ITL / TPOT
🎮 交互:切换看两个阶段的计算形态交互演示 · 静态阅读版

切换 Prefill / Decode,观察输入形状、KV 读取量、瓶颈类型的变化。

在交互版中打开这个演示 →

20.3 TTFT:Time To First Token

先懂直觉

用户发出请求到看到第一个字的时间。长 prompt 的 TTFT 会显著变大——这是用户「感知卡顿」的主要来源。

关键事实:prefill 处理完整个 prompt 后,最后一个 prompt 位置的 logits 就直接用于采样第一个生成 token——不需要额外执行一次 decode iteration。

Request arrival
  ↓  Queue                排队(并发高时的主要等待项)
  ↓  Tokenization        文本 → ids(大 prompt 时不可忽略)
  ↓  Prefill              一次算完整个 prompt;产出 logits[S-1] 与全部 KV
  ↓  Sample first token   直接用 logits[S-1] 采样(不是"再跑一步 decode")
  ↓  Detokenize / stream  拼回文本、流式返回
First Token(用户可见)
TTFT≈Tqueue+Ttokenize+Tprefill+Tsample+TstreamTTFT \approx T_{queue} + T_{tokenize} + T_{prefill} + T_{sample} + T_{stream}

之后才进入 decode 循环:把第一个生成 token append 进序列(KV Cache 中已有 prompt),下一次 decode iteration 产出第二个 token。

⚠️ 一个容易写错的模型

不要写「TTFT = prefill + 一次 decode step」。prefill 本身已经产出了第一个 token 所需的 logits;decode 循环是从第二个 token 开始的续写。

20.4 ITL / TPOT:逐 token 延迟

先懂直觉

第一个字出来后,后面的字是一个个生成的:token1 → token2 → token3…… 相邻两个 token 的间隔叫 ITL(Inter-Token Latency),平均到每个输出 token 叫 TPOT。

⚠️ 为什么 decode 不能并行生成同一序列的未来 token

自回归依赖:生成 token t+1 需要 token t 作为输入。同一序列的生成天然串行——这就是 decode 阶段 GPU 利用率上不去(每步矩阵太瘦)的根本原因,也是 batching 存在的意义。

20.5 Throughput vs Latency:serving 的核心取舍 ★

先懂直觉

批越大,GPU 越满,吞吐(tokens/s)越高;但每个请求要等更久才轮到处理,延迟变差。Serving system 的调度器就是在「一次服务多少人」和「每个人等多久」之间找平衡。

🎮 交互:Serving 指标模拟器(教学模型)交互演示 · 静态阅读版

调整请求数 / prompt 长度 / 输出长度 / batch size / prefill 与 decode 速度,实时观察 TTFT、ITL、tokens/s 与 KV 显存占用的变化趋势。

在交互版中打开这个演示 →

⚠️ 该模拟器是教学模型(简化假设、不是真实硬件 benchmark),用于建立「参数怎么影响指标」的直觉。

20.6 Static Batching:GPU 空位的浪费

先懂直觉

最朴素的组批:把同时到达的请求凑成一批,一起 prefill、一起 decode,直到最长的那个生成完才释放整批资源。

Request A: 10 tokens
Request B: 100 tokens
Request C: 30 tokens
→ 固定批:B 还在生成时,A/C 已经结束但槽位仍被占用(空转)
🎮 交互:Static vs Continuous Batching交互演示 · 静态阅读版

切换两种策略,逐步播放生成过程,观察 GPU 槽位的空转与利用率差别。

在交互版中打开这个演示 →

20.7 Continuous Batching:批的「动态进出」★

先懂直觉

不等整批结束:哪个请求生成完了,立刻把它移出、把等待队列里的新请求塞进来。批的组成是流动的,提高动态请求下的 GPU 利用率(相比 static batching 显著减少槽位空转,但并非「永远不浪费」)。

这是 vLLM / SGLang 等现代 serving 系统最重要的调度思想之一:

20.8 KV Cache 的系统视角

先懂直觉

第 10 章从模型角度讲了 KV Cache(缓存历史 K/V,避免前缀重算)。在 serving 里,KV Cache 升级成一个资源管理问题:同时服务 N 个请求,每个请求都要一块可持续增长的 KV 空间。

🧮 KV Cache 显存:三种口径(避免 batch 与序列长度重复计数)

注意:SS 的含义要统一,否则会把 batch 维乘两次。

① 单个请求(序列长度 SS):

MKV=2 L Hkv dhead S bM_{KV} = 2\,L\,H_{kv}\,d_{head}\,S\,b

② BB 个等长请求(每个长度 SS):

MKV=2 L Hkv dhead B S bM_{KV} = 2\,L\,H_{kv}\,d_{head}\,B\,S\,b

③ 变长 serving batch(第 ii 个请求长度 SiS_i,总 token 数 ∑iSi\sum_i S_i):

MKV=2 L Hkv dhead(∑i=1BSi)bM_{KV} = 2\,L\,H_{kv}\,d_{head}\left(\sum_{i=1}^{B} S_i\right)b

其中 bb = 每元素字节数(FP16 为 2)。口径 ③ 对理解 continuous batching 与分页 KV 最重要——资源消耗正比于「所有活跃请求的 token 总数」,而不是「请求数 × 某个固定长度」。

特性 影响
随序列动态增长 无法预知每个请求要多少(生成多久不知道)
生命周期与请求绑定 请求结束才释放
体量巨大 长上下文 + 高并发时,KV 显存常常先于权重成为瓶颈

20.9 KV Cache 碎片化

先懂直觉

如果给每个请求预分配一整块连续的显存,问题立刻出现:A 要 0.8GB、B 要 0.3GB、C 要 1.2GB——申请大小未知、生命周期不同,很快产生大量无法利用的碎片。

连续内存布局(碎片化示意):
[ A(已占用) ][ 空洞 ][ B(已占用) ][ 空洞 ][ C … ]
→ 新请求 D 需要连续 1GB:虽然总空闲够,但没有连续块 → 失败或浪费预留

20.10 PagedAttention:从操作系统借来的思路 ★

先懂直觉

操作系统解决「内存碎片」的经典方案是虚拟内存分页:进程看到连续地址,物理内存却不连续。PagedAttention 把 KV Cache 也切成固定大小的 page/block(如 16 个 token 一页),逻辑上连续、物理上可以散落。

逻辑视图(请求看到的):  block0 → block1 → block2 → …
物理视图(显存里的):    block 在池子里任意位置分配
→ 按块按需分配:生成到哪、分配到哪;请求结束 → 整块回收(显著降低预分配与变长序列带来的浪费)
收益 说明
减少碎片 固定大小的块可被任意复用
按需增长 不再预分配「最大长度」
内存共享 相同前缀的块可以共享(→ Prefix Cache)
🎮 交互:分页分配演示交互演示 · 静态阅读版

切换「连续分配」与「分页分配」,观察碎片率与「能否放下新请求」的差别。

在交互版中打开这个演示 →

📘 类比不是巧合

PagedAttention(vLLM 的核心机制之一)就是显式借用 OS 虚拟内存/分页的设计哲学。想通「为什么 OS 要用分页」,就理解了它在 serving 中要解决什么。

20.11 Prefix Cache:复用相同的开头

先懂直觉

很多请求共享同一段前缀:同一个 system prompt、同一篇长文档(问答场景)、同一个 few-shot 示例。前缀的 KV 完全一样——算一次、缓存起来、后续请求直接复用。

🎮 交互:cache hit / miss交互演示 · 静态阅读版

切换「无缓存 / 有前缀缓存」,看相同前缀的请求是否需要重新 prefill。

在交互版中打开这个演示 →

命中条件 说明
前缀 token 完全一致 通常按块匹配(paged blocks)
多请求共享 引用计数 + 写时复制(copy-on-write)

20.12 Chunked Prefill:把长 prompt 切开

先懂直觉

一个 8K token 的 prompt 如果一次性 prefill,会长时间占住 GPU,让其他正在 decode 的请求「卡住」——ITL 抖动。Chunked prefill 把它切成几个小块,与 decode 交错执行:prefill chunk → decode → prefill chunk → decode …,改善延迟公平性。

策略 TTFT 其他人的 ITL
一次性 prefill 最短 被长请求阻塞,抖动大
Chunked prefill 略长 更稳定(交错执行)

20.13 Speculative Decoding:用小模型「试稿」

先懂直觉

Decode 每步只算一个 token,太浪费。投机解码让一个小模型(draft)先连续猜出 k 个 token,然后大模型(target)一次前向并行验证这 k 个位置:接受正确的、拒绝错误的。因为验证是并行的,只要接受率够高,等效速度提升。

draft:  猜 A B C D
target: 并行打分 → A B C 接受(D 拒绝)
→ 一次 target 前向推进了 3 个 token
📘 为什么输出分布仍然正确

验证阶段的接受/拒绝是按概率比进行的(拒绝时回退到 target 自己的采样),数学上保证最终输出分布与「完全用 target 逐步采样」一致。本课程只建立直觉;完整证明见原始论文。

🎮 交互:draft 与 verify交互演示 · 静态阅读版

逐步播放:draft 猜 4 个 → target 验证 → 接受前缀、拒绝并重采样。可以调接受率看平均推进速度。

在交互版中打开这个演示 →

20.14 CUDA Graph:省掉 launch 开销

先懂直觉

Decode 每步是一串很多小 kernel(每层好几个)。每个 kernel 的 CPU 启动(launch)都有固定开销;当 kernel 又小又多时,GPU 在等 CPU 发指令。CUDA Graph 把一串 kernel 的调用序列「录下来」,之后一键回放,跳过逐条 launch 的开销。

常规 CUDA Graph
每步 逐个 launch kernel replay 预录制的图
适合 形状多变 固定形状的重复循环(decode 典型)

20.15 Quantization:Why it matters for serving

先懂直觉

推理的瓶颈常是显存容量与带宽。把权重从 FP16/BF16 降到 INT8/FP8/INT4:权重显存减半/再减半,每步读取的字节数下降,decode 直接受益——代价是精度与实现复杂度。

FP16/BF16 → INT8 / FP8 → INT4
权重显存 ↓    带宽需求 ↓    质量风险 ↑

本节只建立连接:量化是 serving 的关键优化方向之一。完整的量化理论与工程(校准、per-channel、KV 量化等)适合作为后续专题。

20.16 vLLM:它到底解决什么问题

先懂直觉

vLLM 不是「一个更快的模型」,而是一个推理引擎/服务系统。它的价值在于把前面几节的问题系统性地解决:

问题 vLLM 的答案
KV 显存碎片 PagedAttention(分页化的 KV 管理)
批利用率 Continuous batching(动态进出)
相同前缀重复算 前缀缓存(分页块共享)
高并发调度 调度器 + 分块 prefill 等策略
分布式推理 多卡并行(张量并行等)

不要把它当「pip install 教程」——真正要理解的是:它为什么需要这些机制(就是本章前面 15 节)。

20.17 SGLang:Serving / Runtime 的另一个方向

SGLang 关注的方向包括:

不比较「框架谁快」——不同系统的优化重心不同,且版本迭代很快。学完本章你应该能看懂它们的 README 在说什么。

20.18 生态对照:FlashInfer 与 TensorRT-LLM

项目 定位 一句话
FlashInfer 推理 kernel 库 attention/采样等 serving 核心 kernel 的高性能实现,位于 kernel 层
TensorRT-LLM NVIDIA 的推理栈 编译式优化 + 融合 kernel + engine 构建

层次关系(重要):

你的应用
  ↓
Serving runtime(vLLM / SGLang / TensorRT-LLM runtime)
  ↓
Kernel 库(cuBLAS / FlashAttention / FlashInfer / Triton kernels)
  ↓
GPU

前面第 19 章的 kernel 知识在这里闭环:serving 系统的下限由 kernel 与调度的质量决定。

20.19 综合练习:三步定位性能问题

先懂直觉

给你一个「变慢了」的推理服务,按顺序问三个问题:

步骤 问题 可能结论
① 看指标 TTFT 大还是 ITL 大? TTFT 大 → prefill/排队问题;ITL 大 → decode/带宽问题
② 看资源 KV 显存是否接近上限? 是 → 并发被 KV 卡住(分页/量化/换更长序列策略)
③ 看瓶颈 GPU 利用率和带宽读数? 双低 → 调度/launch 问题(batching、CUDA Graph)
课堂案例(教学模拟)

案例 1:短 prompt 很快,长 prompt TTFT 暴涨 → prefill 计算随长度增长;考虑 chunked prefill 改善公平性,或限制最大 prompt。 案例 2:并发一高,吞吐不升反降,还出现请求失败 → KV 显存耗尽(OOM);分页管理 + 限制并发 + 量化。 案例 3:GPU 利用率只有 20%,ITL 却很大 → decode 每步太瘦(batch 太小 / launch 开销);continuous batching + CUDA Graph。

🔑 本章必须记住
概念 一句话
Prefill 一次算完 prompt(大 GEMM),产出第一个生成 token 的 logits;决定 TTFT
Decode 每步 1 token:瘦矩阵 + 读 KV,带宽受限,决定 ITL
Continuous batching 批动态进出,消除 static batching 的空转
KV 碎片 变长 + 生命周期不同 → 连续分配必碎
PagedAttention 分页化 KV:逻辑连续、物理分块,按需增长与共享
Prefix Cache 相同前缀的 KV 复用
Chunked Prefill 长 prompt 切开与 decode 交错,稳定 ITL
Speculative Decoding 小模型试稿 + 大模型并行验证
CUDA Graph 录制重复的 kernel 序列,省 launch 开销
vLLM / SGLang 把上述机制工程化的 serving 系统
📝 小测验
为什么 decode 阶段通常比 prefill 更「吃带宽」?
Adecode 的计算量更大
Bdecode 每步只算 1 个 token(矩阵很瘦),但要读取全部历史 KV,计算单元常闲着等数据
Cdecode 不需要 GPU
Ddecode 的 batch 更大
显示答案与解析

正确答案:B

prefill 通常是大矩阵计算(更接近 compute-bound);decode 每步输入只有 1 个 token,主要成本往往是 KV/权重的读取(更受内存带宽影响),计算单元利用率低。具体瓶颈随 batch、并行方式与 kernel 变化。
📝 小测验
PagedAttention 主要解决什么问题?
A提高模型精度
BKV Cache 的显存碎片与按需增长问题(借用 OS 分页思想)
C加快 tokenizer
D减少模型参数
显示答案与解析

正确答案:B

变长序列 + 不同生命周期导致连续分配产生碎片;分页化后固定大小的块可任意复用、按需分配、支持前缀共享(copy-on-write)。
📝 小测验
Continuous batching 相比 static batching 的核心优势是?
A模型变小
B请求完成后立即腾出槽位,新请求随时加入,GPU 空转大幅减少
C不需要 KV Cache
D不需要调度器
显示答案与解析

正确答案:B

static batching 要等整批最慢的请求结束才释放资源;continuous batching 让批「流动」起来,每个 decode 步后检查完成/加入,提高动态请求下的 GPU 利用率(显著减少空转,但不是「永远不浪费」)。
← FlashAttention 与 Triton:为什么 Attention 还能更快🏠 回到交互版首页训练生命周期:Pretrain 与 SFT →
本章完。原始 Markdown:content/20-inference.md