Prhub

#44667 [NVFP4][Emulation] Fuse NVFP4 weight dequantization with compute in triton kernel for w13/w2 MOE MLP linears

原始 PR 作者 fxmarty-amd 合并时间 2026-06-26 10:33 文件变更 3 提交数 17 评论 12 代码增减 +906 / -68

执行摘要

融合 NVFP4 MoE 反量化与计算内核,性能提升可达 10x

AMD MI350 等平台不原生支持 NVFP4 硬件,此前 vLLM 通过 Nvfp4QuantizationEmulationTritonExperts 将全部专家权重反量化为 BF16 后再调用标准 TritonExperts GEMM,中间大张量(w13, w2)严重消耗显存和带宽。PR body 指出 "materializing large intermediate BF16 tensors and adding memory bandwidth pressure",且该实现需要进一步融合以消除开销。

值得精读,尤其关注 Triton 融合反量化与 GEMM 的模式(类似 GPTQ/AWQ 的 fused kernel)、_e2m1_inline 的位运算优化,以及 invoke_fused_moe_nvfp4_emulation_kernel 中如何复用 moe_align_block_sizewrite_zeros_to_output 等现有基础设施。

讨论亮点
  • mgoin 评论指出 benchmark 代码不应混在测试文件中(“I don't think a benchmark should be in a test”),作者随后移除相关部分。
  • mgoin 质疑 LoRA 和偏置检查应在 supports_lora / is_supported_config 静态方法中而非运行时断言,作者随即添加了这两个方法并将检查外移到静态校验。
  • fxmarty-amd 在评论中指出此修复了 PR#42120 引入的 moe_kernel_quantize_input 双重应用问题,因为 fused kernel 绕过了父类 TritonExperts.apply 中的冗余量化。

实现拆解

  1. 新增融合 Triton 内核 (fused_moe_nvfp4_emulation_kernel):位于 vllm/model_executor/layers/fused_moe/experts/nvfp4_emulation_moe.py。内核采用 N-major tile 加载 packed uint8 权重(每字节两个 FP4 值),通过 tl.interleave 拆包反量化后立即送入 tl.dot,避免写入 HBM 中间结果。
  2. 新增 Python 入口函数 (invoke_fused_moe_nvfp4_emulation_kernel):负责参数校验、根据 expert_ids 排序 tokens 并调用 Torch CUDA graph 兼容的 fused kernel,替代原有的 super().apply() 路径。
  3. 修改 apply 方法:将 Nvfp4QuantizationEmulationTritonExperts.apply 从全量反量化 + 调用父类改为直接路由到新的 fused kernel,并添加了 LoRA 及量化配置的运行时检查(supports_lora, is_supported_config)。
  4. 优化 E2M1 解码器 (vllm/model_executor/layers/quantization/utils/nvfp4_emulation_utils.py):将 _e2m1_inline 从二元树分支查找改为直接 IEEE 754 位运算构造(0x3F000000 + (mag << 22)),减少 Triton 中的条件开销。同时修复了 _dequantize_nvfp4_kernelrow_idx 整数溢出 bug(.to(tl.int64))。
  5. 测试配套:在 tests/kernels/quantization/test_nvfp4_emulation.py 中新增 Nvfp4QuantizationEmulationTritonExpertsReference 类作为精确参考实现,并添加 test_nvfp4_moe_correctness 参数化测试,覆盖多种 token 数和 TP 规模,对比 fused 与 unfused 的输出正确性与性能。
文件 模块 状态 重要度
vllm/model_executor/layers/fused_moe/experts/nvfp4_emulation_moe.py MoE 模拟 modified 9.05
tests/kernels/quantization/test_nvfp4_emulation.py 测试 modified 7.24
vllm/model_executor/layers/quantization/utils/nvfp4_emulation_utils.py 量化工具 modified 7.17

关键符号

fused_moe_nvfp4_emulation_kernel invoke_fused_moe_nvfp4_emulation_kernel supports_lora is_supported_config _e2m1_inline Nvfp4QuantizationEmulationTritonExperts.apply

关键源码片段

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

核心文件,新增 `fused_moe_nvfp4_emulation_kernel` Triton JIT 内核及 Python 调度入口 `invoke_fused_moe_nvfp4_emulation_kernel`,重写 `apply` 方法以使用融合计算。

# vllm/model_executor/layers/fused_moe/experts/nvfp4_emulation_moe.py@triton.jit
def fused_moe_nvfp4_emulation_kernel(
    a_ptr, b_ptr, c_ptr,
    b_scale_ptr, w_global_scale_ptr,
    topk_weights_ptr, sorted_token_ids_ptr, expert_ids_ptr,
    num_tokens_post_padded_ptr,
    N: tl.constexpr, K: tl.constexpr,
    EM, num_valid_tokens,
    stride_am, stride_ak, # A [M, K]
    stride_be, stride_bk, stride_bn, # B [E, N, K//2] packed
    stride_cm, stride_cn, # C [M, topk, N]
    stride_bse, stride_bsk, stride_bsn, # B_scale [E, N, K//BLOCK]
    block_k_diviable: tl.constexpr,
    BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr,
    BLOCK_SIZE_K: tl.constexpr, GROUP_SIZE_M: tl.constexpr,
    MUL_ROUTED_WEIGHT: tl.constexpr, top_k: tl.constexpr,
    compute_type: tl.constexpr, group_size: tl.constexpr,
):
    """
    融合 NVFP4 反量化与 MoE GEMM 的 Triton 内核。
    激活值 A 为 BF16(外部已完成量化-反量化循环)。
    权重 B 为 packed uint8 [E, N, K//2],每字节含两个 FP4 值(沿 K 维度打包)。
    B_scale 为每块 FP8-E4M3 缩放系数 [E, N, K//group_size],
    w_global_scale 为每个 expert 的全局标量缩放。
    反量化公式:w_float = e2m1_decode(nibble) * (block_scale_fp8 * global_scale)
    """
    BLOCK_SIZE_K_PACKED: tl.constexpr = BLOCK_SIZE_K // 2
​
    # 计算程序 ID 映射到 C 块的偏移
    pid = tl.program_id(axis=0)
    num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M)
    num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
    num_pid_in_group = GROUP_SIZE_M * num_pid_n
    group_id = pid // num_pid_in_group
    first_pid_m = group_id * GROUP_SIZE_M
    group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
    pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
    pid_n = (pid % num_pid_in_group) // group_size_m
