Scheduler event loop 矩阵与运行模式切换#

这章解决什么问题#

运行时架构前面已经把分层、装配、消息织网和 data parallel controller 讲出来了,但还有一个非常容易让读者困惑的点没有单独展开:Scheduler 并不是只有一个 event loop。根据 overlap、pipeline parallel、pdmux 以及 disaggregation mode 的组合,系统会进入一组不同的 event loop 变体。

这一章的目标,就是把这组“模式矩阵”讲清楚。

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

很多读者第一次读 scheduler.py 时,会天然把 event_loop_normal() 当成唯一主循环,然后在后面遇到 event_loop_overlap()event_loop_*_disagg_prefill()event_loop_*_disagg_decode() 时逐渐迷失。实际上,源码已经把这件事抽象得很明确:dispatch_event_loop(scheduler) 会根据一组模式开关选择不同主循环。

一本更像大部头技术书的作品,不应该让读者在这里靠自己“猜矩阵”,而应该把矩阵写出来。

一张图:Scheduler 主循环不是单条,而是一组模式选择#

这张图解决的理解障碍是:读者容易把不同 event loop 看成互不相干的特殊实现,而不是同一调度器在不同模式下的主循环矩阵。

flowchart TD
    Start["dispatch_event_loop(scheduler)"] --> Mode["disaggregation_mode"]
    Mode --> Null["NULL"]
    Mode --> Prefill["PREFILL"]
    Mode --> Decode["DECODE"]
    Null --> N1["pdmux / pp / overlap / normal"]
    Prefill --> P1["pp_disagg_prefill / overlap_disagg_prefill / normal_disagg_prefill"]
    Decode --> D1["pp_disagg_decode / overlap_disagg_decode / normal_disagg_decode"]

图比纯文字多解释的一点是:Scheduler 的主循环选择,是一棵正式的模式树,而不是零散的 if/else 补丁。

run_event_loop() 的意义:先建调度 stream,再选主循环#

scheduler.pyrun_event_loop() 很短,但很关键:

  1. 先创建 schedule_stream
  2. CPU 情况下再做一个 no-op synchronize 修正
  3. 最后进入 dispatch_event_loop(self)

这说明 event loop 选择并不是在 scheduler 外部偷偷决定的,而是 scheduler 自己运行入口的一部分。

dispatch_event_loop(...) 已经把模式树写得很清楚#

源码里这棵树大致是:

  • DisaggregationMode.NULL
    • enable_pdmux -> event_loop_pdmux()
    • pp_size > 1 -> event_loop_pp()
    • enable_overlap -> event_loop_overlap()
    • else -> event_loop_normal()
  • DisaggregationMode.PREFILL
    • pp_size > 1 -> event_loop_pp_disagg_prefill()
    • enable_overlap -> event_loop_overlap_disagg_prefill()
    • else -> event_loop_normal_disagg_prefill()
  • DisaggregationMode.DECODE
    • pp_size > 1 -> event_loop_pp_disagg_decode()
    • enable_overlap -> event_loop_overlap_disagg_decode()
    • else -> event_loop_normal_disagg_decode()

从技术书角度看,这段代码非常值得被直接翻译成模式矩阵,因为它能显著降低读者对 scheduler 模式爆炸的心理负担。

为什么这层不是“只是几个实现变体”#

因为它会直接改变:

  • 结果队列是否存在
  • CPU/GPU overlap 是否发生
  • prefill/decode 是否分离
  • pipeline parallel 的推进方式
  • 哪些结果在什么时候被 process_batch_result(...) 吸收

这意味着 event loop 的选择不是局部优化,而是调度器整体行为边界的一部分。

event_loop_normal()event_loop_overlap() 的根本差异#

event_loop_normal()#

它比较直观:

  1. 收请求
  2. get_next_batch_to_run()
  3. run_batch(batch)
  4. process_batch_result(batch, result)
  5. 更新 last_batch

event_loop_overlap()#

它则显式维护 result_queue,把:

  • 当前 batch 的 GPU 运行
  • 上一个 batch 的 CPU 结果处理

重叠起来。这说明 overlap 模式并不是把普通循环“稍微并行一下”,而是从主循环结构上引入了新的中间状态。

这也解释了为什么前面的 execution / scheduling 章节经常要专门讨论 overlap 对 grammar、result queue 和 last_batch 的影响。

为什么 disaggregation 还要再乘一层矩阵#

因为一旦进入 PREFILLDECODE disaggregation mode,排队结构和 batch 结果处理方式都变了。也就是说:

  • overlap 与 normal 是一层变化
  • disaggregation 又是另一层变化

这正是 scheduler 模式矩阵看起来复杂的根本原因:它不是单轴变化,而是多轴组合。

这层和前面几章怎样回扣#

这章和整本书很多地方会自然回扣:

  • 与运行时架构:解释为什么一个 scheduler 会有多种运行人格
  • 与调度与内存:解释不同 event loop 会怎样改变 waiting/batch/update 的形状
  • 与 execution model:解释 overlap 为什么会影响结果处理和 grammar 同步
  • 与扩展与调试:解释排障时为什么先确认当前运行模式非常重要

这也是为什么它值得单独成章,而不是只在各章边角料里零散出现。

这一层最容易出现的误判#

1. 把 event_loop_normal() 当成唯一主循环#

这会让你在 overlap / disaggregation 场景下误读很多后续逻辑。

2. 以为 overlap 只是“性能优化开关”#

它其实直接改变主循环结构。

3. 以为 disaggregation 只改数据传输,不改调度主循环#

源码明确说明它也改 event loop 选择。

如果你要排查 scheduler 行为异常,先怎么走#

建议先问这三个问题:

  1. 当前 disaggregation_mode 是什么?
  2. 是否启用了 overlap?
  3. 是否启用了 pp_size > 1enable_pdmux

这三个问题一旦答不清,后面看任何调度异常都容易走偏。

小结#

这一章真正想补齐的,是运行时架构里对调度主循环的“模式树解释”:

  • Scheduler 不是一个 event loop
  • 它是一组按模式矩阵切换的主循环
  • 只有把这层矩阵看清,后面调度、执行和排障章节里的很多分支才会真正连成一套系统

到这里,运行时架构对 scheduler 的解释就不只停在“有个主循环”,而开始解释“为什么主循环本身会有多种人格”。