Prhub

#46009 [Bugfix][MoE] Preserve unquantized weight storage on ROCm

原始 PR 作者 aaab8b 合并时间 2026-09-03 11:06 文件变更 4 提交数 12 评论 36 代码增减 +315 / -9

执行摘要

修复 ROCm MoE 权重加载时破坏 CUDA 图捕获地址的问题,确保 RL 训练中权重更新后图回放正确。

在 RL 风格的推理和训练循环中,外部权重更新会触发 process_weights_after_loading() 多次调用。重新赋值 param.data 会替换底层参数存储,这对 CUDA 图或运行时内核已捕获存储地址的场景是不安全的。作者在训练时观察到,权重更新后的 rollout 出现损坏,生成无效响应且奖励/分数崩溃为 0。核心需求是保持已捕获的参数存储地址稳定,同时仍能应用必要的填充布局。

该 PR 值得精读。它修复了一个在生产级 RL 训练中可能出现的严重正确性 bug,且修复方案简洁、精准(仅将 = 改为 .copy_())。PR 提供了清晰的动机、详细的本地验证数据(包括基准测试结果)以及一个设计良好的专项回归测试。讨论过程展示了作者根据审阅意见迭代改进测试的过程。核心的技术洞察——维护 CUDA 图捕获的参数存储地址的稳定性——对 AI 框架的开发者具有普遍参考价值。建议关注其测试方法(如何直接测试层边界问题)和 .copy_() 作为维护张量身份(identity)的惯用模式。

讨论亮点

Review 讨论主要围绕测试编写质量和 CI 运行展开。

  • 测试必要性质疑:审阅者 AndreasKaratzas 明确表示 “I think this test is badly written. I don't even know where to start here.” 并多次质疑测试中环境变量设置(如 VLLM_ROCM_USE_AITER_UNIFIED_ATTENTION)和代码结构(“What is this? Why do we need all of that?”)。
  • CI 运行问题:审阅者 tjtanaa 最初发现测试被跳过(“This test doesn't seem to run”),并指出应检查 HIP_VISIBLE_DEVICES 而非即将弃用的 CUDA_VISIBLE_DEVICES,同时强调测试必须在 AMD CI 中实际运行而非被跳过(“We should make sure that this is picked up by AMD CI”)。
  • 作者的迭代与澄清:作者 aaab8b 回应并进行了多轮重写:a) 解释了环境变量用于切换 TRITON 和 AITER 后端测试;b) 将默认 TP 大小从过大调整为更实际的 TP2;c) 最终简化了测试,移除了不必要的 LLM 初始化代码和多余的环境设置。
  • 核心修复的确认:关于 oracle/unquantized.py 中添加的 AITER 提前返回,作者解释这是基于端到端的性能测量结果,AITER 后端需要特定填充以获得轻微加速。该修改未引起争议。

实现拆解

  1. 核心修复(数据契约变更):修改 vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py 中的 process_weights_after_loading 方法。将对 layer.w13_weight.data 和 layer.w2_weight.data 的直接赋值(=)操作改为原地复制(.copy_())。这是修复的关键,确保填充操作在原张量存储上进行,而非指向新分配的存储。
  2. AITER 后端优化:修改 vllm/model_executor/layers/fused_moe/oracle/unquantized.py 中的 convert_to_unquantized_kernel_format 函数。为 AITER 后端在 shuffle_weights() 操作后立即添加 return,避免其返回的张量进入后续可能执行的 .contiguous() 转换,这在 AITER 已经产出运行时布局的情况下是多余的,并可能带来轻微性能损耗。
  3. 新增专项测试:添加 tests/rocm/test_moe_weight_replay.py 文件。该测试直接构造 MoE 层,模拟权重加载、CUDA 图捕获、sleep/wake 语义下的权重重放,然后验证:a) 参数存储地址(data_ptr)在重放后保持不变;b) CUDA 图回放结果在重放前后保持确定性。测试覆盖了 TRITON 和 AITER 两种后端。
  4. CI 配置更新:在 .buildkite/test-amd.yaml 中,将新的测试文件 tests/rocm/test_moe_weight_replay.py 添加到“MI300 Quantization”测试组的执行命令和源文件依赖中,确保其在 AMD CI 中被运行。
