Prhub

#33925 config: route DCP topology reads through get_parallel()

原始 PR 作者 kpham-sgl 合并时间 2026-08-08 05:53 文件变更 14 提交数 4 评论 18 代码增减 +31 / -35

执行摘要

DCP 拓扑读取全面迁移到 get_parallel() 实时来源

33170 已将 106 个并行配置叶读取改经 get_parallel(),但故意留下 5 个 live-shadowed 拓扑尺寸(tp/pp/dcp/attn_cp/moe_dp_size)在 server_args 上,因为实时 property 在访问器上胜出,而条件初始化的组会在无条件调用点 fail loud。DCP 恰是典型:_DCP 只在 dcp_size > 1 时安装,DCP 关闭时直接读 get_parallel().dcp_size 会断言。本 PR 用 attention 后端已用的 no-assert 三元组化解,让 13 个分布式初始化后运行的读取点安全读取实时拓扑而非配置种子。

值得精读。本 PR 是"配置种子 → 实时拓扑"渐进迁移的样板:no-assert 访问器三元组(dcp_enabled / attn_dcp_size / attn_dcp_rank)的设计、"哪些读取可以迁移、哪些必须留在 server_args"的边界判断,以及自审式 review 中对命名属性、注释和局部变量的删减,都是可复用的工程实践。

讨论亮点

本 PR 的 10 条 review 评论全部来自作者本人(kpham-sgl),是 Claude Code 生成代码后的自审模式,核心交锋:

  • "Seems redundant?"(model_runner.py):dcp_size / dcp_rank 读取上方的说明注释被判冗余后删除。
  • "dont need to add redundant comment"(dsa/utils.py):docstring 中解释 DCP 实时拓扑来源的段落被要求删掉。
  • "its okay you can call get_parallel(), no need to spawn object"(base_runner.py):parallel = get_parallel() 局部绑定被改为每个使用点直接调用。
  • "just in-line get_parallel().attn_dcp_size later"(scheduler.py):作者原本引入 aggregate_max_total_num_tokens 命名属性,自审后认为扩大 API 面,改为两处直接内联。
  • "why is this needed?"(test_scheduler_init_req_max_new_tokens.py):fixture 补 stub 的改动被追问,结论是"完全不需要"——attn_dcp_size 在无 DCP group 时经 get_dcp_group_no_assert() 返回 1,且 get_parallel() 是模块级单例、没有上下文可注入,直接删行而非替换。

实现拆解

  1. 访问器契约:迁移的调用点统一使用 dcp_enabled(布尔门控)、attn_dcp_size(KV 池加宽系数与聚合边界)、attn_dcp_rank(复制权重索引)三个 no-assert 访问器;DCP 关闭或分布式初始化前分别安全返回 False / 1 / 0,避免 get_parallel().dcp_size 的断言。
  2. 调用点迁移(10 个源码文件):attention_registry.pydcp_size > 1dcp_enableddsa/utils.pydcp_size == 1not dcp_enabledbase_runner.py 的 fi_a2a 工作空间预分配门控改为 dcp_enabled and dcp_comm_backend == "fi_a2a"pool_configurator.py 的 dflash cell-size 缩放改用 attn_dcp_sizeallocation.py 的页大小分支改用 dcp_enableddisaggregation/common/conn.py 改用 parallel.attn_dcp_size / attn_dcp_rank。每个位置只换数据源,不改决策逻辑。
  3. model_runner.py 初始化顺序调整self.dcp_size / self.dcp_rank__init__ 开头挪到 init_torch_distributed() 之后、从实时拓扑读取;原 ps.tp_rank % dcp_size 算术随之删除——DCP group 按每个 TP group 的连续 dcp_size 分片构建,tp_rank % dcp_size 恰好等于 attn_dcp_rank。q-proj 复制门控也改为 dcp_enabled and dcp_replicate_q_proj
  4. 调度器与 KV 缓存聚合边界scheduler.py 的两处 max_total_num_tokens * dcp_sizeinit_pool_stats_observerinit_req_max_new_tokens 的请求长度封顶)改读 get_parallel().attn_dcp_sizekv_cache_configurator.py 的 4 处(loc_space_scale、分配器选型分支、PagedTokenToKVPoolAllocator 的容量与页大小加宽)同步迁移。
  5. 清理与配套invariant_checker.py 删除 server_args dataclass 字段及其两个消费点;allocation.py 失去 test_global_config_read_ratchet.py 中的 config-intent 豁免;测试更新 4 个文件:test_scheduler_init_req_max_new_tokens.py 删除 SimpleNamespace(dcp_size=1) stand-in、test_disaggregation_wire.py 删除失效的 dcp_size=1 字段、test_dcp_layout_unit.py 改为驱动真实 get_parallel() 路径、ratchet 基线复核(direct 0 / alias 12 不变)。
