Prhub

#41792 [CI][Elastic EP] Fix Elastic EP Scaling Test Failure

原始 PR 作者 haosdent 合并时间 2026-05-09 03:17 文件变更 3 提交数 4 评论 14 代码增减 +62 / -11

执行摘要

修复弹性 EP 扩展测试中的标识冲突和工作区过小

PR body 指出 #41421 使 RayExecutorV2 成为默认 Ray DP 执行器,破坏了 tests/distributed/test_elastic_ep.py。两个问题:scale_up_elastic_ep 没有像 _init_engines 那样为 instance_id 和 kv_transfer_config.engine_id 追加 _dp{rank} 后缀,导致 Ray actor 名称冲突;EPLB 重排后,锁定的 MoE 工作区尺寸基于 cudagraph 捕获批次(约 14 MB),而实际流量需要数百 MB,导致越界。

值得精读,特别是 DP 标识统一和工作区重热的设计模式。但需注意性能开销,关注后续 PR #42203。对于弹性 EP 功能的使用者,建议合入此修复。

讨论亮点
  • engine_id 使用全局 DP rank 确保唯一性:gemini-code-assist 指出应使用全局 dp_rank 而非 local_dp_rank 避免多节点冲突。最终版本已采纳。
  • 模型运行器状态是否完整保存:tlrmchlsmth 询问 rewarm_workspace 是否遗漏了模型运行器状态。haosdent 承认代码是从 switch_and_prepare 复制的,作为临时措施接受,后续考虑抽象公共方法。
  • 第二次 cudagraph 捕获的性能开销:LucasWilkinson 和 SageMoore 关注 rewarm_workspace 增加了额外的 CUDA 图捕获开销。SageMoore 认为若紧急修复可接受,但需尽快移除,已跟踪 #42203。

实现拆解

  1. 提取 DP 标识后缀函数:在 vllm/v1/engine/utils.py 中新增 _apply_dp_identity_suffix 函数,同时设置 instance_idkv_transfer_config.engine_id 的 DP 后缀。使用全局 DP rank 以保证多节点唯一性。
  2. 统一调用点:将 CoreEngineActorManager.__init__ 中内联的后缀逻辑替换为调用 _apply_dp_identity_suffix,并移除原有的仅对 instance_id 处理的分支。同时在 scale_up_elastic_ep 中添加相应的调用,确保新引擎也获得唯一标识。
  3. 新增工作区重热方法:在 vllm/distributed/elastic_ep/elastic_execute.py 中实现 rewarm_workspace。该方法保存当前 block table 并清空,释放 CUDA 图并解锁工作区,以 max_num_tokens 运行 _dummy_run(跳过 EPLB 统计更新),然后重新编译/预热模型并恢复 block table。
  4. 集成到重排流程:在 vllm/distributed/elastic_ep/elastic_state.py_eplb_reshuffle 中,在 perform_eplb_reshuffle 之后通过 collective RPC 调用 rewarm_workspace,确保所有 DP 副本同步执行。
  5. 无测试文件变更:此 PR 未添加新测试,而是修复现有测试的失败。
文件 模块 状态 重要度
vllm/v1/engine/utils.py 引擎工具 modified 7.4
vllm/distributed/elastic_ep/elastic_execute.py 弹性 EP modified 7.07
vllm/distributed/elastic_ep/elastic_state.py 弹性 EP modified 4.96

关键符号

_apply_dp_identity_suffix rewarm_workspace

关键源码片段

vllm/v1/engine/utils.py core-logic

提取并统一 DP 标识后缀函数,修复 scale_up 路径标识缺失问题

# vllm/v1/engine/utils.py —— _apply_dp_identity_suffix 函数及调用点def _apply_dp_identity_suffix(dp_vllm_config, dp_rank: int) -> None:
    # Ray actor names (RayExecutorV2) and KV-connector engine_ids must
    # be unique across sibling DP engines or registration collides.
    # Use the global DP rank, not a node-local rank, since sibling DP
    # engines can span multiple nodes.
    dp_vllm_config.instance_id = f"{dp_vllm_config.instance_id}_dp{dp_rank}"
    if dp_vllm_config.kv_transfer_config is not None:
        dp_vllm_config.kv_transfer_config.engine_id = (
            f"{dp_vllm_config.kv_transfer_config.engine_id}_dp{dp_rank}"
        )# 在 CoreEngineActorManager.__init__ 中的调用(替换原有内联代码)
if dp_size > 1:
    _apply_dp_identity_suffix(dp_vllm_config, index)# 在 scale_up_elastic_ep 中的新增调用
