Prhub

#7227 [ckpt, rollout, vllm] feat: add vLLM consumer for delta-sharded weight sync

原始 PR 作者 ShuoleiWang 合并时间 2026-08-31 09:59 文件变更 11 提交数 2 评论 1 代码增减 +578 / -62

执行摘要

vLLM 新增 delta_sharded 稀疏权重同步,实测提速约 8 倍

PR body 指出,在本次变更前只有 SGLang 能消费 delta_sharded 的 DeltaFlush 流,vLLM rollout 仍只能使用全量权重同步(full-weight sync),无法享受 verl #6974 带来的 trainer 端物化、gather 流量与跨节点 payload 的削减。同时 delta 以 checkpoint/HF 坐标表达,而 vLLM 运行时权重可能是 TP/EP 分片或 packed QKV/MoE 张量,直接应用需要解决坐标映射难题;因此本 PR 采取“VERL 负责校验与解码、vLLM 原生 model.load_weights() 负责映射”的分工,规避重复实现。

值得精读。这是 delta_sharded 从 SGLang 扩展到 vLLM 的关键一步,对理解 VERL checkpoint 引擎的扩展模式很有价值。重点看:① 坐标映射交由 vLLM 原生 loader 的架构分工(decode_delta_payload + CheckpointWeightPatch);② 失败锁死与“整 session 重建”的运维语义;③ require_vllm_delta_support 的接口探测写法与 _preprocess_engine_kwargs 的拓扑 guard;④ 在无法运行真实路径的 CI 环境里,如何用版本无关回归测试(abort 传播、字节对齐)保护基础设施。

讨论亮点

本 PR 没有产生实质 review 评论:wuxibin89 直接 APPROVED,唯一的 issue 评论是 CLA 签署确认。真正值得关注的决策沉淀在 PR body 与代码注释中:① 依赖策略:作者明确询问“是否等待包含 #50723 的 vLLM release,还是作为需要特定 vLLM main revision 的 opt-in 功能”,最终选择后者,且不新增假装运行成功路径的 CI;② 失败语义:更新非事务性,失败可能留下部分更新的 rollout 权重且 trainer 端 snapshot 已前进,无法原地重试,vLLM worker 单独重启不安全;③ 性能权衡:wire 侧与 trainer gather 是 O(nnz),但 vLLM 当前 patch API 会为每个 patch 创建完整 checkpoint 形状的 NaN 张量并做 dense masked copy,应用侧并非稀疏。

实现拆解

第 1 步:新增 vLLM 消费端核心模块

新增 verl/workers/rollout/vllm_rollout/delta_weight_transfer.py(约 347 行,全 PR 核心):require_vllm_delta_support()inspect.signature 探测四参 WeightTransferEngine.__init__ 并探测 checkpoint_weight_patch 模块,缺失时抛出带版本信息的 RuntimeError;decode_delta_payload() 解析 __delta_spec__/__positions__/__values__ 三元组、校验 checksum、区分 dense/indices 编码并构造 CheckpointWeightPatch 列表;VerlDeltaIPCWeightTransferEngine.receive_weights() 依据会话编码决定首次 dense seed 走 initialize_layerwise_reload 生命周期、稳态 sparse 直接 patch,任何异常设置 _update_failed 锁死 worker;is_moe_model() 对齐 vLLM 自身 MoE 判定(覆盖 Dbrx/DeepSeek/Mixtral 多属性与多模态嵌套 text config)。

第 2 步:打通 Rollout 驱动链路

verl/workers/rollout/vllm_rollout/vllm_rollout.py 的 update_weights()wire_format 分流,新增 _update_delta_weights():先用 ensure_async_iterator 取首个 flush,空流(无变化)只推进 global_steps;非空则先触发 init_weight_transfer_engine,再对每个 flush 以非阻塞 collective_rpc 发起 update_verl_delta_weights、同步 BucketedWeightSenderuse_shm=False)发送 payload,最后 finish_weight_update 并清理 KV cache。utils.py 在 vLLM worker 子进程 __new__ 中按 weight_transfer_config.backend == 'verl_delta_ipc' 调用 register_verl_delta_weight_transfer_engine() 完成注册(必须先于 vLLM 构造 worker 侧 transfer engine),并新增 update_verl_delta_weights() 向 update_info 注入本 worker 的 ZMQ handle。bucketed_weight_transfer.py 增加按 element_size 的字节对齐,保证 odd 字节 uint8 前缀后的 BF16 值可被接收端正确 reinterpret。

