Prhub

#52204 [Kernel] Add FlashInfer TRTLLM MXFP8 linear backend

原始 PR 作者 seonjinn 合并时间 2026-08-20 22:07 文件变更 5 提交数 6 评论 24 代码增减 +265 / -0

执行摘要

新增 FlashInfer TensorRT-LLM 的 MXFP8 线性内核并接入后端选择器

FlashInfer 已暴露针对密集 MXFP8 GEMM 的 TensorRT-LLM 后端,但 vLLM 的 flashinfer_trtllm linear selector 目前只覆盖 NVFP4(见 PR body 原话)。本 PR 的目标是把该路径以新内核形式接入现有选择器,让 SM100 系列 GPU 上量化到 MXFP8 且激活为 BF16 的模型可以通过 --linear-backend flashinfer_trtllm 显式启用。作者明确说明“Model-level accuracy and same-version performance results are not yet available, so this PR makes no accuracy or performance claim”,因此这是一个先打通算子路径、再补验证的增量功能。

值得精读。该 PR 展示了如何在一个既有线性后端选择器上安全地增加新 GPU 内核:通过 _POSSIBLE_MXFP8_KERNELS 优先级列表而非特殊分支控制默认选择、在权重加载后做一次性布局转换而非每次前向重复处理、用 custom op + fake 实现同时支持 eager 与 CUDA Graph/torch.compile 两条路径。对要扩展 FlashInfer 或 Quantization 内核的工程师,process_weights_after_loading 与 apply_weights 的职责切分以及 review 中关于“不要特殊 case”的取舍尤其值得参考。

讨论亮点

review 中最核心的讨论围绕“默认选择策略”和“测试裁剪”展开:

  • mgoin 在 vllm/model_executor/kernels/linear/__init__.py 上指出:Maybe you could add it to the bottom of _POSSIBLE_MXFP8_KERNELS if you don't want this chosen by default? I'd prefer to not have a special case。作者随后移除特殊过滤逻辑,改为把内核放在 MXFP8 候选列表末尾,回复 Done. The kernel is now the last CUDA MXFP8 candidate, and the special-case selection logic is removed.

  • mgoin 对 apply_weights 中的输出切片提出疑问:Are we missing padding the input to match the weight?。作者解释 No input padding is needed. Only N is padded, so the operands remain A[M, K] and B[K, padded_N]; the result is sliced back to N. The 8x4 quantizer handles M-side scale-layout padding internally.

  • mgoin 对原 mocked 测试套件评价 I think a lot of these mocked tests are overkill,作者最终删除该文件并回复 Reduced the mocked CPU suite from 252 lines to one backend-selection test. Numerics, padding, custom-op checks, and CUDA Graph coverage remain in the real GPU tests.

  • 其余为小 nit:把 BF16 dtype 检查从 ValueError 改为 assert,用 current_platform.is_device_capability_family(100) 简化 is_supported,以及修正文档措辞为“models quantized to MXFP8 with BF16 activations”。

实现拆解