文件 模块 状态 重要度
python/sglang/srt/managers/scheduler.py 调度器 modified 5.94
python/sglang/srt/model_executor/model_runner.py 模型运行 modified 6.01
python/sglang/srt/managers/scheduler_components/invariant_checker.py 不变量检查 modified 6.12
python/sglang/srt/mem_cache/kv_cache_configurator.py KV 缓存 modified 5.59
python/sglang/srt/model_executor/runner/base_runner.py 模型运行 modified 5.56
python/sglang/srt/mem_cache/allocation.py 内存分配 modified 5.27
python/sglang/srt/disaggregation/common/conn.py 断开连接 modified 5.03
python/sglang/srt/model_executor/pool_configurator.py 内存池 modified 4.88
python/sglang/srt/layers/attention/attention_registry.py 注意力 modified 4.59
python/sglang/srt/layers/attention/dsa/utils.py 注意力 modified 4.12
test/registered/unit/managers/test_scheduler_init_req_max_new_tokens.py 测试 modified 3.57
test/registered/dcp/test_dcp_layout_unit.py 测试 modified 3.51
test/registered/disaggregation/test_disaggregation_wire.py 测试 modified 3.0
test/registered/unit/config/test_global_config_read_ratchet.py 测试 modified 3.0

关键符号

ModelRunner.__init__ ModelRunner.init_attention_backends BaseRunner._pre_initialize_fi_a2a_workspace Scheduler.init_pool_stats_observer Scheduler.init_invariant_checker Scheduler.init_req_max_new_tokens SchedulerInvariantChecker._check_full_pool KVCacheConfigurator.loc_space_scale KVCacheConfigurator._build_token_to_kv_pool_allocator allocation._alloc_page_size dsa.utils.should_remap_pd_dsa_seed_to_local_slots

关键源码片段

python/sglang/srt/managers/scheduler.py core-logic

核心调度路径:两处 max_total_num_tokens * dcp_size 聚合边界(池统计观测与请求 max_new_tokens 封顶)改读实时 attn_dcp_size,并移除传给 invariant_checker 的 server_args 参数。

# scheduler.py —— 请求上限与池观测的聚合边界统一走实时拓扑。
# DCP 会把 KV pool 分片到各 rank,所以单请求上限与池统计都应以
# 聚合池(本 rank 份额 x attn_dcp_size)为准,而非本 rank 份额。def init_pool_stats_observer(self) -> None:
    self.pool_stats_observer = SchedulerPoolStatsObserver(
        ...
        # DCP 开启时即聚合池大小;attn_dcp_size 在 DCP 关闭时返回 1,
        # 所以这里无需像旧代码那样读 server_args.dcp_size 配置种子
        max_total_num_tokens=self.max_total_num_tokens
        * get_parallel().attn_dcp_size,
        ...
    )def init_req_max_new_tokens(self, req):
    ...
    req.sampling_params.max_new_tokens = max(
        0,
        min(
            max_new_tokens,
            self.max_req_len - input_len - 1,
            # 与 PrefillAdder 的准入预算保持一致:ceil_page(input_len)
            # + max_new_tokens + page_size 必须严格小于聚合的
            # max_total_num_tokens,否则请求进等待队列却永远无法被调度,
            # 最终导致健康检查失败
            self.max_total_num_tokens * get_parallel().attn_dcp_size
            - paged_input_len - self.page_size - 1,
        ),
    )