第 3 步:收紧配置与拓扑 Guard

vllm_async_server.py 的 _preprocess_engine_kwargs()delta_sharded 时校验并注入:拒绝 verify_every>0、DP/PP 大于 1、PD 分离、EPLB;MoE 模型强制 moe_backend='triton';强制 weight_transfer_config={'backend': 'verl_delta_ipc'} 并拒绝用户配置第二个 vLLM WTE 后端。同时 abort_all_requests() 由吞掉异常改为直接 raise,保证请求未清理时权重更新不会继续(新增测试覆盖)。base.py 的 CheckpointEngineWorkerdelta_sharded 允许的 rollout 从仅 sglang 扩展为 {sglang, vllm};delta_checkpoint_engine.py 将 _shard_seeded 置位后移到 prime_delta_snapshots() 之后,并把 verify_every 澄清为仅 SGLang 支持。

第 4 步:测试与文档配套

tests/utils/test_bucketed_weight_transfer.py 的 mixed-dtype 用例加入 odd 字节 uint8 manifest/positions 前缀 + BF16 values;tests/workers/rollout/rollout_vllm/test_vllm_abort.py 新增 pause 失败传播测试;docs/advance/delta_weight_sync.md 补充 vLLM 配置示例(encoding=indicesverify_every=0)与依赖边界。默认 CI 仍 pin vllm 0.24.0,不包含真实 delta_sharded 消费路径自动化;bit-exact 与性能结论来自 H20 手动验证。

文件 模块 状态 重要度
verl/workers/rollout/vllm_rollout/delta_weight_transfer.py 权重适配 added 8.89
verl/workers/rollout/vllm_rollout/vllm_rollout.py 权重同步 modified 7.79
verl/workers/rollout/vllm_rollout/vllm_async_server.py 配置校验 modified 6.99
verl/checkpoint_engine/delta_checkpoint_engine.py 同步引擎 modified 6.72
verl/workers/rollout/vllm_rollout/utils.py 进程钩子 modified 6.39
verl/checkpoint_engine/base.py 引擎基类 modified 5.58
verl/workers/rollout/vllm_rollout/bucketed_weight_transfer.py IPC 传输 modified 5.13
tests/workers/rollout/rollout_vllm/test_vllm_abort.py 测试 modified 5.09
tests/utils/test_bucketed_weight_transfer.py 测试 modified 3.35
docs/advance/delta_weight_sync.md 文档 modified 2.52

关键符号

decode_delta_payload receive_weights start_weight_update finish_weight_update _validate_configuration require_vllm_delta_support is_moe_model _update_delta_weights send_flush update_verl_delta_weights register_verl_delta_weight_transfer_engine

关键源码片段

verl/workers/rollout/vllm_rollout/delta_weight_transfer.py core-logic

本 PR 的核心新增:vLLM 侧 delta_sharded 消费者,把 DeltaFlush 校验 / 解码为 checkpoint patches 并驱动 vLLM 原生 loader 应用,含 dense seed 的 layerwise reload 生命周期与失败锁死。

VerlDeltaIPCWeightTransferEngine 是 vLLM worker 内消费 DeltaFlush 的入口:接收同机 CUDA-IPC payload → 解码校验 → 应用 patch;首次 dense seed 走 vLLM layerwise reload,稳态 sparse 更新直接写运行时权重,任何失败锁死 worker。

def receive_weights(self, update_info: VerlDeltaIPCUpdateInfo) -> None:
    assert update_info.zmq_handle is not None
    try:
        payload = self._receive_payload(zmq_handle=update_info.zmq_handle)
        encoding, patches = decode_delta_payload(payload)
