Prhub

#47288 [Elastic EP] Async preparation

原始 PR 作者 itayalroy 合并时间 2026-07-28 20:19 文件变更 21 提交数 17 评论 40 代码增减 +701 / -677

执行摘要

Elastic EP 准备异步化,停机时间降低 75%+

减少弹性扩展期间的服务停机时间。原有实现同步执行所有准备步骤,导致客户端长时间收到 503 错误。异步准备使只在最后提交阶段阻塞,从而大幅提升可用性。详见 PR body 性能表。

值得精读,尤其关注状态机简化和异步准备的设计模式。对于关注弹性扩展的团队,此 PR 是核心基础。后续需配套跟进错误处理和多节点验证。

讨论亮点

错误处理安全性:depthfirst-app[bot] 指出如果 commit_elastic_ep 抛出异常,set_scaling_elastic_ep(False) 不执行导致永久阻塞。作者回应这是有意设计,因已进入不一致状态,需单独 PR 改进。审查者接受。

异步执行器选择:SageMoore 建议用 ThreadPoolExecutor 替代自定义 SingleMethodAsyncRunner,作者采纳并移除自定义类。

内核预热拆分:tlrmchlsmth 和 LopezCastroRoberto 讨论本地 vs 协调预热,同意本地预热应在准备阶段运行,作者调整了函数顺序。

参数命名:tlrmchlsmth 建议将 switch bool 重命名为 is_existing_worker,作者同意。

整体评价:SageMoore 和 tlrmchlsmth 均认可代码简化,但担忧单一 API server 依赖待解。

实现拆解

  1. API 拆分 Prepare + Commitcore_client.py, async_llm.py):将原 scale_elastic_ep 拆分为 prepare_elastic_epcommit_elastic_epprepare 触发后台线程执行所有非阻塞步骤,commit 执行最终同步切换。
  2. 简化状态机elastic_state.py):ScaleUpExistingEngineState 从 9 个状态减少到 6 个,去除 WAIT、TRANSFER_EXPERT_MAPPING、EPLB_RESHUFFLE 等中间状态,明确区分准备(CREATE_STANDBY_GROUPS, STAGE_QUANT_METHODS, TRANSFER_WEIGHTS, SYNC_KV_CACHE_MEMORY_SIZE)和提交(COMMIT_SCALE_UP/COMMIT_SCALE_DOWN)。
  3. 引入异步执行器elastic_state.py, elastic_execute.py):使用 ThreadPoolExecutor 在后台线程执行准备步骤。ElasticEPScalingState 新增 _prepare_executor_prepare_futureElasticEPScalingExecutor 新增 start_async/_run_async,通过 TCPStore 通知协调者完成。
  4. 内核预热调整kernel_warmup.py, core.py):将本地预热与协调预热分离,新 worker 在准备阶段即执行 kernel_warmup(process_local_only=True),避免在提交阶段支付开销。
  5. 测试与 CI 配套tests/distributed/test_elastic_ep.py):扩展测试覆盖无流量、轻载、重载及 CUDA graphs 模式,新增 _traffic_loop_downtime 等辅助函数。CI 中安装 NIXL 确保依赖。
文件 模块 状态 重要度
vllm/v1/engine/core_client.py 引擎核心 modified 9.21
vllm/distributed/elastic_ep/elastic_state.py 分布式层 modified 8.93
vllm/distributed/elastic_ep/elastic_execute.py 分布式层 modified 8.79
vllm/v1/engine/core.py 引擎核心 modified 8.06
vllm/v1/engine/async_llm.py 异步引擎 modified 7.83
tests/distributed/test_elastic_ep.py 弹性扩展测试 modified 7.63
vllm/model_executor/warmup/kernel_warmup.py 内核预热 modified 7.06
vllm/v1/engine/utils.py 引擎工具 modified 6.73
vllm/entrypoints/serve/elastic_ep/api_router.py API 路由 modified 6.13
vllm/config/parallel.py 配置层 modified 5.9

关键符号

prepare_elastic_ep commit_elastic_ep start_async _run_async _mark_async_done _execute_async is_ready_for_switch commit_prepared_elastic_ep commit_scale_up

关键源码片段

vllm/distributed/elastic_ep/elastic_state.py dependency-wiring

状态机简化核心:状态数从 9 减至 6,移除了 staged barriers 和跨 DP 同步逻辑,新增异步准备字段。

# vllm/distributed/elastic_ep/elastic_state.py
# 弹性状态机:准备阶段与提交阶段分离,异步执行准备步骤class ScaleUpExistingEngineState(enum.IntEnum):
    CREATE_STANDBY_GROUPS = 0 # 准备:创建目标进程组
    STAGE_QUANT_METHODS = 1 # 准备:暂存量化方法
    TRANSFER_WEIGHTS = 2 # 准备:非专家权重复制
    SYNC_KV_CACHE_MEMORY_SIZE = 3 # 准备:KV 缓存大小同步
    COMMIT_SCALE_UP = 4 # 提交:阻塞前向传递
    COMPLETE = 5class ElasticEPScalingState:
    def __init__(self, model_executor, engine_core, vllm_config,
                 new_parallel_config, worker_type, scale_type, reconfig_request=None):
        # ... 各种引用初始化 ...
        self.commit_requested = False
        self._prepare_executor = ThreadPoolExecutor(
            max_workers=1, thread_name_prefix="ElasticEPPrepare"
        )
        self._prepare_future: Future[Any] | None = None
        self._new_dp_sync: tuple[object, Any] | None = None
        # 初始状态
        if scale_type == "scale_up":
            self.state = ScaleUpNewEngineState.PRE_KV_INIT if worker_type == "new" \
                else ScaleUpExistingEngineState.CREATE_STANDBY_GROUPS
        else:
            self.state = ScaleDownRemovingEngineState.PREPARE if worker_type == "removing" \
                else ScaleDownRemainingEngineState.PREPARE
