Prhub

#42774 [Perf] Padded nvfp4 quant kernel to remove additional copy, 2.4%~5.7% e2e performance improvement

原始 PR 作者 yewentao256 合并时间 2026-05-19 07:38 文件变更 5 提交数 5 评论 7 代码增减 +119 / -43

执行摘要

padded nvfp4 量化 kernel 消除额外 copy

NVFP4量化后的输出需要对齐到CUTLASS要求的大小,原有的做法是在量化kernel之后再执行一次padding复制,这带来了额外的带宽开销。通过将padding合并到量化kernel内部,可以减少一次全局内存访问,提升量化阶段性能,进而提升端到端推理吞吐。

值得精读。该PR展示了如何通过将后处理步骤融合到CUDA kernel中来消除冗余内存访问,是典型的性能优化案例。对于从事量化推理、CUDA kernel优化或CUTLASS集成工作的开发者具有很好的参考价值。建议关注其设计权衡:何时适合将padding内联到kernel,以及如何保持向后兼容。

讨论亮点

审阅中主要围绕性能增益和代码质量。LopezCastroRoberto质疑e2e提升是否足够显著,作者提供benchmark数据后确认有效并approve。关于sf_major kernel中变量命名,gemini-code-assist建议重命名validvalid_input以与swizzled kernel保持一致,但未在本PR中采纳。另指出8x4 SF layout下不支持padded_n,作者添加TODO记录。独立benchmark脚本被要求移除,作者已执行。

实现拆解

  1. vllm/_custom_ops.py中,修改create_fp4_output_tensorsscaled_fp4_quant函数,新增可选的padded_n参数。当提供padded_n时,分配的output和scale buffer尺寸对应padded_n(更大的K维),允许CUDA kernel直接写入padded区域。
  2. 修改CUDA kernel文件csrc/libtorch_stable/quantization/fp4/nvfp4_quant_kernels.cu中的cvt_fp16_to_fp4cvt_fp16_to_fp4_sf_major。引入outputColsnum_padded_cols参数,调整线程网格计算,仅当valid_output时才写入有效数据,padding区域保持为零。
  3. 更新线性层kernel的apply_weights方法:在vllm/model_executor/kernels/linear/nvfp4/cutlass.pyflashinfer.py中,移除调用pad_nvfp4_activation_for_cutlass的步骤,改为将padded_n参数传递给scaled_fp4_quant,由量化kernel直接完成padding。同时移除不再需要的pad_nvfp4_activation_for_cutlass导入。
  4. 在测试文件tests/kernels/quantization/test_nvfp4_quant.py中,新增test_quantize_to_fp4_with_padded_output测试用例,覆盖多种shape和layout,验证padding区域为零且有效值与reference一致。同时提取了round_up工具函数。
  5. (附加)删除了独立的benchmark脚本benchmarks/kernels/benchmark_nvfp4_padded_quant.py,避免重复(应LopezCastroRoberto审查意见)。
文件 模块 状态 重要度
vllm/_custom_ops.py 量化接口 modified 6.71
tests/kernels/quantization/test_nvfp4_quant.py 量化测试 modified 5.88
vllm/model_executor/kernels/linear/nvfp4/cutlass.py 线性层 modified 5.44
vllm/model_executor/kernels/linear/nvfp4/flashinfer.py 线性层 modified 5.64
csrc/libtorch_stable/quantization/fp4/nvfp4_quant_kernels.cu CUDA 内核 modified 4.92

关键符号

create_fp4_output_tensors scaled_fp4_quant cvt_fp16_to_fp4 cvt_fp16_to_fp4_sf_major FlashInferCutlassNvFp4LinearKernel.apply_weights FlashInferTrtllmNvFp4LinearKernel.apply_weights CutlassNvFp4LinearKernel.apply_weights

关键源码片段

vllm/_custom_ops.py core-logic

核心入口,新增 `padded_n` 参数控制量化输出 buffer 大小,消除后续 padding 复制

