数据科学

模型量化:借助 NVIDIA TensorRT 将 FP8 检查点转变为高性能推理引擎

将量化检查点转换为 NVIDIA TensorRT 引擎可以弥合模型优化与生产部署之间的差距,从而实现更快的推理速度、更高的吞吐量和更高效的 GPU 大规模利用率。

在上一篇文章中,我们使用 NVIDIA TensorRT Model Optimizer 生成了高质量的 FP8 量化对比语言图像预训练 (CLIP) 检查点。

本文将介绍我们中断的部分,介绍如何将检查点导出到 ONNX,并将其编译成 NVIDIA TensorRT 引擎,以便进行生产推理。我们还根据 FP16 基准分析生成的 FP8 TensorRT 引擎,以测量量化模型在现实世界中的加速效果。

图 1 显示了典型的端到端量化工作流程的五个阶段。这是部署量化 CLIP 模型的标准工作流。量化 LLM 通过 TensorRT-LLM 遵循不同的路径,本教程将对此进行介绍。

将模型导出为 ONNX 格式

第一步是将 ModelOpt 检查点导出到 ONNX。以下伪代码使用 Modelopt 的内置辅助程序针对 FP8 量化 CLIP 检查点执行此操作 (导出目标是 ONNX opset 20%,其中完全支持 FP8 QuantizeLinear/ DequantizeLinear) 。它将每个权重侧量化 – 然后量化 (Q-DQ) 对折叠到一个仅由 FP8 存储的 DQ 链中,显著缩小了 ONNX 文件。

原则上,原生 torch.onnx.export 也可以正常工作,但需要我们编写自定义转换脚本。

import torch
from transformers import CLIPModel, CLIPTokenizer
from transformers.models.clip.modeling_clip import CLIPAttention
import modelopt.torch.opt as mto
import modelopt.torch.quantization as mtq
from modelopt.torch._deploy.utils import OnnxBytes, get_onnx_bytes_and_metadata
from modelopt.torch.quantization.plugins.diffusion.diffusers import _QuantAttention
# Thin wrappers expose a single forward to the ONNX exporter
class TextEncoder(torch.nn.Module):
    def __init__(self, m):
        super().__init__(); self.m = m
    def forward(self, x):
        return self.m.get_text_features(x)
class ImageEncoder(torch.nn.Module):
    def __init__(self, m):
        super().__init__(); self.m = m
    def forward(self, x):
        return self.m.get_image_features(x)
def prepare_for_fp8_onnx_export(model):
    # 1) turn on FP8 attention fusion (off by default, lost on reload).
    # 2) clear CLIP's float `scale` — exporter chokes on it.
    for _, mod in model.named_modules():
        if isinstance(mod, _QuantAttention):
            mod._disable_fp8_mha = False
        if isinstance(mod, CLIPAttention) and getattr(mod, "scale", None) is not None:
            mod.scale = None
def export(wrapper, dummy, axis_name, out_name):
    """ModelOpt's exporter folds Q+DQ on weights into FP8-stored DQ-only chains
    and rewrites TRT custom ops to native ONNX QDQ — output is TRT-ready."""
    onnx_bytes, _ = get_onnx_bytes_and_metadata(
        model=wrapper, dummy_input=(dummy,), model_name=out_name,
        dynamic_axes={axis_name: {0: "batch"}}, onnx_opset=20, weights_dtype="fp16",
    )
    OnnxBytes.from_bytes(onnx_bytes).write_to_disk("./onnx_output", clean_dir=False)
# Restore the FP8-quantized CLIPModel from the ModelOpt checkpoint
mto.enable_huggingface_checkpointing()
mtq.QuantModuleRegistry.register({CLIPAttention: "CLIPAttention"})(_QuantAttention)
model = (
    CLIPModel.from_pretrained(modelopt_ckpt, attn_implementation="sdpa", torch_dtype=torch.float16)
    .eval().cuda()
)
prepare_for_fp8_onnx_export(model)
# Export Text encoder to ONNX
tok = CLIPTokenizer.from_pretrained(model_ckpt)
dummy_text = tok(["a photo of a cat"], return_tensors="pt", padding="max_length", max_length=77)["input_ids"].cuda()
export(TextEncoder(model), dummy_text, "text_input", "text_clip_fp8")
# Export Image encoder to ONNX
dummy_image = torch.randn(16, 3, 224, 224, dtype=torch.float16).cuda()
export(ImageEncoder(model), dummy_image, "image_input", "image_clip_fp8")
模型组件 FP8 Modelopt 检查点 FP16 HuggingFace 检查点 尺寸缩小
CLIP 文本编码器 ONNX 156 MB 237 MB 增加 34%
CLIP 图像编码器 ONNX 292 MB 582 MB 增加 50%
表 1. CLIP ONNX 模型大小:FP8 与 FP16

