Prhub

#7161 [fsdp] refactor: move unfuse_moe_params to FSDP backend

原始 PR 作者 wuxibin89 合并时间 2026-07-27 23:42 文件变更 10 提交数 5 评论 1 代码增减 +59 / -73

执行摘要

将 unfuse_moe_params 从 vLLM rollout 移至 FSDP 后端

PR body 明确指出要「Move unfuse_moe_params from vllm to FSDP backend」,让两种 rollout 后端(vLLM / SGLang)都能复用 MoE 参数展开逻辑,避免重复实现。

值得阅读,展示了模块职责重新划分的典型手法——将 rollout 特定的转换下沉到 engine 层,提升复用性。关注点在于移除版本检查可能带来的兼容性缺口,建议确认 vLLM 最新版本下 per-expert 展开是否依然安全。

讨论亮点

该 PR 未收到 review 评论或讨论逻辑争议。

实现拆解

  1. 在 verl/workers/engine/fsdp/utils.py 新增同步生成器函数 unfuse_moe_params,逻辑与原来的 _iter_vllm_compatible_moe_params 完全一致,但不再依赖 asyncio。
  2. 在 verl/workers/engine/fsdp/transformer_impl.py 中导入该函数,在 get_per_tensor_param 中迭代参数后立即调用 unfuse_moe_params,将 packed 的 MoE 专家权重展开为 per-expert 格式。
  3. 从 verl/workers/rollout/vllm_rollout/vllm_rollout.py 中删除 _should_expand_vllm_moe_params 和 _iter_vllm_compatible_moe_params 两个函数,以及 update_weights 中的相关调用条件。
  4. 更新测试文件 test_vllm_moe_param_converter_on_cpu.py,将导入改为从 FSDP utils 引入同步版本,并移除 asyncio 包装。
  5. 调整其他测试文件(test_engine.py、test_rollout_trace_on_cpu.py 等)以保持 CI 通过,修复了因模块重构引起的导入或断言差异。
文件 模块 状态 重要度
verl/workers/engine/fsdp/utils.py FSDP 工具 modified 7.22
verl/workers/rollout/vllm_rollout/vllm_rollout.py vLLM rollout modified 7.1
verl/workers/engine/fsdp/transformer_impl.py FSDP 引擎 modified 5.82
tests/workers/rollout/test_vllm_moe_param_converter_on_cpu.py 测试 /CPU modified 4.19
tests/models/test_engine.py 测试 / 引擎 modified 3.89
tests/utils/test_rollout_trace_on_cpu.py 测试 / 工具 modified 3.44
tests/checkpoint_engine/test_correctness_on_npu.py 测试 /NPU modified 3.4
tests/checkpoint_engine/test_utils.py 测试 / 工具 modified 3.24
tests/workers/test_engine_return_model_output_on_cpu.py 测试 / 引擎输出 modified 3.02
tests/special_e2e/run_fully_async_policy.sh 测试 /e2e modified 2.94

关键符号

unfuse_moe_params _iter_vllm_compatible_moe_params _should_expand_vllm_moe_params

关键源码片段

verl/workers/engine/fsdp/utils.py core-logic

新增核心函数 unfuse_moe_params,是本次重构的核心输出。

# verl/workers/engine/fsdp/utils.py
​
def unfuse_moe_params(weights, model_type: str | None = None):
    """Expand Transformers 5 packed MoE expert tensors to vLLM checkpoint keys.
​
    Transformers 5 stores Qwen-style MoE experts as packed 3D parameters:
    ``mlp.experts.gate_up_proj`` with shape
    ``[num_experts, 2 * intermediate_size, hidden_size]`` and
    ``mlp.experts.down_proj`` with shape
    ``[num_experts, hidden_size, intermediate_size]``. vLLM's Qwen MoE reload
    path still accepts the original per-expert checkpoint keys during live
    weight sync, so stream those keys without materializing a full dict.
    """
    for name, tensor in weights:
        # GPT-OSS checkpoint weights use packed 3D expert tensors directly.
        # Splitting them into per-expert 2D tensors makes vLLM's GPT-OSS loader
        # index a 2D tensor as 3D during TP slicing.
        if model_type == "gpt_oss" and ".mlp.experts." in name and tensor.dim() == 3:
            yield name, tensor
            continue
​
        if name.endswith(".mlp.experts.gate_up_proj") and tensor.dim() == 3:
            gate, up = tensor.chunk(2, dim=1)
            base = name.removesuffix(".gate_up_proj")
            for expert_id in range(tensor.size(0)):
                yield f"{base}.{expert_id}.gate_proj.weight", gate[expert_id].contiguous()
                yield f"{base}.{expert_id}.up_proj.weight", up[expert_id].contiguous()
            continue
​
        if name.endswith(".mlp.experts.down_proj") and tensor.dim() == 3:
            base = name.removesuffix(".down_proj")
            for expert_id in range(tensor.size(0)):
                yield f"{base}.{expert_id}.down_proj.weight", tensor[expert_id].contiguous()
            continue
​
        yield name, tensor
verl/workers/rollout/vllm_rollout/vllm_rollout.py dependency-wiring

移除了 MoE 展开的旧函数和调用,是重构的主要受益方,简化了 weight update 流程。

# verl/workers/rollout/vllm_rollout/vllm_rollout.py
# 删除前 ( 旧代码 ):
# def _should_expand_vllm_moe_params() -> bool: ...
# async def _iter_vllm_compatible_moe_params(weights, model_type): ...
# 在 update_weights 中原条件 :
# if _should_expand_vllm_moe_params() and kwargs.get("peft_config") is None:
# weights = _iter_vllm_compatible_moe_params(weights, self.model_config.hf_config.model_type)
​
# 删除后 : 上述代码全部移除 , weights 直接传给 async_send_weights
verl/workers/engine/fsdp/transformer_impl.py dependency-wiring

在 get_per_tensor_param 方法中引入 unfuse_moe_params 调用,是 FSDP 后端使用新函数的位置。

# verl/workers/engine/fsdp/transformer_impl.py
# 导入 :
from .utils import create_device_mesh, get_sharding_strategy, unfuse_moe_params
​
# 在 get_per_tensor_param 方法中 :
per_tensor_param = (
    (
        name,
        param.to(device, non_blocking=True).full_tensor() if isinstance(param, DTensor) else param,
    )
    for name, param in params.items()
)
# 注意:移除了之前的 .to(torch.bfloat16) 转换,因为现在假设已由外部保证 dtype
per_tensor_param = unfuse_moe_params(per_tensor_param, self.model_config.hf_config.model_type)

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

核心风险在于移除了 _should_expand_vllm_moe_params 中的版本检查(vLLM <= 0.24.0),现在 FSDP engine 中无条件展开 MoE 参数。对于 vLLM > 0.24.0 的版本,直接展开是否仍兼容存在不确定性,需要依赖上游兼容性。另外,从异步生成器变为同步生成器,若调用方期望异步迭代则可能出错,但 FSDP engine 中是同步上下文,此变更安全。影响范围涉及所有使用 FSDP engine 的 rollout(vLLM / SGLang),MoE 权重同步路径成为统一代码。

影响所有使用 FSDP engine 和 MoE 模型的训练工作流。权重同步阶段的参数格式发生了变化,从 rollout 层处理变为 engine 层统一处理,对用户透明,但需要确保测试覆盖 MoE+非MoE 场景。CI 中多个测试文件被连带修改,表明该变更触发了多个下游测试的适配。

移除版本检查 同步 / 异步语义变化 核心权重路径

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论