Prhub

#47106 [Kernel] Support Nvfp4 Cutedsl Moe Swiglu-oai and Relu2(non-gated) Activation

原始 PR 作者 vitamin-chaos 合并时间 2026-08-07 08:42 文件变更 9 提交数 8 评论 8 代码增减 +216 / -14

执行摘要

CuteDSL NVFP4 MoE 新增 SwiGLU-OAI 与 ReLU² 激活支持

PR body 说明 FlashInfer 修改了 CuteDSL NVFP4 MoE kernel 以支持 SwiGLU-OAI 激活(flashinfer-ai/flashinfer#3737),kernel 同时支持 ReLU² 非门控激活,本 PR 的目标是让 vLLM 通过传递激活类型和相关参数与这两个新激活兼容。commit 消息进一步指出,上游 vLLM PR #49580 已落地 ReLU² 路径并开始无条件传递 activation_type,因此本 PR 在保留该路径的基础上重点补齐 SwiGLU-OAI 变体的相关参数透传与权重布局适配。

值得精读。重点看三处设计:一是 apply 中按激活类型构造参数并按需过滤 None 的透传模式,这是避免 kernel 默认值被意外覆盖的通用做法;二是 reorder_w13_to_w31_for_flashinfer_cutedsl 对两种 gated 布局的归一化处理;三是测试中发现的容差陷阱——当参考输出量级接近量化噪声时,需要叠加方向性判别(cosine)才能防止零输出误通过。

讨论亮点

核心 review 交锋发生在 gemm1 参数默认值的处理上:zyongye 在 flashinfer_cutedsl_moe.py 的 __init__ 处指出,作者最初将 quant_config 中为 None 的参数回退为 1.702/1.0/7.0 默认值,这样其他复用该文件的模型会因为这些默认值被应用而引入精度 bug,建议保持 None 并在非 None 时才应用。vitamin-chaos 承认该问题(虽然只在 SwiGLU-OAI 激活下才影响精度)并修改为 None 过滤方案,zyongye 最终批准。

实现拆解

本 PR 按 4 个步骤完成 CuteDSL NVFP4 MoE 后端对 4 类激活的兼容:

  1. 扩展量化配置数据契约:vllm/model_executor/layers/fused_moe/config.py 的 nvfp4_moe_quant_config 新增 gemm1_alpha、gemm1_beta 可选参数并透传给 FusedMoEQuantConfig.make;oracle/nvfp4.py 的 make_nvfp4_moe_quant_config 同步增加 swiglu_alpha、swiglu_beta 透传参数;同时把 FLASHINFER_CUTEDSL 加入 NVFP4_BACKENDS_WITH_CLAMP 集合,使带 SwiGLU clamp 的模型在自动后端选择时不再被排除;quark、compressed_tensors、modelopt 三个量化方法的 get_fused_moe_quant_config 通过 getattr(layer, "swiglu_alpha/beta", None) 读取模型层属性并透传。

  2. w13 权重布局归一化:vllm/model_executor/layers/quantization/utils/flashinfer_fp4_moe.py 新增 reorder_w13_to_w31_for_flashinfer_cutedsl,处理两种 gated 布局——SwiGLU-OAI 的 checkpoint(如 gpt-oss)以 [gate0, up0, gate1, ...] 交错存放,需按奇偶列拆开重拼为 [up; gate] 顺序;其余激活(SILU、SWIGLUOAI_UNINTERLEAVE)为 [gate; up] packed 布局,对半翻转即可。prepare_nvfp4_moe_layer_for_flashinfer_cutedsl 改为调用该函数,保持原有 interleave 逻辑不变。

  3. kernel 激活参数按需透传:vllm/model_executor/layers/fused_moe/experts/flashinfer_cutedsl_moe.py 的 _supports_activation 扩展支持 SWIGLUOAI、SWIGLUOAI_UNINTERLEAVE;__init__ 从 quant_config 读取 gemm1_alpha/beta/clamp_limit;apply 中按激活类型构造 swiglu 参数 dict,None 值过滤后再以 **swiglu_kwargs 传给 flashinfer_cute_dsl_fused_moe_nvfp4,避免把 None 传给内核的 float 类型参数而覆盖其默认值。

  4. 测试配套:新增 tests/kernels/moe/test_flashinfer_cutedsl_layout.py 覆盖 SwiGLU-OAI 交错布局与 packed 布局两种重排;tests/kernels/moe/test_flashinfer_cutedsl_nvfp4_moe.py 参数化为 5 个激活用例(SILU、SILU+clamp、RELU2_NO_MUL、SWIGLUOAI、SWIGLUOAI_UNINTERLEAVE),收紧容差至 atol=3e-2 并新增 cosine similarity > 0.99 判别;原 relu2 专用用例并入参数化体系。

文件 模块 状态 重要度
vllm/model_executor/layers/fused_moe/experts/flashinfer_cutedsl_moe.py MoE 专家 modified 7.02
vllm/model_executor/layers/quantization/utils/flashinfer_fp4_moe.py 量化工具 modified 7.15
tests/kernels/moe/test_flashinfer_cutedsl_nvfp4_moe.py 内核测试 modified 6.95
vllm/model_executor/layers/fused_moe/oracle/nvfp4.py 后端选择 modified 5.58
vllm/model_executor/layers/fused_moe/config.py 配置契约 modified 4.88
tests/kernels/moe/test_flashinfer_cutedsl_layout.py 布局测试 added 5.94
vllm/model_executor/layers/quantization/quark/quark_moe.py 量化方法 modified 4.4
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_w4a4_nvfp4.py 量化方法 modified 4.3
vllm/model_executor/layers/quantization/modelopt.py 量化方法 modified 4.3

关键符号

FlashInferCuteDSLExperts.apply FlashInferCuteDSLExperts._supports_activation reorder_w13_to_w31_for_flashinfer_cutedsl prepare_nvfp4_moe_layer_for_flashinfer_cutedsl make_nvfp4_moe_quant_config nvfp4_moe_quant_config _reference_activation _torch_moe_reference test_flashinfer_cutedsl_fp4_moe

关键源码片段

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

核心实现文件:扩展激活支持列表并通过 quant_config 读取 gemm1 参数,在 apply 中按激活类型构造并过滤 swiglu 参数后透传给 FlashInfer CuteDSL kernel,是激活兼容的主入口。

def apply(self, output, hidden_states, w1, w2, topk_weights, topk_ids,
          activation, global_num_experts, expert_map, a1q_scale, a2_scale,
          workspace13, workspace2, expert_tokens_meta,
          apply_router_weight_on_input):
    # 本后端仅支持 NVFP4 路径,且激活与权重 scale 必须已就位
    assert self.quant_dtype == "nvfp4"
    assert a1q_scale is not None
    assert self.w1_scale is not None
    assert self.w2_scale is not None
​
    # a1q_scale 为 (M, K//16) 的 float8_e4m3fn 张量(来自 fp4_quantize),
    # 函数式 API 要求 x_sf 带 trailing dim: (M, K//16, 1)
    x_sf = a1q_scale.unsqueeze(-1)
​
    # 内核的 swiglu_{alpha,beta,limit} 自带普通 SwiGLU 默认值,
    # 因此只按激活类型转发模型实际设置的参数;SILU 只关心 clamp limit,
    # 而 SwiGLU-OAI 需要 alpha/beta/limit 三者
    swiglu_params: dict[str, float | None] = {}
    if activation == MoEActivation.SILU:
        swiglu_params = {"swiglu_limit": self.gemm1_clamp_limit}
    elif activation in (MoEActivation.SWIGLUOAI,
                        MoEActivation.SWIGLUOAI_UNINTERLEAVE):
        swiglu_params = {
            "swiglu_alpha": self.gemm1_alpha,
            "swiglu_beta": self.gemm1_beta,
            "swiglu_limit": self.gemm1_clamp_limit,
        }
    # 关键防线:未设置的参数必须以 None 过滤掉,
    # 否则把 None 传给内核的 float 类型参数会覆盖其默认值,造成精度偏差
    swiglu_kwargs = {k: v for k, v in swiglu_params.items() if v is not None}
​
    flashinfer_cute_dsl_fused_moe_nvfp4(
        x=hidden_states,
        x_sf=x_sf,
        token_selected_experts=topk_ids.to(torch.int32),
        token_final_scales=topk_weights.float(),
        w1_weight=w1,
        w1_weight_sf=self.w1_scale,
        w1_alpha=self.g1_alphas,
        fc2_input_scale=self.a2_gscale,
        w2_weight=w2,
        w2_weight_sf=self.w2_scale,
        w2_alpha=self.g2_alphas,
        num_experts=self.global_num_experts,
        top_k=self.topk,
        num_local_experts=self.local_num_experts,
        local_expert_offset=self.local_expert_offset,
        moe_output=output,
        activation_type=activation_to_flashinfer_int(activation),
        **swiglu_kwargs,
    )
vllm/model_executor/layers/quantization/utils/flashinfer_fp4_moe.py core-logic

新增 reorder_w13_to_w31_for_flashinfer_cutedsl,区分 SwiGLU-OAI 交错布局与 packed 布局,是权重正确性的关键前置处理。

def reorder_w13_to_w31_for_flashinfer_cutedsl(
    activation: MoEActivation,
    w13: torch.Tensor,
    w13_scale: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
    """把门控 w13 行顺序归一化为 FlashInfer 要求的 [up; gate]。"""
    if activation == MoEActivation.SWIGLUOAI:
        # gpt-oss 这类 checkpoint 的 w13 以 [gate0, up0, gate1, ...] 交错存放,
        # 必须按奇偶列拆开再拼接成 [up; gate],
        # 否则门控与线性投影互换会静默产生错误结果
        gate, up = w13[:, 0::2], w13[:, 1::2]
        gate_scale, up_scale = w13_scale[:, 0::2], w13_scale[:, 1::2]
        return (
            torch.cat([up, gate], dim=1).contiguous(),
            torch.cat([up_scale, gate_scale], dim=1).contiguous(),
        )
​
    # 其余门控激活(如 SILU、SWIGLUOAI_UNINTERLEAVE)为 [gate; up] packed 布局,
    # 直接对半翻转即可得到 [up; gate]
    half = w13.shape[1] // 2
    return (
        torch.cat([w13[:, half:], w13[:, :half]], dim=1).contiguous(),
        torch.cat([w13_scale[:, half:], w13_scale[:, :half]], dim=1).contiguous(),
    )
tests/kernels/moe/test_flashinfer_cutedsl_nvfp4_moe.py test-coverage

测试核心:参数化 5 个激活用例,收紧容差并加入 cosine 相似度判别,修复了原测试零输出可误通过的问题。

def test_flashinfer_cutedsl_fp4_moe(m, n, k, e, topk, activation, alpha, beta,
                                   limit, dtype, workspace_init):
    # ... 准备隐藏状态、量化 w13/w2、topk 路由与 quant_config...
​
    # 输出量级约 1e-2,而 NVFP4 量化噪声约 1e-3;
    # 若只用绝对容差,即使内核返回全零张量也能通过
    # (零与 1e-2 的差在 atol=3e-2 内)
    torch.testing.assert_close(
        torch_output, cutedsl_output, atol=3e-2, rtol=2e-1)
​
    # 因此比较方向(cosine similarity),一旦 SwiGLU 参数没传进内核、
    # 或 w13 布局排错,方向就会明显偏离
    cosine = torch.nn.functional.cosine_similarity(
        cutedsl_output.flatten().float(),
        torch_output.flatten().float(),
        dim=0,
    )
    assert cosine > 0.99, f"cosine similarity {cosine:.4f} below 0.99"

评论区精华

gemm1 参数默认值处理 正确性

zyongye 指出作者最初把 quant_config 中为 None 的 gemm1_alpha/beta/clamp_limit 回退为 1.702/1.0/7.0 默认值,其他复用该文件的模型会因为默认值被应用而引入精度 bug,建议保持 None 并在非 None 时才应用。

结论:vitamin-chaos 认同并修改为 None 默认 + 仅转发非 None 参数(commit 93f6073),zyongye 批准。 · 已解决

风险与影响

  1. 激活参数透传依赖 None 过滤:swiglu_kwargs 的过滤逻辑是该路径正确性的关键防线,若后续有人直接转发 None 给内核 float 参数,会覆盖内核默认值导致静默精度偏差。
  2. 后端自动选择行为变化:FLASHINFER_CUTEDSL 被加入 NVFP4_BACKENDS_WITH_CLAMP 后,带 swiglu_limit 的模型会自动选到该后端,而 kernel 级测试仅覆盖 SILU-clamp 一个用例,未见模型级 e2e 验证;若 CuteDSL 的 clamp 语义与其他后端不一致会造成精度回归。
  3. 布局假设风险:SwiGLU-OAI 的 interleaved 布局假设来自 gpt-oss checkpoint,若其他模型的 w13 实际存储顺序与此假设不符,会静默产生错误输出(单测仅覆盖固定张量)。
  4. 依赖新版 FlashInfer:需要包含 flashinfer#3737 的版本;commit 119af87 放弃旧版本探测后,SM100 上旧版 FlashInfer 会在 CuteDSL 路径直接报错而非优雅降级。
  5. 该路径仅限 SM100 + NVFP4 + CuteDSL 后端,影响面受硬件与后端选择约束。

用户侧:gpt-oss 等 SwiGLU-OAI 模型的 NVFP4 + CuteDSL MoE 推断从不可用变为可用;ReLU² 非门控激活获得正式测试保障;带 SwiGLU clamp 的模型自动获得该后端支持。系统侧:量化配置数据契约新增 gemm1_alpha/beta 字段,波及 oracle/nvfp4.py、config.py 及 quark、compressed_tensors、modelopt 三个量化方法模块;NVFP4_BACKENDS_WITH_CLAMP 集合变化影响自动后端选择顺序。团队侧:测试中"数值容差 + cosine 相似度"的判别组合对 kernel 测试有推广价值。

激活参数透传依赖 None 过滤 SwiGLU-OAI 布局依赖模型假设 依赖新版 FlashInfer 后端自动选择行为变化(clamp) 仅 SM100 生效

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论