​
    # 跳过无效 token 块
    num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
    if pid_m * BLOCK_SIZE_M >= num_tokens_post_padded:
        return
​
    offs_token_id = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
    offs_token = tl.load(sorted_token_ids_ptr + offs_token_id).to(tl.int64)
    token_mask = offs_token < num_valid_tokens
​
    off_experts = tl.load(expert_ids_ptr + pid_m).to(tl.int64)
    if off_experts == -1:
        # 填充块输出 0
        write_zeros_to_output(...)
        return
​
    # 为当前 expert 加载 packed 权重 [BLOCK_SIZE_N, BLOCK_SIZE_K_PACKED] 并反量化
    offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)) % N
    offs_k = tl.arange(0, BLOCK_SIZE_K)
    offs_k_packed = tl.arange(0, BLOCK_SIZE_K_PACKED)
​
    # 加载 packed 字节(每个字节包含 2 个 FP4)
    packed = tl.load(...) # 形状 [BLOCK_SIZE_N, BLOCK_SIZE_K_PACKED]
​
    low_nibble = packed & 0x0F
    high_nibble = (packed >> 4) & 0x0F
​
    # 使用内联的 E2M1 解码并缩放
    low_val = _e2m1_inline(low_nibble) * scale
    high_val = _e2m1_inline(high_nibble) * scale
​
    # 交错两个 nibble 得到完整 [BLOCK_SIZE_N, BLOCK_SIZE_K],再转置为 K-major 用于 tl.dot
    dequant = tl.interleave(low_val, high_val)
    dequant_t = tl.trans(dequant)
​
    # 加载激活块并执行 GEMM
    a = tl.load(...) # [BLOCK_SIZE_M, BLOCK_SIZE_K]
    c = tl.dot(a, dequant_t, input_precision="ieee", out_dtype=tl.float32)
​
    # 累积到输出
    tl.store(c_ptr + offsets, c, mask=token_mask[:, None])

评论区精华

Benchmark 代码不应出现在测试文件中 测试

mgoin 评论 : "I don't think a benchmark should be in a test :)"

结论:作者随后移除了测试文件中的 benchmark 代码。 · 已解决

LoRA 支持应通过静态检查而非运行时断言 设计

mgoin 指出 LoRA 和偏置配置应在 supports_lora / is_supported_config 静态方法中检查,而不是在 apply 中抛出 NotImplementedError。

结论:作者添加了 supports_lora 和 is_supported_config 静态方法。 · 已解决

修复 PR#42120 引入的双重量化输入问题 正确性

fxmarty 在评论中解释 fused kernel 绕过了父类 TritonExperts.apply 中的 moe_kernel_quantize_input,从而修复了 #42120 导致的 activation 量化重复应用。

结论:新 fused 内核不再调用 super().apply,因此问题自动修复。 · 已解决

风险与影响

  1. 数值精度风险:新内核使用 float32 累加并调整了 _e2m1_inline 实现,虽测试通过(atol 放宽),但极端稀疏部署下可能仍有微小差异。
  2. LoRA 兼容性:明确不支持 LoRA,若后续框架尝试在该后端启用 LoRA 会抛出 NotImplementedError,但需确保上层调用不会误选此后端。
  3. 其他硬件平台:新内核仅对 NVFP4 模拟路径生效,不影响原生 Blackwell NVFP4 或其他量化方案。
  4. 回归覆盖:当前测试仅覆盖单一模型(Kimi-K2.6),Qwen3-30B-A3B 权重未包含在内,可能存在未发现的形状异常。

用户影响:在 AMD MI350 等非原生平台上,使用 NVFP4 量化的 MoE 模型(如 Kimi-K2.6)的推理时延降低 2–10 倍,显存占用显著下降。
系统影响:新增约 400 行 Triton 内核与 Python 逻辑,编译开销可控;测试文件增加 ~460 行。
团队协作:需关注后续对 moe_kernel_quantize_input 等上层接口的变更可能影响此 fused 路径。

核心路径变更 新内核数值精度依赖测试 LoRA 不兼容 仅覆盖单模型测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论