# PR #54985 完整报告

- 仓库：`vllm-project/vllm`
- 标题：[Elastic EP] Reuse CUDA graphs across reconfiguration
- 合并时间：2026-09-12 22:26
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/54985

---

# 执行摘要

- 一句话：Elastic EP 重配置复用 CUDA graph，停机时间降 95%+
- 推荐动作：值得精读。这是 Elastic EP 从「能扩缩容」走向「扩缩容不打断服务」的关键一步，建议重点看三处设计决策：一是 `can_reuse_fused_moe_kernel` 这种「后端能力开关」如何把不确定性收敛到一个返回布尔值的函数，从而让 commit 路径免于分支扩散；二是 `all2all.py` 中 `stage_ep_size` / `commit_ep_size` / `mask_remote_ranks` 构成的两阶段状态机，配合 `torch.accelerator.synchronize()` 处理 mask 可见性；三是 `eplb_state.py` 里用「最大容量 buffer + 视图」把「CUDA graph 复用」这一性能诉求转化为显存布局约束的做法，以及 `_commit_eplb_maps` 由换 tensor 改为换视图的细节。同时建议对照 PR body 的容量 - 吞吐对照表评估自己使用的专家内核是否需要调参，并留意 `VLLM_NIXL_EP_MAX_NUM_RANKS` 移除与 nixl 版本升级对运维脚本的影响。

# 功能与动机

PR body 指出：早期的 Async preparation（vllm-project/vllm#47288）与 eager-mode optimizations（vllm-project/vllm#51885）已把大部分 Elastic EP 步骤移出阻塞式 commit 路径，但在 CUDA graph 模式下，每次重配置仍要在 commit 时做内核 warmup 与 graph capture，期间已有请求停滞、新请求被拒。作者因此提出两条主线：一是让已有 rank 复用融合 MoE 运行时与 CUDA graph（NIXL EP 保留 graph 入口，EPLB mapping、dispatch 输出 buffer 等 graph-facing tensor 保持同一显存地址）；二是让新 rank 在 preparation 阶段完成 warmup 与 capture，期间屏蔽远端 peer、跳过 DP batch 协调、并从已有 rank 复制 FlashInfer 调优结果。PR body 的结论是 “With this PR, we consider Elastic EP reconfiguration downtime solved.”，同时主动披露了最大容量输入带来的内核效率代价。

# 实现拆解

**1. 引入「内核可复用」判定，把重捕获移出 commit 路径**

- `vllm/distributed/elastic_ep/elastic_execute.py` 新增模块级 `can_reuse_fused_moe_kernel(parallel_config)`，仅当 `all2all_backend == "nixl_ep"` 返回 `True`，并新增类内包装 `_can_reuse_fused_moe_kernel()`。
- 复用为真时，`prepare_reconfiguration` 不再调用 `stage_standby_moe_quant_methods`（改为传入 `all2all_manager`，只在旧路径调用）；`_release_cuda_graphs` 中对 `UBatchWrapper` 的处理由 `raise RuntimeError("DBO is not yet supported in elastic EP")` 改为 `self.worker.model_runner.model.clear_graphs()`。
- 影响：commit 阶段不再执行 `torch.compiler.reset()` + 内核预热 + graph capture，这是停机时间下降的直接来源；其他 all2all 后端保持原 recapture 行为，形成两条分支。

**2. NIXL EP 通信层改为「按最大容量预分配 + 两阶段切换」**

- `vllm/distributed/device_communicators/all2all.py`：`_NixlEPBufferState` 增加 `max_num_ep_ranks` 字段，`_init_buffer` 改为按 `elastic_ep_max_dp_size` 推导 `max_num_ep_ranks` 并一次申请 RDMA/expert buffer；`max_num_ep_ranks` 从 `envs.VLLM_NIXL_EP_MAX_NUM_RANKS` 改为读 buffer 状态的 property。
- `_ensure_ep_size(stage=...)` 拆分为 `stage_ep_size()`（scale-up 只 connect、不 activate，新 rank 处于 masked）与 `commit_ep_size()`（disconnect 或 connect+activate 后 unmask），二者都加 `_lock`；新增 `mask_remote_ranks()` 上下文管理器供新 rank warmup 时屏蔽全部远端 peer，mask 更新后补 `torch.accelerator.synchronize()`。
- `get_handle` 去掉 `stage` 形参，`_init_buffer` 直接用 `kwargs["num_local_experts"]`，`query_active_mask` 按 `active_ep_size` 返回切片。`base_device_communicator.py` 为基类补上 no-op 的 `stage_ep_size` / `commit_ep_size` / `mask_remote_ranks`，避免非 NIXL 后端 AttributeError。

