Prhub

#55579 [Bugfix][MoE] Fix batched CUTLASS workspace overallocation

原始 PR 作者 itayalroy 合并时间 2026-09-10 23:33 文件变更 1 提交数 2 评论 3 代码增减 +2 / -4

执行摘要

修复 batched CUTLASS MoE workspace 重复乘 DP 的显存超配

PR body 明确说明问题根源:“Batched CUTLASS receives one token buffer per local expert. Its M dimension is already the total token capacity from all dispatch ranks: M = max_tokens_per_rank * num_dispatchers”,而“The kernels reserve M token slots per expert, but workspace sizing multiplied M by num_dispatchers again. This counted the dispatch ranks twice and allocated memory that the kernels never use.”。后果是显存被大量无效预留(“For DeepSeek-V3 at DP 32 with 16 local experts, this reduces the shared workspace from 112 GiB to 3.5 GiB per GPU”),直接限制 EP+DP 规模并可能触发 OOM,因此需要把两个 workspace 都改为直接由 M 定尺。

值得精读,但阅读成本极低(单文件 +2/-4),重点不是 diff 本身而是它背后的语义:FusedMoEActivationFormat.BatchedExperts 下 M 已经是 max_tokens_per_rank * num_dispatchers 的全局容量,因此 workspace 的 token 维度不能再乘 num_dispatchers。建议关注三点:一是 assert num_dp is not None 被移除后是否还有别处依赖该断言做早期校验;二是 output 与两个 workspace 现在尺寸语义一致是否可写成注释/文档固化;三是强烈建议补一个 workspace_shapes 的参数化单测(覆盖不同 DP、local experts、activation)把内存契约锁住,避免这类“显存放大约 32 倍”的静默 bug 再次出现。

讨论亮点

本次 review 讨论非常短,呈“无争议快速合并”形态:

  • SageMoore 直接给出正面评价并 approve:“Nice find @itayalroy”;tlrmchlsmth 同样 approve 并最终合并(merged_by 为 tlrmchlsmth)。没有出现对 M 语义、num_dispatchers 是否可安全移除的质疑。
  • 唯一带信息量的流程性评论来自 claude[bot]:“This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.”,CodeRabbit 也“No actionable comments were generated”,因此合并前没有自动化深度审查兜底。
  • tlrmchlsmth 以 /ci run 触发了 Buildkite CI #88132,这是本 PR 唯一的机器验证。

结论:正确性判断完全建立在作者对 M 语义的解释与两位维护者的经验认可之上,没有留下未解决的争议,但也没有形成可回归的测试约束。

