Skip to content

批处理与请求调度

本页速览 batching 是 GPU 推理性能的核心旋钮——静态 batching 凑齐才跑(延迟高)、动态 batching 折中、continuous batching 每 step 都可换请求(vLLM/SGLang 的革新)。本文讲透调度策略、prefill/decode 分离调度、vLLM 调度器机制。

批处理与请求调度

概念定义:把多个请求合并一次算

GPU 的设计哲学是"成千上万线程并行"——单请求喂给它大量算力和带宽闲置。**批处理(batching)**把多个独立请求合并成一个大 batch 一次算,把 Roofline 上的算术强度(AI)拉高,让 GPU 跑满。

理解 batching 的两个关键认知:

  1. batch 越大吞吐越高,但延迟也越高——这是延迟-吞吐权衡在 batching 上的直接体现;
  2. LLM batching 有特殊性——各请求长度不同、生成长度不同、KV cache 显存占用差异大,传统 static batching 没法处理。

LLM 推理 batching 经历三代演进:static batching(凑齐才跑)→ dynamic batching(到达即合并)→ continuous batching(每 step 都可换)。后者是 vLLM、SGLang 的核心创新。

一、三代 batching 演进

1. Static Batching(静态批处理)

最朴素:凑齐一个 batch(如 8 个请求)才执行,所有请求都生成完才返回。

text
时间:  t=0           t=1   t=2  ...  t=50  t=200
请求1: [prefill]──────decode─────────────────[done]
请求2: [prefill]──────decode────[done]        [waiting]   ← 早结束也要等
请求3: [prefill]──────decode────────────[done][waiting]
...
请求8: [prefill]──────decode─────────────────────────[done]

                          最长请求决定整个 batch 时长
  • 优点:实现简单、kernel 调度开销小;
  • 缺点:① 短请求要等长请求;② 凑不齐 batch 就发不出去(延迟高);③ GPU 大量时间在等长请求的尾巴。

Static batching 的尾部浪费

8 个请求里 7 个生成 50 token、1 个生成 200 token,static batching 后整个 batch 要跑 200 步——前 50 步 GPU 满,后 150 步只有 1 个请求在跑,算力闲置 87%。这就是为什么 LLM 服务几乎没人用 static batching。

2. Dynamic Batching(动态批处理)

到达即合并——只要凑到一定数量或等待超时,就组 batch 执行。允许不同请求批次同时存在。

text
t=0:    请求1,2,3 到达 → 组 batch_A → 执行
t=1:    请求4,5 到达 → 等
t=2:    请求4,5,6 凑到 3 个 → 组 batch_B → 执行
        (batch_A 和 batch_B 并行执行)
  • 优点:相比 static 减少等待、吞吐提升;
  • 缺点:仍然以"batch 为单位"切换,单个请求一旦进入 batch 不能中途换;NVIDIA Triton Inference Server 早年默认。

3. Continuous Batching(连续批处理)

又叫 in-flight batching / iteration-level batching——每生成一个 token 都重新组合 batch,可以加入新请求、移出已结束请求。

text
t=0:    请求1,2,3 都在 decode
t=1:    请求3 结束 → 移出;请求4 到达 → 加入 → 现在请求1,2,4 在 decode
t=2:    请求5 到达 → 加入 → 现在请求1,2,4,5
t=3:    请求1 结束 → 移出 → 现在请求2,4,5
...
  • 优点:① 没有尾部浪费;② 新请求无需等当前 batch 结束;③ GPU 持续满载;
  • 缺点:调度复杂(要管理动态 batch、动态 KV cache、动态 attention mask);早期 vLLM/SGLang 才支持。

continuous batching 是 LLM 服务的标准

现代 LLM 服务栈(vLLM、SGLang、TensorRT-LLM、TGI)都用 continuous batching——这是把单 GPU 吞吐从 ~10 tokens/s 提升到 ~3000 tokens/s 的关键工程。几乎所有 LLM 在线服务都该用 continuous batching。详见vLLM 案例研究

二、调度策略:决定哪个请求先跑

请求到达后如何排队、组 batch,要靠调度策略:

常见策略

