Prhub

#49096 Fix Humming non-gated MoE

原始 PR 作者 netanel-haber 合并时间 2026-07-28 06:56 文件变更 3 提交数 7 评论 3 代码增减 +111 / -7

执行摘要

修复 Humming MoE 非门控专家形状假设

支持 NemotronH 等使用非门控 squared-ReLU 专家的模型在 Humming MoE 后端上运行。此前 Humming 假设每个 w13 包含两个门控投影,对非门控激活产生错误的权重和缓冲区形状。

本 PR 值得精读,因为它展示了如何在已有的门控假设后端中安全地引入非门控支持,通过 is_gated 属性做条件分支,并配套形状契约测试确保正确性。对于 MoE 量化后端的开发者具有参考价值。

讨论亮点
  • amirkl94fused_humming_moe.py 中询问为什么 moe_problem_size 不能继续使用 meta1.shape_n // 2。作者 netanel-haber 回应 Humming 后端实际不使用该返回值,但增加了注释说明其表示逻辑中间宽度,对门控和非门控均适用。
  • amirkl94humming_utils.py 中评论 "Redundant",指向新增的 intermediate_size 临时变量。作者未进一步回复,但最终合并,推测已 inline 解决。

实现拆解

  1. 在 humming_utils.py 中根据激活类型调整 w13 堆叠逻辑:将 _convert_sublayer_to_humming 中的无条件堆叠拆分为 w13is_gated 时才拆成两半。同时修正 convert_to_humming_moe_kernel_format 中的 sublayer_configs,使 w13shape_n 根据 is_gatedintermediate_sizeintermediate_size*2w2shape_k 固定为 intermediate_size 而不是原先非门控时的翻转。
  2. 在 fused_humming_moe.py 中统一问题大小计算:将 moe_problem_size 返回的中间维度从 meta1.shape_n // 2 改为 self.layer.intermediate_size_per_partition,因为该值表示逻辑中间宽度,对门控和非门控一致。get_buffer_metas 中引入 gate_up_size 变量,门控时为 N*2,非门控时为 N*1down_input_size 固定为 N,不再依赖是否门控,确保缓冲区形状正确。
  3. 新增形状契约测试:在 tests/kernels/moe/test_moe.py 中添加 test_humming_gated_non_gated_shape_contract,使用参数化的 MoEActivation.SILU(门控)和 MoEActivation.RELU2_NO_MUL(非门控),构造虚拟 MoE 层并验证转换后的 meta 形状、缓冲区形状、moe_problem_size 返回值是否符合预期。
文件 模块 状态 重要度
vllm/model_executor/layers/fused_moe/experts/fused_humming_moe.py MoE 专家层 modified 6.8
tests/kernels/moe/test_moe.py MoE 测试 modified 6.51
vllm/model_executor/layers/quantization/utils/humming_utils.py 量化工具 modified 6.34

关键符号

moe_problem_size get_buffer_metas _convert_sublayer_to_humming convert_to_humming_moe_kernel_format test_humming_gated_non_gated_shape_contract

关键源码片段

vllm/model_executor/layers/fused_moe/experts/fused_humming_moe.py core-logic

核心计算路径,修改了问题大小计算和缓冲区 meta 定义,直接决定非门控激活的正确性

    def moe_problem_size(
        self,
        a1: torch.Tensor,
        w1: torch.Tensor,
        w2: torch.Tensor,
        topk_ids: torch.Tensor,
    ) -> tuple[int, int, int, int, int]:
        from vllm.utils.humming import HummingLayerMeta
​
        meta1: HummingLayerMeta = self.layer.humming_metas["w13"]
        meta2: HummingLayerMeta = self.layer.humming_metas["w2"]
​
        assert meta1.num_experts == meta2.num_experts
​
        num_experts = meta1.num_experts
        top_k = topk_ids.size(1)
        assert w1.size(0) == num_experts
        assert w2.size(0) == num_experts
​
        if not self.is_batched():
            num_tokens = a1.size(0)
            assert topk_ids.size(0) == num_tokens
        else:
            assert a1.dim() == 3
            assert a1.size(0) == num_experts
            num_tokens = a1.size(1)
​
        return (
            meta1.num_experts,
            num_tokens,
            # 逻辑中间宽度:之前是 meta1.shape_n // 2,但 shape_n 对于非门控可能已是 intermediate_size
            self.layer.intermediate_size_per_partition,
            meta1.shape_k,
            top_k,
        )