python/sglang/srt/model_executor/model_runner.py data-contract

dcp_size / dcp_rank 从构造器开头移到 init_torch_distributed() 之后并读取实时拓扑,删除 tp_rank % dcp_size 手工算术;q-proj 复制门控改用 dcp_enabled。

# model_runner.py —— DCP 拓扑读取从配置种子迁移到实时来源。
# 关键点:DCP group 只在分布式初始化后安装,所以 dcp_size / dcp_rank
# 必须放在 init_torch_distributed() 之后读取,否则会拿到 server_args 的
# 配置种子;而 DCP group 按 TP group 的连续 dcp_size 分片构建,
# 原 ps.tp_rank % dcp_size 恰好等于 rank 在 DCP group 内的索引,
# 所以直接用权威来源 attn_dcp_rank 即可。def __init__(self, model_config, mem_fraction_static, gpu_id, ps, ...):
    self.ps = ps
    self.model_config = model_config
    ...
    # Mooncake TransferEngine 必须在分布式初始化前创建,以复用共享 TE
    self.init_shared_mooncake_transfer_engine()
    # 在这里之前 DCP group 不存在,提前读 attn_dcp_size 会静默拿到 1
    self.init_torch_distributed()
​
    # 从实时拓扑读取,而不是配置种子
    self.dcp_size = get_parallel().attn_dcp_size
    self.dcp_rank = get_parallel().attn_dcp_rank
    ...
    # q-proj 复制门控同样用 dcp_enabled(实时)而非 server_args.dcp_size:
    # DCP 关闭时 dcp_enabled 安全返回 False(经 get_dcp_group_no_assert)
    if get_parallel().dcp_enabled and get_parallel().dcp_replicate_q_proj:
        self._prepare_replicated_q_proj()
python/sglang/srt/managers/scheduler_components/invariant_checker.py dependency-wiring

删除 server_args dataclass 字段及其两个消费点,两处防御性 getattr(self.server_args, "dcp_size", 1) 改为 get_parallel().dcp_enabled,是迁移中依赖清理最彻底的文件。

# invariant_checker.py —— 防御性 getattr 读取让位于 no-assert 实时访问器。
# 之前:getattr(self.server_args, "dcp_size", 1) > 1(配置种子 + 防御默认值)
# 现在:get_parallel().dcp_enabled(实时拓扑,无断言、无默认值兜底)def _check_full_pool(self, ps: PoolStats, uncached: int = 0) -> Tuple[bool, str]:
    ...
    full_evictable_size = ps.full_evictable_size
    allocator = self.token_to_kv_pool_allocator
    if get_parallel().dcp_enabled and allocator.page_size > 1:
        # DCP 在加宽后的物理页里存逻辑 token:前缀缓存按逻辑 token 计数,
        # 而分配器按整页释放,所以要把缓存的 token 向上取整到物理页单位
        full_evictable_size = (
            (full_evictable_size + allocator.page_size - 1)
            // allocator.page_size
            * allocator.page_size
        )
    leak, msg = self._check_pool_invariant(
        "full",
        ps.full_available_size,
        full_evictable_size,
        protected,
        session_held,
        total,
        uncached,
    )
    if leak and get_parallel().dcp_enabled and allocator.page_size > 1:
        # 前缀缓存会计按逻辑 token,而 DCP full KV 分配按物理页进行,
        # 即使所有页都被持有,也可能残留页面级 slack,需要单独提示
        ...

评论区精华

aggregate_max_total_num_tokens 命名属性 vs 直接内联 设计