策略做法优势劣势
FIFO(先到先服务)按到达顺序排队公平、实现简单短请求被长请求拖累
SJF(最短作业优先)优先跑估计短的请求平均延迟最低估计 prompt→生成长度难、长请求饿死
Priority按业务优先级VIP 优先实现复杂、要业务方配合
Fair Scheduling按用户/租户轮转公平、避免饿死整体延迟略高
SRTF(Shortest Remaining Time First)优先跑剩 token 最少的理论最优频繁切换开销

LLM 服务的实用做法

  • vLLM 默认 FCFS——简单稳定;
  • 加优先级队列——VIP 用户单独队列,单独 batch 调度;
  • 基于 token budget 的公平——每个用户每秒 N 个 token 配额,调度器尽量满足;
  • prefill 与 decode 分开调度(详见下文)。

三、prefill vs decode:分阶段调度

LLM 推理 prefill(计算密集)和 decode(访存密集)瓶颈完全不同(见延迟、吞吐与并发)。最优调度策略要分开

阶段算子特性调度目标策略
prefillcompute-bound单 prompt 算力喂满、prompt 长度差异大单 prompt 单独成批;不同 prompt 长度分桶
decodememory-bound把显存装满,所有并发请求一起 decode大批合并;尽量塞显存上限

为什么 prefill 和 decode 要分开调度

  • prefill 长 prompt 是 compute-bound——单独成批可以让单 prompt 把 Tensor Core 算力喂满;
  • prefill 短 prompt 与长 prompt 一起算不公平——短 prompt 要等长 prompt 的算力;
  • decode 阶段所有请求共享权重,堆 batch 几乎免费(带宽不变、算力被分摊)——大批合并最优;
  • prefill 与 decode 同时跑时,prefill 会与 decode 抢带宽 → 用 chunked prefill 平滑资源。

Chunked Prefill(分块 prefill)

长 prompt(如 8K tokens)的 prefill 一次跑会霸占 GPU 几百毫秒,挡住正在 decode 的请求。Chunked prefill 把长 prefill 切成多个 chunk(如 256 tokens),与 decode 请求交替执行:

text
原本:   长prefill(8K) ━━━━━━━━━━━━━━ → 等待长prefill结束才能decode
Chunk:  prefill_chunk1(256) | decode*3 | prefill_chunk2(256) | decode*3 | ...
        ↑ 与 decode 交替,prefill 不抢带宽,decode 不被饿死

vLLM 0.5+ 默认开启 chunked prefill,TTFT 和 decode 吞吐双赢。

四、vLLM 调度器机制详解

作为 continuous batching 的工业典范,看 vLLM 的调度器是怎么做的:

1. 核心数据结构

  • waiting queue:新到达未开始 prefill 的请求;
  • running queue:正在 decode 的请求;
  • KV cache blocks:用 PagedAttention 分页管理 KV cache(详见显存层次与带宽墙)。

2. 调度循环

text
循环:
1. 看显存预算(available KV blocks)
2. 决定本 step 的 batch:
   a. 把 running queue 的所有请求塞进 batch(decode)
   b. 如果还有显存,从 waiting queue 选 prompt 进入 prefill
   c. 如果显存不够,把某些 running 请求 swap 出去(fallback)
3. 执行一个 step(prefill + decode 一起)
4. 检查每个请求是否生成完 → 移出
5. 回到 1

3. 显存预算与抢占

当显存不够装下所有 running + 新 waiting 请求时,vLLM 用抢占式调度

  • 把某些 running 请求的 KV cache 重新计算(recompute)或 swap 到 CPU 内存;
  • 等显存空出来再 swap 回来继续 decode。

代价高但保证公平——vLLM 在极端情况下保 service 可用,详见vLLM 案例研究

4. Prefix Caching 优化

相同 system prompt 的多个请求复用 KV cache:

text
请求1: [system_prompt + Q1] → prefill 算 [system_prompt]
请求2: [system_prompt + Q2] → 复用 system_prompt 的 KV,只算 Q2

       TTFT 大幅下降

vLLM 的 enable_prefix_caching 默认开。

五、调度指标:怎么算"调度得好"

调度好坏看四类指标:

指标含义优化方向
吞吐(tokens/s)每秒产出 token 总数满显存、满带宽
TTFT 分位(p50/p99)首个 token 延迟分布chunked prefill + prefix cache
TPOT 分位(p50/p99)每 token 生成延迟分布显存预算充足、不触发 swap
尾部延迟(p99.9)极端长尾公平调度、防止某些请求饥饿