**3. 用「最大容量 buffer + 视图」固化 CUDA graph 友好的显存地址**

- `vllm/distributed/eplb/eplb_state.py`：`EplbModelState` 新增 `physical_to_logical_map_buffer` 与 `expert_load_pass_buffer`；`add_model` 在 `enable_elastic_ep` 时把 `physical_to_logical_map` pad 到按 `elastic_ep_max_dp_size` 推出的 `physical_expert_capacity`，`expert_load_pass` 同样改为 buffer 切片。
- 新增 `reconfigure_physical_expert_slots(model_config, num_physical_experts)`：只清理新旧规模之间的槽位（`fill_(-1)` / `zero_()`），再重建两个视图，必要时 pad `expert_load_window`。
- `_commit_eplb_maps` 在 expert 数量变化时不再整体替换 tensor，而是先更新视图再 `copy_`；`broadcast_expert_mapping` 简化为只广播 buffer 本身（去掉 `num_local_physical_experts` / `num_logical_experts` 元数据）。
- `vllm/model_executor/layers/fused_moe/all2all_utils.py` 与 `prepare_finalize/nixl_ep.py`：NIXL P/F 新增 `expert_capacity` 参数，`num_dispatchers` 用 `all2all_manager.max_num_ep_ranks`，容量取 `num_local_experts * max_num_ep_ranks`，`prepare_async` 中把 `num_experts` 换成 `self.expert_capacity`；同时删除 `modular_kernel.py` / `nixl_ep.py` 中的 `on_commit()` 钩子。

**4. 让新 rank 的 warmup / capture 不打扰已有 rank**

- `vllm/v1/worker/dp_utils.py` 新增 `_skip_dp_coordination` ContextVar、`skip_dp_coordination()` 与 `should_skip_dp_coordination()`，`coordinate_batch_across_dp` 在半路短路，直接用本地 `num_tokens_padded` 构造 `num_tokens_after_padding`；`vllm/v1/worker/gpu/dp_utils.py` 为 MRv2 的 `sync_cudagraph_and_dp_padding` 做等价短路。
- `vllm/model_executor/warmup/flashinfer_autotune_cache.py` 新增 `sync_flashinfer_autotune_cache(runner, group)`：rank 0 导出 `AutoTuner.save_configs` 后 `broadcast_object`，其余 rank 写临时文件并 `load_configs`，失败以字符串回传并抛 `RuntimeError`。
- `elastic_execute.py` 新增 `_disable_flashinfer_autotune()` 上下文管理器，扩容时通过 `get_standby_world_group()` 广播调优结果。

**5. 配置面收敛**

- `vllm/config/parallel.py` 新增 `elastic_ep_max_dp_size`（`Field(default=None, ge=1)`，未开 Elastic EP 时回填为 `data_parallel_size`），小于当前 `data_parallel_size` 直接 `ValueError`；`vllm/engine/arg_utils.py` 暴露 `--elastic-ep-max-dp-size`；`vllm/envs.py` 删除 `VLLM_NIXL_EP_MAX_NUM_RANKS`。
- `get_ep_all2all_manager(eep_stage: bool)` 改为 `get_ep_all2all_manager(ep_group: GroupCoordinator | None)`，`maybe_make_prepare_finalize(..., eep_stage=...)` 改为 `all2all_manager=...`，调用点 `eep_reconfigure.py`、`layer.py`、`utils.py`、`config.py` 随之调整。

**6. 测试与 CI 配套**

