3、模型格式与转换工具链:ONNX/TensorRT/TFLite 格式对比、PyTorch 模型导出为 ONNX、ONNX 简化与优化、模型量化原理(INT8/FP16)

模型部署的第一步,就是把训练好的模型「翻译」成目标硬件能听懂的语言。说白了,PyTorch 或 TensorFlow 训练出来的模型,在嵌入式设备上基本没法直接跑。你得先把它转成中间格式,再针对具体芯片做优化。

我个人习惯把这一步叫做「模型格式的巴别塔工程」。不同框架、不同芯片,都有自己的「方言」。ONNX 就是那个通用的世界语,TensorRT 是 NVIDIA 的专属方言,TFLite 则是 Google 为移动端和边缘设备定制的精简版。

3.1 三大主流格式:ONNX / TensorRT / TFLite

先看一张对比表,心里有个底:

特性 ONNX TensorRT TFLite
定位 中间交换格式 NVIDIA GPU 推理引擎 移动端/嵌入式推理格式
核心优势 跨框架互转 极致推理加速 轻量、跨平台
量化支持 仅定义量化接口 INT8/FP16 原生支持 INT8/FP16 原生支持
典型硬件 通用 CPU/GPU Jetson / 桌面 GPU ARM CPU / DSP / NPU
算子覆盖 较全(持续扩展) 有限(需插件支持) 有限(需自定义算子)
调试难度 中等 较高 较低

ONNX 是模型转换的「中间人」。你从 PyTorch 导出 ONNX,再从 ONNX 转成 TensorRT 或 TFLite。它不负责推理加速,只负责格式统一。

TensorRT 是 NVIDIA 的王牌。它会把计算图做层融合、精度校准、内存复用。我在 Jetson Orin 上部署过 YOLOv8,用 TensorRT 做 INT8 量化后,推理速度比原始 PyTorch 快了将近 8 倍。嗯,代价就是调试起来比较折腾——算子不支持就得自己写插件。

TFLite 是 Google 为移动端和嵌入式设备准备的。它支持委托(Delegate)机制,可以把计算卸载到 GPU、DSP 或 NPU 上。我曾在树莓派上用 TFLite 跑过 MobileNet,虽然速度比不上 Jetson,但胜在生态成熟、文档齐全。

我的选择建议:
  • 如果你用 NVIDIA 平台(Jetson / 桌面 GPU),优先走 PyTorch → ONNX → TensorRT 路线。
  • 如果你用 ARM CPU / 高通 / 联发科,优先走 PyTorch → ONNX → TFLite 路线。
  • 如果你只是做原型验证,ONNX Runtime 直接跑 ONNX 模型就够了。

3.2 PyTorch 模型导出为 ONNX

这一步其实不复杂,但坑不少。核心 API 就一个:torch.onnx.export()。

我直接给一个我常用的导出模板:

import torch
import torch.onnx

# 假设你有一个训练好的模型
model = YourModel()
model.load_state_dict(torch.load('model.pth'))
model.eval()

# 构造一个 dummy input(形状要和实际输入一致)
dummy_input = torch.randn(1, 3, 224, 224)

