LLM 推理的两种阶段——Prefill(计算密集)与 Decode(带宽受限)——在同一张 GPU 上天然互相拖累:长 prompt 的 prefill 阻塞住一批 decode 请求,decode 的低算力利用又浪费了 prefill 抢来的算力。连续批处理(Continuous Batching)让请求动态进出批次,Chunked Prefill 把长 prefill 拆碎与 decode 交错,而 Prefill/Decode 物理分离(Disaggregated Serving)则彻底解耦两阶段。本文把这套「吞吐与延迟平衡」的调度方法论讲透。
前置:/ai-vllm-system/(vLLM 连续批处理实现)、/ai-llm-inference-architecture/(推理服务架构)、/ai-inference-engine-comparison/(引擎对比)、/ai-inference-benchmark/(吞吐与延迟指标)。
目录
- 1. Prefill 与 Decode:两种截然不同的计算特征
- 2. 连续批处理:动态增删请求的迭代调度
- 3. 队列策略与抢占:Waiting/Running/Swapped 三态
- 4. Chunked Prefill:长 prompt 分块交错执行
- 5. Prefill 与 Decode 物理分离:Disaggregated Serving
- 6. Decode 阶段的优化:带宽、批大小与引擎
- 7. 吞吐与延迟的权衡:调度参数与目标
- 8. 引擎实现对比:vLLM、SGLang 与 TensorRT-LLM
- 9. 生产调优与踩坑
- 10. 速查表与一句话记忆
- 延伸阅读
1. Prefill 与 Decode:两种截然不同的计算特征
先建立两个阶段的「性格画像」,后面所有调度手段都从这出发。
Prefill(预填充):
□ 处理整个 prompt,并行算所有位置的注意力
□ 计算密集:矩阵乘法为主,GPU 算力吃满
□ 高算术强度(arithmetic intensity)
□ 延迟与 prompt 长度成正比 → TTFT 的来源
Decode(解码):
□ 一次只生成一个 token,逐 token 自回归
□ 带宽受限:读全部 KV + 权重,算一点点
□ 低算术强度,算力利用率常 < 10%
□ 延迟相对稳定 → TPOT 的来源
矛盾:
□ 同 GPU 上,长 prefill 阻塞 decode 请求 → P99 抖动
□ decode 批太小,算力闲置 → 吞吐上不去
算术强度对比:
prefill: 算力需求 ≈ 2 × 2 × N_tokens × d_model(FLOPs 大)
decode: 每 token 读一遍 KV+权重(带宽需求大)
→ 两阶段的最优 batch 策略完全不同
工程要点:Prefill 是「算力密集型、吃矩阵乘法」,Decode 是「带宽密集型、算力闲置」——同一张 GPU 上把两者混排,会让长 prefill 拖垮 decode 的延迟、decode 又浪费 prefill 抢来的算力,这是调度问题的根源。
2. 连续批处理:动态增删请求的迭代调度
传统静态批处理让同批请求同时开始、同时结束,最短请求完成后 slot 也不能复用。连续批处理按「迭代」动态调度。
静态批处理的问题:
□ 整批请求同进同出,最长请求决定批次生命周期
□ 短请求完成后 slot 闲置,直到整批结束
→ GPU 空转,算力浪费严重
连续批处理(Continuous Batching):
□ 每次 forward 之间动态增删请求
□ decode 完成/停止的请求移出批次,释放 slot
□ 从等待队列挑新请求加入,做 prefill 后并入 decode 批次
□ 混合批次(mixed batch):prefill 与 decode 一次 kernel 启动
Token Budget Scheduling:
□ 限制每轮迭代处理的 token 总数
□ 防止单轮被长 prefill 占满,保障 decode 的及时性
每轮迭代调度示意:
┌─ 等待队列 ─┐
│ 新请求 A B │
└────┬───────┘
▼
每轮 forward:running 批次 {r1, r2, r3}
├ r1 到达 stop → 移出,释放 KV 块
├ r2 继续 decode
├ r3 继续 decode
└ 新 A 做 prefill → 并入批次
→ 下一轮 {r2, r3, A, B}
工程要点:连续批处理的核心是「每次 forward 之间动态增删请求」——完成即走、新请求即进,slot 不再被长请求绑架。配合 token 预算调度,短请求不被长 prefill 拖死,GPU 空闲窗口被压缩到每次迭代之间。
3. 队列策略与抢占:Waiting/Running/Swapped 三态
请求在引擎里并非简单先进先出,而是有一套三态机与抢占策略。
请求三态:
□ Waiting:到达等待 GPU 资源执行 prefill
□ Running:正在 prefill 或 decode
□ Swapped:KV Cache 被换出到 CPU RAM,等待重新调度
显存不足时的抢占:
□ 抢占(Preemption):把部分 Running 请求降级为 Swapped
□ 换出(Swap out):KV 块搬到 CPU RAM
□ 重调度:换入时可「恢复 KV」或「丢弃重算」
- vLLM 默认丢弃重算:prefill 开销 < 换入换出的带宽开销
队列策略:
□ FCFS:公平简单,但长请求阻塞短请求
□ Shortest-Job-First:平均等待最小,长请求可能饿死
□ 预算调度:按预估 token 数分配权重,兼顾公平与效率
三态迁移:
[Waiting] ──prefill──▶ [Running] ──完成──▶ [Done]
▲ │
│ │ 显存不足
│ ┌──swapped─── ▼
└──换入──┤ [Swapped]
└──丢弃重算──▶ Running
工程要点:调度器用「Waiting/Running/Swapped 三态」管理请求生命周期,显存不足时优先抢占低优先级请求;换入时「丢弃重算」通常比「换回 KV」更划算。队列策略要在公平(FCFS)与效率(SJF/预算)之间按业务选型。
4. Chunked Prefill:长 prompt 分块交错执行
长 prompt 的 prefill 一次算完会长时间独占 GPU。Chunked Prefill 把它拆碎,与 decode 步交错执行。
问题:
□ 一条 10K token 的 prefill 单轮执行可能几百 ms
□ 期间所有 decode 请求被阻塞 → P99 飙升
□ 交互式体验崩塌(用户干等首 token)
Chunked Prefill:
□ 长 prefill 拆成多个 chunk(如每 512 token 一块)
□ 每轮迭代执行「一个 chunk 的 prefill + 若干 decode」
□ 长 prompt 的首 token 延迟被「摊平」,不阻塞 decode
□ prefill 总吞吐略降,但延迟分布大幅变稳
调度参数:
□ max_num_batched_tokens:每轮 token 预算
□ chunk 大小与 decode 数量需按硬件实测
交错示意(每轮迭代):
轮 1:prefill(A 前半 512) + decode(B, C)
轮 2:prefill(A 后半 512) + decode(B, C)
轮 3:decode(B, C) + A 进入 decode 阶段
→ A 的首 token 延迟 = 2 轮而非整条 prefill 一次算完
工程要点:Chunked Prefill 把「单轮独占 GPU 的长 prefill」切成「多轮小片」,与 decode 交错执行——首 token 延迟被摊平、P99 变稳,代价是 prefill 吞吐略降。高并发在线服务强烈推荐开启。
5. Prefill 与 Decode 物理分离:Disaggregated Serving
如果调度层面仍然不够,就把两阶段放到不同的 GPU 池里——这就是 Disaggregated Serving。
为什么还要物理分离:
□ chunked prefill 只是「摊平」,并未消除互相拖累
□ prefill 最优在「高算力」GPU(H100),decode 最优在「高带宽」
□ 两阶段资源可独立扩缩容
Disaggregated Serving:
□ Prefill Pool:高算力 GPU,吃大 batch 的 prefill
- 追求高 prefill 吞吐,短 TTFT
□ Decode Pool:高带宽 GPU,吃大 batch 的 decode
- 追求高 token 吞吐,短 TPOT
代价与挑战:
□ KV Cache 要从 prefill 池「传」到 decode 池(关键工程点)
- 直接传输 vs 重建:需要带压缩的 KV 传输协议
□ 调度器要全局统筹两个池,避免排队失衡
□ 多一跳网络,短 prompt 场景收益可能被传输开销吃掉
典型拓扑:
请求 ─▶ 全局调度器
├─▶ Prefill Pool(算 prefill + 生成 KV)
│ │ KV 传输(压缩)
│ ▼
└─▶ Decode Pool(基于传入 KV 逐 token 生成)
└─▶ 流式返回
工程要点:Disaggregated Serving 把 prefill 与 decode 放到不同 GPU 池,两阶段各自最优、独立扩缩容——是吞吐与延迟平衡的终极形态。代价是 KV 跨池传输和全局调度复杂度,短 prompt 场景要评估传输开销是否划算。
6. Decode 阶段的优化:带宽、批大小与引擎
Decode 是带宽受限阶段,优化方向集中在「怎么把带宽用满」。
Decode 的物理限制:
□ 每 token 都要读一遍「全部权重 + 相关 KV」
□ 权重越读越多 → 带宽是硬约束
优化手段:
□ 增大 batch:多个 decode 请求共享同一次权重读取
- 权重读一次,服务多个请求 → 带宽摊销
□ 权重量化:INT8/FP8 把权重体积减半 → 等效带宽翻倍
□ KV 量化:减小 KV 读取体积
□ 投机解码:一次 forward 验证多个候选,减少串行步数
□ 更小模型/蒸馏:减小权重体积本身
批大小与吞吐的关系:
□ batch 增大 → 单 token 延迟略升,吞吐近似线性增
□ 拐点:达到带宽饱和后,再增大 batch 收益递减
带宽模型(直觉):
单卡 A100 带宽 ≈ 2 TB/s
8B 模型 FP16 权重 ≈ 16 GB → 每 token 读权重耗时 ≈ 8μs
batch=1:每 token ≈ 8μs + 计算开销
batch=16:权重只读一遍 → 每 token 摊销 ≈ 0.5μs + 计算
→ 吞吐近似 ×16,延迟只升一点点
工程要点:Decode 优化的核心是「摊销权重读取」——增大 batch 让一次权重读取服务多个请求,权重量化等效翻倍带宽,投机解码减少串行步数。带宽是硬约束,量化与批大小是两条最直接的杠杆。
7. 吞吐与延迟的权衡:调度参数与目标
调度不是「越快越好」,而是在吞吐与延迟之间按业务目标取点。
两套目标体系:
□ 吞吐优先(离线批处理/大批量):
- 最大化 tokens/sec,接受较高单请求延迟
- 大 batch、长 prefill、少抢占
□ 延迟优先(在线交互/流式):
- 保 TTFT、TPOT、P99
- 小 batch 或 token 预算约束,chunked prefill 摊平
关键调度参数(互相牵制):
□ max_num_batched_tokens:每轮 token 预算
□ max_num_seqs:最大并发请求数
□ chunk 大小:长 prefill 的拆分粒度
□ 抢占策略:换出 vs 重算的阈值
□ 队列权重:新请求 vs 存量请求的优先级
一个「既要又要」的真相:
□ 同硬件下吞吐与延迟几乎必然此消彼长
□ 通过「按租户/按请求类型」分级,让两种流量各取所需
决策表:
场景 倾向 关键参数
离线批处理 吞吐优先 大 batch、token 预算拉满
在线聊天 延迟优先 小 batch、chunked prefill
混合流量 分级调度 高优先级快车道 + 低优先级队列
工程要点:吞吐与延迟是同一硬件上的「跷跷板」——离线场景拉大 batch 冲吞吐,在线场景收紧 token 预算保延迟,混合流量用分级调度让两类请求各取所需。调度参数的调整必须挂到业务 SLO 上验证,而不是拍脑袋调。
8. 引擎实现对比:vLLM、SGLang 与 TensorRT-LLM
三大引擎的调度理念各有侧重,选型看团队诉求。
vLLM:
□ Continuous Batching + PagedAttention + Chunked Prefill
□ 迭代级调度,开源通用,社区活跃
□ 新特性跟进快(prefix caching、投机解码、FP8)
SGLang:
□ RadixAttention(前缀树)+ FastAPI 级调度
□ 强在多轮对话/嵌套调用的前缀复用
□ 深度对接各类模型、工具调用场景
TensorRT-LLM:
□ NVIDIA 官方,in-flight batching + 内核融合
□ 在 NVIDIA 硬件上极致性能,编译优化
□ 闭源生态 + 硬件锁定
比较维度:
□ 性能上限:TensorRT-LLM(N 卡)≈ SGLang ≈ vLLM
□ 通用性/生态:vLLM 最优
□ 前缀共享:SGLang 最强
□ 集成深度:TensorRT-LLM(与 CUDA 生态最贴)
实测口径差异:
□ 各引擎的 benchmark 场景、tokenizer、采样参数不一致
□ 对比必须「同模型、同输入分布、同硬件」
□ 用你自己的负载压测,而不是信别人的数字
工程要点:引擎选型是「性能 / 通用 / 生态」三选二——TensorRT-LLM 在 N 卡上性能极致但硬件锁定,SGLang 前缀共享最强适合多轮对话,vLLM 开源通用最适合大多数团队起步。任何引擎对比都要用自己的负载重测。
9. 生产调优与踩坑
调度系统的生产调优,一半在参数,一半在观测。
高频踩坑:
□ 只压测固定长度输入 → 真实「长+短混合」下 P99 崩掉
□ 盲目拉大 max_num_seqs → 长请求进来直接 OOM
□ 开启 chunked prefill 但 chunk 太小 → 吞吐暴跌
□ 抢占阈值设太激进 → 频繁换出换入,NVMe 带宽被吃光
□ 不观测 → 队列堆积到几秒才发现,恢复极慢
观测指标:
□ TTFT / TPOT / P99:业务体验的直接度量
□ 每轮迭代的 batched tokens:调度效率
□ 抢占/换出次数:显存压力信号
□ 队列深度:过载的前置信号,应最先告警
# vLLM 压测脚本(覆盖混合长度)
python benchmarks/benchmark_throughput.py \
--model meta-llama/Meta-Llama-3-8B-Instruct \
--input-len 1024 --output-len 256 \
--num-prompts 2000 \
--enable-chunked-prefill
# 用两份负载:纯短请求(聊天)+ 长短混合(RAG)
# 分别看吞吐与 P99 两套数字
调优顺序建议:
1. 定业务 SLO(TTFT < 1s,P99 < 3s,吞吐下限)
2. 用真实长度分布压测,找到 batch/token 预算上限
3. 开启 chunked prefill,逐步调 chunk 大小
4. 挂上队列深度告警,设置自动扩容阈值
5. 周期性回归:模型更新、流量变化后重测
工程要点:生产调优的失败模式集中在「用固定长度压测 + 不观测」——真实负载是长短混合的,必须按业务 SLO 反推参数,并让队列深度成为最先告警的指标。调优顺序永远是「先定 SLO,再压测,后调参」。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| Prefill 什么特征 | 计算密集、矩阵乘法为主、决定 TTFT |
| Decode 什么特征 | 带宽受限、算力利用率 <10%、决定 TPOT |
| 静态批处理问题 | 同进同出,短请求的 slot 被长请求绑架 |
| 连续批处理是什么 | 每轮迭代动态增删请求,完成即走、新请求即进 |
| 显存不足怎么办 | 抢占 + 换出,换入时「丢弃重算」更划算 |
| Chunked Prefill 解决什么 | 长 prefill 拆碎与 decode 交错,摊平 P99 |
| Disaggregated Serving 是什么 | prefill 池与 decode 池物理分离,独立扩缩容 |
| Decode 怎么提速 | 增大 batch 摊销权重读取 + 量化 |
| 吞吐与延迟怎么选 | 离线冲吞吐、在线保延迟、混合用分级调度 |
| 最该监控什么 | 队列深度、抢占次数、每轮 batched tokens |
一句话记忆:Prefill 吃算力、Decode 吃带宽 → 连续批处理让请求动态进出批次 + Chunked Prefill 把长 prefill 摊平 + Disaggregated Serving 把两阶段物理解耦(KV 跨池传输)→ Decode 靠「大 batch 摊销权重读取 + 量化」提速 → 吞吐与延迟是跷跷板,用分级调度各取所需。
延伸阅读
- /ai-vllm-system/ — vLLM 连续批处理与调度实现
- /ai-llm-inference-architecture/ — 推理服务架构与并行范式
- /ai-inference-engine-comparison/ — 主流推理引擎对比
- /ai-inference-benchmark/ — 吞吐与延迟的压测方法论
- /ai-speculative-decoding/ — 投机采样加速 Decode
- 分布式系统专题 — 分布式调度与队列
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。