学习手册 / 连续批处理与调度课程官网 ↗

CHAPTER 10 / 推理与系统

连续批处理与调度

让请求及时进出批次,同时守住等待时间与公平性。

进阶建议阅读 30 分钟,练习另计自编配套教程
读完这一章,你应该能
  • 通过时间线解释静态批处理与连续批处理的区别
  • 分析 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

比较两种调度策略的收益与代价

  1. 运行模拟器,给四个不同长短分布记录每个请求的完成步数。
  2. 增加任务到达时间,确保只接纳已经到达的请求。
  3. 设计一个“短请求总是插队”的策略,并构造长请求持续被推迟的反例。
  4. 写出服务容量报告应包含的负载、延迟、吞吐与公平性指标。

验收标准

  • 原始 A/B/C 示例得到 C 的完成步数 5 与 2,总步数 5 与 4。
  • 自定义策略的每个任务都能追溯到到达时间与完成时间。
  • 报告明确固定迭代假设,包含至少一个策略让部分请求变差的反例。
下载配套离线实验包 ↓ · 脚本使用模拟模型与数据,运行无需 API。

合上书,检查一下

先作答,再看解释。答错的地方,正好是值得再读一遍的地方。

1. 连续批处理最核心的变化是什么?
2. 总是优先最短请求最需要防范哪种问题?

配合本章看视频

下面按本站章节主题选配。为什么没有采用原课录播 →

YouTube · 英语同主题参考 · 非 CS2680 原课

Stanford CS336 · Lecture 10: Inference (Spring 2025) ↗

用同一套性能模型串起推理成本、KV cache 与批处理,是系统部分的主参考课。对应第 7 章时只补充服务侧成本取舍,不代替模型路由策略。

带着问题看 · prefill 与逐 token 解码的计算/内存差异;TTFT、延迟、吞吐之间的取舍;KV cache 大小、continuous batching 与 PagedAttention;量化和 speculative sampling 的不同精度保证。

查看来源与观看说明 →
YouTube · 英语同主题参考 · 非 CS2680 原课

vLLM: Easy, Fast, and Cheap LLM Serving ↗

由 vLLM 共同作者讲解推理服务;Stanford CS336 官方推理讲义也直接推荐这条演讲。

带着问题看 · 将单模型生成转变为面向多用户的服务;KV cache 的内存浪费与分页管理;从内存管理理解批量吞吐收益。

查看来源与观看说明 →

继续读原始资料

完成状态与自测成绩独立记录。