跳转到主要内容

批处理与调度:推理服务的灵魂

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 问题

  1. 木桶效应:整个 batch 必须等最长的请求完成
  2. GPU 空转:短请求完成后,对应的 GPU 资源闲置
  3. 延迟不可控:新请求必须等当前 batch 全部完成才能开始
  4. 显存浪费:必须为 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. 延伸阅读

修改历史1 次提交