# 导出 ONNX
torch.onnx.export(
    model,                      # 模型
    dummy_input,                # 示例输入
    'model.onnx',               # 输出路径
    export_params=True,         # 导出参数
    opset_version=11,           # 算子集版本(建议 11 或更高)
    do_constant_folding=True,   # 常量折叠优化
    input_names=['input'],      # 输入名称
    output_names=['output'],    # 输出名称
    dynamic_axes={              # 动态轴(可选)
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)

print("ONNX 导出完成!")

这里有几个关键点:

  • opset_version:我建议用 11 或 13。版本太低会缺少一些算子,版本太高有些推理引擎不支持。
  • dynamic_axes:如果你的模型需要支持动态 batch size,一定要配这个。否则导出的 ONNX 会固定 batch size 为 1。
  • do_constant_folding:建议开启。它会把一些常量计算提前算好,减少推理时的计算量。
我曾经踩过的坑:
有一次导出 ONNX 时,模型里用了 torch.nn.functional.interpolate,默认的 align_corners 参数在 ONNX 里不支持。折腾了半天才发现,需要显式设置 align_corners=False 才能导出成功。所以,导出前最好用 torch.onnx.export() 的 check 参数做一次验证。

3.3 ONNX 简化与优化

导出的 ONNX 模型往往包含很多冗余操作。比如,PyTorch 的 nn.Dropout 在推理模式下虽然不生效,但 ONNX 里可能还会保留它的计算图。这时候就需要做简化。

我常用的工具是 onnx-simplifier:

pip install onnx-simplifier

python -m onnxsim model.onnx model_simplified.onnx \
    --input-shape 1,3,224,224 \
    --dynamic-input-shape

它会做以下几件事:

  • 删除无用的节点(比如恒等映射、冗余 reshape)
  • 常量折叠(把能提前算的常量计算掉)
  • 合并相邻的算子(比如 Conv + BN → Conv)

简化后的模型体积通常会缩小 10%~30%,推理速度也有提升。

除了简化,还可以用 onnxruntime 做性能分析:

import onnxruntime as ort

session = ort.InferenceSession('model_simplified.onnx')
# 获取 profiling 数据
session.run_with_ort_format(...)

我个人习惯在简化后,用 onnxruntime 跑一遍推理,对比输出精度。如果精度偏差超过 1%,就要检查是不是简化过程出了问题。

3.4 模型量化原理:INT8 / FP16

量化,说白了就是用更少的数据位宽来表示模型参数和激活值。FP32 是 32 位浮点,FP16 是 16 位浮点,INT8 是 8 位整数。

为什么要量化?

  • 模型体积缩小:FP32 → INT8,体积缩小 4 倍。
  • 推理速度提升:INT8 计算比 FP32 快 2~4 倍(取决于硬件)。
  • 功耗降低:更少的位宽意味着更少的内存访问和计算能耗。

量化的核心公式很简单:

q = round(r / scale) + zero_point

其中 r 是浮点值,scale 是缩放因子,zero_point 是零点偏移。反量化就是逆过程:

r = (q - zero_point) * scale

量化方式主要有两种:

量化方式 原理 精度损失 适用场景
训练后量化(PTQ) 用少量校准数据统计激活值的分布,计算 scale 和 zero_point 较小(通常 < 1%) 大多数场景
量化感知训练(QAT) 在训练过程中模拟量化误差,让模型适应低精度 极小(通常 < 0.5%) 对精度要求极高的场景

FP16 量化相对简单,直接把 FP32 的权重截断为 FP16 即可。INT8 量化则需要做校准,因为 INT8 的动态范围比 FP32 小得多。

我的经验:
  • FP16 量化几乎无损,我通常在 TensorRT 上默认开启。
  • INT8 量化需要选好校准数据集。我一般从训练集中随机抽 500~1000 张图片做校准。如果校准数据分布和实际推理数据差异太大,精度会崩。
  • QAT 虽然精度高,但训练流程复杂。我只有在 PTQ 精度损失超过 2% 时才会考虑 QAT。

3.5 知识体系总览

下面这张图总结了本章的核心流程:

模型格式与转换工具链知识体系 PyTorch 训练模型 torch.onnx.export() ONNX 中间模型 ONNX 简化与优化 TensorRT 推理引擎 TFLite 推理格式 ONNX Runtime 推理 量化:FP16 / INT8 嵌入式设备部署 PTQ / QAT 两种量化策略

从 PyTorch 训练模型开始,经过 ONNX 导出、简化优化,再根据目标硬件选择 TensorRT、TFLite 或 ONNX Runtime,最后做量化部署。每一步都有对应的工具和注意事项。

避坑指南:
  • 导出 ONNX 前,务必用 torch.onnx.export() 的 check 参数做验证。
  • 简化 ONNX 后,一定要对比输出精度,防止简化过程引入误差。
  • INT8 量化时,校准数据集要覆盖实际推理场景的数据分布。
  • 如果遇到算子不支持,先查 ONNX 算子文档,再考虑写自定义算子或插件。