第4章:ONNX模型导出与标准化

好,咱们进入实战环节的第一个关键步骤——ONNX导出。很多同学在PyTorch里跑得飞起的模型,一到TensorRT就各种报错,十有八九是ONNX这步没处理好。我见过太多人花了两周调精度,最后发现是导出时一个参数设错了。

说白了,ONNX就是PyTorch和TensorRT之间的翻译官。翻译得好不好,直接决定你后面的部署能不能顺利。

4.1 torch.onnx.export核心参数

先看最常用的导出方式。我习惯把参数分成三组:必填项、动态形状相关、算子控制相关。

import torch

# 假设你有一个训练好的模型
model = YourDetectionModel()
model.eval()

# 构造一个 dummy input,形状要和实际推理时一致
dummy_input = torch.randn(1, 3, 640, 640)

torch.onnx.export(
    model,                    # 模型
    dummy_input,              # 示例输入
    "model.onnx",             # 输出路径
    export_params=True,       # 导出训练好的权重
    opset_version=11,         # 算子集版本
    do_constant_folding=True, # 常量折叠优化
    input_names=["input"],    # 输入张量名称
    output_names=["output"],  # 输出张量名称
    dynamic_axes={            # 动态轴设置
        "input": {0: "batch_size"},
        "output": {0: "batch_size"}
    }
)

关键参数速查表

参数推荐值说明
opset_version11 或 13工业检测建议用11,兼容性最好
do_constant_foldingTrue能减少ONNX中的冗余计算节点
export_paramsTrue不导出权重的话,后面没法转TRT
keep_initializers_as_inputsFalse减少输入列表的冗余项

这里有个坑——opset_version不是越高越好。我踩过这个坑:用opset=15导出,TensorRT直接报不支持的算子。后来老老实实退回11,一切正常。工业场景求稳,别追新。

4.2 动态轴与静态轴

工业检测里,输入尺寸经常不固定。比如有的产线拍到的产品大小不一样,你不可能每次都resize到固定尺寸。这时候就需要动态轴。

动态轴说白了就是告诉ONNX:这个维度在推理时可以变化。最常见的动态轴是batch size和图像宽高。

dynamic_axes = {
    "input": {
        0: "batch_size",    # 动态batch
        2: "height",        # 动态高
        3: "width"          # 动态宽
    },
    "output": {
        0: "batch_size",
        2: "height",
        3: "width"
    }
}

注意:动态轴越多,TensorRT优化空间越小。我建议只对必要的维度设动态,其他保持静态。比如你的模型只接受640x640输入,那就只设batch为动态,宽高固定。

为什么会这样?因为TensorRT在做层融合时,需要知道每个张量的确切形状。动态轴意味着它要做更多运行时推断,优化效果自然打折扣。

我个人的经验是:能用静态就别用动态。如果产线图像尺寸变化不大,干脆统一resize到固定尺寸,性能能提升20%以上。

4.3 算子兼容性检查

这是最容易出问题的一环。PyTorch里很多操作在ONNX里没有对应算子,导出时会自动拆成多个基础算子。拆得好还行,拆不好就出幺蛾子。

常见的坑有哪些?

  • torch.where:ONNX里没有直接对应,会拆成Cast + NonZero + Gather,容易出精度问题
  • torch.topk:opset<11时支持不好,建议升到11以上
  • torch.nn.functional.grid_sample:工业检测里做仿射变换常用,ONNX支持但TensorRT支持有限
  • 自定义算子:比如你写了个C++扩展,ONNX根本不认识

检查方法:导出后用onnxruntime跑一遍,对比PyTorch输出。如果误差超过1e-5,说明算子拆解有问题。

import onnxruntime as ort
import numpy as np

# 用ONNX Runtime验证
ort_session = ort.InferenceSession("model.onnx")
ort_inputs = {ort_session.get_inputs()[0].name: dummy_input.numpy()}
ort_outputs = ort_session.run(None, ort_inputs)

# 对比PyTorch输出
with torch.no_grad():
    torch_outputs = model(dummy_input)

# 计算误差
diff = np.abs(ort_outputs[0] - torch_outputs.numpy())
print(f"最大误差: {diff.max():.6f}")

我曾经遇到一个案例:模型里用了F.interpolate做上采样,默认的mode='nearest'在ONNX里没问题,但换成mode='bilinear'后,TensorRT推理结果和PyTorch差了0.5个像素。查了两天才发现是align_corners参数默认值不一致。PyTorch里默认False,但ONNX导出时某些版本会强制转成True。

4.4 导出流程与验证

我总结了一套标准流程,每次导出都按这个来,基本没出过问题。

  1. 模型准备:确保model.eval(),关闭dropout和batch norm的训练模式
  2. dummy input构造:形状、数值范围要和真实数据一致
  3. 导出:按上面的参数设置,opset=11起步
  4. ONNX检查:用onnx.checker.check_model验证结构完整性
  5. 精度验证:用onnxruntime跑推理,对比PyTorch输出
  6. TensorRT初步转换:用trtexec转一次,看有没有报错

避坑指南:我曾经在导出时忘了设model.eval(),结果ONNX里包含了dropout层,推理结果每次都不一样。排查了整整一天才发现这个低级错误。

4.5 知识结构图

下面这张图概括了ONNX导出的核心流程和关键决策点,建议你保存下来,每次导出前对照检查。

ONNX导出决策流程 PyTorch模型 是否需要动态轴? 是 否 设dynamic_axes 固定所有维度 算子兼容性检查 ONNX Runtime精度验证 标准ONNX模型 验证通过后再转TensorRT

4.6 常见问题与解决方案

问题现象原因解决方案
导出时报"Unsupported operator"PyTorch算子没有ONNX映射替换为ONNX支持的算子,或用torch.onnx.register_custom_op
ONNX Runtime推理结果全0export_params=False设置export_params=True
动态轴设置后TensorRT报错某些层不支持动态形状减少动态轴数量,或使用TRT的优化策略
精度误差超过1%算子拆解导致数值差异检查align_corners、eps等参数默认值