实现拆解

  1. 变更入口:唯一改动文件是 vllm/model_executor/layers/fused_moe/experts/cutlass_moe.py,核心符号为 CutlassBatchedExpertsFp8.workspace_shapes。该方法是 batched CUTLASS FP8 专家实现向 MoE runner 申报 (workspace1, workspace2, output) 形状的唯一出口,返回值由上层一次性分配共享 workspace,因此尺寸公式直接决定每 GPU 显存占用。

  2. 定位重复计数:在 FusedMoEActivationFormat.BatchedExperts 语义下,每个本地专家只持有 m 份 token buffer,传入的 M 已经是跨所有 dispatch rank 的总容量 max_tokens_per_rank * num_dispatchers,kernel 每个专家最多写 M 个 token 槽。原实现先取 num_dp = self.num_dispatchers 并通过 assert num_dp is not None 强制其存在,再把 workspace1、workspace2 的第二维写成 M * num_dp,等于把 dispatch rank 重复计入一次。

  3. 核心改动:删除 num_dp 取值与随之失效的断言,将两个 workspace 的第二维统一改回 M;output = (experts_per_worker, M, K) 原本就没有乘 num_dp,三者因此语义一致。附带效果是 batched 路径不再硬性依赖 num_dispatchers 非空,放宽了配置约束。

  4. 行为与性能影响:workspace1(W1/gate-up GEMM 输出缓冲)与 workspace2(activation 后 W2/down GEMM 输入缓冲)各缩小 num_dispatchers 倍,按 PR body 在 DP 32 场景为 112 GiB → 3.5 GiB。shared workspace 是一次性分配的预留显存,缩小它意味着更多显存可留给 KV cache、更大的 DP/EP 配置或更高的并发度。

  5. 测试与配套:没有新增或修改任何测试,也没有配置、schema、文档改动;验证仅依赖 reviewer 手动触发的 Buildkite CI(#88132),未针对 workspace_shapes 的尺寸契约补充单测或显存回归断言。

文件 模块 状态 重要度
vllm/model_executor/layers/fused_moe/experts/cutlass_moe.py 专家计算 modified 5.8

关键符号

CutlassBatchedExpertsFp8.workspace_shapes

关键源码片段

vllm/model_executor/layers/fused_moe/experts/cutlass_moe.py data-contract

唯一改动文件:`CutlassBatchedExpertsFp8.workspace_shapes` 决定 batched CUTLASS FP8 专家的共享 workspace 尺寸,本 PR 在此删除重复的 `num_dispatchers` 因子,从而把 DP 32 场景的共享 workspace 从 112 GiB 降到 3.5 GiB/GPU。

class CutlassBatchedExpertsFp8(CutlassExpertsFp8Base):
    """Batched CUTLASS FP8 fused MoE expert implementation."""
​
    @staticmethod
    def activation_format() -> mk.FusedMoEActivationFormat:
        # 只有 BatchedExperts 格式才会走“每个本地专家一份 token buffer”的 workspace 语义,
        # 正是这个语义决定了下面 M 已经是全局 token 容量。
        return mk.FusedMoEActivationFormat.BatchedExperts
​
    def workspace_shapes(
        self,
        M: int,
        N: int,
        K: int,
        topk: int,
        global_num_experts: int,
        local_num_experts: int,
        expert_tokens_meta: mk.ExpertTokensMetadata | None,
        activation: MoEActivation,
    ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
        # 关键契约:进入这里的 M 已经是“来自所有 dispatch rank 的总 token 容量”,
        # 即 M = max_tokens_per_rank * num_dispatchers,kernel 每个专家最多只写 M 个 token 槽。
        # 修复前此处还有 num_dp = self.num_dispatchers(含 assert num_dp is not None),
        # 并把两个 workspace 的第二维写成 M * num_dp,等于把 dispatch rank 重复计入一次,
        # 在 DP 32 场景下把 workspace 放大 32 倍(112 GiB -> 3.5 GiB)。
        experts_per_worker = self.moe_config.num_local_experts
        activation_out_dim = self.adjust_N_for_activation(N, activation)
​
        # workspace1:W1(gate/up)GEMM 的输出缓冲,token 维直接取 M。
        workspace1 = (experts_per_worker, M, max(N, K))
​
        # workspace2:activation 之后 W2(down)GEMM 的输入缓冲,token 维同样取 M,
        # 不再乘 num_dispatchers,避免第二次放大。
        workspace2 = (
            experts_per_worker,
            M,
            max(activation_out_dim, K),
        )
​
        # output 一直是 (experts_per_worker, M, K),其 token 维从未乘 num_dispatchers,
        # 这也是“M 不应再乘 num_dp”的旁证:三者现在与 kernel 实际使用量完全一致。
        output = (experts_per_worker, M, K)
        return (workspace1, workspace2, output)

评论区精华

workspace 尺寸重复计入 dispatch rank 的定位确认 正确性

PR 描述论证 `M` 已等于 `max_tokens_per_rank * num_dispatchers`,kernel 每个本地专家只保留 `M` 个 token 槽,因此 `workspace_shapes` 再乘 `num_dp` 属于重复计数;SageMoore 评论“Nice find @itayalroy”并 approve,tlrmchlsmth 亦 approve 并完成合并。

结论:两位维护者认可该正确性判断,改动按原方案合入;无人提出对 `M` 语义或删除 `assert num_dp is not None` 的反例。 · 已解决

fork PR 跳过自动审查,缺少针对 workspace 契约的回归测试 测试

claude[bot] 提示“This pull request is from a fork — automated review is disabled.”,CodeRabbit 也“No actionable comments were generated”;合并前的机器验证只有 tlrmchlsmth 手动触发的 `/ci run`(Buildkite CI #88132),而改动本身没有任何测试文件。

结论:未形成对 `workspace_shapes` 尺寸契约的单测或显存断言,测试缺口仍处于未解决状态,后续若被误改回 `M * num_dp` 不会有测试失败。 · unresolved

风险与影响

  1. 契约依赖风险(正确性):整条修复依赖“M 在 BatchedExperts 激活格式下已是全局 token 容量”这一隐含契约。如果某些调用路径(例如非 EP/DP 组合、或 PCP/DCP 等并行场景)传入的 M 并非该语义,按 M 分配会小于 kernel 实际写入量,可能导致越界写或结果错乱——而 output 仍按 (experts_per_worker, M, K) 分配,说明该契约在输出侧是自洽的,但缺少单测锁定。
  2. 缺少测试覆盖:+2/-4 的改动没有任何测试变更,CI 只跑通用用例;显存降低 32 倍这种量级变化无法被现有测试发现,若后续被误改回 M * num_dp 也不会失败。
  3. 约束放宽的副作用:删除 assert num_dp is not None 后,num_dispatchers 未设置的场景不再得到显式报错,问题可能从“早失败”变成“晚出现”的形状/显存异常。
  4. 回归范围有限:workspace_shapes 只影响 batched CUTLASS FP8 专家路径(CutlassBatchedExpertsFp8),不影响非 batched 的 CutlassExpertsFp8Base.workspace_shapes,因此其他 MoE 后端风险很低。

用户侧:在 NVIDIA 平台上以 EP+DP 方式部署 FP8 MoE(如 DeepSeek 系列风格模型、DP 32 + 每卡 16 local experts)时,每 GPU 共享 workspace 从 112 GiB 降到 3.5 GiB,直接消除一类 OOM 或显存无效预留问题,使更大的 DP/EP 配置、更大的 KV cache 或更高并发成为可能;对吞吐是间接利好,对正确性应无变化。

系统侧:只影响 batched experts 的共享 workspace 申报尺寸,不改变 dispatch/combine 通信、算子选择或数值路径,属于分配器层面的修正;同时松开了 batched 路径对 num_dispatchers 必须存在的依赖。

团队侧:这是一次“内存分配契约错误未被测试捕获”的提醒,暴露了 workspace_shapes 语义(M 是全局容量还是单 rank 容量)缺少文档与单元测试约束,建议后续在基类或注释中固化该契约。

核心 MoE 路径变更 缺少测试覆盖 显存分配契约变更 多卡 DP 语义依赖 早失败断言被移除

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论