​
    def get_buffer_metas(self, M: int, topk: int, activation: MoEActivation):
        from vllm.utils.humming import GemmType as HummingGemmType
        from vllm.utils.humming import dtypes
​
        num_experts = self.num_experts
        N = self.layer.intermediate_size_per_partition
        K = self.layer.hidden_size
​
        # ... 其他不变
​
        gate_up_size = N * (2 if activation.is_gated else 1) # 门控时为 2N,非门控时为 N
        down_input_size = N # 固定为 N,之前非门控时错误地设为 2N
​
        buffer_metas = {
            "quanted_gate_up_input": {
                "shape": (input_shape_m, K),
                "dtype": torch_dtype_map[a_dtype],
            },
            "gate_up_output": {
                "shape": (real_shape_m, gate_up_size), # 之前固定 N*2
                "dtype": torch_dtype_map[c_dtype],
            },
            "activation_output": {
                "shape": (real_shape_m, down_input_size), # 之前依赖 is_gated
                "dtype": torch_dtype_map[c_dtype],
            },
            # ... 其他缓冲区
        }
        return buffer_metas
tests/kernels/moe/test_moe.py test-coverage

新增形状契约测试,确保门控与非门控场景的 meta 形状、缓冲区形状、问题大小均正确

@pytest.mark.parametrize(
    "activation",
    [
        MoEActivation.SILU,
        MoEActivation.RELU2_NO_MUL,
    ],
    ids=["gated", "non_gated"],
)
def test_humming_gated_non_gated_shape_contract(activation: MoEActivation):
    pytest.importorskip("humming")
    from vllm.model_executor.layers.fused_moe.experts.fused_humming_moe import (
        HummingIndexedExperts,
    )
    from vllm.model_executor.layers.quantization.utils import humming_utils
    from vllm.utils import humming
​
    top_k, num_experts = 6, 12
    hidden_size, intermediate_size = 2688, 1856
    # 根据激活类型计算 gate_up_size 和 num_w13_stacks
    gate_up_size = intermediate_size * 2 if activation.is_gated else intermediate_size
    num_w13_stacks = 2 if activation.is_gated else 1
​
    # 创建虚拟 MoE 配置和层
    moe_config = make_dummy_moe_config(
        num_experts=num_experts,
        experts_per_token=top_k,
        hidden_dim=hidden_size,
        intermediate_size=intermediate_size,
        activation=activation,
    )
    layer = torch.nn.Module()
    layer.moe_config = moe_config
    layer.params_dtype = torch.bfloat16
​
    # 注册权重参数
    weight_schema = humming.ModeloptNvfp4WeightSchema()
    for sublayer_name, shape_n, shape_k, stack_size in (
        ("w13", gate_up_size, hidden_size, num_w13_stacks),
        ("w2", hidden_size, intermediate_size, 1),
    ):
        tensor_attrs = weight_schema.get_tensors_attrs(
            shape_n=shape_n,
            shape_k=shape_k,
            param_dtype=layer.params_dtype,
            num_experts=num_experts,
            stack_size=stack_size,
        )
        for tensor_name, attrs in tensor_attrs.items():
            layer.register_parameter(
                f"{sublayer_name}_{tensor_name}",
                Parameter(torch.ones(attrs["shape"], dtype=attrs["dtype"], device="cuda"), requires_grad=False),
            )
​
    # 转换为 Humming 格式
    humming_utils.convert_to_humming_moe_kernel_format(
        layer,
        weight_schema=weight_schema,
        input_schema=humming.HummingInputSchema(a_dtype=humming.dtypes.bfloat16),
    )
​
    # 验证 meta 形状
    w13_meta, w2_meta = (layer.humming_metas[name] for name in ("w13", "w2"))
    assert w13_meta.shape_n - w13_meta.pad_shape_n == gate_up_size
    assert w2_meta.shape_k - w2_meta.pad_shape_k == intermediate_size
​
    # 创建专家模块并验证缓冲区形状
    layer.local_num_experts = layer.global_num_experts = num_experts
    layer.hidden_size = hidden_size
    layer.intermediate_size_per_partition = intermediate_size
    quant_config = humming_utils.get_humming_moe_quant_config(layer)
    experts = HummingIndexedExperts(layer, moe_config, quant_config)