- `tests/distributed/test_elastic_ep.py`：`_base_serve_args` 增加 `max_dp_size` / `all2all_backend` 参数与 `--elastic-ep-max-dp-size`；`test_elastic_ep_scaling` 参数化到 `(enforce_eager, traffic_mode, all2all_backend)` 并新增 `cuda_graphs_heavy_nixl_ep` 组合；删除 `test_elastic_ep_scaling_uneven`（2->3 场景被 max_dp_size 参数覆盖）。
- `tests/distributed/test_eplb_utils.py` 同步更新 map / 负载相关断言。

关键文件：
- `vllm/distributed/elastic_ep/elastic_execute.py`（模块 扩容编排；类别 source；类型 core-logic；符号 can_reuse_fused_moe_kernel, _can_reuse_fused_moe_kernel, _disable_flashinfer_autotune, prepare_reconfiguration）: Elastic EP 扩缩容编排主入口，本 PR 的复用判定、FlashInfer autotune 同步、standby MoE quant method 跳过、DBO graph 释放策略与 broadcast_expert_mapping 简化都在此落地，是理解整体收益来源的核心文件。
- `vllm/distributed/device_communicators/all2all.py`（模块 通信后端；类别 source；类型 core-logic；符号 _NixlEPBufferState, max_num_ep_ranks, _init_buffer, stage_ep_size）: NIXL EP 通信层从「按当前规模初始化」改为「按最大容量预分配 + stage/commit 两阶段切换」，并新增 mask_remote_ranks 上下文管理器；这是 CUDA graph 复用在通信侧的根基。
- `vllm/distributed/eplb/eplb_state.py`（模块 专家负载均衡；类别 source；类型 core-logic；符号 EplbModelState, physical_to_logical_map_buffer, expert_load_pass_buffer, add_model）: EPLB 状态从「固定尺寸 tensor」改为「最大容量 buffer + 动态视图」，并新增 reconfigure_physical_expert_slots；这是 CUDA graph 复用所依赖的显存地址稳定性的关键实现。
- `vllm/model_executor/layers/fused_moe/all2all_utils.py`（模块 专家并行；类别 source；类型 data-contract；符号 get_ep_all2all_manager, maybe_make_prepare_finalize）: get_ep_all2all_manager 由 eep_stage 布尔改为显式 ep_group，NixlEP P/F 改用 max_num_ep_ranks 与 expert_capacity，是把「最大容量」契约传到 MoE 内核构造点的位置。
- `vllm/v1/worker/dp_utils.py`（模块 批量协调；类别 source；类型 core-logic；符号 skip_dp_coordination, should_skip_dp_coordination, coordinate_batch_across_dp）: 新增 skip_dp_coordination 上下文与 should_skip_dp_coordination，并在 coordinate_batch_across_dp 内短路；是「新 rank warmup 不与其他 DP rank 协调」的实现点，也是 review 讨论最集中的文件。
- `vllm/model_executor/warmup/flashinfer_autotune_cache.py`（模块 内核预热；类别 source；类型 data-contract；符号 sync_flashinfer_autotune_cache）: 新增 sync_flashinfer_autotune_cache，让扩容时新 rank 直接复用已有 rank 的 FlashInfer 调优结果，避免在 warmup 阶段触发会破坏 graph 一致性的 autotune。
- `vllm/model_executor/layers/fused_moe/prepare_finalize/nixl_ep.py`（模块 分发合并；类别 source；类型 data-contract；符号 expert_capacity, on_commit, prepare_async）: NIXL P/F 新增 expert_capacity 并移除 on_commit 钩子，prepare_async 用 expert_capacity 取代 num_experts 传给 dispatch，是「最大容量」在 MoE 前向路径上的落点。
- `vllm/config/parallel.py`（模块 并行配置；类别 source；类型 configuration；符号 elastic_ep_max_dp_size, __post_init__）: 新增 elastic_ep_max_dp_size 配置项与校验，把「最大 DP 规模」从环境变量提升为一等配置，是 NIXL EP buffer 与 EPLB 容量的统一来源。
- `tests/distributed/test_elastic_ep.py`（模块 弹性扩缩容；类别 test；类型 test-coverage；符号 _base_serve_args, test_elastic_ep_scaling, test_elastic_ep_scaling_uneven）: 端到端测试适配新参数与新后端组合：新增 --elastic-ep-max-dp-size、参数化 all2all_backend 并加入 nixl_ep + CUDA graph 组合，同时删除被覆盖的 uneven scaling 用例。

