CHAPTER 10 / 推理与系统
连续批处理与调度
让请求及时进出批次,同时守住等待时间与公平性。
- 通过时间线解释静态批处理与连续批处理的区别
- 分析 prefill、decode、token 预算与显存容量之间的关系
- 同时评估吞吐、尾部延迟与不同请求群体的公平性
同一批里的请求不会同时结束
假设服务器一次能处理两条生成序列。请求 A 需要四个 decode 步,请求 B 只需要一个。静态批处理把它们一起启动,并等整批结束再加入 C。B 完成后,属于它的处理位置可能空着,而 C 还在队列等待。输出长度难以准确预知,因此这类空洞在长短混合请求中很常见。
连续批处理在迭代边界重新决定活跃请求集合。B 完成后,下一个边界就可以移走它并接纳 C,让资源持续处理有效工作。ORCA 论文讨论的 iteration-level scheduling 是理解这一机制的经典入口。具体系统如何混合 attention、prefill 与 decode 有实现差异,不能把“支持连续批处理”直接等同于某个固定加速倍数。
先用理想化时间线看机制
继续使用两个位置,三个请求同时到达,长度依次为 A=4、B=1、C=1,假设每次迭代固定一秒,忽略 prefill。静态批处理第一批 A、B 在第 4 秒结束,C 在第 5 秒结束;即便允许 B 在第 1 秒把答案发给用户,C 仍要等到第 4 秒才入批。
连续批处理让 A、B 在第 1 秒各生成一个 token,B 离开;C 在第 2 秒完成,A 在第 4 秒完成。总工作量没有改变,空闲位置却被利用起来。真实硬件中迭代时间会随批大小、上下文和内存访问变化,所以这个算例解释的是调度机会,不是 GPU 速度预测。上线结论必须来自真实负载测量。
调度器面对两份预算
一份是当前迭代可以处理的 token 预算,影响每次运行量与延迟;另一份是长期保留的 KV 内存预算,约束活跃序列总长度。一个新请求可能只有几百个输入 token,但它之后还会不断生成,缓存会继续增长。只看当前能否入批,忽略未来增长,容易在运行中触发抢占或重计算。
长 prefill 会占用较多计算,影响已经开始流式输出的请求。chunked prefill 把长输入切成多段,在调度中与 decode 交错。较小分块有机会改善输出间隔,但也可能延长新请求首 token 等待;较大分块可能更高效,却制造更长停顿。最佳设置依赖负载与硬件,要查看实际版本的调度规则后测量。
本机实验:观察空位置如何被填上
下面模拟只有 decode、固定迭代时间、所有请求同时到达的系统。保存为 batching_lab.py 后运行。static 允许完成的请求立刻返回,但不提前补位;continuous 则每一步都补位。
from collections import deque
jobs = [("A", 4), ("B", 1), ("C", 1)]
def simulate(continuous):
waiting = deque(jobs)
active, done, tick = [], {}, 0
while waiting or active:
if continuous or not active:
while waiting and len(active) < 2:
name, steps = waiting.popleft()
active.append([name, steps])
tick += 1
remaining = []
for name, steps in active:
steps -= 1
if steps == 0:
done[name] = tick
else:
remaining.append([name, steps])
active = remaining
return done, tick
static, t_static = simulate(False)
continuous, t_cont = simulate(True)
print("static", static, "total", t_static)
print("continuous", continuous, "total", t_cont)
assert (static["C"], continuous["C"]) == (5, 2)
assert (t_static, t_cont) == (5, 4)把 A 改成二十步,你会看到 C 的等待差距扩大。再把所有任务改成相同长度,观察连续补位的收益是否缩小。两种情况下都不要把步数换算为真实毫秒,因为模拟没有建模批大小对每步耗时的影响。
吞吐更高不代表每个人都更快
如果总是优先短请求,平均延迟可能很好,但长请求会被不断到来的短请求推迟,这叫饥饿。反过来严格先到先服务,长 prefill 又可能让后面许多短请求排队。公平性必须具体定义,例如按租户限制并发、让等待时间随优先级增长,或给长请求保留最低服务份额。
除了全局 p50、p95,还要按长短输入、长短输出与租户分组。p95 是排序后约 95% 样本不超过的值,它不保证剩余 5% 的体验,也不说明谁一直受苦。交互助手更关注首 token 与流式停顿,离线批处理更关注完工时间与费用,因此两种产品可以有不同调度目标。
用负载曲线找容量边界
测试时逐渐提高请求到达率,保持任务分布不变,记录完成吞吐、排队长度、TTFT、ITL 和失败率。开始阶段吞吐随负载上升;接近饱和后,队列可能持续增长,延迟恶化,即使 GPU 利用率看起来漂亮也无法满足用户要求。合理容量应以延迟目标和错误率约束下的稳定吞吐定义。
闭环测试是一个请求完成后再发下一个,容易在服务变慢时自动减少到达量;开环测试按预先指定节奏发请求,更能暴露排队,但要防止负载发生器自身成为瓶颈。报告应说明采用哪种方式。常见误区是同时调大并发、缩短输出、预热缓存,再把收益全部归给批处理参数。
在达到稳定状态的系统里,可以用平均在途请求数约等于到达率乘平均停留时间做一致性检查。例如每秒接收五个请求、平均停留两秒,长期平均约有十个请求在排队或执行。这个关系不能用来掩盖不断增长的队列,也不能把平均值替代尾部指标;它帮助发现统计窗口、单位或请求计数存在明显矛盾。
LEARN BY DOING
比较两种调度策略的收益与代价
- 运行模拟器,给四个不同长短分布记录每个请求的完成步数。
- 增加任务到达时间,确保只接纳已经到达的请求。
- 设计一个“短请求总是插队”的策略,并构造长请求持续被推迟的反例。
- 写出服务容量报告应包含的负载、延迟、吞吐与公平性指标。
验收标准
- 原始 A/B/C 示例得到 C 的完成步数 5 与 2,总步数 5 与 4。
- 自定义策略的每个任务都能追溯到到达时间与完成时间。
- 报告明确固定迭代假设,包含至少一个策略让部分请求变差的反例。
合上书,检查一下
先作答,再看解释。答错的地方,正好是值得再读一遍的地方。
配合本章看视频
下面按本站章节主题选配。为什么没有采用原课录播 →
vLLM推理框架|持续批处理 continuous batch 介绍 ↗
作者简介直接比较 request-level 与 iteration-level 调度,适合给连续批处理实验提供中文原理说明。
带着问题看 · 观察短请求提前结束后留下的空闲位置,以及新请求何时能加入;再运行本章调度实验,对照吞吐量与请求等待时间的变化。
查看来源与观看说明 →Stanford CS336 · Lecture 10: Inference (Spring 2025) ↗
用同一套性能模型串起推理成本、KV cache 与批处理,是系统部分的主参考课。对应第 7 章时只补充服务侧成本取舍,不代替模型路由策略。
带着问题看 · prefill 与逐 token 解码的计算/内存差异;TTFT、延迟、吞吐之间的取舍;KV cache 大小、continuous batching 与 PagedAttention;量化和 speculative sampling 的不同精度保证。
查看来源与观看说明 →vLLM: Easy, Fast, and Cheap LLM Serving ↗
由 vLLM 共同作者讲解推理服务;Stanford CS336 官方推理讲义也直接推荐这条演讲。
带着问题看 · 将单模型生成转变为面向多用户的服务;KV cache 的内存浪费与分页管理;从内存管理理解批量吞吐收益。
查看来源与观看说明 →MIT 6.5940 · Lecture 13: LLM Deployment Techniques (Fall 2024) ↗
用高校公开课补充大模型部署与压缩优化的背景,配合本站量化章节理解资源约束。
带着问题看 · 对比 SmoothQuant、AWQ 等量化方法;区分低比特量化与推测解码的机制;结合官方讲义中的 speculative decoding 和 batching,检查加速的硬件、负载和质量前提。
查看来源与观看说明 →