Prhub

#7327 [vllm] fix: resolve .base_layer on the vLLM receiver for non-merged LoRA sync

原始 PR 作者 HollowMan6 合并时间 2026-08-10 19:13 文件变更 12 提交数 6 评论 6 代码增减 +849 / -142

执行摘要

修复非合并 LoRA 同步时 vLLM 权重名 .base_layer 解析

PR body 明确说明:"With LoRA enabled, vLLM wraps every linear layer (exposing base weights under .base_layer), while Megatron only LoRA-wraps the user's target_modules and exports plain HF names for the rest. The old sender-side add_base_layer_suffix relied on hard-coded module lists (MEGATRON_TO_HF_MODULES / STACKED_PARAMS) that cannot track which names actually need the suffix",由此引发 Qwen3.5-VL vision merger(merger.linear_fc1)、packed shared-expert projections(shared_expert.gate_up_proj,报 "There is no module ... named 'shared_expert.gate_up_proj.weight'")、GDN in_proj_qkv、LoRA-wrapped fused-MoE experts 等场景崩溃。此外还顺带修复 LoRA adapter 跨 bucket 时的 TODO("this is buggy if lora span multiple buckets")以及 #6454 的 IPC buffer 复用崩溃,和 trainer 侧 spec-decode 指标的 KeyError。

值得精读。核心看 resolve_weight_name 的分支设计与 vLLM 版本探测逻辑,以及 review 中"非 LoRA 路径性能隔离"和"把解析函数移出扩展类"两条决策;对维护 Megatron + vLLM LoRA 同步链路的工程师尤其重要。建议合入后持续观察不同 vLLM 版本下的回归。

讨论亮点
  1. wuxibin89 问 remove_lora 是否会被多次调用:"Is it safe to remove lora multiple times now?",HollowMan6 回答 "It will not remove lora multiple times, remove_lora runs exactly once per sync, at the prep step, not per bucket."
  2. wuxibin89 质疑 resolve_weight_name 是否影响非 LoRA 路径:"does it affect non-lora?",HollowMan6 承认性能担忧合理,"I make a branch so that non-lora will not go through resolve_weight_name",最终代码用 peft_config is None 分支隔离。
  3. wuxibin89 建议把 _resolve_weight_namevLLMColocateWorkerExtension 移到 verl/utils/vllm/utils.py,理由是"It's better to keep vLLMColocateWorkerExtension concise",HollowMan6 回复 "done"。