​
        if self._session_encoding is None:
            # 会话第一个 flush 决定本 update 的编码:dense seed 需要
            # 走 vLLM 的 layerwise reload 生命周期,以便执行模型后加载动作
            self._session_encoding = encoding
            if encoding == 'dense':
                from vllm.model_executor.model_loader.reload import (
                    initialize_layerwise_reload,
                )
                initialize_layerwise_reload(self.model)
        elif encoding != self._session_encoding:
            raise ValueError(
                '一次 weight update 不能混用 dense 与 sparse DeltaFlush '
                f'({self._session_encoding!r} then {encoding!r})'
            )
​
        _, load_checkpoint_weight_patches = _checkpoint_patch_api()
        # 可信 producer 保证每个变化位置最多出现一次,跳过 vLLM 的
        # sort-based 重复校验以节省 rollout GPU 开销
        load_checkpoint_weight_patches(
            self.model,
            patches,
            validate_unique_indices=False,
        )
    except BaseException:
        # 早期 flush 可能已改动 runtime weights:后续 update 将基于
        # 未知的部分状态,因此锁死本 worker 直到整个 session 重建
        self._update_failed = True
        raisedef finish_weight_update(self) -> None:
    if self._session_encoding == 'dense':
        from vllm.model_executor.model_loader.reload import (
            finalize_layerwise_reload,
        )
        try:
            finalize_layerwise_reload(self.model, self.model_config)
        except BaseException:
            self._update_failed = True
            raise
    # sparse 更新直接写已初始化的 runtime tensors,无需额外收尾
verl/workers/rollout/vllm_rollout/vllm_rollout.py core-logic

rollout 驱动入口:update_weights 按 wire_format 分流,新增 _update_delta_weights 流式驱动 WTE 生命周期并处理空更新。

ServerAdapter._update_delta_weights() 是 vLLM 侧 delta 更新的驱动端:把 checkpoint 引擎吐出的 DeltaFlush 流逐条转发到同机 vLLM worker,并管理 WTE 生命周期与 KV cache 清理。

async def _update_delta_weights(self, weights, *, global_steps: int | None) -> None:
    """发送一次 delta weight update,即一串 DeltaFlush payload。"""
    from verl.workers.rollout.utils import ensure_async_iterator
​
    if self.use_shm:
        raise NotImplementedError('delta_sharded 与 vLLM 配合要求同机 CUDA IPC')
​
    flushes = ensure_async_iterator(weights)
    try:
        first_item = await anext(flushes)
    except StopAsyncIteration:
        # 稳态同步没有变化值时会收到纯终止标记:权重未变,只推进
        # global_steps,不触碰 transfer engine 与 KV cache
        if global_steps is not None and self._ensure_server_handle():
            await self.server_handle.set_global_steps.remote(global_steps)
        return
​
    first_named_tensors, saw_last = first_item
    if not self._delta_weight_transfer_engine_initialized:
        await self._execute_method(
            'init_weight_transfer_engine',
            kwargs={'init_info': {}},
        )
        self._delta_weight_transfer_engine_initialized = True
​
    await self._execute_method('start_weight_update')
​
    async def send_flush(flush_tensors: list[tuple[str, torch.Tensor]]) -> None:
        # 先异步发起接收端 collective_rpc(只传控制信息),
        # 再通过同机 ZMQ/CUDA-IPC 发送 payload,最后等待接收端完成
        receiver_future = await self._execute_method(
            'update_verl_delta_weights',
            non_block=True,
            kwargs={'update_info': {}},
        )
        sender = BucketedWeightSender(
            zmq_handle=self.zmq_handle,
            bucket_size_mb=self.config.checkpoint_engine.update_weights_bucket_megabytes,
            use_shm=False,
        )
        await sender.async_send_weights(iter(flush_tensors))
        if receiver_future is not None:
            await receiver_future
