读 scheduler.py 与 schedule_batch.py:主循环和对象骨架#

代码导读讲到这里,读者已经知道怎样从入口读到 runtime、从协议对象读到内部对象,但真正最硬的一条源码阅读路径仍然还在眼前:如果你真的准备深入 scheduler.py,怎样避免被几千行代码和一堆辅助对象淹没?更具体地说,scheduler.pyschedule_batch.py 到底该怎样配对阅读,才能先抓住主循环和对象骨架,再去读 priority、retract、disaggregation、overlap 这些复杂分支。

这一章真正要解决的,不是“告诉你去看 scheduler”,而是给出一条面对最难核心文件时仍然能稳住的阅读路径。

先把这两棵树看成一对,而不是两个平行文件#

单独读 scheduler.py,你会看到大量流程:收请求、排队、选 batch、跑 forward、处理结果、retract、flush、abort、disaggregation event loop。单独读 schedule_batch.py,你又会看到一整组对象:ReqScheduleBatchPrefillAdder、各种 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 主循环从来不是在空中飞,而是始终围绕 ReqScheduleBatch 这两层对象骨架反复推进。

文件头注释已经把最重要的阅读入口写给你了#

schedule_batch.py 开头最值钱的不是 import,而是那句非常直白的结构流向:

  • ScheduleBatch -> ModelWorkerBatch -> ForwardBatch

并且它还明确区分了各自职责:

  • ScheduleBatch 更偏 scheduler 侧的高层 CPU 视图
  • ModelWorkerBatch 是给 worker 的 GPU 相关子集
  • ForwardBatch 则是 ModelRunner 真正消费的低层 tensor 视图

这意味着这棵树不是随手定义几个类,而是在非常明确地告诉你:scheduler 操作的对象会一路降成执行对象。因此更好的阅读策略不是“把这个文件当成实现细节”,而是先把它看成调度对象的总谱。

真正的阅读顺序应该先抓 Req,再抓 ScheduleBatch#

面对这对文件时,最稳的顺序不是先盯最大函数,而是先确认对象骨架:

  1. Req
  2. ScheduleBatch
  3. 再回头看 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_batchlast_batchcur_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_batchlast_batch 或 finished request 相关状态。

这也是这对文件必须配对读的原因:只看“怎么选 batch”而不看“结果怎样被吸收”,你永远看不懂为什么 scheduler 状态会长成那样。反过来,只看 process_batch_result(...) 而不理解 batch 是怎么选出来的,很多后处理逻辑也会像凭空冒出来的一样。

真正的稳法不是“从头读到尾”,而是先抓骨架,再补复杂人格#

更稳的源码阅读顺序通常是:

  1. Req
  2. ScheduleBatch
  3. get_next_batch_to_run()
  4. update_running_batch(...)
  5. process_batch_result(...)
  6. 最后再回去补 priority、routing、retract、disaggregation、overlap 分支

这条顺序很像整本书本身的写法:先抓主干,再下钻复杂路径,而不是一开始就掉进最深的局部分支里。

最容易出现的三种误判#

第一,误把 scheduler.py 当成纯流程文件。
它的很多复杂度都来自它反复操纵的 batch 对象骨架。

第二,误把 schedule_batch.py 当成纯数据定义文件。
里面塞满了会直接改写调度行为的方法与状态。

第三,一上来就读边缘模式。
这样最容易把 overlap、disaggregation、retract 误当主线,而不是主线上的复杂人格。

真正第一次深入调度核心时,最稳的问题顺序#

如果要把这章压成几个更可操作的问题,可以这样问自己:

  1. 单请求的真实运行时主语是谁?
    答案应先回到 Req
  2. 请求集合怎样被组织成当前可执行 batch?
    答案应回到 ScheduleBatchPrefillAdder
  3. scheduler 每一轮真正做哪三个动作?
    选择、执行、更新。
  4. 结果回来以后,哪一层对象首先被改写?
    先是 batch 状态,再是请求状态。

只要这些问题先稳住,再去读最复杂的模式分支,理解就不会散。

小结#

这一章真正想补齐的,不是“再强调一下 scheduler 很重要”,而是给出一条面对最难核心文件时仍然稳健的阅读法:

  • scheduler.py 是主循环
  • schedule_batch.py 是对象骨架
  • 两者必须配对阅读,调度主线才不会被读散

到这里,代码导读部分才真正开始回答另一个更难的问题:面对最硬的核心文件,应该怎么读。