实现拆解

  1. 新增接收端名称解析verl/utils/vllm/utils.py 新增 resolve_weight_name_HAS_LORA_LOAD_WEIGHTS 版本探测(检查 BaseLayerWithLoRA.__dict__ 是否含 load_weights),基于 vLLM 模型实际命名空间、hf_to_vllm_mapperpacked_modules_mapping 决定 .base_layer 段的去留,并保证返回值始终是未映射名称,避免 vLLM 内部二次映射(如 in_proj_qkvz -> in_proj_qkvzz);verl/utils/vllm/__init__.py 同步导出 resolve_weight_name
  2. 移除发送端硬编码verl/utils/megatron_peft_utils.py 删除 MEGATRON_TO_HF_MODULESSTACKED_PARAMSconvert_megatron_to_hf_target_modulesadd_base_layer_suffix,并把 build_peft_config_for_vllm 简化为 target_modules: "all-linear"verl/workers/engine/megatron/transformer_impl.pyget_per_tensor_param 不再调用 add_base_layer_suffix,发送端导出纯 HF 名称。
  3. 接收端接入与性能隔离verl/workers/rollout/vllm_rollout/utils.py_update_weights 仅在 peft_config 非空时对参数名做 resolve_weight_name,非 LoRA 分支保持原 model.load_weights(param_updates) 路径,回应 review 中关于非 LoRA 性能影响的担忧。
  4. LoRA 跨 bucket 累积bucketed_weight_transfer.pyreceive_weights 回调增加 is_last 参数;update_weights_from_ipcon_bucket_received 累积 tensor(并 clone 以规避 IPC buffer 复用,见 #6454),is_last 后统一 _update_weights,最终只调用一次 add_lora,解决原 TODO。
  5. LoRA MoE 兼容补丁verl/utils/vllm/patch.pypatch_vllm_moe_model_weight_loaderweight_loader guard 之前清除 routed_experts.lora_base_layer_prefix,使 plain HF 名称可解析到 LoRA-wrapped MoE。
  6. Trainer 防御与测试配套verl/trainer/ppo/v1/trainer_base.py_compute_metrics 在 rollout 后端不报告 spec_num_* 时把三个指标置 None;新增 tests/utils/test_vllm_weight_name_normalization_on_cpu.py(650 行,stub vLLM 依赖,覆盖 released/newer vLLM 两条分支及 Qwen3.5-VL merger、packed experts 回归),并更新 tests/utils/test_bucketed_weight_transfer.py 适配 is_last 回调。
文件 模块 状态 重要度
verl/utils/vllm/utils.py 名称解析 modified 8.13
verl/workers/rollout/vllm_rollout/utils.py 权重接收 modified 7.01
verl/utils/megatron_peft_utils.py PEFT 配置 modified 7.63
verl/workers/engine/megatron/transformer_impl.py Megatron 引擎 modified 5.73
verl/trainer/ppo/v1/trainer_base.py 训练器 modified 5.19
verl/utils/vllm/patch.py vLLM 补丁 modified 5.59
verl/workers/rollout/vllm_rollout/bucketed_weight_transfer.py 分桶传输 modified 5.54
tests/utils/test_vllm_weight_name_normalization_on_cpu.py CPU 测试 added 7.76
verl/utils/vllm/__init__.py vLLM 导出 modified 4.15

关键符号

resolve_weight_name on_bucket_received _update_weights update_weights_from_ipc receive_weights build_peft_config_for_vllm patch_vllm_moe_model_weight_loader _compute_metrics

关键源码片段

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

接收端在 LoRA 分支调用 resolve_weight_name,并通过 on_bucket_received 累积跨 bucket 的 LoRA 权重、延迟到 is_last 后统一 add_lora。

# LoRA adapter 需要一次性拿到完整 tensor dict 再调用 add_lora,但分桶
# 传输可能把一个 adapter 拆到多个 bucket;这里先累积,等 is_last 到达后
# 再统一 apply。clone 是为了避免 add_lora 在回调返回后继续引用被复用
# 覆盖的 IPC buffer(见 #6454)。
lora_weights: dict[str, torch.Tensor] | None = {} if (peft_config and base_sync_done) else None
​
​
def on_bucket_received(weights: list[tuple[str, torch.Tensor]], is_last: bool) -> None:
    if lora_weights is not None:
        lora_weights.update((name, tensor.clone()) for name, tensor in weights)
        if not is_last:
            return
        self._update_weights(
            list(lora_weights.items()),
            peft_config=peft_config,
            base_sync_done=base_sync_done,
        )
        lora_weights.clear()
        return
    self._update_weights(weights, peft_config=peft_config, base_sync_done=base_sync_done)
​
​
# _update_weights 内部:非 LoRA 分支保持原路径(避免逐参数解析的开销),
# 仅 LoRA 分支对每个名称调用 resolve_weight_name 对齐 .base_layer。
if param_updates:
    for model in self._iter_all_models():
        if peft_config is None:
            model.load_weights(param_updates)
        else:
            names = {n for n, _ in model.named_parameters(remove_duplicate=False)}
            names.update(n for n, _ in model.named_buffers())
            model.load_weights((resolve_weight_name(model, n, names), t) for n, t in param_updates)

评论区精华

remove_lora 是否会被多次调用 正确性

wuxibin89 问 "Is it safe to remove lora multiple times now?",HollowMan6 回答 "It will not remove lora multiple times, `remove_lora` runs exactly once per sync, at the prep step, not per bucket."

结论:remove_lora 每轮同步仅在 prep 阶段执行一次,不随 bucket 重复。 · 已解决

resolve_weight_name 对非 LoRA 路径的影响 性能

wuxibin89 问 "does it affect non-lora?",HollowMan6 承认性能担忧合理,"I make a branch so that non-lora will not go through `resolve_weight_name`"。

结论:最终代码用 peft_config is None 分支隔离,非 LoRA 保持原 load_weights 路径。 · 已解决

resolve_weight_name 放置位置 设计

wuxibin89 建议把 _resolve_weight_name 从 vLLMColocateWorkerExtension 移到 verl/utils/vllm/utils.py,理由是 "It's better to keep vLLMColocateWorkerExtension concise",HollowMan6 回复 "done"。

结论:迁移至 verl/utils/vllm/utils.py,保持扩展类精简。 · 已解决

风险与影响

  1. 名称解析逻辑复杂度resolve_weight_name 依赖 vLLM 内部命名约定(base_layerpacked_modules_mappinghf_to_vllm_mapper),测试用 _FakeMapper 近似真实 mapper;若真实模型出现未覆盖的命名形态,可能仍匹配失败,需要持续用真实模型回归验证。
  2. 跨 vLLM 版本分支_HAS_LORA_LOAD_WEIGHTS 通过 BaseLayerWithLoRA.__dict__ 探测,vLLM 后续改动实现会翻转分支行为;虽然两条分支都有 CPU 测试覆盖,但真实环境仍需验证。
  3. 性能风险:LoRA 模式下每轮同步需要遍历 named_parameters / named_buffers 构建名称集合并逐参数名解析,超大模型有额外 CPU 开销;非 LoRA 分支已绕过该路径。
  4. 状态一致性remove_lora 在 prep 阶段执行,新 adapter 在 is_last 后加入;若中途失败(异常/断流),可能出现旧 adapter 已删而新 adapter 未加的窗口。
  5. API 兼容verl/utils/megatron_peft_utils.py 删除了 convert_megatron_to_hf_target_modulesadd_base_layer_suffix 等公共函数,仓库内调用点已清理,但外部脚本或插件若引用会破坏。
  1. 用户侧:修复非合并 LoRA 模式下 Megatron(或 vanilla bridge)权重同步到 vLLM 的多类崩溃,涉及 Qwen3.5-VL、GDN、共享专家 MoE、fused-MoE 等模型;LoRA adapter 跨 bucket 的隐藏 bug 也被修复。
  2. 系统侧:权重同步协议回调签名变化(新增 is_last),所有调用 receive_weights 的路径需要同步;trainer spec-decode 指标采集更健壮,不再对缺失统计信息抛 KeyError。
  3. 团队侧:设计上把名称对齐职责从发送端(Megatron engine)移到接收端(vLLM worker),megatron_peft_utils 职责缩小,后续扩展新模型不再需要维护 STACKED_PARAMS 硬编码列表。
核心路径变更 跨 vLLM 版本分支 外部 API 删除 LoRA 权重同步 名称解析复杂度

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论