表 1 比较了 FP8 ModelOpt 检查点导出的 ONNX 文件大小与原始 FP16 HuggingFace 检查点导出的 ONNX 文件大小。FP8 检查点文件导出会生成更小的 ONNX 文件,其中文本编码器的文件大小约为 34%,图像编码器的文件大小约为 50%。

请注意,压缩 ONNX 文件是一种方便,而非必要条件。TensorRT 会在引擎构建时将权重侧 Q 节点折叠到 FP8 权重中。ModelOpt ONNX 导出工具会在 ONNX 侧折叠之前的内容,以减小磁盘上的文件。

我们可以使用 NVIDIA Nsight 深度学习设计器,用于 ONNX 模型编辑、性能分析和 TensorRT 引擎构建的高效工具 检查导出的 ONNX 文件。

图 2 显示了在 Nsight Deep Learning Designer 中可视化的导出 ONNX 图形的一部分。我们可以看到,该图现在包含 QuantizeLinear/ DequantizeLinear (Q/ DQ) 节点,标记了 FP8 边界。

在构建引擎期间,TensorRT 会将这些节点与相邻层融合,以优化推理性能。这种融合消除了不必要的量化 – 再量化 – 量化转换,支持使用优化的 FP8 内核进行计算。

使用 TensorRT 分析 ONNX 模型

导出 FP8 ONNX 模型后,下一步是将其传递给 TensorRT 并测量其运行速度。在开始之前,请按照本教程的操作,确保正确下载并安装 TensorRT。准备就绪后,我们将使用 trtexec (TensorRT 命令行包装器) ,使用以下命令对 ONNX 模型进行基准测试:

# Set up the TensorRT environment
export PATH=<TensorRT-${version}/bin>:$PATH
export LD_LIBRARY_PATH=<TensorRT-${version}/lib>:$LD_LIBRARY_PATH
# Benchmark the ONNX model with trtexec
trtexec --onnx=text_clip_fp8.onnx \
        --shapes=text_input:128x77 \
        --stronglyTyped \
        --saveEngine=text_clip_fp8.plan
trtexec --onnx=image_clip_fp8.onnx \
        --shapes=image_input:128x3x224x224 \
        --stronglyTyped \
        --saveEngine=image_clip_fp8.plan
  • --onnx 指定 TensorRT 用于构建引擎的输入 ONNX 模型。
  • --shapes 会固定输入形状,以便 TensorRT 针对该确切大小构建经过优化的引擎。
  • --stronglyTyped 强制 TensorRT 遵守 ModelOpt 烘焙到 ONNX 图形中的精度标注,确保 FP8 权重和激活函数在 FP8 中实际执行。
  • --saveEngine 将构建的 TensorRT 引擎写入磁盘,以便以后重用,无论是用于独立的 TensorRT 推理运行时,还是通过 NVIDIA Triton 推理服务器 提供服务 (请参阅此示例 )。

需要注意的是:ModelOpt 的导出器将注意力缩放封装在 FP32 往返行程中,而 --stronglyTyped 会拒绝 (您可能会看到 Float vs Half 类型不匹配错误) 。在进行 trtexec 基准测试之前,我们投射这些缩放常量,并将运算结果投射回 FP16,以获得清晰、强类型的引擎。

# Re-type all FP32 initializers and Cast(to=FP32) ops in the FP8 ONNX to FP16.
import numpy as np
import onnx
from onnx import TensorProto, numpy_helper, shape_inference
model = onnx.load("clip_fp8.onnx")
for init in model.graph.initializer:
    if init.data_type == TensorProto.FLOAT:
        arr = numpy_helper.to_array(init).astype(np.float16)
        init.CopyFrom(numpy_helper.from_array(arr, name=init.name))
for node in model.graph.node:
    if node.op_type == "Cast":
        to_attr = next(a for a in node.attribute if a.name == "to")
        if to_attr.i == TensorProto.FLOAT:
            to_attr.i = TensorProto.FLOAT16
model = shape_inference.infer_shapes(model, data_prop=True, check_type=False)
onnx.save(model, "clip_fp8_strongtyped.onnx")