不要只看吞吐

高吞吐不等于好服务。一个能跑 1000 tokens/s 的服务,如果 p99 TTFT 是 30s(VIP 用户首 token 等 30 秒),用户体验仍差。生产服务必须看分位延迟——尤其交互式场景 p99 比 mean 重要 10 倍。

六、其他引擎的调度特色

引擎调度特色
vLLMcontinuous batching + PagedAttention + chunked prefill(详见vLLM 案例研究
SGLangcontinuous batching + RadixAttention(前缀树自动 prefix cache)
TensorRT-LLMin-flight batching + 显存规划器 + INT8/FP8/FP4 量化调度
TGIcontinuous batching(早期贡献者)
Triton Inference Server通用 dynamic batching(支持 LLM 后端,详见Triton 案例研究
DeepSpeed-FastGenDynPI(Dynamic Prefill and decode batching with Importance)

七、调度与并发控制的工程权衡

维度权衡点经验
batch size 上限大 = 高吞吐 vs 小 = 低延迟在线服务 max_batch=128-256;批处理可到 1024+
max_num_seqs(vLLM)大 = 多并发 vs 小 = 显存够80GB H100 跑 Llama-70B 推荐 128-256
prefill chunk size大 = 算力满 vs 小 = decode 不饿128-512 tokens
请求超时长 = 不打断 vs 短 = 防卡死60-300 秒
队列长度长 = 利用率高 vs 短 = 响应快在线 < 8;批处理可大
swap 启用on = 极端情况下保活 vs off = 不浪费显存够关、显存紧开

调参起点

  • vLLM 默认参数对大部分场景已经够好——max_num_seqs=128、enable_prefix_caching=True、use_cuda_graph=True;
  • 想压低 TTFT:开 prefix cache、调大 prefill chunk size;
  • 想压低 TPOT:调大 max_num_seqs(但要注意显存);
  • 想压低 p99 尾延迟:调小 max_batch、开公平调度、关闭 swap;
  • 想压成本:调大 max_num_seqs 直到显存上限。

八、调度与延迟模型的结合

延迟-吞吐权衡的 Little's Law 落到调度:

text
并发数 = 吞吐 × 平均延迟

例:单 GPU 跑 Llama-2-70B
- 吞吐上限: 3000 tokens/s(H100 + AWQ + continuous batching)
- 平均每请求: 200 输出 token / 50ms = 4 req/s × 5s = 20 并发
- 检查: 20 并发 × 200 tokens = 4000 tokens 在生成中(合理,KV cache 约 100GB)
- 推论: 要扛 1000 QPS(输入+输出短请求),需要 ~50 张 H100

这是容量规划的起点——调度器要把并发维持在曲线拐点(延迟-吞吐权衡)。

九、调度常见坑

  1. 只看吞吐不看延迟——很多 benchmark 报"vLLM 单卡 3000 tokens/s",但实际生产 p99 TTFT 可能 5s;
  2. 不分组桶——长请求和短请求混跑时长请求占显存槽,短请求饿死;
  3. 不开 prefix cache——长 system prompt 每个请求重算,TTFT 虚高;
  4. CUDA graph 与 continuous batching 冲突——CUDA graph 要固定 shape,continuous batching 要动态;解法是分桶 capture(详见计算图优化);
  5. swap 频繁触发——显存预算太小导致 swap 反复,吞吐暴跌;解法是显存预算留 10-20% 余量。

十、权衡与取舍

  • 吞吐 vs 延迟:交互式选小 batch + continuous batching + chunked prefill;批处理选大 batch + 简单调度;
  • 公平 vs 优先级:默认公平;VIP 业务加优先级队列;
  • 显存利用率 vs 稳定性:调大并发拉满显存 vs 留余量防 swap;通常留 10-20% 余量;
  • 复杂调度 vs 简单调度:vLLM 默认够用;极致优化(如区分 VIP/普通用户)才上 SGLang 或自研;
  • 自建 vs 平台:自建用 vLLM 简单可控;大规摸用 Triton Inference Server 或 火山方舟

延伸阅读

参考资料