running_batch、cur_batch、last_batch 的状态机#

这章解决什么问题#

前面的调度与内存章节已经解释了 waiting queue 排序、PrefillAdder 准入预算、retract 和 disaggregation 队列,但还有一个对理解 scheduler.py 极其关键、同时又很容易被埋在流程细节里的问题没有单独讲:running_batchcur_batchlast_batch 这三个状态对象到底各自代表什么,它们之间又是怎样在一轮轮循环里流转的?

这一章就是把这条 batch 状态机讲清楚。

为什么这层值得单独成章#

因为在复杂调度循环里,很多误读其实都来自没分清这三个对象:

  • cur_batch:这一轮真正送去跑的 batch
  • last_batch:上一轮已经跑完、但其结果仍会影响下一轮的 batch
  • running_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_batch
  • self.cur_batch
  • self.last_batch

这说明这三者不是后来随手出现的局部变量,而是 scheduler 对运行世界的长期状态表示。

get_next_batch_to_run() 为什么是理解三态流转的最佳入口#

这段函数的最大价值,不只是“选下一个 batch”,而是把三态流转写得很清楚:

  1. 先处理 timeout / 已完成请求。
  2. 再考虑 last_batch 如何过滤、合并进 running_batch
  3. 然后决定是否构造一个新的 prefill batch。
  4. 如果没有新 prefill,再考虑把 running_batch 更新成 decode batch。
  5. 最后才返回真正的 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 主循环读代码,先怎么走#

建议按这个顺序:

  1. 先看 init_running_status() 中三态对象的初始化。
  2. 再看 get_next_batch_to_run() 如何迁移三态。
  3. 然后看 update_running_batch(...)
  4. 最后再看 process_batch_result(...) 怎么闭环。

小结#

这一章真正要补齐的,是调度章节里最核心的一层状态机心智模型:

  • scheduler 真正维护的是 running_batchcur_batchlast_batch 三态系统
  • 新 batch 生成、旧 batch 吸收、结果处理和 decode 延续都围绕这三态展开
  • 只有看清这层状态机,整个调度主循环才会真正“成书”

到这里,调度与内存章节对 scheduler 主循环的解释就更接近一本完整系统书的水准了。