或者,我们还可以按照官方用户指南中的性能分析部分,使用 Nsight Deep Learning Designer 使用 TensorRT 分析 ONNX 模型。请参考官方用户指南

我们使用 trtexec 命令在 TensorRT 10.16 的 NVIDIA RTX 6000 Ada GPU 上运行基准测试,静态批量大小为 128。每个报告的延迟是默认测量窗口内所有推理迭代的中值。请注意,FP8 仅适用于 Ada 及更高架构 (计算能力 8.9 或更高) 上的矩阵乘法 (GEMM) 。如需详细了解哪些 GPU 支持哪些数据类型,请参阅 TensorRT 支持矩阵

图 3 显示了在 TensorRT 引擎大小和推理延迟方面,FP8 量化相较于 FP16 的优势。左侧的图像编码器从 588 MB 缩小到 306 MB (减少 48%) ,文本编码器从 238 MB 缩小到 156 MB (减少 34%) ,将磁盘总占用空间减少近一半。由于较小的引擎需要更少的内存来加载和运行,因此在推理时,GPU VRAM 的使用也节省了同样的成本。

在右侧,延迟案例同样引人入胜。图像编码器从 166.2 毫秒降至 119.8 毫秒,文本编码器从 13.2 毫秒降至 9.1 毫秒,图像和文本的速度分别提高了 1.39 倍和 1.45 倍。

FP8 加速究竟来自哪里?除了原始数据 trtexec 报告之外,Nsight Deep Learning Designer 还提供了更丰富的视觉分解,为我们提供了一个清晰的答案。

图 4 将 FP16 和 FP8 图像编码器配置文件并排放置,其中有三个区别显而易见。

  1. GEMM 柱形图从约 1.8 毫秒降至 0.84 毫秒,在主要的 Matmul 层 (由 NVIDIA RTX 6000 Ada GPU 的 FP8 Tensor Core 内核提供) 上,速度提升了 2 倍以上。
  2. FP16 配置文件中可见的“融合”层类别在 FP8 配置文件中消失了,因为 TensorRT 现在通过专用的 FP8 MHA 内核来路由整个注意力块,并生成更精简的执行路径。
  3. 精确的甜甜圈会从橙色为主 (FP16) 的图形转变为紫色为主 (FP8) 的图形。这些信号证实了我们的量化权重和激活函数在 FP8 Tensor Core 上运行,这正是 FP8 增益的来源 — — 在每个矩阵密集型步骤中,更高的计算吞吐量和更低的内存带宽占用。

量化在 TensorRT 中的工作原理

导入 ONNX 模型时,TensorRT 会查找 QuantizeLinear/ DequantizeLinear (Q/ DQ) 节点,这些节点会标记图中张量在全精度和低精度数据类型 (如 FP8) 之间转换的点。

在内部,TensorRT 要求每个可量化层的每个输入都有一个 Q/ DQ 层对。在引擎构建时,优化器将这些 Q/ DQ 节点融合到其相邻层中,并将原始层替换为直接在低精度张量上运行的专用内核。这消除了往返量化 – 再 – 去量化过渡,并使引擎能够以更高的计算吞吐量和更低的内存带宽执行任务。

图 5 显示了 FP8 GEMM 的这种转换。在导出的 ONNX 中,激活 \(x_f\) 和权重张量都封装在 QuantizeLinear/ DequantizeLinear 对中,在 TensorRT 的优化器融合它们后,剩下的是单个 FP8 GEMM 内核,该内核直接接受 FP8 量化激活和预存储的 FP8 权重张量。

如需深入了解 TensorRT 量化机制,请参阅文档

开始使用

在本文中,我们介绍了用于部署量化模型的完整 ModelOpt → ONNX → TensorRT 工作流。我们使用 Q/ DQ 节点将 CLIP 检查点导出到 ONNX,构建了 TensorRT 引擎,并使用 trtexec 和 Nsight 深度学习设计器根据 FP16 基准对其进行了基准测试。

结果表明,与 RTX 6000 Ada GPU 上的原始 FP16 模型相比,FP8 量化可显著提高速度和显存占用。我们还简要介绍了 TensorRT 如何在构建时将 Q/ DQ 节点融合到专用的低精度内核中,从而实现这些收益。

试用 NVIDIA Model OptimizerNVIDIA TensorRT,探索模型量化可以带来的效率提升。

标签