Prhub

#53014 [Kernel] add Flashinfer cutedsl w4a16 linear

原始 PR 作者 IwakuraRein 合并时间 2026-09-01 10:49 文件变更 8 提交数 4 评论 9 代码增减 +230 / -10

执行摘要

新增 FlashInfer W4A16 内核,SM100 默认启用

PR body 明确说明动机:FlashInfer 已提供 sm12x 的 cute-dsl w4a16 linear kernel,flashinfer-ai/flashinfer#4466 又新增了 sm100 版本并随 0.6.18 发布;vLLM 的目标是“在 sm100/103 上默认使用 flashinfer cute-dsl 而不是 Marlin”。也就是说,ModelOptNvFp4W4A16LinearMethod 此前强制走 MarlinNvFp4LinearKernel,Blackwell 用户无法利用 CuTe-DSL 在 SM100/103 上的新指令与调度优势。验证方面:作者用 nvidia/Qwen3.6-27B-NVFP4(ModelOpt 量化,含 W4A16_NVFP4 层)在单张 B200 上测试,GSM8K 准确率 94.8%,并附了 Pareto 性能对比图。

值得精读。它为“接入第三方 GEMM 内核并切换默认内核”提供了完整范本:custom op + fake 注册、能力探测、分层回退(is_supported -> auto -> Marlin)、测试与文档配套。重点关注 init_nvfp4_linear_kernel 的 auto 分支决策和 apply_weights 的 BF16 断言;任何在 Blackwell 上运行 ModelOpt FP4 模型的同学都应了解这一默认行为变化。

讨论亮点

review 中唯一的技术讨论来自 pavanimajety 在 vllm/config/kernel.py 上的提问:“was MXFP8 removed?”,担心 flashinfer_cutedsl 的能力注释从 (NVFP4, MXFP8) 改为 (NVFP4, W4A16_NVFP4) 后丢失 MXFP8;作者回复 “Fixed after rebasing”,最终注释为 (BF16, NVFP4, MXFP8, W4A16_NVFP4),问题已解决。流程层面:mergify 曾因 merge conflict 阻塞合并,作者 rebase 后解决;CI 有一次失败经 /ci retry 重试通过;vadiklyutiy 最终 approve;claude[bot] 因 fork PR 自动跳过 review。

实现拆解

  1. 新增 W4A16 kernel 类(vllm/model_executor/kernels/linear/nvfp4/flashinfer.py,+80 行):新增 FlashInferCuteDslNvFp4W4A16LinearKernel。is_supported 限定 CUDA 且计算能力为 100/103 或 120-129,并要求 flashinfer.gemm 同时提供 mm_bf16_fp4 与 prepare_bf16_fp4_weights。权重处理路径为:先 pad_nvfp4_weight_for_cutlass(alignment=64) 补零,再 swizzle_blockscale,最后 flashinfer_prepare_bf16_fp4_weights(backend="cute-dsl") 生成运行时布局,并原地重写 weight、weight_scale、weight_global_scale 三个 Parameter。apply_weights 强制 BF16 输入,按 padding 字节数乘 2 对激活补零后调用新 custom op,最后用 slice_nvfp4_output 裁剪输出并加 bias。
  2. 注册 custom op 与能力探测(vllm/utils/flashinfer.py,+49 行):新增 torch.library.custom_op("vllm::flashinfer_mm_bf16_fp4"),延迟导入 flashinfer.gemm.mm_bf16_fp4 并固定 backend="cute-dsl";配套 register_fake 实现 flashinfer_mm_bf16_fp4_fake 用于 torch.compile 下的 shape 推导;新增 has_flashinfer_bf16_fp4() 能力探测与 flashinfer_prepare_bf16_fp4_weights lazy wrapper,并同步更新 __all__。
  3. 内核注册与默认选择(vllm/model_executor/kernels/linear/__init__.py,+20/-3):新 kernel 进入 _LINEAR_BACKEND_KERNEL_MAP["flashinfer_cutedsl"]、_POSSIBLE_NVFP4_KERNELS[PlatformEnum.CUDA] 与 __all__;init_nvfp4_linear_kernel(use_a16=True) 的 a16_kernels 扩容为三选,auto 分支改为:计算能力为 100/103 且 is_supported 通过时强制选 cutedsl,否则回退 MarlinNvFp4LinearKernel。vllm/model_executor/layers/quantization/modelopt.py 同步更新 ModelOptNvFp4W4A16LinearMethod.__init__ 的注释(SM100 用 cutedsl、其它用 Marlin)。
  4. 测试与文档配套:tests/kernels/quantization/test_flashinfer_nvfp4_scaled_mm.py 新增 test_flashinfer_cutedsl_nvfp4_w4a16_linear,用随机权重经 scaled_fp4_quant 量化后与反量化参考的 F.linear 输出对比(atol/rtol=1e-1);tests/quantization/test_modelopt.py 为 test_modelopt_w4a16_respects_linear_backend 增加 ("flashinfer_cutedsl", FlashInferCuteDslNvFp4W4A16LinearKernel) 参数,非 auto 后端先 is_supported 再 skip;vllm/config/kernel.py 更新 KernelConfig 注释(保留 MXFP8,补充 W4A16_NVFP4);docs/features/quantization/modelopt.md 补充 W4A16_NVFP4 checkpoint 说明与 --linear-backend flashinfer_cutedsl 用法。