关键符号：can_reuse_fused_moe_kernel, _can_reuse_fused_moe_kernel, _disable_flashinfer_autotune, prepare_reconfiguration, warm_and_capture, _release_cuda_graphs, stage_ep_size, commit_ep_size, _init_buffer, mask_remote_ranks, reconfigure_physical_expert_slots, _commit_eplb_maps, sync_flashinfer_autotune_cache, skip_dp_coordination, should_skip_dp_coordination, coordinate_batch_across_dp, get_ep_all2all_manager, maybe_make_prepare_finalize

## 关键源码片段

### `vllm/distributed/elastic_ep/elastic_execute.py`

Elastic EP 扩缩容编排主入口，本 PR 的复用判定、FlashInfer autotune 同步、standby MoE quant method 跳过、DBO graph 释放策略与 broadcast_expert_mapping 简化都在此落地，是理解整体收益来源的核心文件。

`elastic_execute.py` 中复用判定与新 rank warmup 隔离的核心实现（依据 diff 与 PR body 重新整理）：

```python
def can_reuse_fused_moe_kernel(parallel_config: ParallelConfig) -> bool:
    """Whether fused MoE kernels can be reused across EP rank changes.

    The EP backend must preserve its CUDA graph entries when ranks are
    connected, disconnected, masked, or unmasked.
    """
    # 只有 NIXL EP 保证 connect / disconnect / mask / unmask 之后
    # 显存地址与 CUDA graph 入口都不变，其余后端继续走「重捕获」老路径
    return parallel_config.all2all_backend == "nixl_ep"


class ElasticEPScalingExecutor:
    def _can_reuse_fused_moe_kernel(self) -> bool:
        return can_reuse_fused_moe_kernel(
            self.worker.vllm_config.parallel_config,
        )

    @contextmanager
    def _disable_flashinfer_autotune(self) -> Iterator[None]:
        # 新 rank 直接复用已有 rank 广播过来的调优结果，
        # warmup 期间禁止再次 autotune，避免选到与已捕获 graph 不一致的内核
        kernel_config = self.worker.vllm_config.kernel_config
        enabled = kernel_config.enable_flashinfer_autotune
        kernel_config.enable_flashinfer_autotune = False
        try:
            yield
        finally:
            kernel_config.enable_flashinfer_autotune = enabled

    def prepare_reconfiguration(self, new_dp_size: int) -> None:
        # 准备阶段：先把新 rank 接进 NIXL EP，但保持 masked，已有 rank 继续服务
        standby_ep_group = get_standby_ep_group()
        assert standby_ep_group is not None
        all2all_manager = get_ep_all2all_manager(standby_ep_group)
        all2all_manager.stage_ep_size()
        if not self._can_reuse_fused_moe_kernel():
            # 老路径：为 standby 组重建 MoE quant method，需要重新捕获 graph
            self.stage_standby_moe_quant_methods(all2all_manager)
        self._prepare_eplb_communicator(get_standby_eplb_group())
        if new_dp_size > old_dp_size:
            self.transfer_weights(old_dp_size, new_dp_size)
        self._warm_target_groups(get_standby_dp_group(), standby_ep_group)
        if new_dp_size > old_dp_size and self._can_reuse_fused_moe_kernel():
            # 扩容时把已有 rank 的 FlashInfer 调优结果广播给全体
            target_world_group = get_standby_world_group()
            assert target_world_group is not None
            sync_flashinfer_autotune_cache(self.worker.model_runner, target_world_group)

    def warm_and_capture(self) -> None:
        # 新 rank 的 dummy run 走 skip_eplb=True，避免污染刚 rebalance 的 EPLB 统计
        runner = self.worker.model_runner
        all2all_manager = get_ep_all2all_manager()
        reuse_kernel = self._can_reuse_fused_moe_kernel()
        with (
            # 复用模式下：跳过 DP padding 协调，并屏蔽全部远端 peer，
            # 让 dispatch / combine 只在本 rank 内完成，不影响已有 rank 的在途请求
            skip_dp_coordination() if reuse_kernel else nullcontext(),
            all2all_manager.mask_remote_ranks() if reuse_kernel else nullcontext(),
            self._disable_flashinfer_autotune() if reuse_kernel else nullcontext(),
        ):
            runner._dummy_run(runner.max_num_tokens, is_profile=True, skip_eplb=True)
            self.worker.compile_or_warm_up_model()

    def _release_cuda_graphs(self) -> None:
        # 仍需释放 graph 的路径上，DBO 由直接报错改为清空 wrapper 内的 graph
        if isinstance(self.worker.model_runner.model, UBatchWrapper):
            self.worker.model_runner.model.clear_graphs()
        torch.compiler.reset()

```

