Prhub

#40390 [Bugfix][Rocm]Aiter MoE re-uses existing tensor addresses after weight update.

原始 PR 作者 yuankaichen-amd 合并时间 2026-05-06 18:32 文件变更 2 提交数 11 评论 14 代码增减 +59 / -18

执行摘要

AMD Aiter MoE 保留 tensor 地址以支持权重更新

The AMD aiter FusedMoE requires model weights be shuffled before use. When using this feature in RL scenario, however, the MoE weights will be re-shuffled time and again whenever a weight update is carried out. The issue is that the weight shuffle should NOT change the tensor addresses -- otherwise captured CUDA graph will not be able to use the new weights.

值得精读。本 PR 展示了一种通用模式:在权重更新场景下通过原地复制保留 tensor 地址以兼容 CUDA Graph。replace_parameterprefer_copy 参数可作为后续其他后端处理权重更新的参考。此外,对 moe_kernel 初始化状态的判断和内核重建的跳过逻辑也值得关注。

讨论亮点
  • 重构建议:yzong-rh 建议将 prefer_copy 逻辑内迁到 replace_parameter 函数中,使 MoE 方法调用更简洁。作者采纳并实现。
  • 地址保留完整性:gemini-code-assist[bot] 指出 process_weights_after_loading 中的 .data 赋值可能破坏地址保留。作者解释 ROCm 的 _maybe_pad_weight 是幂等的(后续调用返回原张量),并声明 XPU 路径不在本 PR 范围内,由 Intel 团队维护。
  • 冗余初始化:bnellnm 指出 __init__ 中显式设置 self.moe_kernel = None 是冗余的(基类已做),作者随后移除。
  • 权重更新触发路径:bnellnm 询问 _setup_kernel 何时被多次调用,作者说明 RL 场景中 process_weights_after_loading 会因权重更新被重新触发。

实现拆解

  1. 增强 replace_parameter:在 vllm/model_executor/utils.py 中为该函数新增 prefer_copy 布尔参数。当 prefer_copy=True 且旧参数与 new_data 的 shape、dtype、device 完全一致时,直接调用 old_param.copy_(new_data) 原地复制,保留存储地址;否则按原路径创建新 Parameter
  2. 检测权重更新:在 UnquantizedFusedMoEMethod._setup_kernelvllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py)中,将权重 shuffle 后的赋值从无条件调用 replace_parameter 改为通过 self.moe_kernel is not None 判断是否为权重更新。首次加载时 moe_kernelNoneprefer_copy=False,正常替换;后续权重更新时 prefer_copy=True,触发原地复制。
  3. 避免内核重复初始化:将 make_unquantized_moe_kernel 的调用包裹在 if not is_weight_update: 条件内,仅在首次加载时构建内核。同时增加注释说明 _maybe_pad_weight 在第二次调用时因 stride 条件不满足而返回原张量,从而 .data 赋值成为空操作。
  4. 清理冗余代码:移除 __init__ 中重复的 self.moe_kernel = None 赋值(基类已处理)。
  5. 验证方式:通过设置环境变量 VLLM_ROCM_USE_AITER_MOE=1 在 veRL + vLLM 的 RL 环境中测试,修复前输出乱码,修复后正确。
文件 模块 状态 重要度
vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py MoE 方法 modified 6.95
vllm/model_executor/utils.py 工具函数 modified 6.42

关键符号

_setup_kernel process_weights_after_loading replace_parameter

关键源码片段

vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py core-logic

核心 MoE 权重处理逻辑,修改 _setup_kernel 和 process_weights_after_loading 以支持权重更新时保留 tensor 地址

def _setup_kernel(
    self,
    layer: Module,
    w13: torch.Tensor,
    w2: torch.Tensor,
) -> None:
    # 将权重 shuffle 到运行时格式
    w13_new, w2_new = convert_to_unquantized_kernel_format(
        self.unquantized_backend,
        layer=layer,
        w13_weight=w13,
        w2_weight=w2,
    )
    # moe_kernel 在基类 __init__ 中被初始化为 None;
    # 首次调用时正常替换参数;后续调用(例如 RL 权重更新
    # 重新触发 process_weights_after_loading)时,
    # moe kernel 已设置,且 CUDA 图可能捕获了参数地址,
    # 因此将 shuffle 后的数据复制到现有存储中,
    # 而不是重新注册新的 Parameter。
    is_weight_update = self.moe_kernel is not None # type: ignore[has-type]
    replace_parameter(layer, "w13_weight", w13_new, prefer_copy=is_weight_update)
    replace_parameter(layer, "w2_weight", w2_new, prefer_copy=is_weight_update)