实现拆解如下:

  1. 新增内核类并接入选择器:在 vllm/model_executor/kernels/linear/mxfp8/flashinfer.py 新增 FlashInferTrtllmMxfp8LinearKernel,is_supported 同时要求 CUDA 与 SM100 算力族、且安装了 FlashInfer;can_implement 直接返回可支持;process_weights_after_loading 完成权重预处理,apply_weights 完成推理前向。随后在 vllm/model_executor/kernels/linear/__init__.py 中把它加入 flashinfer_trtllm 的 _LINEAR_BACKEND_KERNEL_MAP、_POSSIBLE_MXFP8_KERNELS 列表末尾以及允许注册的内核名列表。

  2. 权重与 scale 的 TRTLLM 布局预处理:在 process_weights_after_loading 中做一次性转换:要求 K 能被 256 整除;从 layer.weight_scale 截取前 N 行、前 K // 32 列作为 scale;把 N 向 128 对齐进行物理 padding;最后用 shuffle_matrix_a 与 shuffle_matrix_sf_a 生成 TensorRT-LLM 期望的矩阵/scale 排列,并用 layer._mxfp8_trtllm_output_size 保存逻辑输出宽度,供 apply_weights 在结果切片时恢复真实 N。

  3. 激活量化与 GEMM 调用:apply_weights 断言输入为 BF16,将激活 reshape 为 2D 后调用新增 custom op vllm::flashinfer_mxfp8_quantize_8x4 做 8x4 scale 布局量化,再调用 mm_mxfp8(新增 use_8x4_sf_layout 参数透传 FlashInfer),后端指定为 trtllm;输出若被物理 padding 则切回 output_size 并保证 contiguous,最后加 bias 并还原 shape。

  4. custom op 与 fake 实现配套:在 vllm/utils/flashinfer.py 新增 flashinfer_mxfp8_quantize_8x4 及 flashinfer_mxfp8_quantize_8x4_fake,fake 实现按 8x4 布局计算 scale 大小,保证 torch.compile / CUDA Graph 路径的 shape 推导可用;同时给既有 mm_mxfp8 custom op 增加 use_8x4_sf_layout 参数及 fake 传递。

  5. 测试与文档:新增 tests/kernels/quantization/test_flashinfer_mxfp8_trtllm.py,覆盖三组数值形状(含 N=130 的 padding 切片)、torch.library.opcheck 检查 custom op、以及 CUDA Graph 捕获与重放一致性;根据 review 意见删除了原先 252 行的 mocked CPU 测试套件,只保留一个后端选择测试。docs/features/quantization/modelopt.md 增加说明,提示 SM100 系列 MXFP8 + BF16 模型可使用 --linear-backend flashinfer_trtllm。

文件 模块 状态 重要度
vllm/model_executor/kernels/linear/mxfp8/flashinfer.py 线性内核 modified 8.55
tests/kernels/quantization/test_flashinfer_mxfp8_trtllm.py 内核测试 added 7.24
vllm/utils/flashinfer.py 量化工具 modified 6.93
vllm/model_executor/kernels/linear/__init__.py 内核注册 modified 5.11
docs/features/quantization/modelopt.md 文档 modified 2.34

关键符号

FlashInferTrtllmMxfp8LinearKernel FlashInferTrtllmMxfp8LinearKernel.is_supported FlashInferTrtllmMxfp8LinearKernel.can_implement FlashInferTrtllmMxfp8LinearKernel.process_weights_after_loading FlashInferTrtllmMxfp8LinearKernel.apply_weights flashinfer_mxfp8_quantize_8x4 flashinfer_mxfp8_quantize_8x4_fake mm_mxfp8 test_flashinfer_trtllm_mxfp8_linear_numerics test_flashinfer_trtllm_mxfp8_custom_ops test_flashinfer_trtllm_mxfp8_linear_cuda_graph

关键源码片段

tests/kernels/quantization/test_flashinfer_mxfp8_trtllm.py test-coverage

新增真实 GPU 测试,覆盖数值正确性(含 N=130 padding 切片)、custom op 的 opcheck 校验和 CUDA Graph 捕获 / 重放一致性,是内核可用性的主要保障。

# 仅在 SM100 系列且装有 FlashInfer 的 CUDA 环境运行,其他环境直接跳过
if not (
    current_platform.is_cuda()
    and current_platform.is_device_capability_family(100)
    and has_flashinfer()
):
    pytest.skip(
        reason="FlashInfer TRTLLM MXFP8 requires an SM100-family GPU",
        allow_module_level=True,
    )
​
​
def _make_layer(weight: torch.Tensor) -> torch.nn.Module:
    from flashinfer import SfLayout, mxfp8_quantize