​
    def _collective_rpc(self, *args, **kwargs):
        return self.model_executor.collective_rpc(*args, **kwargs)
​
    def _execute_async(self, execute_method: str, *args) -> bool:
        # 在后台线程启动一个准备步骤
        if self._prepare_future is not None:
            return False
        done_keys = self._collective_rpc(
            "elastic_ep_execute",
            args=("start_async", execute_method, *args),
        )
        self._prepare_future = self._prepare_executor.submit(
            self._run_async, done_keys, execute_method, *args
        )
        return True
​
    def is_ready_for_switch(self) -> bool:
        return (self._prepare_future is not None and self._prepare_future.done())
​
    def _run_async(self, done_keys, execute_method, *args):
        from vllm.platforms import current_platform
        current_platform.set_device(self.worker.device)
        with set_current_vllm_config(self.vllm_config):
            self.model_executor.elastic_ep_execute(execute_method, *args)
        get_cached_tcp_store_client(...).set(done_key, b"1")
vllm/distributed/elastic_ep/elastic_execute.py dependency-wiring

异步执行器实现:`start_async`、`_run_async`、`_mark_async_done` 方法,使用 ThreadPoolExecutor 在后台线程执行准备步骤并通过 TCPStore 通知协调者。

# vllm/distributed/elastic_ep/elastic_execute.py
# Async 执行器:在后台线程执行准备步骤,通过 TCPStore 通知完成class ElasticEPScalingExecutor:
    def __init__(self, worker):
        self.worker_ref = weakref.ref(worker)
        self.reconfig_request = None
        self._staged_moe_quant_methods = {}
        self._async_executor = ThreadPoolExecutor(
            max_workers=1, thread_name_prefix="ElasticEPAsync"
        )
        self._async_future: Future[None] | None = None
​
    def start_async(self, execute_method: str, *args, **kwargs) -> str:
        if self._async_future is not None:
            raise RuntimeError("Another Elastic EP async method is active")
        if args and isinstance(args[0], ReconfigureDistributedRequest):
            self.reconfig_request = args[0]
        dp_rank = self.worker.vllm_config.parallel_config.data_parallel_rank
        done_key = f"eep_async/{execute_method}/{dp_rank}/{self.worker.rank}"
        self._async_future = self._async_executor.submit(
            self._run_async, execute_method, *args, **kwargs
        )
        self._async_future.add_done_callback(
            lambda _: self._mark_async_done(done_key)
        )
        return done_key
​
    def _run_async(self, execute_method: str, *args, **kwargs):
        from vllm.platforms import current_platform
        current_platform.set_device(self.worker.device)
        with set_current_vllm_config(self.worker.vllm_config):
            self.execute(execute_method, *args, **kwargs)
​
    def _mark_async_done(self, done_key: str):
        assert self.reconfig_request is not None
        get_cached_tcp_store_client(
            self.reconfig_request.new_data_parallel_master_ip,
            self.reconfig_request.coord_store_port,
        ).set(done_key, b"1")
​
    def clear_async(self) -> None:
        if self._async_future is None:
            raise RuntimeError("No async execution to clear")
        self._async_future.result() # 确保完成
        self._async_future = None
        self.reconfig_request = None

评论区精华

commit 失败时 scaling flag 未重置 正确性

depthfirst-app[bot] 指出如果 commit_elastic_ep 抛出异常,set_scaling_elastic_ep(False) 不会执行,导致服务器永久阻塞。

结论:itayalroy 回应这是有意设计,因为已进入不一致状态,错误恢复需单独 PR。审查者接受此解释。 · 已解决

用 ThreadPoolExecutor 替代自定义 async_utils 设计

SageMoore 建议使用 concurrent.futures.ThreadPoolExecutor 代替新增的 SingleMethodAsyncRunner 类。

结论:itayalroy 采纳并移除了自定义 async_utils.py 文件。 · 已解决

本地与协调内核预热分离 性能

tlrmchlsmth 和 LopezCastroRoberto 讨论 kernel_warmup 需要区分仅本地执行的预热和需要所有 worker 参与的协调预热。

结论:itayalroy 调整了函数顺序,新增 process_local_only 参数,本地预热在准备阶段执行以避免 commit 阶段开销。 · 已解决

commit_scale_up 的 switch 参数命名 style

tlrmchlsmth 建议将 switch bool 重命名为 is_existing_worker 以更清晰。

结论:itayalroy 同意并重命名。 · 已解决

风险与影响

  1. 错误恢复不完整:prepare 或 commit 失败后系统可能处于不一致状态且无法自动恢复,此风险在改前也存在,需后续改进。
  2. 多线程数据竞争:后台线程访问模型权重可能与 EPLB 写入重叠,当前通过中间缓冲区和 batch_transfer_weights 的排除列表缓解,但仍需警惕。
  3. 配置兼容性:移除 coord_store_port 从 AOT hash 可能导致已缓存编译结果失效,虽有意但首运行可能触发重编译。
  4. 测试局限:仅覆盖单节点,多节点弹性扩展表现未验证。

用户:弹性扩展停机时间从 35s+ 降至 6-7s(Eager 模式),CUDA graphs 仍有 ~12s 需后续优化。
系统:内部状态机简化,维护性提升;引入线程池增加异步协调复杂度。
团队:为后续减少 EPLB 和 graph 重捕获开销奠定基础,需关注多 API server 支持和大规模验证。

核心路径变更 错误恢复不完整 多线程数据竞争 需要后续错误处理

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论