​
    if not is_weight_update:
        # 仅在首次调用时设置 moe kernel。
        self.moe_quant_config = self.get_fused_moe_quant_config(layer)
        assert self.moe_quant_config is not None
        assert self.experts_cls is not None
        self.moe_kernel = make_unquantized_moe_kernel(
            quant_config=self.moe_quant_config,
            moe_config=self.moe,
            backend=self.unquantized_backend,
            experts_cls=self.experts_cls,
            routing_tables=layer._maybe_init_expert_routing_tables(),
            shared_experts=layer.shared_experts,
        )
vllm/model_executor/utils.py data-contract

通用工具函数 replace_parameter 新增 prefer_copy 参数,支持原地复制保留地址

def replace_parameter(
    layer: torch.nn.Module,
    param_name: str,
    new_data: torch.Tensor | None,
    prefer_copy: bool = False,
):
    """
    替换 layer 中的参数,同时保持权重重载能力。
    通常在 process_weights_after_loading 方法中调用。
    不应用于已绑定/共享的权重。    Args:
        layer: 包含要替换参数的层
        param_name: 要替换的参数名
        new_data: 新参数的数据,或者 None 表示将参数设为 None
        prefer_copy: 如果为 True 且现有参数与 new_data 兼容
            (相同 shape, dtype, device),则原地复制 new_data
            到现有参数,而不是重新注册新参数。这保留了参数的
            存储地址(data_ptr),对于权重更新(如 RL 训练循环)
            中保持 CUDA 图有效是必要的。
    """
    if new_data is None:
        setattr(layer, param_name, None)
        return
​
    if isinstance(new_data, torch.nn.Parameter):
        new_data = new_data.data
​
    old_param: torch.nn.Parameter | None = getattr(layer, param_name, None)
​
    # 如果 prefer_copy 且参数兼容,直接原地复制
    if (prefer_copy and old_param is not None
        and old_param.shape == new_data.shape
        and old_param.dtype == new_data.dtype
        and old_param.device == new_data.device):
        old_param.copy_(new_data)
        return
​
    # 否则创建新 Parameter
    new_param = torch.nn.Parameter(new_data, requires_grad=False)
    if old_param is not None and hasattr(old_param, "weight_loader"):
        weight_loader = old_param.weight_loader
        set_weight_attrs(new_param, {"weight_loader": weight_loader})
    setattr(layer, param_name, new_param)

评论区精华

将 prefer_copy 逻辑移至 replace_parameter 设计

yzong-rh 建议在 replace_parameter 中添加 prefer_copy 选项,这样 MoE 方法调用更简洁,且其他调用方也可复用。

结论:作者采纳建议,重构了 replace_parameter 并更新调用处。 · 已解决

地址保留完整性:ROCm padding 和 XPU 路径 正确性

gemini-code-assist[bot] 指出 process_weights_after_loading 中的 .data 赋值会创建新分配,破坏地址保留。

结论:作者解释 ROCm 的 _maybe_pad_weight 是幂等的(后续调用返回原张量),XPU 路径不在本 PR 范围内。 · 已解决

moe_kernel None 初始化冗余 style

bnellnm 指出 __init__ 中显式设置 self.moe_kernel = None 是冗余的,因为基类已做。

结论:作者确认并移除了冗余代码。 · 已解决

风险与影响

  1. XPU 路径未覆盖:gemini-code-assist 指出的 XPU 逻辑(.data 赋值)可能仍会改变地址,但作者将其定为未来工作,当前不影响 ROCm。
  2. 条件兼容性假设prefer_copy 依赖 shape、dtype、device 完全一致,若未来权重更新改变形状或类型,将退回创建新参数,但此场景不合理。
  3. 测试覆盖不足:未包含单元测试,仅依赖集成测试(veRL 环境),可能在边缘情况下出现回归。

正面影响:修复了 AMD ROCm + Aiter MoE 在强化学习场景中权重更新后生成的乱码问题,恢复了 CUDA Graph 捕获的正确性。影响范围限定于使用 VLLM_ROCM_USE_AITER_MOE=1 + enforce_eager=False 的用户,但这类用户此前完全无法正常工作。
负面影响:无已知负面影响,代码修改向后兼容,默认行为不变。

CUDA Graph 地址依赖 XPU 路径未经测试 缺少单元测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论