# vllm/_custom_ops.py — 关键片段
# 修改 create_fp4_output_tensors 以支持 padded_n 参数def create_fp4_output_tensors(
    m: int,
    n: int,
    device: torch.device,
    is_sf_swizzled_layout: bool,
    padded_n: int | None = None, # <-- 新增:可选的目标 K 维度(含 padding)
) -> tuple[torch.Tensor, torch.Tensor]:
    """
    ...
    When ``padded_n`` is provided, allocate a larger packed-FP4 output/scale
    buffer so the quantization kernel can write CUTLASS-compatible K padding directly
    """
    # 实际分配时使用 padded_n(若提供)否则用原始的 n
    physical_n = padded_n if padded_n is not None else n
    output = torch.empty((m, physical_n // 2), device=device, dtype=torch.uint8)
    output_scale = create_fp4_scale_tensor(m, physical_n, device, is_sf_swizzled_layout)
    return output, output_scale# scaled_fp4_quant 同样新增 padded_n 参数
​
​
def scaled_fp4_quant(
    input: torch.Tensor,
    input_global_scale: torch.Tensor,
    is_sf_swizzled_layout: bool = True,
    backend: str = "none",
    padded_n: int | None = None, # <-- 新增
) -> tuple[torch.Tensor, torch.Tensor]:
    # ... 断言 ...
    if padded_n is not None:
        assert padded_n >= n
        assert padded_n % block_size == 0
    # 对于 8x4 SF layout 不支持 padded_n,抛出异常(TBD 后续扩展)
    if use_8x4_sf_layout and padded_n is not None and padded_n != n:
        raise ValueError("padded_n is not supported with TRTLLM 8x4 scale layout.")
    if use_8x4_sf_layout:
        # ... 使用 flashinfer 量化路径,不支持 padding
        pass
    else:
        # 预分配包含 padding 的 buffer,调用 .out 变体
        output, output_scale = create_fp4_output_tensors(
            m, n, input.device, is_sf_swizzled_layout, padded_n=padded_n,
        )
        torch.ops._C.scaled_fp4_quant.out(
            input, input_global_scale, is_sf_swizzled_layout,
            output=output, output_scale=output_scale
        )
tests/kernels/quantization/test_nvfp4_quant.py test-coverage

新增 paded 输出测试用例,覆盖多种 shape 和 layout,验证 padding 为零及有效值正确

# tests/kernels/quantization/test_nvfp4_quant.py — 新增测试片段# 全局 round_up 辅助函数(提取为独立函数)
def round_up(x: int, y: int) -> int:
    return (x + y - 1) // y * y# PADDED_OUTPUT_SHAPES 列表包含非对齐 shape(如 (64, 7152))
PADDED_OUTPUT_SHAPES = [(128, 48), (128, 80), (150, 48), (150, 80), (64, 7152)]
​
​
@pytest.mark.parametrize("shape", PADDED_OUTPUT_SHAPES)
@pytest.mark.parametrize("is_sf_swizzled_layout", [True, False])
@torch.inference_mode()
def test_quantize_to_fp4_with_padded_output(
    shape: tuple[int, int],
    is_sf_swizzled_layout: bool,
) -> None:
    from vllm._custom_ops import create_fp4_output_tensors
​
    dtype = torch.float16
    set_random_seed(42)
    torch.set_default_device("cuda:0")
    m, n = shape
    # 将 n 向上取整到 32 的倍数作为 padded_n
    padded_n = round_up(n, 32)
    assert padded_n > n
​
    x = torch.randn((m, n), dtype=dtype)
    tensor_amax = torch.abs(x).max().to(torch.float32)
    global_scale = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / tensor_amax
​
    # 计算参考输出(无 padding)
    out_ref, scale_ref = ref_nvfp4_quant(x, global_scale)
​
    # 使用 padded_n 调用 ops.scaled_fp4_quant
    out, out_scale = ops.scaled_fp4_quant(
        x,
        global_scale,
        is_sf_swizzled_layout=is_sf_swizzled_layout,
        padded_n=padded_n,
    )
    # 验证 output shape 为 (m, padded_n // 2)
    assert out.shape == (m, padded_n // 2)
    # 验证 padding 区域(后半部分量 packed 后)全为零
    assert torch.count_nonzero(out[:, n // 2 :]) == 0
​
    # 解量化有效部分并与 reference 对比
    out_ans = cast_from_fp4(out[:, : n // 2], m, n)
    torch.testing.assert_close(out_ans, out_ref)
​
    # 验证 scale 的有效部分
    if is_sf_swizzled_layout:
        scale_ans = recover_swizzled_scales(out_scale, m, padded_n)
        torch.testing.assert_close(scale_ans[:, : n // BLOCK_SIZE], scale_ref)
        assert torch.count_nonzero(scale_ans[:, n // BLOCK_SIZE :]) == 0
    else:
        scale_ans = out_scale.to(torch.float32)
        torch.testing.assert_close(scale_ans[:, : n // BLOCK_SIZE], scale_ref)
        assert torch.count_nonzero(scale_ans[:, n // BLOCK_SIZE :]) == 0

评论区精华

e2e 性能提升质疑 性能

LopezCastroRoberto 询问 e2e 提升是否显著,yewentao256 提供了 benchmark 数据,显示 2.4%~5.7% e2e 提升

结论:LCR 确认提升有效并 approve · 已解决

独立 benchmark 脚本必要性 other

LopezCastroRoberto 建议合并或移除独立 benchmark 脚本,author 同意移除

结论:已移除独立 benchmark 脚本 · 已解决

sf_major kernel 变量命名一致性 style

gemini-code-assist 建议将 sf_major kernel 中的 valid 变量重命名为 valid_input 以与 swizzled kernel 保持一致

结论:未采纳,PR 合入时仍使用原命名 · unresolved

8x4 SF layout 不支持 padded_n 设计

LopezCastroRoberto 指出 8x4 SF layout 不支持 padded_n,author 添加 TODO

结论:添加了 TODO 注释,计划未来扩展支持 · resolved (via TODO)

风险与影响

主要风险包括:(1) CUDA kernel修改可能引入精度或数值问题,但已通过新增padding测试覆盖多种非对齐形状;(2) TRTLLM 8x4 SF layout与padded_n冲突,scaled_fp4_quant中已添加断言和TODO拦截;(3) 移除pad_nvfp4_activation_for_cutlass后,若某些自定义后端未设置weights_padding_colspadded_n计算可能异常,但默认padded_n=None维持向后兼容;(4) 性能风险低,benchmark已验证正向收益。

直接影响使用NVFP4量化(modelopt_fp4)且在sm_100+ Nvidia GPU上运行的模型推理性能,特别是采用CUTLASS/FlashInfer后端的线性层。端到端吞吐提升2.4%~5.7%,kernel级提升15%~40%。对TRTLLM 8x4 layout用户无影响(会触发ValueError)。对不使用NVFP4的模型无影响。修改集中在量化路径,不涉及调度器、前端等模块。

CUDA kernel 修改 量化精度风险 兼容性:TRTLLM 8x4 layout 不支持 padded_n

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论