批处理与调度:推理服务的灵魂
1. 为什么 Batching 是推理服务的灵魂?
回忆第 2 章:decode 是 memory-bound,单请求时 GPU 计算利用率极低(<5%)。
单请求 decode:
读 16GB 权重 → 做 1 次矩阵向量乘 → 输出 1 个 token
GPU 算力利用率: ~1-5%
batch=32 decode:
读 16GB 权重 → 做 32 次矩阵向量乘 → 输出 32 个 token
GPU 算力利用率: ~30-50%
权重只读一次,服务 32 个请求!
Batching 的本质:让同一份权重读取服务更多请求,摊薄访存开销。
2. 静态批处理 (Static Batching)
2.1 工作方式
最简单的方式:收集 N 个请求,一起处理,全部完成后再处理下一批。
时间 →
请求 A: [prefill████|decode████████████|完成]
请求 B: [prefill██|decode████████████████████|完成]
请求 C: [prefill██████|decode████|完成........等B]
↑ C 已完成但必须等 B
2.2 问题
- 木桶效应:整个 batch 必须等最长的请求完成
- GPU 空转:短请求完成后,对应的 GPU 资源闲置
- 延迟不可控:新请求必须等当前 batch 全部完成才能开始
- 显存浪费:必须为 batch 中最长的序列预留 KV Cache
效率分析:
请求 A: 生成 50 tokens
请求 B: 生成 200 tokens
请求 C: 生成 30 tokens
静态 batch: 所有请求都要等 200 步
A 空转 150 步 (75% 浪费)
C 空转 170 步 (85% 浪费)
平均 GPU 利用率: ~40%
3. 动态批处理 (Dynamic Batching)
3.1 改进思路
不等固定数量的请求,而是设置一个短暂的等待窗口,在窗口内到达的请求合并成一个 batch。
等待窗口 = 50ms
t=0ms: 请求 A 到达
t=20ms: 请求 B 到达
t=35ms: 请求 C 到达
t=50ms: 窗口关闭 → 合并 A+B+C 成一个 batch 开始处理
t=60ms: 请求 D 到达 → 等下一个窗口
比静态 batch 好一些,但核心问题没解决:batch 内的请求仍然要等最长的那个完成。
4. 连续批处理 (Continuous Batching / Iteration-Level Scheduling)
4.1 核心思想
这是 vLLM、TGI 等现代推理引擎的核心调度策略。
关键突破:不在请求级别做 batch,而在 iteration(token)级别做 batch。
每一步 decode iteration:
1. 检查哪些请求已完成 → 移出 batch
2. 检查等待队列有没有新请求 → 加入 batch
3. 对当前 batch 中所有请求执行一步 decode
4.2 可视化对比
静态批处理:
Step: 1 2 3 4 5 6 7 8 9 10
A: ■ ■ ■ ■ ■ · · · · · ← 完成后空转
B: ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ← 最长的请求
C: ■ ■ ■ · · · · · · · ← 完成后空转
D: (等待中..............................) ← 必须等 B 完成
连续批处理:
Step: 1 2 3 4 5 6 7 8 9 10
A: ■ ■ ■ ■ ■ ·
B: ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
C: ■ ■ ■ ·
D: ■ ■ ■ ■ ■ ■ ← C 完成后立即加入
E: ■ ■ ■ ■ ■ ← A 完成后立即加入
4.3 连续批处理的优势
| 指标 | 静态批处理 | 连续批处理 |
|---|---|---|
| GPU 利用率 | 40-60% | 85-95% |
| 请求等待时间 | 长(等整个 batch) | 短(下一个 iteration 就能加入) |
| 吞吐量 | 低 | 高 2-4 倍 |
| 尾延迟 | 高 | 低 |
4.4 与 PagedAttention 的协同
连续批处理 + PagedAttention 是完美搭配:
没有 PagedAttention:
新请求加入 batch → 需要分配连续 KV Cache 空间
→ 可能因为碎片分配失败 → 无法加入 batch
有 PagedAttention:
新请求加入 batch → 按需分配 KV block(不需要连续)
→ 几乎总能成功 → batch 利用率最大化
5. Prefill-Decode 分离调度
5.1 问题:Prefill 和 Decode 互相干扰
当前 batch 正在 decode(每步很快,~5ms)
新请求到达,需要 prefill(可能要 100ms)
如果混在一起:
decode 请求被 prefill 拖慢(等 prefill 算完才能继续 decode)
→ 正在生成的请求突然卡顿
→ 用户体验:流畅流畅...突然停顿...继续流畅
5.2 解决方案:Chunked Prefill
把长 prefill 切成小块,和 decode 交替执行:
传统方式:
[prefill 2048 tokens ████████████████] [decode] [decode] ...
↑ 这段时间 decode 请求全部停顿
Chunked Prefill (chunk=512):
[prefill 512][decode][prefill 512][decode][prefill 512][decode][prefill 512][decode]
↑ decode 请求每隔一小段就能执行,延迟可控
vLLM 的 --enable-chunked-prefill 就是这个功能。
5.3 更激进:Prefill-Decode 分离部署 (Disaggregated Serving)
把 prefill 和 decode 跑在不同的 GPU 上:
Prefill GPU 集群 (选高算力卡):
专门处理 prompt → 生成 KV Cache → 传给 Decode 集群
Decode GPU 集群 (选高带宽卡):
接收 KV Cache → 专门做 token 生成
优势:
- 两个阶段独立扩缩容
- 硬件可以针对性选型
- 互不干扰
挑战:
- KV Cache 传输开销(网络带宽)
- 系统复杂度高
- 需要高速互联(NVLink / InfiniBand)
Mooncake(月饼)、DistServe、Splitwise 等系统在探索这个方向。
6. 调度策略细节
6.1 请求优先级
FCFS (First Come First Served):
最简单,按到达顺序处理
问题:长请求阻塞短请求
SJF (Shortest Job First):
优先处理预计生成长度短的请求
问题:长请求可能饿死
优先级队列:
VIP 请求优先处理
适合多租户场景
6.2 Preemption(抢占)
当显存不够时,vLLM 可以暂停低优先级请求:
显存快满了:
1. 选择一个低优先级请求
2. 把它的 KV Cache swap 到 CPU 内存
3. 释放 GPU 显存给高优先级请求
4. 等有空间了再 swap 回来继续生成
7. 关键要点总结
┌──────────────────────────────────────────────────────────┐
│ 批处理与调度核心认知 │
├──────────────────────────────────────────────────────────┤
│ 1. Batching 本质 = 同一份权重服务多请求,摊薄访存开销 │
│ 2. 静态 batch 有木桶效应,GPU 利用率低 │
│ 3. 连续批处理在 token 级别调度,利用率 85-95% │
│ 4. 连续批处理 + PagedAttention = 现代推理引擎的基石 │
│ 5. Chunked Prefill 解决 prefill 阻塞 decode 的问题 │
│ 6. Prefill-Decode 分离是下一代架构方向 │
└──────────────────────────────────────────────────────────┘
8. 延伸阅读
- Inside vLLM: Anatomy of a High-Throughput LLM Inference System — vLLM 官方架构解析
- Continuous Batching: The Secret Sauce of High-Throughput LLM Inference — 连续批处理深度解读
- vLLM and Parallelized Inference — vLLM 并行推理策略
- LLM Inference Optimization: vLLM and TensorRT-LLM — 全面的推理优化分析(含 benchmark)
修改历史1 次提交
- docs(ai-systems): add comprehensive LLM inference documentationxiaocheng··
7c98505