读 scheduler.py 与 schedule_batch.py:主循环和对象骨架#
代码导读讲到这里,读者已经知道怎样从入口读到 runtime、从协议对象读到内部对象,但真正最硬的一条源码阅读路径仍然还在眼前:如果你真的准备深入 scheduler.py,怎样避免被几千行代码和一堆辅助对象淹没?更具体地说,scheduler.py 和 schedule_batch.py 到底该怎样配对阅读,才能先抓住主循环和对象骨架,再去读 priority、retract、disaggregation、overlap 这些复杂分支。
这一章真正要解决的,不是“告诉你去看 scheduler”,而是给出一条面对最难核心文件时仍然能稳住的阅读路径。
先把这两棵树看成一对,而不是两个平行文件#
单独读 scheduler.py,你会看到大量流程:收请求、排队、选 batch、跑 forward、处理结果、retract、flush、abort、disaggregation event loop。单独读 schedule_batch.py,你又会看到一整组对象:Req、ScheduleBatch、PrefillAdder、各种 mixin、各种状态字段。
真正高效的读法,是先接受一个简单判断:
scheduler.py讲“流程怎么走”schedule_batch.py讲“流程到底在操作哪些对象”
也就是说,这两棵树分别提供动作骨架和对象骨架。把它们拆开读,你很容易只记住函数名和类名;配对读,你才更容易看清系统到底在推进什么状态。
下面这张图的价值也就在这里,它不是在抽象 scheduler,而是在明确这对文件之间的关系:
flowchart LR
Recv["recv request"] --> Build["Req / ScheduleBatch build-up"]
Build --> Select["get_next_batch_to_run()"]
Select --> Run["forward / process_batch_result"]
Run --> Update["running_batch / last_batch update"]
Update --> Build图里最重要的不是“循环长这样”,而是它说明 scheduler 主循环从来不是在空中飞,而是始终围绕 Req 和 ScheduleBatch 这两层对象骨架反复推进。
文件头注释已经把最重要的阅读入口写给你了#
schedule_batch.py 开头最值钱的不是 import,而是那句非常直白的结构流向:
ScheduleBatch -> ModelWorkerBatch -> ForwardBatch
并且它还明确区分了各自职责:
ScheduleBatch更偏 scheduler 侧的高层 CPU 视图ModelWorkerBatch是给 worker 的 GPU 相关子集ForwardBatch则是ModelRunner真正消费的低层 tensor 视图
这意味着这棵树不是随手定义几个类,而是在非常明确地告诉你:scheduler 操作的对象会一路降成执行对象。因此更好的阅读策略不是“把这个文件当成实现细节”,而是先把它看成调度对象的总谱。
真正的阅读顺序应该先抓 Req,再抓 ScheduleBatch#
面对这对文件时,最稳的顺序不是先盯最大函数,而是先确认对象骨架:
ReqScheduleBatch- 再回头看 scheduler 怎样不断改变这两个对象
原因很简单:
Req是单请求状态骨架ScheduleBatch是请求集合状态骨架
后面你看到的大多数流程,无非就是围绕这两层对象做增删改查、切 batch、做 merge、回写结果。如果对象骨架先没稳住,主循环再怎么读都会像函数海。
scheduler.py 真正的主循环不是一个函数,而是一组稳定入口#
对第一次深入这个文件的读者来说,最值得盯住的通常不是所有分支,而是这一组相对稳定的入口:
get_next_batch_to_run()get_new_batch_prefill()update_running_batch(...)process_batch_result(...)
它们合起来,几乎就是 scheduler 的“选择 -> 执行 -> 更新”闭环。只要先把这几段抓住,priority、disaggregation、overlap、retract 这些复杂度就会自然变成“主循环上的复杂分支”,而不再像一开始就扑面而来的噪音。
这也是一本技术书在代码导读部分最该替读者做的事:不是重复告诉你“这个文件很重要”,而是帮你降低第一次接触的认知成本。
running_batch、last_batch、cur_batch 是最值得先记住的三块状态#
很多人第一次读 scheduler.py 时会被分支淹没,实际更稳的做法是先把这三个变量当成主循环对世界的三种视图:
running_batch:当前仍在推进的 decode 批次cur_batch:这一轮真正送去跑的批次last_batch:上一轮结果回来后仍然会影响下一轮 merge / update 的批次
只要这三者关系理不清,后面的 overlap、prefill merge、disaggregation、retract 都会显得异常困难。反过来,只要这三块状态先稳住,主循环里再多的模式分支也更容易被看成是在改写这几块状态。
PrefillAdder 属于这条阅读路径,不是旁支#
很多人第一次读调度核心时会忽略 PrefillAdder,但它其实站在非常关键的门口:
- waiting queue 排序之后
- 真正构造 prefill batch 之前
它的作用不是“又一个 helper”,而是在 admission 现实里决定:哪些请求还能进这一轮新的 prefill batch。把它放进这条阅读路径,读者就更容易理解 scheduler 并不是先有一个 batch 再去跑,而是要先经过一层专门的“添加器”来把 waiting queue 里的请求真正塑形成 batch。
process_batch_result(...) 是主循环的另一半灵魂#
如果说 get_next_batch_to_run() 负责“选”,那 process_batch_result(...) 就负责“收”。它会根据 forward mode 或 disaggregation mode,把执行结果送进不同后处理路径,并进一步更新 running_batch、last_batch 或 finished request 相关状态。
这也是这对文件必须配对读的原因:只看“怎么选 batch”而不看“结果怎样被吸收”,你永远看不懂为什么 scheduler 状态会长成那样。反过来,只看 process_batch_result(...) 而不理解 batch 是怎么选出来的,很多后处理逻辑也会像凭空冒出来的一样。
真正的稳法不是“从头读到尾”,而是先抓骨架,再补复杂人格#
更稳的源码阅读顺序通常是:
ReqScheduleBatchget_next_batch_to_run()update_running_batch(...)process_batch_result(...)- 最后再回去补 priority、routing、retract、disaggregation、overlap 分支
这条顺序很像整本书本身的写法:先抓主干,再下钻复杂路径,而不是一开始就掉进最深的局部分支里。
最容易出现的三种误判#
第一,误把 scheduler.py 当成纯流程文件。
它的很多复杂度都来自它反复操纵的 batch 对象骨架。
第二,误把 schedule_batch.py 当成纯数据定义文件。
里面塞满了会直接改写调度行为的方法与状态。
第三,一上来就读边缘模式。
这样最容易把 overlap、disaggregation、retract 误当主线,而不是主线上的复杂人格。
真正第一次深入调度核心时,最稳的问题顺序#
如果要把这章压成几个更可操作的问题,可以这样问自己:
- 单请求的真实运行时主语是谁?
答案应先回到Req。 - 请求集合怎样被组织成当前可执行 batch?
答案应回到ScheduleBatch和PrefillAdder。 - scheduler 每一轮真正做哪三个动作?
选择、执行、更新。 - 结果回来以后,哪一层对象首先被改写?
先是 batch 状态,再是请求状态。
只要这些问题先稳住,再去读最复杂的模式分支,理解就不会散。
小结#
这一章真正想补齐的,不是“再强调一下 scheduler 很重要”,而是给出一条面对最难核心文件时仍然稳健的阅读法:
scheduler.py是主循环schedule_batch.py是对象骨架- 两者必须配对阅读,调度主线才不会被读散
到这里,代码导读部分才真正开始回答另一个更难的问题:面对最硬的核心文件,应该怎么读。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。