执行摘要
- 一句话:Elastic EP 准备异步化,停机时间降低 75%+
- 推荐动作:值得精读,尤其关注状态机简化和异步准备的设计模式。对于关注弹性扩展的团队,此 PR 是核心基础。后续需配套跟进错误处理和多节点验证。
功能与动机
减少弹性扩展期间的服务停机时间。原有实现同步执行所有准备步骤,导致客户端长时间收到 503 错误。异步准备使只在最后提交阶段阻塞,从而大幅提升可用性。详见 PR body 性能表。
实现拆解
- API 拆分 Prepare + Commit(
core_client.py, async_llm.py):将原 scale_elastic_ep 拆分为 prepare_elastic_ep 和 commit_elastic_ep。prepare 触发后台线程执行所有非阻塞步骤,commit 执行最终同步切换。
- 简化状态机(
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)。
- 引入异步执行器(
elastic_state.py, elastic_execute.py):使用 ThreadPoolExecutor 在后台线程执行准备步骤。ElasticEPScalingState 新增 _prepare_executor 和 _prepare_future,ElasticEPScalingExecutor 新增 start_async/_run_async,通过 TCPStore 通知协调者完成。
- 内核预热调整(
kernel_warmup.py, core.py):将本地预热与协调预热分离,新 worker 在准备阶段即执行 kernel_warmup(process_local_only=True),避免在提交阶段支付开销。
- 测试与 CI 配套(
tests/distributed/test_elastic_ep.py):扩展测试覆盖无流量、轻载、重载及 CUDA graphs 模式,新增 _traffic_loop、_downtime 等辅助函数。CI 中安装 NIXL 确保依赖。
关键文件:
vllm/v1/engine/core_client.py(模块 引擎核心;类别 source;类型 core-logic;符号 scale_elastic_ep, commit_elastic_ep, prepare_elastic_ep, _eep_wait_for_setup_switch_complete): API 拆分主战场:prepare_elastic_ep 和 commit_elastic_ep 的定义与实现,以及_prepared_elastic_ep 状态管理。
vllm/distributed/elastic_ep/elastic_state.py(模块 分布式层;类别 source;类型 dependency-wiring;符号 _BarrierTimeoutError, _collective_rpc, _execute_async, _execute_tcp_store_barrier): 状态机简化核心:状态数从 9 减至 6,移除了 staged barriers 和跨 DP 同步逻辑,新增异步准备字段。
vllm/distributed/elastic_ep/elastic_execute.py(模块 分布式层;类别 source;类型 dependency-wiring;符号 _set_eplb_suppressed, start_async, _run_async, _mark_async_done): 异步执行器实现:start_async、_run_async、_mark_async_done 方法,使用 ThreadPoolExecutor 在后台线程执行准备步骤并通过 TCPStore 通知协调者。
vllm/v1/engine/core.py(模块 引擎核心;类别 source;类型 core-logic;符号 commit_prepared_elastic_ep, eep_handle_engine_core_notification): commit_prepared_elastic_ep 方法入口,以及 _initialize_kv_caches 中跳过 compile/warmup 的逻辑调整。
vllm/v1/engine/async_llm.py(模块 异步引擎;类别 source;类型 core-logic;符号 _drain_requests_for_elastic_ep, _scale_elastic_ep): scale_elastic_ep 重构为 prepare + commit 两步,引入 _elastic_ep_lock 防止并发缩放。
tests/distributed/test_elastic_ep.py(模块 弹性扩展测试;类别 test;类型 test-coverage;符号 _traffic_loop, _downtime, _scale_with_traffic, _base_serve_args): 测试增强:新增流量模拟、停机时间度量、多模式测试(无流量/轻载/重载/CUDA graphs),移除 sync_eplb 参数。
vllm/model_executor/warmup/kernel_warmup.py(模块 内核预热;类别 source;类型 data-contract;符号 kernel_warmup): 分离本地预热与协调预热,新增 process_local_only 参数,重构函数执行顺序。
vllm/v1/engine/utils.py(模块 引擎工具;类别 source;类型 core-logic): 伴随性调整:移除或简化了与通知相关的工具函数。
vllm/entrypoints/serve/elastic_ep/api_router.py(模块 API 路由;类别 source;类型 entrypoint): 入口调整:移除不再需要的通知流程。
vllm/config/parallel.py(模块 配置层;类别 source;类型 core-logic;符号 use_all2all): 新增 use_all2all 属性,以支持显式指定设备通信器是否需要 all2all。
关键符号: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
状态机简化核心:状态数从 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 = 5
class 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
异步执行器实现: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
评论区精华
错误处理安全性: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 依赖待解。
- commit 失败时 scaling flag 未重置 (correctness): itayalroy 回应这是有意设计,因为已进入不一致状态,错误恢复需单独 PR。审查者接受此解释。
- 用 ThreadPoolExecutor 替代自定义 async_utils (design): itayalroy 采纳并移除了自定义 async_utils.py 文件。
- 本地与协调内核预热分离 (performance): itayalroy 调整了函数顺序,新增 process_local_only 参数,本地预热在准备阶段执行以避免 commit 阶段开销。
- commit_scale_up 的 switch 参数命名 (style): itayalroy 同意并重命名。
风险与影响
关联脉络
- PR #42562 Introduce local kernel warmup: 该 PR 添加的本地内核预热在此 PR 中被移到异步准备阶段,避免 commit 阶段开销。讨论中提及 #42562。
- PR #47206 AMD batch_transfer_weights exclusion fix: 讨论中 itayalroy 提到 #47206 修复了 batch_transfer_weights 的排除机制,与此 PR 的权重传输数据竞争间接相关。
参与讨论