文件 模块 状态 重要度
vllm/model_executor/kernels/linear/nvfp4/flashinfer.py 量化内核 modified 8.47
vllm/utils/flashinfer.py 工具层 modified 7.58
vllm/model_executor/kernels/linear/__init__.py 内核注册 modified 6.39
tests/kernels/quantization/test_flashinfer_nvfp4_scaled_mm.py 量化测试 modified 5.66
tests/quantization/test_modelopt.py 量化测试 modified 4.54
vllm/model_executor/layers/quantization/modelopt.py 量化配置 modified 4.29
vllm/config/kernel.py 内核配置 modified 3.83
docs/features/quantization/modelopt.md 量化文档 modified 2.38

关键符号

FlashInferCuteDslNvFp4W4A16LinearKernel is_supported can_implement process_weights_after_loading apply_weights has_flashinfer_bf16_fp4 flashinfer_mm_bf16_fp4 flashinfer_mm_bf16_fp4_fake init_nvfp4_linear_kernel

关键源码片段

vllm/model_executor/kernels/linear/nvfp4/flashinfer.py core-logic

本 PR 的核心:新增 FlashInferCuteDslNvFp4W4A16LinearKernel,实现 W4A16 内核的权重布局转换与 GEMM 调用

FlashInferCuteDslNvFp4W4A16LinearKernel(vllm/model_executor/kernels/linear/nvfp4/flashinfer.py)

# 新增的 W4A16 kernel 类:process_weights_after_loading 把 ModelOpt 打包权重
# 转换为 flashinfer CuTe-DSL 运行时布局,apply_weights 调用新注册的 custom op
class FlashInferCuteDslNvFp4W4A16LinearKernel(NvFp4LinearKernel):
    """BF16 x NVFP4 GEMM via FlashInfer's CuTe-DSL backend."""
​
    @classmethod
    def can_implement(cls, config: NvFp4LinearLayerConfig) -> tuple[bool, str | None]:
        return True, None
​
    @classmethod
    def is_supported(cls, compute_capability: int | None = None) -> tuple[bool, str | None]:
        # 未显式传入时,从当前 CUDA 设备读取计算能力
        if compute_capability is None:
            if not current_platform.is_cuda():
                return False, "FlashInfer CuTe-DSL W4A16 requires CUDA"
            capability = current_platform.get_device_capability()
            if capability is None:
                return False, "CUDA compute capability is unavailable"
            compute_capability = capability.to_int()