### `vllm/distributed/eplb/eplb_state.py`

EPLB 状态从「固定尺寸 tensor」改为「最大容量 buffer + 动态视图」，并新增 reconfigure_physical_expert_slots；这是 CUDA graph 复用所依赖的显存地址稳定性的关键实现。

`eplb_state.py` 中最大容量 buffer、视图重建与 commit 时的地址保持（依据 diff 重新整理）：

```python
@dataclass
class EplbModelState:
    physical_to_logical_map: torch.Tensor
    physical_to_logical_map_buffer: torch.Tensor
    """Maximum-capacity buffer backing ``physical_to_logical_map``."""
    # ... logical_to_physical_map / logical_replica_count 语义不变 ...
    expert_load_pass: torch.Tensor
    expert_load_pass_buffer: torch.Tensor
    """Maximum-capacity buffer backing ``expert_load_pass``."""


def add_model(self, model: MixtureOfExperts, model_config: ModelConfig) -> None:
    """Build the initial EPLB state."""
    # ... 构造 initial physical_to_logical_map ...
    physical_expert_capacity = model.num_physical_experts
    if self.parallel_config.enable_elastic_ep:
        # 按 elastic_ep_max_dp_size 预留 expert 槽位：扩容后仍是同一块显存，
        # EPLB map 地址不变，已捕获的 CUDA graph 不会失效
        physical_expert_capacity = (
            model.num_local_physical_experts
            * self.parallel_config.elastic_ep_max_dp_size
            * get_ep_group().world_size
            // self.parallel_config.data_parallel_size
        )
        physical_to_logical_map = torch.nn.functional.pad(
            physical_to_logical_map,
            (0, physical_expert_capacity - model.num_physical_experts),
            value=-1,
        )
    # ... 构造 logical_to_physical_map / logical_replica_count ...
    physical_to_logical_map_buffer = physical_to_logical_map
    # 当前有效视图：只覆盖本配置下的 active expert 数
    physical_to_logical_map = physical_to_logical_map_buffer[
        :, : model.num_physical_experts
    ]
    expert_load_pass_buffer = torch.zeros(
        (model.num_moe_layers, physical_expert_capacity),
        dtype=torch.int32,
        device=self.device,
    )
    expert_load_pass = expert_load_pass_buffer[:, : model.num_physical_experts]
    # MoE 层持有最大容量 buffer，供 kernel 按需切片
    model.set_eplb_state(
        expert_load_pass_buffer,
        logical_to_physical_map,
        logical_replica_count,
    )


def reconfigure_physical_expert_slots(
    self,
    model_config: ModelConfig,
    num_physical_experts: int,
) -> None:
    """Replace physical_to_logical_map and expert_load_pass with views
    covering the new active size.
    """
    model_state = self.model_states[model_config.compute_hash()]
    old_num_physical_experts = model_state.model.num_physical_experts
    # 只清理发生变化的那段槽位，未动的 expert 保留已有 EPLB 统计
    first_slot, last_slot = sorted((old_num_physical_experts, num_physical_experts))
    expert_slots = slice(first_slot, last_slot)
    model_state.physical_to_logical_map_buffer[:, expert_slots].fill_(-1)
    model_state.expert_load_pass_buffer[:, expert_slots].zero_()
    model_state.physical_to_logical_map = (
        model_state.physical_to_logical_map_buffer[:, :num_physical_experts]
    )
    model_state.expert_load_pass = model_state.expert_load_pass_buffer[
        :, :num_physical_experts
    ]
    # 负载滑窗按需补齐（scale-down 时为负值即截断）
    pad_size = num_physical_experts - model_state.expert_load_window.shape[-1]
    model_state.expert_load_window = torch.nn.functional.pad(
        model_state.expert_load_window, (0, pad_size)
    )


def _commit_eplb_maps(self, model_config, src) -> None:
    # 旧实现：expert 数变化时整体替换 physical_to_logical_map（地址变了，graph 失效）；
    # 新实现：先刷新 active 视图，再 copy_ 到最大容量 buffer，地址保持不变
    if src.shape[1] != dst.shape[1]:
        dst = model_state.physical_to_logical_map_buffer[:, : src.shape[1]]
        assert dst.shape == src.shape
        model_state.physical_to_logical_map = dst
    dst.copy_(src, non_blocking=True)

```

