3. 量化工具与框架:TensorRT、ONNX Runtime、PyTorch

做模型量化,光懂理论远远不够。

你得有趁手的工具。市场上量化工具不少,但我个人最常用的,就是这三板斧:TensorRT、ONNX Runtime 和 PyTorch 自带工具。它们各有各的脾气,也各有各的绝活。

3.1 先看一张总览图

在聊细节之前,我先把它们的定位理清楚。下图展示了三者的核心关系。

量化工具与框架总览 TensorRT GPU 专用 Blackwell FP8 ONNX Runtime 跨平台 CPU/GPU 通用 PyTorch (FX/量化) 训练后量化/感知量化 核心能力 INT8/FP8 算子融合优化 动态形状支持 延迟最低 动态量化 静态量化 QAT 支持 CPU/GPU 通用 FX 图模式 Eager 模式 QAT 自定义 灵活性强 适用场景:云端 GPU 推理 → 跨平台部署 → 研究与自定义量化

3.2 TensorRT 量化:GPU 上的极致性能

如果你追求推理速度,那 TensorRT 就是绕不开的选择。我最早接触 TensorRT 是在一个视频分析项目里,模型死活跑不到 30fps,量化做完,直接飙到 90fps。差距就是这么大。

3.2.1 核心思路

TensorRT 量化走的是 校准(Calibration) 路线。你给它几百张代表性图片,它自己统计每层激活值的分布,然后算出最优的 INT8 缩放系数和零点。这比粗暴的 min-max 要好得多。

我记得有一次,我用 500 张校准图就搞定了,精度损失不到 0.5%。关键是校准集要覆盖真实场景。你拿高清写真图去校准,结果部署时全是监控视频,那精度大概率会崩。

3.2.2 基本流程

TensorRT 做 INT8 量化,步骤不复杂。我给出一个典型的代码骨架:

// 1. 构建推理引擎,指定精度为 INT8
IBuilder* builder = createInferBuilder(gLogger);
IBuilderConfig* config = builder->createBuilderConfig();
config->setFlag(BuilderFlag::kINT8);

// 2. 设置校准器(IInt8Calibrator)
//    这里需要自己实现,核心是提供校准数据和缓存
// 来自定义校正器
class MyCalibrator : public IInt8EntropyCalibrator2 {
    bool getBatch(void* bindings[], const char* names[], int nbBindings) override {
        // 填充校准数据到 bindings,返回 false 表示结束
        return true;
    }
    const void* readCalibrationCache(size_t& length) override { return nullptr; }
    void writeCalibrationCache(const void* cache, size_t length) override {}
};

// 3. 创建网络定义,导出量化引擎
INetworkDefinition* network = builder->createNetworkV2(0);
// 添加网络层...
auto calibrator = new MyCalibrator();
auto engine = builder->buildSerializedNetwork(*network, *config);
我的习惯: 校准批次大小不要太大,16 或 32 就够了。校准图太多反而会引入噪声,我一般只选 200-1000 张。

3.2.3 避坑指南

我曾经在一个 NLP 模型上做 INT8 量化,结果精度直接掉了 3 个点。反复排查后发现是校准集大小写错了,只用了 10 张图。校准集必须能代表真实数据的分布,不是随便拿几张就算完。

还有一点:TensorRT 对动态形状支持有限。如果模型输入尺寸频繁变化,量化后的引擎可能报错。要么固定输入尺寸,要么用多 Profile 方案。

3.3 ONNX Runtime 量化:跨平台的稳健选择

如果你要在 CPU 上跑,或者需要同时支持多平台,那 ONNX Runtime 更合适。它的量化工具集成在 onnxruntime-tools 和 onnxruntime-quantization 里。

3.3.1 两种模式

量化模式 适用硬件 精度损失 推荐场景
动态量化(Dynamic) CPU 优先 较小(0.5% - 1%) 快速验证、语言模型
静态量化(Static) CPU / 部分 GPU 更小(< 0.5%) 算力充足、精度敏感任务

我一般先用动态量化跑一遍,看看效果。如果精度掉得厉害,再试试静态量化。动态量化不需要校准数据,改几行代码就能用,性价比很高。

3.3.2 实操:动态量化

动态量化的代码极其简洁。说白了,就是对权重做 INT8,激活值还保持 FP32 或 BF16。

import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType

model_fp32 = "model.onnx"
model_int8 = "model_int8.onnx"

quantize_dynamic(
    model_input=model_fp32,
    model_output=model_int8,
    weight_type=QuantType.QInt8
)
print("动态量化完成!")