​
        # 只接受 SM100/103 与 SM120-129;其它架构走 Marlin 回退
        if compute_capability not in (100, 103) and not (
            compute_capability >= 120 and compute_capability < 130
        ):
            return False, "FlashInfer CuTe-DSL W4A16 requires sm_100 or sm_12x"
        if not has_flashinfer_bf16_fp4():
            return False, "FlashInfer CuTe-DSL BF16 x FP4 GEMM is unavailable"
        return True, None
​
    def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
        # 与 cutlass 路径一致:先按 64 字节对齐补零,再做 blockscale swizzle
        padded_weight, weights_padding_bytes = pad_nvfp4_weight_for_cutlass(
            layer.weight.data, alignment=64
        )
        swizzled_scale = swizzle_blockscale(layer.weight_scale.data)
        weight, weight_scale, global_scale = flashinfer_prepare_bf16_fp4_weights(
            padded_weight,
            swizzled_scale,
            layer.weight_global_scale.data.reshape(1),
            backend="cute-dsl",
        )
        assert global_scale is not None
​
        # 重写权重参数为 flashinfer 运行时布局,注意 weights_padding_cols 字段实际存字节数
        layer.weight = torch.nn.Parameter(weight, requires_grad=False)
        layer.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False)
        layer.weight_global_scale = torch.nn.Parameter(global_scale, requires_grad=False)
        layer.weights_padding_cols = weights_padding_bytes
​
    def apply_weights(
        self,
        layer: torch.nn.Module,
        x: torch.Tensor,
        bias: torch.Tensor | None = None,
    ) -> torch.Tensor:
        # CuTe-DSL 内核只接受 BF16 激活,fp16 输入直接报错以免产生错误结果
        if x.dtype != torch.bfloat16:
            raise ValueError(
                f"FlashInfer CuTe-DSL W4A16 requires BF16 input, got {x.dtype}"
            )
​
        output_size = layer.output_size_per_partition
        output_shape = [*x.shape[:-1], output_size]
        x_2d = x.reshape(-1, x.shape[-1])
        # FP4 每字节打包 2 个元素,激活补零数量为字节数的 2 倍
        weights_padding_bytes = getattr(layer, "weights_padding_cols", 0)
        if weights_padding_bytes:
            x_2d = torch.nn.functional.pad(x_2d, (0, weights_padding_bytes * 2))
        x_2d = x_2d.contiguous()
​
        out = torch.ops.vllm.flashinfer_mm_bf16_fp4(
            x_2d, layer.weight, layer.weight_scale, layer.weight_global_scale
        )
        out = slice_nvfp4_output(out, output_size)
        if bias is not None:
            out = out + bias
        return out.view(*output_shape)
vllm/utils/flashinfer.py core-logic

注册 vllm::flashinfer_mm_bf16_fp4 custom op 及 fake 实现,新增 has_flashinfer_bf16_fp4 能力探测,是 kernel 可运行的基础设施

custom op 与能力探测(vllm/utils/flashinfer.py)

# 能力探测:要求 flashinfer.cute_dsl 与 flashinfer.gemm 中两个符号同时存在
@functools.cache
def has_flashinfer_bf16_fp4() -> bool:
    """Return ``True`` if FlashInfer's CuTe-DSL W4A16 GEMM is available."""
    if not has_flashinfer_cutedsl():
        return False
    mod = _get_submodule("flashinfer.gemm")
    return mod is not None and all(
        hasattr(mod, name) for name in ("mm_bf16_fp4", "prepare_bf16_fp4_weights")
    )
​
​
# 注册 custom op:延迟导入 flashinfer,未安装该包时 vllm 也能正常导入
@torch.library.custom_op(
    "vllm::flashinfer_mm_bf16_fp4",
    mutates_args=[],
    device_types="cuda",
)
def flashinfer_mm_bf16_fp4(
    A: torch.Tensor,
    B: torch.Tensor,
    B_scale: torch.Tensor,
    global_scale: torch.Tensor,
) -> torch.Tensor:
    from flashinfer.gemm import mm_bf16_fp4