### `vllm/v1/worker/dp_utils.py`

新增 skip_dp_coordination 上下文与 should_skip_dp_coordination，并在 coordinate_batch_across_dp 内短路；是「新 rank warmup 不与其他 DP rank 协调」的实现点，也是 review 讨论最集中的文件。

`dp_utils.py` 中跳过 DP 协调的开关与短路分支（依据 diff 重新整理，函数名已按 review 意见从 `assume_uniform_dp_batch` 改为 `skip_dp_coordination`）：

```python
# 默认 False：只有显式进入上下文才会跳过 DP 协调，避免影响正常请求路径
_skip_dp_coordination = ContextVar("skip_dp_coordination", default=False)


@contextmanager
def skip_dp_coordination():
    """Run without coordinating DP metadata with other DP ranks."""
    token = _skip_dp_coordination.set(True)
    try:
        yield
    finally:
        _skip_dp_coordination.reset(token)


def should_skip_dp_coordination() -> bool:
    return _skip_dp_coordination.get()


def coordinate_batch_across_dp(...):
    # ... 前置参数校验与 should_attempt_ubatching 计算 ...
    if should_skip_dp_coordination():
        # Elastic EP 新 rank warmup 时不会与其它 DP rank 同步：
        # 直接按本地 token 数填出 DP size 长度的 padding 结果，
        # cudagraph_mode 也原样返回，避免任何 all_reduce 阻塞已有 rank
        should_ubatch = should_attempt_ubatching and not is_last_ubatch_empty(
            num_tokens_unpadded,
            num_tokens_padded,
            parallel_config.num_ubatches,
        )
        num_tokens_after_padding = torch.full(
            (parallel_config.data_parallel_size,),
            num_tokens_padded,
            dtype=torch.int32,
        )
        return should_ubatch, num_tokens_after_padding, cudagraph_mode

    (should_ubatch, num_tokens_after_padding, synced_cudagraph_mode) = (
        _synchronize_dp_ranks(
            num_tokens_unpadded,
            num_tokens_padded,
            should_attempt_ubatching,
            cudagraph_mode,
            parallel_config,
        )
    )
    return (should_ubatch, num_tokens_after_padding, synced_cudagraph_mode)

```

# 评论区精华

讨论集中在命名语义、短路位置、并发前提与依赖耦合四类问题，均已闭环：

> SageMoore：”Ok I spent too long figuring out that 'assume' really means 'pretend' here. Can we rename this to something like `skip_dp_coordination` ... to indicate to readers that we are going to be running a single rank batch and bi-passing all of the dp coordination?“ —— 作者采纳，`assume_uniform_dp_batch` 重命名为 `skip_dp_coordination`。