这就完了。你想想看,就这么几行代码,模型大小直接减到 1/4,推理速度快 2-3 倍。不过要注意,动态量化本质上是 weight-only,算力瓶颈在计算上,所以实际加速可能不如静态量化明显。

3.3.3 实操:静态量化

静态量化需要校准数据。它会在推理前跑一遍校准,收集激活值的范围,然后生成量化表。

import onnx
from onnxruntime.quantization import quantize_static, QuantType, CalibrationDataReader

# 自己写一个数据读取器
class MyDataReader(CalibrationDataReader):
    def get_next(self):
        # 返回校准数据,格式为 dict: {input_name: np_array}
        return {"input": your_calib_data}
    def __init__(self):
        self.iterator = iter(your_dataset)

# 触发静态量化
quantize_static(
    model_input="model.onnx",
    model_output="model_static_int8.onnx",
    calibration_data_reader=MyDataReader(),
    quant_format=QuantType.QInt8,
    per_channel=True
)
注意: ONNX Runtime 的静态量化对算子覆盖有限。如果模型中含自定义算子,可能会跳过量化。我用过一个带有 Gather 算子的 BERT 模型,部分层落灰了。解决方案是手动标记要走 INT8 的节点。

3.4 PyTorch 量化:研究者的好工具

如果你在做模型压缩研究,或者需要精细控制每个量化参数,PyTorch 自带的量化工具是最灵活的。它支持两种模式:Eager 模式 和 FX 图模式。

3.4.1 FX 图模式

我个人强烈推荐 FX 图模式。它做了一次图捕获,把整个计算图转换成统一表示,然后自动插入量化/反量化节点。你不需要手动改动每一层。

import torch
import torch.quantization as quant

model_fp32 = MyModel().eval()

# 准备:配置量化后端
model_prepared = quant.prepare(
    model_fp32,
    inplace=False,
    backend='fbgemm'  # x86 用 fbgemm,arm 用 qnnpack
)

# 校准:跑几个 batch,收集统计量
for images in calib_loader:
    model_prepared(images)

# 转换:执行量化
model_int8 = quant.convert(model_prepared, inplace=False)
print("FX 图模式量化完成!")

为什么好用?你仔细看,这里没有一句 for 循环去手动改层。FX 会自动分析模型结构,识别哪些层可以融合,哪些层需要跳过。它聪明着呢。

3.4.2 Eager 模式:老派但可控

Eager 模式需要你手动在模型前向中定义量化节点。适合你对每个模块有精确控制。

import torch
import torch.nn as nn
import torch.quantization as quant

class QuantModel(nn.Module):
    def __init__(self, model):
        super().__init__()
        self.quant = quant.QuantStub()
        self.dequant = quant.DeQuantStub()
        self.model = model

    def forward(self, x):
        x = self.quant(x)        # 输入量化
        x = self.model(x)        # 推理在 INT8 中
        x = self.dequant(x)      # 输出反量化
        return x

嗯,这样写起来确实有点原始,但如果你需要对特定层手动设置量化参数,Eager 模式更顺手。

3.4.3 QAT(量化感知训练)

如果 Post-Training Quantization 掉精度,你可以试试 QAT。PyTorch 的 QAT 集成得很好。

# 准备 QAT 模型
model_qat = quant.prepare_qat(model_fp32, inplace=False)

# 微调几个 epoch
for images, labels in train_loader:
    output = model_qat(images)
    loss = criterion(output, labels)
    loss.backward()
    optimizer.step()

# 转换
model_int8 = quant.convert(model_qat, inplace=False)

我记得在一个人脸识别模型上,PTQ 掉了 2 个点,QAT 只掉了 0.2 个点。多花两天时间微调,值了。

3.5 如何选?一张决策表

你的需求 推荐工具 原因
极致 GPU 推理延迟 TensorRT 算子融合 + FP8/INT8 原生支持
跨平台 CPU 部署 ONNX Runtime 动态量化开箱即用,跨平台 API 统一
研究/自定义量化策略 PyTorch FX 灵活的图操作和自定义量化节点
快速验证量化效果 ONNX Runtime 动态量化 只需加 2 行代码
精度敏感、需要微调 PyTorch QAT 精度恢复效果最好

没有银弹。我的建议是:先用 ONNX Runtime 动态量化跑一遍,看基线。如果精度能接受,顺便减一下模型大小。然后再拿 TensorRT 做 GPU 上的极致优化。至于 PyTorch,适合你在研究阶段就规划好量化策略。

一句话总结:工具是死的,业务场景是活的。挑顺手的上。