running_batch、cur_batch、last_batch 的状态机#
这章解决什么问题#
前面的调度与内存章节已经解释了 waiting queue 排序、PrefillAdder 准入预算、retract 和 disaggregation 队列,但还有一个对理解 scheduler.py 极其关键、同时又很容易被埋在流程细节里的问题没有单独讲:running_batch、cur_batch、last_batch 这三个状态对象到底各自代表什么,它们之间又是怎样在一轮轮循环里流转的?
这一章就是把这条 batch 状态机讲清楚。
为什么这层值得单独成章#
因为在复杂调度循环里,很多误读其实都来自没分清这三个对象:
cur_batch:这一轮真正送去跑的 batchlast_batch:上一轮已经跑完、但其结果仍会影响下一轮的 batchrunning_batch:持续被维护、准备用于 decode / 合并 / 更新的活动 batch
如果这三者关系不清楚,读 overlap、merge、retract、prefill 结果吸收都会非常痛苦。
一张图:调度循环真正维护的是三态 batch,而不是单个“当前 batch”#
这张图解决的理解障碍是:很多人默认 scheduler 每轮只会关心“当前 batch”,而源码实际维护的是一个小状态机。
flowchart LR
New["new prefill batch"] --> Cur["cur_batch"]
Cur --> Last["last_batch"]
Last --> Run["merge into running_batch"]
Run --> Cur这张图比纯文字多解释的一点是:调度主循环不是“生成一个 batch 然后忘掉”,而是在三个批次状态之间持续迁移和吸收结果。
init_running_status() 已经把三态框架写出来了#
源码在初始化时就明确了:
self.running_batchself.cur_batchself.last_batch
这说明这三者不是后来随手出现的局部变量,而是 scheduler 对运行世界的长期状态表示。
get_next_batch_to_run() 为什么是理解三态流转的最佳入口#
这段函数的最大价值,不只是“选下一个 batch”,而是把三态流转写得很清楚:
- 先处理 timeout / 已完成请求。
- 再考虑
last_batch如何过滤、合并进running_batch。 - 然后决定是否构造一个新的 prefill batch。
- 如果没有新 prefill,再考虑把
running_batch更新成 decode batch。 - 最后才返回真正的
cur_batch。
也就是说,它其实在做的是:
- 吸收旧结果
- 更新活动状态
- 选择当前执行单元
这比“选 batch”四个字复杂得多。
为什么 last_batch 会先于新 batch 决策发生作用#
因为上一轮的结果会改变当前世界的状态:
- 哪些请求已经 finished
- 哪些 chunked request 需要被排除或 stash
- 哪些 prefill 请求需要合并进
running_batch
如果没有先吸收 last_batch,当前这一轮就会在过时状态上做决策。也正因为这样,last_batch 不是历史包袱,而是“当前调度世界观的一部分”。
running_batch 为什么不是单纯“当前在跑的 batch”#
它在源码里的职责比字面更广:
- 持有持续推进中的 decode 请求
- 吸收上一轮 prefill merge 进来的请求
- 在需要时被
update_running_batch(...)做过滤、retract、prepare_for_decode
所以它更准确地说,是“持续演化的活动 batch 状态”,而不是“这一轮 GPU 正在跑的那一批”。
cur_batch 的意义为什么反而最局部#
这点很容易被误读。cur_batch 虽然字面上像“核心对象”,但实际上它只代表这一轮要送去执行的那个 batch。它比 running_batch 更短命,也比 last_batch 更少承担历史吸收职责。
这正好体现出一个很好的系统设计:
- 生命周期长、语义稳定的状态对象单独保留
- 当轮执行对象作为短期视图存在
update_running_batch(...) 为什么是这条状态机的第二入口#
如果说 get_next_batch_to_run() 负责“选”和“吸收”,那 update_running_batch(...) 就负责:
- 清理 finished request
- 必要时做 retract
- 调整
new_token_ratio prepare_for_decode()
也就是说,它是在对 running_batch 做一次状态压缩,让它变成下一轮还能继续推进的形状。
这说明状态机不只发生在 batch 生成阶段,也发生在 batch 结果吸收之后。
process_batch_result(...) 怎样完成这一轮到下一轮的闭环#
这个函数会根据 forward mode 把当前 batch 送到不同结果处理路径,但其最关键的作用之一是:
- 让这一轮
cur_batch的结果真正进入可被下一轮吸收的状态
没有这一步,last_batch 在下一轮就无从谈起。因此从状态机角度看:
run_batch只是执行process_batch_result才是把执行结果编织回状态机
overlap 和这条状态机为什么特别容易纠缠#
因为 overlap 模式下,“上一轮结果处理”和“这一轮 batch 启动”会在时间上交错。这会让读者更容易混淆:
- 哪个是 still running 的 batch
- 哪个是刚跑完等待吸收的 batch
也正因为这样,这一章才值得单独写,否则 overlap 章节和主调度循环很难真正读通。
这一层最容易出现的误判#
1. 把 running_batch 当成 cur_batch#
这会让很多 merge / update / retract 行为显得莫名其妙。
2. 把 last_batch 当成纯历史#
实际上它是当前世界观更新的重要输入。
3. 以为主循环每轮都从零开始挑 batch#
事实上它每轮都在吸收和延续已有状态。
如果你要顺着 scheduler 主循环读代码,先怎么走#
建议按这个顺序:
- 先看
init_running_status()中三态对象的初始化。 - 再看
get_next_batch_to_run()如何迁移三态。 - 然后看
update_running_batch(...)。 - 最后再看
process_batch_result(...)怎么闭环。
小结#
这一章真正要补齐的,是调度章节里最核心的一层状态机心智模型:
- scheduler 真正维护的是
running_batch、cur_batch、last_batch三态系统 - 新 batch 生成、旧 batch 吸收、结果处理和 decode 延续都围绕这三态展开
- 只有看清这层状态机,整个调度主循环才会真正“成书”
到这里,调度与内存章节对 scheduler 主循环的解释就更接近一本完整系统书的水准了。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。