DataParallelController、rank 路由与负载分发#

运行时架构前面已经讲了 manager 边界、typed IPC、模板解释层和热变更控制面,但如果 dp_size > 1,请求进入系统之后其实还多了一层非常关键的控制面:它不会直接落到某个固定 scheduler,而是先经过 DataParallelController 做一次 rank 级分发决策。也就是说,运行时除了要回答“请求怎样进系统”,还必须回答“它最终被送到哪一个 data parallel worker,以及为什么是它”。

这章真正要补齐的,就是这层 rank 路由与负载分发控制面。它的价值不在于“再多一个组件名”,而在于让多 DP 场景不再被简化成“复制多份 scheduler”,而能被理解成 runtime 自己正式拥有的一层 placement control plane。

先把 DataParallelController 放回请求主线#

很多读者默认“发送给 scheduler”就意味着“发送给某个固定 scheduler”。在 dp_size > 1 场景下,真实路径要更长。下面这张图的职责,就是把这层中间控制面明确画出来:

flowchart LR
    TM["TokenizerManager"] --> DPC["DataParallelController"]
    DPC --> W0["DP worker 0 / scheduler"]
    DPC --> W1["DP worker 1 / scheduler"]
    DPC --> WN["DP worker N / scheduler"]
    Load["load budget / active ranks / routing hints"] --> DPC

这张图最重要的地方是:请求在进入某个具体 scheduler 之前,先经过一次 rank 级别的路由决策。这正是多 DP 运行时和单实例运行时最值得单独成章的差别。

这不是 round-robin 小工具,而是一层正式控制面#

DataParallelController 如果只从名字猜,很容易被低估成“多一个 router”。更稳的理解是,它至少同时负责:

  • 接收来自 tokenizer 侧的请求
  • 根据 LoadBalanceMethod 选择目标 worker
  • 处理显式 routed_dp_rank
  • enable_dp_attention 场景下组织更复杂的 worker 拓扑
  • 更新负载预算与 active ranks

也就是说,它本身已经是运行时架构里的正式节点,而不是部署脚本外的一层代理。把它写进运行时架构,而不是留在运维附录里,正是为了让读者接受:placement 不是外围设施,而是 runtime 行为的一部分。

启动阶段就已经决定了“它依据什么来分发”#

从构造器的角度看,这层控制面最先确定的并不是某条请求去哪,而是系统接下来用什么人格去回答“去哪”:

  • load_balance_method
  • dp_budget
  • workers
  • status
  • 是否启用 dp_attention

这说明路由决策并不是请求来了以后再临时判断,而是在运行时启动时就已经把“世界怎样被划分、根据什么分配”钉成了一套控制人格。只要这一点先稳住,后面看不同分配策略时就不会像在看一堆分散 if/else。

LoadBalanceMethod 是最稳的阅读入口#

如果只抓一个入口,最值得先看的其实不是 controller 主循环,而是 LoadBalanceMethod。因为它先告诉你系统到底承认哪几种分发人格:

  • ROUND_ROBIN
  • FOLLOW_BOOTSTRAP_ROOM
  • TOTAL_REQUESTS
  • TOTAL_TOKENS

这会让你在读后续逻辑时不至于把所有分支都看成杂乱的实现细节,而会意识到:这些分支本质上是在兑现不同的 placement 策略。

尤其后两种基于 budget 的路径,更明确说明这层控制面不是 stateless router,而是带有负载模型的运行时决策器。

DPBudget 让这层真正变成“带状态的放置器”#

DPBudget 维护的并不是抽象统计,而是 controller 当前用于决策的活预算,例如:

  • total_requests
  • total_tokens

这说明 controller 问的从来不是“下一个发给谁最平均”,而是“当前谁更有余量、这条请求在什么负载模型下更适合被送到哪里”。这类设计特别值得系统书单独指出,因为它会直接影响读者对多 DP 运行时的理解:

  • placement 不是偶然发生的
  • placement 是 runtime 自己维护的一部分状态

routed_dp_rank 之所以关键,是因为运行时并不垄断最终去向#

serving_base.py 允许从 header 提取 routed_dp_rank,而 DataParallelController.maybe_external_dp_rank_routing(...) 会优先尊重它。这说明系统承认两种放置来源会并存:

  • 有时由内部策略和预算决定
  • 有时由更外层的 router 或调度系统显式指定

从平台视角看,这是很成熟的设计:runtime 提供内建路由能力,但不垄断最终决策权。对读者来说,这一点也特别值钱,因为它能直接改变你对“为什么同一类请求有时总落到某个固定 rank”的解释方式。

dispatching_with_trace(...) 把 placement 也放进了请求时间线#

这也是很多读者第一次会忽略的点。controller 不只是把请求转发出去,它还会把请求时间统计升级成 DPControllerReqTimeStats,并记录:

  • set_dp_dispatch_time()
  • set_dp_dispatch_finish_time()

这意味着 data parallel 路由本身也是请求时间线里的正式阶段,而不是透明零成本操作。前面讲过的 ReqTimeStats 在这里就会自然回扣:placement 也是一段值得被观察、被解释的 runtime 行为。

active ranksstatus 告诉你:分发策略从来不只看负载#

update_active_ranks(...) 会更新 status,而 round-robin 等逻辑又会跳过不活跃 worker。这说明 controller 决策时不只看预算,还要同时看:

  • 谁当前还活着
  • 谁当前还能接请求

也就是说,多 DP 运行时里的路由问题从来不是纯 throughput 问题,而是“路由 + 活性 + 预算”三者共同作用的结果。技术书如果只讲其中一个,就会把这层控制面写扁。

enable_dp_attention 让这层从“多 worker 分发”变成更复杂的拓扑问题#

当启用 DP attention 时,worker 启动方式和 control_message_step 都会变化。这说明 data parallel 在这里不是简单“复制多份 scheduler”,而可能和 attention 并行布局绑定。也正因此,读者不能把所有多 rank 场景都简化成同一种 worker 拓扑。

这类差异特别值得在运行时架构里讲,而不是留到某个性能注记里,因为它已经影响到 controller 自己的控制复杂度。

最容易出现的三种误判#

第一,误把 DP controller 当成普通消息转发器。
实际上它在做策略决策、状态维护和时间线打点。

第二,误以为所有请求都由内部策略决定去向。
routed_dp_rank 明确说明外部 router 可以直接指定 rank。

第三,误以为 DP 只是 throughput 问题。
从 controller 设计看,它同时影响请求时间线、活性判断和控制消息传播。

真正怀疑问题和 DP 分发有关时,更稳的顺序#

建议按下面顺序:

  1. 看当前 dp_sizeload_balance_method 是什么。
  2. 看是否有 routed_dp_rank 从 header 或 body 进入。
  3. 看 controller 记录的 statusDPBudget 是否符合预期。
  4. 看请求是否真的被送到了你以为的 rank。
  5. 如果开启了 DP attention,再额外确认当前 worker 拓扑和 control_message_step

这条顺序最有价值的地方,是它把 placement 问题先拆成“策略 / 外部指定 / 活性 / 拓扑”几个层次,而不是一上来就去怪 scheduler。

小结#

DataParallelController 真正值得单独成章的地方,不在于它又多了一层路由,而在于它把多 DP 运行时里的 placement 正式变成了 runtime 自己的控制面:请求在进入具体 scheduler 之前,会先经过一次 rank 级分发决策,而这个决策同时受策略、预算、活性和外部指定影响。

到这里,运行时架构就不只解释“单实例怎样工作”,也开始解释“多 rank 运行时怎样把请求送到正确地方”。