Disaggregation 队列、bootstrap 与 transfer 阶段#

这章解决什么问题#

前面的调度与内存章节大多默认请求在单条本地 runtime 主线里排队、等待、forward、完成。但一旦进入 prefill/decode disaggregation,队列形状会明显变化:不再只是一个 waiting_queue,而会出现 bootstrap queue、prealloc queue、transfer queue,甚至 retracted queue。没有这一章,调度与内存章节仍然会偏单机稳态,而覆盖不到分离式运行时的真正队列结构。

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

scheduler.pyreq_time_stats.py 看,disaggregation 不是纯传输细节,而是会直接改变:

  • 请求进入哪个队列
  • 哪些阶段被记录进时间线
  • queue time 如何拆解
  • transfer / bootstrap 是否成为新的瓶颈

这意味着它理应属于调度与内存主线,而不是只留在 disaggregation 实现目录里。

一张图:disaggregation 模式下,请求不再只经过一个 waiting queue#

这张图解决的理解障碍是:很多读者脑中默认只有 waiting_queue -> forward,但 disaggregation 会把中段拆成更多阶段。

flowchart LR
    Req["incoming req"] --> P0["prefill bootstrap queue"]
    P0 --> P1["wait queue"]
    P1 --> PF["prefill forward"]
    PF --> T["transfer queue / KV transfer"]
    T --> D0["decode prealloc queue"]
    D0 --> D1["decode transfer queue"]
    D1 --> DW["decode waiting"]
    DW --> DF["decode forward"]

图比纯文字多解释的一点是:disaggregation 不是只在 forward 之后多一个传输动作,而是把整个排队结构改写成多段阶段链。

scheduler.init_disaggregation() 真正改变了什么#

scheduler.py 会根据 disaggregation_mode 初始化不同结构:

  • decode 模式会建立 DecodeTransferQueueDecodePreallocQueue
  • prefill 模式会建立 PrefillBootstrapQueue

这说明 disaggregation 不是某个 runtime flag 轻微影响行为,而是从 scheduler 初始化时就改变了队列拓扑。

_add_request_to_queue(...) 为什么是阅读入口#

这段代码非常能说明问题:

  • DisaggregationMode.NULL:进普通 waiting_queue
  • DisaggregationMode.PREFILL:进 disagg_prefill_bootstrap_queue
  • DisaggregationMode.DECODE:进 disagg_decode_prealloc_queue

并且在进入不同队列时,还会更新不同的 ReqTimeStats 阶段:

  • set_prefill_bootstrap_queue_entry_time()
  • set_decode_prealloc_queue_entry_time()
  • set_retract_time()

这说明 queue 类型和时间线语义是同步变化的,不是两个独立系统。

ReqTimeStats 为什么在这里特别重要#

req_time_stats.py 对 disaggregation 路径做了比普通请求更细的拆分:

  • prefill_bootstrap
  • prefill_transfer_kv_cache
  • decode_prealloc
  • decode_transfer
  • decode_transferred

这说明系统不是只想“支持 transfer”,而是想让 transfer 成为可被独立观察、可被独立归因的阶段。

也正因为这样,disaggregation 章节和前面写过的 RequestStage/ReqTimeStats 可以形成非常强的回扣。

prefill 路径与 decode 路径为什么不对称#

ReqTimeStats 文件里已经直接写出两条典型路径:

  • Prefill: bootstrap_queue -> wait_queue -> forward -> transfer_queue -> completion
  • Decode: prealloc_queue -> transfer_queue -> wait_queue -> forward -> completion

这说明两条路径并不是镜像关系。prefill 更像“先建立 bootstrap,再去等和跑”;decode 更像“先预分配,再等待 transfer,再进入正常 decode waiting/forward”。

这点很值得写进书里,因为它能防止读者把 disaggregation 简化成“prefill 和 decode 只是拆成两个节点”。

transfer metrics 为什么属于调度证据而不只是网络指标#

ReqTimeStats.compute_and_observe_kv_transfer_metrics(...) 会计算:

  • transfer latency
  • transfer total MB
  • transfer speed GB/s

这说明 KV transfer 在这里不是隐藏在底层 transport 里的黑箱,而是被提升成调度可见的瓶颈证据。

对维护者来说,这意味着:

  • 如果请求慢,不一定是 scheduler 排序问题
  • 也可能是 bootstrap / transfer 阶段本身在拖后腿

scheduler.is_fully_idle() 为什么还要管 disaggregation 队列#

源码里明确写了,waiting_queue 之外,bootstrap / prealloc / transfer 队列也会影响“系统是否 truly idle”的判断。因为这些队列里的请求虽然还没进入最终 forward,但它们同样代表系统仍在承担活跃工作。

这是一种非常现实的设计:调度系统不会只盯着 running batch,而会把“尚未完成 transfer/bootstrapping 的请求”也视作活跃负担。

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

1. 把 disaggregation 问题当成单纯网络问题#

实际上它先表现为队列结构和时间线结构变化。

2. 以为只有 waiting_queue 影响调度#

disaggregation 场景下有多条正式队列。

3. 把 transfer 时间看成外部附加信息#

系统已经把它纳入 request stage 和 metrics 主线。

如果你怀疑问题出在 disaggregation 队列,先怎么查#

建议按这个顺序:

  1. 先确认当前 disaggregation_modeNULLPREFILL 还是 DECODE
  2. 看请求进入的是哪一条队列,而不是只盯 waiting_queue
  3. ReqTimeStats 当前卡在哪个 stage:bootstrap、transfer 还是真正 forward。
  4. 再看 transfer metrics、bootstrap room 或 offload/transfer 状态。
  5. 最后才深入具体 transport backend 或 staging handler。

小结#

这一章真正想补齐的,是调度与内存主线里对分离式运行时的覆盖:

  • disaggregation 会改变队列拓扑。
  • 队列拓扑变化会同步改变 request stage 时间线。
  • 只有把 bootstrap / prealloc / transfer 阶段当成正式调度阶段看,才能真正理解这类运行时的行为。