​
    # 用 FlashInfer 的 linear 布局量化权重,模拟真实 MXFP8 checkpoint 形态
    weight_mxfp8, weight_scale = mxfp8_quantize(
        weight,
        sf_swizzle_layout=SfLayout.layout_linear,
    )
    layer = torch.nn.Module()
    layer.weight = torch.nn.Parameter(weight_mxfp8, requires_grad=False)
    layer.weight_scale = torch.nn.Parameter(
        weight_scale.view(weight.shape[0], weight.shape[1] // 32),
        requires_grad=False,
    )
    return layer
​
​
@pytest.mark.parametrize("shape", [(1, 130, 256), (7, 256, 512), (128, 130, 768)])
@torch.inference_mode()
def test_flashinfer_trtllm_mxfp8_linear_numerics(
    shape: tuple[int, int, int],
) -> None:
    torch.manual_seed(0)
    m, n, k = shape
    x = torch.randn((m, k), dtype=torch.bfloat16, device="cuda")
    weight = torch.randn((n, k), dtype=torch.bfloat16, device="cuda")
    layer = _make_layer(weight)
    kernel = FlashInferTrtllmMxfp8LinearKernel(Mxfp8LinearLayerConfig())
    kernel.process_weights_after_loading(layer)
​
    output = kernel.apply_weights(layer, x)
    reference = torch.mm(x, weight.t())
    # 用余弦相似度做数值近似校验,同时检查输出 shape 与连续性
    similarity = F.cosine_similarity(
        output.float().flatten(), reference.float().flatten(), dim=0
    )
​
    assert output.shape == (m, n)
    assert output.is_contiguous()
    assert similarity.item() > 0.98
vllm/utils/flashinfer.py core-logic

新增 `vllm::flashinfer_mxfp8_quantize_8x4` custom op 及其 fake 实现,并给 `mm_mxfp8` 增加 `use_8x4_sf_layout` 参数,是内核可被 torch.compile / CUDA Graph 使用的底层封装。

@torch.library.custom_op(
    "vllm::flashinfer_mxfp8_quantize_8x4",
    mutates_args=[],
    device_types="cuda",
)
def flashinfer_mxfp8_quantize_8x4(
    a: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
    # 用 8x4 scale 布局量化 BF16 激活,配合 TensorRT-LLM 后端使用
    from flashinfer import SfLayout
    from flashinfer import mxfp8_quantize as mxfp8_quantize_
​
    return mxfp8_quantize_(
        a,
        backend="cuda",
        sf_swizzle_layout=SfLayout.layout_8x4,
    )
​
​
@torch.library.register_fake("vllm::flashinfer_mxfp8_quantize_8x4")
def flashinfer_mxfp8_quantize_8x4_fake(
    a: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
    # Fake 实现用于 torch.compile / CUDA Graph 的 shape 推导,
    # scale 大小按 8x4 布局向上取整计算
    m, k = a.shape
    scale_size = cdiv(m, 8) * 8 * cdiv(k // 32, 4) * 4
    return (
        torch.empty(m, k, dtype=torch.float8_e4m3fn, device=a.device),
        torch.empty(scale_size, dtype=torch.uint8, device=a.device),
    )

评论区精华

默认选择策略:特殊过滤 vs 优先级列表末尾 设计

mgoin 建议不要为 `FlashInferTrtllmMxfp8LinearKernel` 写特殊 case 过滤逻辑:`Maybe you could add it to the bottom of _POSSIBLE_MXFP8_KERNELS if you don't want this chosen by default? I'd prefer to not have a special case`。

结论:作者接受建议,移除 special-case,把内核放到 `_POSSIBLE_MXFP8_KERNELS` 的 CUDA 列表末尾,既有内核优先级不变。 · 已解决

输入侧是否需要 padding 以匹配权重 question

mgoin 对 `apply_weights` 提出 `Are we missing padding the input to match the weight?`。作者解释只对 N 做物理 padding,操作数仍为 A[M, K] 与 B[K, padded_N],M 侧 scale 布局 padding 由 8x4 量化器内部处理。

结论:确认无需输入侧 padding,输出切片回逻辑 N 即可;该结论同时体现在数值测试的 N=130 用例中。 · 已解决

Mocked CPU 测试是否过度 测试

mgoin 评论 `I think a lot of these mocked tests are overkill`。作者先删除原 `test_mxfp8_trtllm_linear.py`,随后回复 `Reduced the mocked CPU suite from 252 lines to one backend-selection test. Numerics, padding, custom-op checks, and CUDA Graph coverage remain in the real GPU tests.`

结论:删除大部 mocked 测试,仅保留一个后端选择测试,真实 GPU 测试保留完整覆盖。 · 已解决

文档措辞:MXFP8 checkpoint vs MXFP8 模型 documentation

mgoin 询问 `Do you mean "MXFP8 models"?`,作者回复 `Updated to "models quantized to MXFP8 with BF16 activations" to distinguish the weight format from the activation dtype.`

结论:文档改为“models quantized to MXFP8 with BF16 activations”,明确权重格式与激活 dtype 的区分。 · 已解决

BF16 检查与算力判断的代码风格 style

mgoin 指出 `nit: assert` 和 `nit: can just do current_platform.is_device_capability_family(100)`,作者分别回复 Done,将 ValueError 改为 assertion,并统一使用算力族 helper。

结论:两处 nit 均在 review 中修掉,最终代码使用 assert 与 `is_device_capability_family(100)`。 · 已解决

风险与影响

  1. 硬约束可能拒跑部分模型:process_weights_after_loading 要求 K % 256 == 0,比通常的 32 对齐更严格,部分 MXFP8 模型可能无法使用该后端,且报错发生在权重加载阶段。
  2. 默认选择兜底风险:虽然内核被放在 _POSSIBLE_MXFP8_KERNELS 末尾,但当所有更优内核都无法实现某层时,can_implement 恒为 True 的该内核仍可能被默认选中;它只支持 SM100 与 BF16 激活,若模型激活为 FP16 会产生运行时断言失败。
  3. 数值与性能未充分验证:PR 自述没有模型级精度与同版本性能结果,测试仅用随机权重和 cosine_similarity > 0.98 阈值做数值验证,且真实 GPU 测试依赖具体的 FlashInfer 版本(0.6.16.post3),存在版本漂移导致行为变化的可能。
  4. padding/切片逻辑的回归风险:N 向 128 对齐的物理 padding 与输出切片集中在 apply_weights 中,若未来修改 _mxfp8_trtllm_output_size 的写入时机或复用权重流程,可能导致输出宽度错误,目前仅靠一个 N=130 的测试覆盖。
  5. 新 custom op 的 fake shape 风险:flashinfer_mxfp8_quantize_8x4_fake 手工推导的 scale 大小若与 FlashInfer 实际布局不一致,会在 torch.compile / CUDA Graph 路径产生静默 shape 错误。

对用户:SM100 系列 GPU 上使用 MXFP8 + BF16 量化的模型多了一条显式内核选择路径,不影响默认行为;文档同步更新,可发现性较好。对系统:新增的 custom op 与 mm_mxfp8 参数扩展都在 FlashInfer 依赖内完成,不引入新外部依赖,但会扩大 vLLM CUDA 内核矩阵的维护面。对团队:这是 FlashInfer TRTLLM 路径在 vLLM 中从 NVFP4 扩展到 MXFP8 的第一块拼图,后续还需补 tactic hints、MoE 内核和 128x4 高 M 激活 scale 路径;作者也在 PR body 中说明这些均不在本 PR 范围内。

SM100 与 FlashInfer 强依赖 K 必须为 256 的倍数 默认优先级列表末尾仍可能兜底选中 无模型级精度与性能验证 依赖 FlashInfer 版本 pin

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论