> SageMoore（vllm/v1/worker/gpu/dp_utils.py）：”Can you check this in `coordinate_batch_across_dp` and short-circuit there instead?“ —— 作者解释 MRv2 不使用 `coordinate_batch_across_dp`，其等价函数就是 `sync_cudagraph_and_dp_padding`，并已在尚未合并的 vllm-project/vllm#53934 之上验证过复用路径，未来无需跟进 PR。

> SageMoore（elastic_execute.py:290）：”Do we need to call this on all ranks or just new ones?“ —— 作者回答 `sync_flashinfer_autotune_cache` 通过 standby world group 广播，包含已有 rank 与新 rank，必须全 rank 参与；单独建组或走 p2p 会增加复杂度，不值得为省一次小 CPU 广播而做。此点保留了实现现状。

> SageMoore（requirements/kv_connectors.txt）：”I hate to be a drag, but should we upgrade nixl outside of this PR?“ —— 作者认为主要想避免 CI 跑两遍和 PR 间依赖，tlrmchlsmth 回复 “I'm good with upgrading here”，NIXL 1.3.2 -> 1.4.1 最终留在本 PR。

> almogtavor：”...the reused NIXL object still holds the old 8-entry tensor. If EPLB now returns physical expert ID `10`, NIXL does `self.global_to_physical[10]` on that old tensor and that's an OOB access?“ —— 作者回应 Elastic EP requires EPLB，EPLB 不支持 round-robin expert placement，因此 `global_to_physical` 恒为 `None`。该质疑以「前置依赖」方式结案。

另有两条风格类意见被采纳：`physical_to_logical_map_storage` / `expert_load_pass_storage` 统一改名为 `..._buffer`（`expert_load_pass` 本身的重命名被明确推迟到后续 PR），以及 `_commit_eplb_maps` 中旧注释的恢复与 `reconfigure_physical_expert_slots` 的 docstring 补全。SageMoore 与 tlrmchlsmth 最终都给出了 APPROVED。

- 命名语义：assume_uniform_dp_batch -> skip_dp_coordination (design): 作者接受并全量重命名（ContextVar、contextmanager 与调用点同步更新），docstring 改为 “Run without coordinating DP metadata with other DP ranks.”。
- MRv2 短路应该做在哪一层 (design): 保留两处短路，MRv2 支持由 vllm-project/vllm#53934 落地后即可直接生效，无需跟进 PR。
- FlashInfer autotune 广播是否需要在全部 rank 执行 (question): 维持现实现，接受一次小规模 CPU 广播的开销。
- NIXL 版本升级是否应拆出本 PR (design): 依赖升级保留在本 PR 内，回滚粒度变粗但避免了跨 PR 依赖。
- global_to_physical 复用在扩容后是否越界 (correctness): 以「前置依赖 EPLB」结案，未增加额外断言或防御代码。
- buffer 命名与注释维护 (style): 已全部采纳：统一改为 buffer 后缀，注释与 docstring 补齐，`expert_load_pass` 重命名明确推迟到后续 PR。

# 风险与影响

