4、模型转换与量化:TensorRT、ONNX Runtime、TFLite 的部署流程,FP16/INT8 量化实战
好,咱们进入正题。模型训练完了,精度看着也不错,但离真正跑在嵌入式设备上,还差最关键的一步——部署。说白了,就是把训练好的“大胖子”模型,变成能在目标硬件上高效运行的“精干小伙”。这一步,我称之为“模型落地前的最后一公里”。
今天咱们就聊聊三个主流的部署工具:TensorRT、ONNX Runtime 和 TFLite。它们各有各的脾气,也各有各的绝活。我会结合 FP16 和 INT8 量化,把整个流程拆开揉碎了讲给你听。
4.1 为什么需要模型转换与量化?
你想想看,训练时我们用 PyTorch 或 TensorFlow,模型里存的参数都是 FP32 的。FP32 精度高,但体积大、计算慢。在嵌入式设备上,内存和算力都金贵得很。
模型转换,就是把模型从训练框架的格式,转成目标推理引擎能识别的格式。比如从 .pt 转成 .onnx,或者 .engine。
量化,则是把 FP32 的权重和激活值,用更低位宽的数据类型来表示。比如 FP16(16位浮点)或者 INT8(8位整型)。
核心收益:
- 体积缩小:FP32 转 INT8,模型体积直接缩到原来的 1/4。
- 速度提升:INT8 计算比 FP32 快 2-4 倍,在专用硬件上甚至更快。
- 功耗降低:更少的计算量,意味着更低的功耗,这对电池供电的设备至关重要。
嗯,这里要注意,量化不是免费的午餐。精度会有损失,关键看你怎么平衡。
4.2 TensorRT:NVIDIA 的看家本领
如果你用的是 NVIDIA 的 Jetson 系列,或者带 GPU 的嵌入式板卡,那 TensorRT 几乎是绕不开的选择。我个人习惯,只要项目里用了 NVIDIA 的硬件,推理引擎首选就是 TensorRT。
TensorRT 的牛逼之处在于,它不只是做量化,还会做图优化、层融合、内核自动调优。说白了,它把你的模型从头到脚重新编排了一遍,让 GPU 跑得最舒服。
4.2.1 转换流程
从 PyTorch 模型到 TensorRT 引擎,我一般走这条路:
- 导出 ONNX:先把 PyTorch 模型转成 ONNX 格式。这是中间桥梁。
- 构建 TensorRT 引擎:用 TensorRT 的 Python API 或者 trtexec 工具,读取 ONNX 模型,指定精度(FP32、FP16 或 INT8),然后构建引擎。
- 序列化与反序列化:构建好的引擎可以保存成 .engine 文件。部署时直接加载,省去每次重新构建的时间。
这里有个坑,我曾经踩过。ONNX 导出时,如果模型里有动态尺寸的输入,比如 batch size 不固定,需要显式指定动态轴。否则 TensorRT 构建时会报错。
避坑指南:我曾经在导出 ONNX 时忘了设置 dynamic_axes,结果 TensorRT 构建死活过不去。后来查了半天才发现,ONNX 默认是静态尺寸。记得在 torch.onnx.export 里加上 dynamic_axes 参数。
4.2.2 FP16 量化实战
FP16 量化是最简单的。在构建 TensorRT 引擎时,指定精度为 FP16 即可。几乎不需要额外操作,精度损失通常很小。
import tensorrt as trt
# 创建 builder 和 network
builder = trt.Builder(TRT_LOGGER)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
# 解析 ONNX 模型
parser = trt.OnnxParser(network, TRT_LOGGER)
with open(onnx_model_path, 'rb') as f:
parser.parse(f.read())
# 配置 builder
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30) # 1GB
# 关键:启用 FP16
if builder.platform_has_fast_fp16:
config.set_flag(trt.BuilderFlag.FP16)
# 构建引擎
engine = builder.build_serialized_network(network, config)
你看,核心就一行代码:config.set_flag(trt.BuilderFlag.FP16)。但要注意,不是所有 GPU 都支持 FP16 加速。Jetson 系列基本都支持,但老一点的桌面 GPU 可能不行。
4.2.3 INT8 量化实战
INT8 量化就复杂多了。它需要一个“校准数据集”,用来统计激活值的分布,从而找到最佳的量化参数。
我一般准备几百张有代表性的图片,不需要标注,只要覆盖模型可能遇到的各种场景就行。
import tensorrt as trt
from calibrator import EntropyCalibrator2 # 自定义校准器
# 创建校准器
calibrator = EntropyCalibrator2(calibration_data, batch_size=32)
# 配置 builder
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)
# 启用 INT8
if builder.platform_has_fast_int8:
config.set_flag(trt.BuilderFlag.INT8)
config.int8_calibrator = calibrator
# 构建引擎
engine = builder.build_serialized_network(network, config)
我的经验:校准数据集的质量直接影响 INT8 量化后的精度。我建议用 500-1000 张图片,覆盖不同光照、角度、背景。如果校准集太单一,量化后的模型在复杂场景下可能会崩。
4.3 ONNX Runtime:跨平台的瑞士军刀
如果你的目标平台不是 NVIDIA,或者你想在 CPU 上跑,那 ONNX Runtime 是个好选择。它支持多种硬件后端,包括 CPU、GPU、甚至一些 NPU。
ONNX Runtime 的部署流程相对简单。你只需要把模型转成 ONNX 格式,然后直接用 ONNX Runtime 加载推理就行。
4.3.1 转换与推理
import onnxruntime as ort
# 创建推理会话
session = ort.InferenceSession("model.onnx")
# 获取输入输出名称
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
# 准备输入数据
import numpy as np
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 推理
result = session.run([output_name], {input_name: input_data})
你看,代码非常简洁。但 ONNX Runtime 的量化,需要借助 onnxruntime.quantization 工具包。
4.3.2 量化实战
ONNX Runtime 支持静态量化和动态量化。静态量化需要校准数据,动态量化不需要,但精度稍差。
from onnxruntime.quantization import quantize_static, QuantType
from onnxruntime.quantization import CalibrationMethod
# 静态量化
quantized_model = quantize_static(
model_input="model.onnx",
model_output="model_quantized.onnx",
calibration_data_reader=calibration_data_reader,
quant_format=QuantType.QInt8,
per_channel=True,
calibration_method=CalibrationMethod.MinMax
)
注意:ONNX Runtime 的 INT8 量化,在 CPU 上加速效果明显,但在 GPU 上可能不如 TensorRT。我建议在 CPU 部署时优先考虑 ONNX Runtime 量化。
4.4 TFLite:移动端和边缘设备的王者
如果你的目标是手机、树莓派、或者微控制器,那 TFLite 是首选。它专门为资源受限的设备做了优化。
TFLite 的转换流程,我一般用 TensorFlow 的 TFLiteConverter。
4.4.1 转换流程
import tensorflow as tf
# 加载 TensorFlow 模型
model = tf.saved_model.load("saved_model")
# 创建转换器
converter = tf.lite.TFLiteConverter.from_saved_model("saved_model")
# 设置优化选项
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# 设置代表数据集(用于量化)
def representative_dataset():
for _ in range(100):
data = np.random.randn(1, 224, 224, 3).astype(np.float32)
yield [data]
converter.representative_dataset = representative_dataset
# 指定目标精度
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
# 转换
tflite_model = converter.convert()
# 保存
with open("model_quantized.tflite", "wb") as f:
f.write(tflite_model)
4.4.2 FP16 与 INT8 选择
TFLite 支持 FP16 和 INT8 量化。FP16 量化后模型体积减半,精度损失极小。INT8 量化体积减到 1/4,但精度损失稍大。
| 量化类型 | 体积缩减 | 速度提升 | 精度损失 | 适用场景 |
|---|---|---|---|---|
| FP16 | ~50% | ~1.5x | 极小 | 对精度要求高的场景 |
| INT8 | ~75% | ~2-4x | 中等 | 对速度要求高的场景 |
我的建议:如果设备支持硬件加速(比如手机上的 NPU),优先用 INT8。如果只是 CPU 推理,FP16 可能更稳妥。我一般在项目初期先用 FP16 跑通,再尝试 INT8 看精度能否接受。
4.5 量化后的精度验证
量化不是终点,验证才是。我每次量化完,都会跑一遍完整的测试集,对比量化前后的精度差异。
如果精度下降超过 1-2%,我会考虑:
- 使用更大量的校准数据集
- 尝试不同的校准方法(比如 KL 散度 vs 均方误差)
- 对敏感层进行混合精度量化(部分层用 FP16,部分层用 INT8)
嗯,这里要提醒一句:不要盲目追求 INT8。如果 FP16 已经能满足性能要求,就别折腾 INT8 了。省下来的时间,多喝杯咖啡不香吗?
4.6 总结
模型转换与量化,是嵌入式视觉部署的核心技能。TensorRT 适合 NVIDIA 平台,ONNX Runtime 适合跨平台 CPU 部署,TFLite 适合移动端和边缘设备。
FP16 量化简单高效,INT8 量化需要更多调优。我个人习惯是:先用 FP16 快速验证,再根据精度和速度需求决定是否上 INT8。
记住,部署不是一锤子买卖。你可能需要反复调整量化参数、校准数据集,甚至修改模型结构,才能达到最佳效果。但当你看到模型在设备上流畅运行时,那种成就感,值得。