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 训练模型开始,经过 ONNX 导出、简化优化,再根据目标硬件选择 TensorRT、TFLite 或 ONNX Runtime,最后做量化部署。每一步都有对应的工具和注意事项。
- 导出 ONNX 前,务必用
torch.onnx.export()的check参数做验证。 - 简化 ONNX 后,一定要对比输出精度,防止简化过程引入误差。
- INT8 量化时,校准数据集要覆盖实际推理场景的数据分布。
- 如果遇到算子不支持,先查 ONNX 算子文档,再考虑写自定义算子或插件。