if new_data_parallel_size > 1:
    _apply_dp_identity_suffix(dp_vllm_config, rank)
vllm/distributed/elastic_ep/elastic_execute.py core-logic

新增 rewarm_workspace 方法,在 EPLB 重排后扩大 MoE 工作区并重新预热模型

# vllm/distributed/elastic_ep/elastic_execute.py —— rewarm_workspace 方法def rewarm_workspace(self) -> None:
    # Must run on every DP sibling in lockstep: _dummy_run calls
    # coordinate_batch_across_dp whenever data_parallel_size > 1
    # (gpu_model_runner.py:3663), which deadlocks if any rank skips it.
​
    # Save and clear block tables so profile_run/compile_or_warm_up_model
    # don't write dummy slot mappings into real KV-cache blocks (mirrors
    # switch_and_prepare's pattern).
    multi_block_table = self.worker.model_runner.input_batch.block_table
    saved_block_tables: list[tuple[torch.Tensor, torch.Tensor]] = []
    for bt in multi_block_table.block_tables:
        saved_block_tables.append(
            (bt.block_table.gpu.clone(), bt.block_table.cpu.clone())
        )
    multi_block_table.clear()
​
    # _ensure_workspace_size allocates a fresh tensor on grow, leaving
    # captured CUDA graphs with stale data pointers; drop graphs before
    # re-warm so captures realign with the resized buffer.
    self._release_cuda_graphs()
    unlock_workspace()
​
    # Grow the MoE workspace at max_num_tokens.
    # compile_or_warm_up_model alone only exercises cudagraph-capture
    # sizes (≤64 tokens for this test) and leaves the workspace at
    # ~10-14 MB; the post-all-to-all per-rank token count under real
    # post-reshuffle routing needs hundreds of MB. Use _dummy_run
    # directly (rather than profile_run) with skip_eplb=True so dummy
    # routing doesn't pollute the just-rebalanced EPLB stats — same
    # convention compile_or_warm_up_model itself uses.
    runner = self.worker.model_runner
    runner._dummy_run(runner.max_num_tokens, is_profile=True, skip_eplb=True)
    self.worker.compile_or_warm_up_model()
​
    lock_workspace()
​
    for bt, (saved_gpu, saved_cpu) in zip(
        multi_block_table.block_tables, saved_block_tables
    ):
        bt.block_table.gpu.copy_(saved_gpu)
        bt.block_table.cpu.copy_(saved_cpu)

评论区精华

engine_id 应使用全局 DP rank 而非 local rank 正确性

gemini-code-assist 指出使用 local_dp_rank 会导致多节点 ID 冲突,与注释要求矛盾。

结论:PR 最终版本已采纳,函数参数改为全局 dp_rank。 · 已解决

rewarm_workspace 可能遗漏模型运行器状态 正确性

tlrmchlsmth 询问是否保存了所有相关状态。haosdent 承认代码从 switch_and_prepare 复制,并建议后期提取公共方法。

结论:未完全解决,但作为紧急修补接受。 · unresolved

第二次 cudagraph 捕获的性能开销 性能

LucasWilkinson 和 SageMoore 关注 rewarm_workspace 增加了额外 CUDA 图捕获。SageMoore 认为若紧急修复可接受,但需尽快移除。

结论:作为紧急措施接受,后续跟踪 issue #42107。 · 已解决

风险与影响

  • 性能风险rewarm_workspace 每次 EPLB 重排都会触发完整的 CUDA 图重新捕获,对扩展操作增加了额外延迟,在高频重排场景下不可接受。
  • 状态完整性:block table 的保存/恢复模式可能遗漏其他运行时状态(如 KV 缓存映射),导致隐藏的语义错误。
  • 代码重复rewarm_workspace 与现有 switch_and_prepare 存在大量重复,增加了维护成本。
  • 多节点标识唯一性:虽然已使用全局 rank,但仍需确认所有场景(包括跨节点)下 instance_idengine_id 不冲突。
  • scale_down 路径:SageMoore 指出 scale_down 路径可能也存在类似的工作区尺寸问题,但当前未修复。

影响使用弹性 EP 功能的用户,修复了扩展测试失败,但引入了额外的 cudagraph 捕获开销。目前弹性 EP 功能使用范围有限,影响程度中等。团队需要关注后续优化(#42203)以消除性能开销。

性能开销(二次 CUDA 图捕获) 代码重复可能遗漏状态 多节点 engine_id 唯一性需验证 scale_down 路径潜在问题

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论