文件 模块 状态 重要度
vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py MoE 层 modified 6.64
tests/rocm/test_moe_weight_replay.py MoE 权重回放 added 7.76
vllm/model_executor/layers/fused_moe/oracle/unquantized.py MoE 后端 modified 4.79
.buildkite/test-amd.yaml CI 配置 modified 3.08

关键符号

process_weights_after_loading convert_to_unquantized_kernel_format test_moe_cuda_graph_replay_after_sleep_wake_weight_update

关键源码片段

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

核心 bug 修复所在。将 `process_weights_after_loading` 中的权重填充操作从重新绑定 `.data` 改为原地复制 `.copy_()`,是维持 CUDA 图捕获地址稳定的关键。

# vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py
    def process_weights_after_loading(self, layer: "RoutedExperts") -> None:
        super().process_weights_after_loading(layer)
​
        # 修复前:直接赋值 `.data` 会指向新的存储,破坏已捕获的地址。
        # layer.w13_weight.data = self._maybe_pad_weight(layer.w13_weight.data)
        # layer.w2_weight.data = self._maybe_pad_weight(layer.w2_weight.data)
​
        # 修复后:将填充后的数据复制回原存储,保持 `data_ptr()` 不变。
        # 这确保了 CUDA 图或运行时内核捕获的设备地址仍然有效。
        layer.w13_weight.data.copy_(self._maybe_pad_weight(layer.w13_weight.data))
        layer.w2_weight.data.copy_(self._maybe_pad_weight(layer.w2_weight.data))
tests/rocm/test_moe_weight_replay.py test-coverage

新增的高质量专项测试。直接验证了核心问题:在模拟 sleep/wake 和权重重放后,MoE 参数的存储地址是否稳定,以及 CUDA 图回放结果是否确定。覆盖了 TRITON 和 AITER 两种后端。

# tests/rocm/test_moe_weight_replay.py
    # 测试核心流程:加载 -> 捕获图 -> 模拟 sleep/wake -> 重载权重 -> 验证
    with allocator.use_memory_pool("weights"), torch.device(DEVICE):
        layer = _make_routed_experts()
        _load_expert_weights(layer, seed=1)
        _process_weights_after_loading(layer) # 应用填充和后处理
    _assert_backend(layer, backend)
​
    x, topk_weights, topk_ids = _make_static_inputs()
    graph, graph_output = _capture_forward_graph(layer, x, topk_weights, topk_ids)
    captured_pointers = _weight_pointers(layer) # 记录初始地址
​
    # 模拟 sleep/wake:释放并重新分配内存,然后重载相同权重
    allocator.sleep(offload_tags=tuple())
    allocator.wake_up()
    _load_expert_weights(layer, seed=1)
    _process_weights_after_loading(layer) # 再次执行填充
​
    # 关键断言:重载后地址必须不变
    assert _weight_pointers(layer) == captured_pointers
​
    graph.replay()
    torch.accelerator.synchronize()
    # 关键断言:图回放结果必须确定
    torch.testing.assert_close(graph_output, first_graph_output, rtol=2e-2, atol=2e-2)
vllm/model_executor/layers/fused_moe/oracle/unquantized.py performance

为 AITER 后端优化。在 `shuffle_weights()` 后添加提前返回,避免后续不必要的 `.contiguous()` 转换,这是一个基于性能测量的优化。

# vllm/model_executor/layers/fused_moe/oracle/unquantized.py
    def convert_to_unquantized_kernel_format(...):
        if unquantized_backend == UnquantizedMoeBackend.AITER:
            w13_weight, w2_weight = rocm_aiter_ops.shuffle_weights(w13_weight, w2_weight)
            w13_weight.is_shuffled = True
            w2_weight.is_shuffled = True
            # 新增:AITER shuffle 后立即返回,布局已是运行时所需,无需后续转换。
            return w13_weight, w2_weight
        # ... 其他后端处理逻辑 ...