​
    return mm_bf16_fp4(A, B, B_scale, global_scale, backend="cute-dsl")
​
​
# fake 实现用于 torch.compile 的 shape 推导:
# uint8 打包权重按行数取输出维,非打包布局按半列取
@torch.library.register_fake("vllm::flashinfer_mm_bf16_fp4")
def flashinfer_mm_bf16_fp4_fake(
    A: torch.Tensor,
    B: torch.Tensor,
    B_scale: torch.Tensor,
    global_scale: torch.Tensor,
) -> torch.Tensor:
    output_size = B.shape[0] if B.dtype == torch.uint8 else B.shape[1] // 2
    return torch.empty(A.shape[0], output_size, dtype=A.dtype, device=A.device)

评论区精华

flashinfer_cutedsl 注释是否误删 MXFP8 documentation

pavanimajety 在 `vllm/config/kernel.py` 的 KernelConfig 注释变更处提问:“was MXFP8 removed?”,因为 `flashinfer_cutedsl` 的能力说明从 `(NVFP4, MXFP8)` 被改为 `(NVFP4, W4A16_NVFP4)`。

结论:作者回复 “Fixed after rebasing”,最终注释恢复为 `(BF16, NVFP4, MXFP8, W4A16_NVFP4)`,MXFP8 未丢失。 · 已解决

合并冲突与 CI 流程 other

mergify 提示本 PR 存在 merge conflict 需 rebase;CI 第一次运行有失败任务,作者执行 `/ci retry` 重试;`claude[bot]` 因 fork PR 自动跳过 review。

结论:作者 rebase 解决冲突,重试后 CI 通过,`vadiklyutiy` 最终 approve。 · 已解决

风险与影响

  1. BF16 输入硬断言:apply_weights 对非 BF16 输入直接抛 ValueError,而 ModelOptNvFp4W4A16LinearMethod 声称支持 fp16/bf16 激活;init_nvfp4_linear_kernel 的 auto 分支只按计算能力选内核、不校验激活 dtype,fp16 激活模型在 SM100/103 上可能在运行期崩溃。
  2. custom op 的 fake shape 推导:flashinfer_mm_bf16_fp4_fake 依赖 B.dtype == torch.uint8 分支推输出宽度,若 process_weights_after_loading 产出的 B 布局不符合假设,torch.compile / CUDA graph 场景会出现 shape 推导错误。
  3. 字段语义漂移:weights_padding_cols 实际存的是补零字节数,apply_weights 中需乘 2 转元素数,与既有 cutedsl W4A4 内核的“列数”语义不一致,后续维护易误用。
  4. 默认行为变更与版本依赖:SM100/103 的 W4A16_NVFP4 默认内核从 Marlin 切到 flashinfer cute-dsl,依赖 flashinfer >= 0.6.18;auto 模式有 is_supported 兜底回退,但显式 --linear-backend flashinfer_cutedsl 在旧版下会直接报错。数值验证仅覆盖单模型单任务(GSM8K),其它形状与模型需持续观察。

用户侧:Blackwell(SM100/103)用户加载 ModelOpt W4A16_NVFP4 checkpoint 时默认获得 CuTe-DSL 内核,潜在性能收益(PR 附 Pareto 图);非 100/103 与旧 flashinfer 环境行为不变,仍走 Marlin 或其它内核。系统侧:新增一个 torch.library.custom_op,torch.compile 下依赖 fake 实现推导;process_weights_after_loading 原地重写三个权重 Parameter,保存/序列化导出的是 flashinfer 运行时布局而非原始布局。团队侧:内核注册表与 init_nvfp4_linear_kernel auto 分支复杂度上升,后续新增 W4A16 内核需同步修改多处映射;文档与配置注释同步更新,降低了 --linear-backend 的认知成本。

默认内核切换 BF16 输入硬断言 依赖 flashinfer 0.6.18+ 新 custom op

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论