​
    buffer_metas, _ = experts.get_buffer_metas(
        M=1, topk=top_k, activation=moe_config.activation,
    )
    assert buffer_metas["gate_up_output"]["shape"][-1] == gate_up_size
    assert buffer_metas["activation_output"]["shape"][-1] == intermediate_size
    assert experts.moe_problem_size(
        a1=torch.empty(1, hidden_size),
        w1=torch.empty(num_experts, 1),
        w2=torch.empty(num_experts, 1),
        topk_ids=torch.empty(1, top_k, dtype=torch.long),
    ) == (num_experts, 1, intermediate_size, hidden_size, top_k)
vllm/model_executor/layers/quantization/utils/humming_utils.py data-contract

权重转换格式逻辑,修改 w13 堆叠条件和非门控时 sublayer_configs 的 shape_n 和 shape_k

def _convert_sublayer_to_humming(
    layer: "RoutedExperts",
    sublayer_name: str,
    shape_n: int,
    shape_k: int,
    weight_schema: Any,
    input_schema: Any,
    num_experts: int,
    param_dtype: torch.dtype,
) -> tuple[Any, Any]:
    # ... 前置代码
​
    shape_k_stacks = [shape_k]
    shape_n_stacks = [shape_n]
    # 仅当是 w13 且激活为门控时才拆分为两个堆叠
    if sublayer_name == "w13" and layer.moe_config.activation.is_gated:
        shape_n_stacks = [shape_n // 2] * 2
​
    # 后续转换代码不变def convert_to_humming_moe_kernel_format(
    layer: "RoutedExperts",
    weight_schema: Any = None,
    input_schema: Any = None,
    quant_config: Any = None,
    sublayer_configs: Optional[dict] = None,
    force_weight_schema: Any = None,
) -> None:
    # ... 前置代码
​
    # Build sublayer configs from layer properties if not provided
    if sublayer_configs is None:
        is_gated = layer.moe_config.activation.is_gated
        intermediate_size = layer.moe_config.intermediate_size_per_partition
        sublayer_configs = {
            "w13": {
                "shape_n": intermediate_size * (2 if is_gated else 1), # 之前固定 *2
                "shape_k": layer.moe_config.hidden_dim,
            },
            "w2": {
                "shape_n": layer.moe_config.hidden_dim,
                "shape_k": intermediate_size, # 之前非门控时错误地设为 intermediate_size*2
            },
        }
​
    # 后续处理代码不变

评论区精华

moe_problem_size 返回值中的中间宽度 正确性

amirkl94 在 fused_humming_moe.py 中问:'Why can't we keep it as `meta1.shape_n // 2`?' netanel-haber 回应:'Added a comment, although it should be noted the humming doesn't even use this value'

结论:保留使用 intermediate_size_per_partition,添加注释解释 · 已解决

humming_utils.py 中新增 intermediate_size 变量 style

amirkl94 评论 'Redundant',指向新增的 `intermediate_size` 临时变量

结论:最终合并,可能认为可读性提升值得 · 已解决

风险与影响

  • 形状假设变更风险moe_problem_size 和缓冲区定义直接决定 MoE 前向计算的正确性。本 PR 同时修改了三个文件中的形状逻辑,若非门控情况下 is_gated 判断不正确,可能导致静默数值错误。但新增的测试覆盖了两种激活类型,降低了风险。
  • 量化后端兼容性风险:修改了 humming_utils.py 中的转换逻辑,影响将模型权重转换为 Humming 格式的路径。对于已支持的门控模型,需要确保回归测试通过(PR 中已包含门控测试)。
  • 性能影响风险:修复后非门控模型可正确使用 Humming 后端,从性能数据看有显著提升,但未提供回归测试确保门控模型性能不退化。不过改动集中在形状推导,对计算核心不构成直接性能风险。
  • 用户影响:使用 NemotronH 等非门控 MoE 模型的用户现在可以启用 --moe-backend humming 并获得比 Marlin 后端更高的吞吐和更低的首 Token 延迟。
  • 系统影响:仅影响 Humming MoE 后端,不影响其他 MoE 后端(如 Marlin、Triton)。新增测试增加了 CI 时间,但仅在使用 Humming 单元时运行。
  • 团队影响:为后续支持更多非门控激活函数(如 SquaredReLU)奠定了代码结构基础。
核心路径变更 形状计算变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论