评论区精华

测试编写质量与必要性 测试

审阅者 AndreasKaratzas 初始认为测试编写不佳('I think this test is badly written'),并质疑其中复杂的环境变量设置和代码结构('What is going on here?','Why do we need all of that?')。作者 aaab8b 解释了环境变量用于切换后端测试,并在多轮迭代中重写了测试,移除了不必要的代码。

结论:测试经过重写,变得更为简洁和直接,最终获得了审阅者的认可。 · 已解决

测试的 CI 集成与运行 测试

审阅者 tjtanaa 发现测试在本地被跳过,并指出需要确保测试在 AMD CI 中被实际执行('We should make sure that this is picked up by AMD CI and is running rather than being skipped')。讨论还涉及环境变量 `HIP_VISIBLE_DEVICES` 与 `CUDA_VISIBLE_DEVICES` 的使用。

结论:测试文件被正确添加到 `.buildkite/test-amd.yaml` 的“MI300 Quantization”组中,确保了其在 CI 环境下的执行。测试跳过条件也得到了解释和验证。 · 已解决

AITER 后端提前返回的性能优化 性能

在 `oracle/unquantized.py` 中为 AITER 后端添加提前返回的修改没有引发争议。作者在评论中解释这是基于端到端性能测量的结果。

结论:修改被接受,作为基于测量的性能优化。 · 已解决

风险与影响

  1. 回归风险(数据契约变更):核心修改将 param.data 的赋值变为 .copy_(),改变了 process_weights_after_loading 的副作用。如果存在其他代码路径依赖于 param.data 被重新绑定(例如某些内存管理或序列化逻辑),可能引入意外行为。但当前分析未发现此类依赖,且修复目标明确,风险可控。
  2. 性能影响:.copy_() 操作比直接赋值增加了内存拷贝开销。但仅在 ROCm 平台、非量化 MoE 的权重填充路径执行,且发生在模型加载阶段而非推理热路径,因此对整体性能影响微乎其微。AITER 的提前返回反而可能带来轻微性能提升。
  3. 平台兼容性:修复针对 ROCm 平台(VLLM_ROCM_MOE_PADDING 环境变量控制)。CUDA 路径不受影响,因为 is_weight_update 标志已控制跳过填充。但修改改变了统一方法的行为,需确保对其他平台(如 XPU、TPU)无副作用。当前代码结构显示,填充逻辑仅在 ROCm 上触发。
  4. 测试覆盖的完整性:新增测试直接验证了核心问题(权重地址稳定性和图回放确定性),并覆盖了 TRITON 和 AITER 两种后端,是一个高质量的回归测试。其局限性在于仅测试了小型 MoE 层,未集成到完整模型推理流程,但作为针对性测试已足够。

直接影响:修复了 ROCm 平台上使用非量化 MoE 模型(如 Qwen3.5-35B)进行 RL 训练(如 GRPO)时,因权重更新后 CUDA 图回放失败导致生成无效、奖励崩溃的关键缺陷。确保了训练循环的正确性和稳定性。
间接影响:

  • 用户体验:对于 ROCm 上使用 MoE 模型进行权重更新的训练用户,将消除一个导致训练中断的严重错误。
  • 系统可靠性:提高了 vLLM 在动态权重更新场景下的鲁棒性,尤其是在 RL 和微调工作流中。
  • 团队流程:新增的 test_moe_weight_replay 测试为类似问题提供了清晰的测试模板,并将其纳入 CI 流程,有助于防止未来回归。
    影响范围:主要局限于 ROCm 平台、非量化 MoE 后端(TRITON 和 AITER),以及涉及 process_weights_after_loading 重复调用的场景(如 sleep/wake 和权重更新)。不影响 CUDA 平台或其他量化后端。
核心数据契约变更 平台特定修复

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论