- 风险：
 1. **复用能力与后端强耦合**：`can_reuse_fused_moe_kernel` 只认 `nixl_ep`，非 NIXL 后端仍走 `torch.compiler.reset()` + 重捕获老路径，代码出现长期双分支；后续新增 all2all 后端或修改 NIXL mask/connect 语义时，需要重新验证「地址不变 + graph 入口保留」这一隐式契约。
 2. **最大容量 padding 带来的吞吐回退**：dispatch 输出 buffer 现在按 `num_local_experts * max_num_ep_ranks` 申请（`all2all_utils.py`）。PR body 自评在 active EP=16、capacity=64 时，Batched Triton decode -17.58%、Batched CUTLASS FP8 decode -77.49% / prefill -63.90%，DeepGEMM 与 FlashInfer CuteDSL 基本无感。使用这些内核的 Elastic EP 部署可能收益被抵消。
 3. **显存地址稳定性成为事实契约**：CUDA graph 复用依赖 EPLB map、dispatch buffer 地址不变，`eplb_state.py` 通过 buffer + 视图 + `reconfigure_physical_expert_slots` 固化该契约；任何后续在这些 tensor 上做 in-place resize 或重新分配的改动都会静默破坏复用（不会报错，只会慢或读到旧地址）。
 4. **配置与兼容性**：新增 `--elastic-ep-max-dp-size` 且必须 >= 初始 `data_parallel_size`，否则 `__post_init__` 直接抛 `ValueError`；`VLLM_NIXL_EP_MAX_NUM_RANKS` 环境变量被删除，未见 deprecation 过渡，依赖该变量的运维脚本会静默失效。
 5. **DP 协调短路的副作用面**：`skip_dp_coordination` 是 ContextVar 全局开关，若异常路径未 reset 会污染真实请求的 padding 协调；当前通过 `contextmanager` + `token` 保证，但测试未覆盖该泄漏场景。
 6. **CI 覆盖有限**：`tests/distributed/test_elastic_ep.py` 只在 4 卡上跑 GSM8K 精度回归，无法覆盖 24 卡多节点的停机时间与容量 64 的性能回退；MRv2 复用路径不在 CI 中（作者基于未合并的 vllm-project/vllm#53934 本地验证）。
 7. **依赖升级耦合**：nixl 1.3.2 -> 1.4.1 与功能改动放在同一 PR，若需回滚功能会影响依赖版本粒度。
 - 影响：**用户侧**：使用 NIXL EP 的 Elastic EP 部署可把重配置停机时间压到 1-2 s 量级（16->24 降 97.5%，24->16 降 95.1%），24->16 的总重配置时间也从 45.531 s 降到 9.406 s（-79.3%），对在线弹性扩缩容场景是质变；代价是需要显式设置 `--elastic-ep-max-dp-size`（不设则等于初始 DP size，扩容超过该值会被 ValueError 拦截），且按最大容量申请的 dispatch buffer 会放大 padding 成本，Batched Triton / CUTLASS FP8 用户可能看到明显吞吐下降。
**系统侧**：NIXL EP 通信层由「按当前规模初始化」变为「按最大规模预分配 + stage/commit 两阶段切换」，EPLB 状态由「同尺寸 copy_」变为「最大容量 buffer + 动态视图」，MoE 的 P/F 层新增 `expert_capacity` 参数并移除 `on_commit` 钩子，形成一条新的、以显存地址稳定性为前提的执行契约。
**团队侧**：MRv2 的 Elastic EP 支持（vllm-project/vllm#53934）可以直接复用 `skip_dp_coordination` 与复用判定；`on_commit` 从抽象接口中移除减少了后端实现负担，但也要求未来新增 EP 后端时重新思考 commit 语义。

- 风险标记：核心路径变更 , 仅 NIXL EP 支持复用 , 最大容量 padding 吞吐回退 , MRv2 路径未进 CI, 环境变量移除 , 依赖升级耦合

# 关联脉络

- PR #54689 [Bugfix][NIXL] Don't evict a remote engine a transfer is still reading from: 同属 NIXL 通信栈的近期修复，说明 NIXL 后端的状态机与并发语义仍在快速演进；本 PR 正是在 NIXL EP 的 connect/mask 语义上构建 CUDA graph 复用能力。
- PR #53781 [3/N] HiSparse: host-resident sparse-MLA decode hot-buffering: 同样以 NIXL kv-connector / worker 为基础设施构建新的数据搬运路径，属于同一 NIXL 生态下的大型功能线，可对照其分阶段（[N/N]）推进方式理解本 PR 在 Elastic EP 序列中的位置。
- PR #47288 Elastic EP async preparation（PR body 中引用为 “Async preparation”）: 本 PR 明确声明的前置工作：把 Elastic EP 的准备步骤异步化，移出阻塞式 commit 路径；本 PR 在此基础上进一步消除了 CUDA graph 模式下的 commit 阻塞。
- PR #51885 Elastic EP eager-mode optimizations（PR body 中引用为 “eager-mode optimizations”）: 另一项被本 PR body 引用的前置优化：去掉 eager 模式下的重配置开销；本 PR 补齐 CUDA graph 模式下的同类优化，两者共同构成 Elastic EP 停机时间治理的完整链路。