​
    await send_flush(list(first_named_tensors))
    async for named_tensors, is_last in flushes:
        if saw_last:
            raise ValueError('DeltaFlush stream 在 is_last=True 之后还有数据')
        saw_last = is_last
        await send_flush(list(named_tensors))
​
    if not saw_last:
        raise ValueError('DeltaFlush stream 在 is_last=True 前结束')
​
    await self._execute_method('finish_weight_update')
​
    if self._has_server:
        await self.server_handle.clear_kv_cache.remote()
        if global_steps is not None:
            await self.server_handle.set_global_steps.remote(global_steps)

评论区精华

vLLM 依赖版本策略:等待 release 还是维持 opt-in 设计

PR body 明确说明 VERL 默认 pin 的 vllm==0.24.0 不具备四参 WeightTransferEngine 接口与 #50723 checkpoint patch API,截至 2026-08-28 无包含 #50723 的 tagged release;作者请求 maintainer 指引是否等待版本发布。

结论:PR 选择不引入 vLLM-main 依赖、不添加假装运行成功路径的 CI,保持 opt-in 并依赖 require_vllm_delta_support 探测报错;最终 wuxibin89 APPROVED。 · 待处理

abort_all_requests 失败语义:吞异常改抛出 正确性

vllm_async_server.py 中 abort_all_requests 原先把 pause 失败记录为 error dict 返回,本 PR 改为直接 raise,理由:权重更新不得在请求未终结、旧权重 cache 未清理时继续。新增测试 test_abort_all_requests_propagates_pause_failure。

结论:已实施并通过新增测试覆盖;行为变化可能影响依赖旧 error dict 的调用方。 · 已解决

风险与影响

  • 依赖上游未发布 API:功能依赖 vLLM #44353(四参 WeightTransferEngine)与 #50723(checkpoint patch),默认 vllm==0.24.0 不可用。require_vllm_delta_support() 能提前给出清晰报错,但未来 vLLM 接口演进会使适配器面临失效风险,需要持续跟踪。
  • 非事务性失败语义:一次失败可能留下部分更新的 rollout 权重,而 trainer 端 delta snapshot 已前进;_update_failed 只锁定单个 worker,多 worker 间部分失败的编排语义需要团队充分认知。
  • 内存峰值风险:接收端每个 patch 会物化完整 checkpoint 形状的 NaN 张量并做 dense masked copy,应用侧内存/计算开销不随稀疏度下降,超大模型需评估。
  • 正确性验证范围有限:仅 H20 + Qwen3-30B-A3B TP8+EP8 验证;decode_delta_payload 用 getattr(torch, spec['dtype']) 取 dtype,未做白名单枚举(spec 来自可信 trainer,风险较低)。
  • 行为变更回归:abort_all_requests 由返回 error dict 改为 raise,影响所有 vLLM weight update 失败路径;bucketed_weight_transfer.py 对齐改动影响全量权重同步路径(不只 delta),依赖新增 mixed-dtype 测试覆盖。
  • 用户影响:启用 delta_sharded + vllm 的配置可获得约 8 倍 weight-sync 提速(低变更率下),但仅限 BF16 非量化、DP=PP=1、无 PD/EPLB/verify_every 的窄场景;默认 vllm==0.24.0 用户不会走到该路径,误配会得到明确的 NotImplementedError/ValueError。
  • 系统影响:trainer 端 NCCL 广播与 ZMQ/CUDA-IPC 通道复用现有 checkpoint communicator,无新增通信拓扑;leader 的 collective_rpc 只传控制信号。abort 失败语义收紧后,vLLM weight update 的失败处理从“记录并继续”变为“直接终止”。
  • 团队影响:明确了 VERL 与 vLLM 上游 weight-transfer API 的依赖边界和探测方式,为后续 vLLM 版本升级提供了可维护的适配点;也为其他 rollout 后端(如 TensorRT-LLM)的 delta consumer 提供了参考模式。
依赖上游未发布 API 非事务性失败语义 默认 CI 不覆盖新路径 单模型验证 sparse 应用非 O(nnz)

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论