作者最初在 scheduler.py 中引入 aggregate_max_total_num_tokens 命名属性封装两处 max_total_num_tokens * dcp_size 聚合边界,自审时认为 "just in-line get_parallel().attn_dcp_size later",理由是引入命名属性会扩大类 API 面并迫使测试 fixture 补 stub。

结论:放弃命名属性,两处调用点直接内联 get_parallel().attn_dcp_size,相应测试 stub 也一并删除。 · 已解决

测试 fixture 为何需要补 aggregate_max_total_num_tokens stub 测试

test_scheduler_init_req_max_new_tokens.py 的 fixture 原本用 SimpleNamespace(dcp_size=1) 替代 server_args,迁移后作者先补了 scheduler.aggregate_max_total_num_tokens = max_total_num_tokens,被自审追问 "why is this needed?"。

结论:完全不需要:attn_dcp_size 在无 DCP group 时经 get_dcp_group_no_assert() 返回 1,且 get_parallel() 是模块级单例、没有任何可注入的上下文,直接删除该行而非替换。 · 已解决

get_parallel() 是否需要局部绑定 style

base_runner.py 的 _pre_initialize_fi_a2a_workspace 最初写了 parallel = get_parallel() 局部绑定,自审意见为 "its okay you can call get_parallel(), no need to spawn object"。

结论:删除局部绑定,每个使用点直接调用 get_parallel(),减少多余变量。 · 已解决

dsa/utils.py 冗余 docstring style

should_remap_pd_dsa_seed_to_local_slots 的 docstring 追加了 "DCP comes from the live topology rather than the passed instance" 的段落,被自审要求删除("dont need to add redundant comment")。

结论:删除新增的 docstring 段落。 · 已解决

model_runner.py DCP 读取上方的注释是否冗余 style

model_runner.py 在 init_torch_distributed() 后新增的 dcp_size / dcp_rank 读取上方写了一行解释性注释,被自审评为 "Seems redundant?"。

结论:删除注释,保留代码本身的清晰性。 · 已解决

风险与影响

  1. model_runner.py 初始化顺序变更self.dcp_size / self.dcp_rank 从构造器开头移到 init_torch_distributed() 之后,任何在分布式初始化前读取这两个属性的调用者会从"拿到配置种子"变为 AttributeError,需确认无此类调用点。
  2. 缺少 DCP e2e 覆盖:作者声明没有 GPU 机箱,最终依赖 CI 重跑(test_kimi_linear_pd_dcp4.pytest_dcp_layout_unit.pytest_dsv31_dcp8_gsm8k.pytest_reduce_scatter_along_dim.py)验证,多 GPU 验证仍薄弱。
  3. 测试与全局单例耦合get_parallel() 是模块级单例、无法注入 stub,test_dcp_layout_unit.py 因此必须真实走 DCP 路径才能驱动分配器加宽逻辑,单测隔离性被削弱。
  4. publish-time 覆盖语义变化dcp_comm_backend 移入并行包(resolvable=True)后开始反映发布后的覆盖值,任何仍从 server_args 实例读取该字段的代码可能短暂看到不一致值。
  1. 架构影响:跨 14 个文件统一 DCP 拓扑数据源,消除配置种子与实时拓扑的分叉,为后续 attn_cp / moe_dp_size 等拓扑尺寸迁移搭好模式。
  2. 用户与运维影响:publish-time 覆盖(如 dcp_comm_backend)现在能被正确反映,提升动态配置场景的一致性;预期无面向用户的可见行为变化。
  3. 团队影响allocation.py 失去 ratchet 豁免、测试 fixture 契约简化,降低了未来改动的认知负担;但 model_runner.py 初始化顺序变更要求后续 PR 必须遵守"DCP 读取必须晚于分布式初始化"的隐式约束。
模型初始化顺序变更 缺少 DCP e2e 覆盖 测试耦合全局单例 核心路径访问器契约

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论