外观
批处理与请求调度
概念定义:把多个请求合并一次算
GPU 的设计哲学是"成千上万线程并行"——单请求喂给它大量算力和带宽闲置。**批处理(batching)**把多个独立请求合并成一个大 batch 一次算,把 Roofline 上的算术强度(AI)拉高,让 GPU 跑满。
理解 batching 的两个关键认知:
- batch 越大吞吐越高,但延迟也越高——这是延迟-吞吐权衡在 batching 上的直接体现;
- 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(访存密集)瓶颈完全不同(见延迟、吞吐与并发)。最优调度策略要分开:
| 阶段 | 算子特性 | 调度目标 | 策略 |
|---|---|---|---|
| prefill | compute-bound | 单 prompt 算力喂满、prompt 长度差异大 | 单 prompt 单独成批;不同 prompt 长度分桶 |
| decode | memory-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. 回到 13. 显存预算与抢占
当显存不够装下所有 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 倍。
六、其他引擎的调度特色
| 引擎 | 调度特色 |
|---|---|
| vLLM | continuous batching + PagedAttention + chunked prefill(详见vLLM 案例研究) |
| SGLang | continuous batching + RadixAttention(前缀树自动 prefix cache) |
| TensorRT-LLM | in-flight batching + 显存规划器 + INT8/FP8/FP4 量化调度 |
| TGI | continuous batching(早期贡献者) |
| Triton Inference Server | 通用 dynamic batching(支持 LLM 后端,详见Triton 案例研究) |
| DeepSpeed-FastGen | DynPI(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这是容量规划的起点——调度器要把并发维持在曲线拐点(延迟-吞吐权衡)。
九、调度常见坑
- 只看吞吐不看延迟——很多 benchmark 报"vLLM 单卡 3000 tokens/s",但实际生产 p99 TTFT 可能 5s;
- 不分组桶——长请求和短请求混跑时长请求占显存槽,短请求饿死;
- 不开 prefix cache——长 system prompt 每个请求重算,TTFT 虚高;
- CUDA graph 与 continuous batching 冲突——CUDA graph 要固定 shape,continuous batching 要动态;解法是分桶 capture(详见计算图优化);
- 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 或 火山方舟。
延伸阅读
- 延迟、吞吐与并发——batching 决定的延迟与吞吐
- 显存层次与带宽墙——为什么 decode 堆 batch 几乎免费
- 模型服务化与编排——把调度集成进生产服务
- 计算图优化——CUDA graph 与 continuous batching 的协调
- vLLM 案例研究——continuous batching 的工业实现
- TensorRT-LLM 案例研究——in-flight batching 的 NVIDIA 实现
- Triton Server 案例研究——通用动态 batching 后端
- 分布式推理案例——多卡场景下的调度协调
- benchmarking 实践——调度器性能的测量
- 常见陷阱与反模式——调度翻车现场
参考资料
- 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 论文
- vLLM Documentation: Continuous Batching and Scheduling —— vLLM 调度器实现细节
- SGLang: Efficient Execution of Structured Language Model Programs(2024) —— RadixAttention 调度
- NVIDIA TensorRT-LLM: In-flight Batching —— in-flight batching 设计
- Agrawal et al. Sarathi-Serve: Coalescing Continuous Batching with Chunked Prefills(2024) —— chunked prefill 论文
- HuggingFace TGI Documentation —— 早期 continuous batching 开源实现
- Little. A Proof for the Queuing Formula L = λW(1961) —— 调度理论基础