第四章:ONNX与模型解析
好,咱们进入第四章。这一章我打算聊聊ONNX和模型解析。说实话,很多做TensorRT部署的朋友,最后卡住的地方往往不是推理优化本身,而是模型根本解析不过去。嗯,ONNX就是那个中间桥梁,桥要是塌了,后面再好的优化也白搭。
4.1 ONNX基础:你总得知道它长什么样
ONNX,全称Open Neural Network Exchange。说白了,它就是AI模型界的“通用语言”。你想想看,PyTorch训练出来的模型,TensorRT不认识;TensorFlow的模型,ONNX Runtime也不一定直接吃。那怎么办?大家约定一个中间格式,ONNX就是这个格式。
我个人习惯把ONNX理解成一个“计算图描述文件”。它里面记录了:
- 计算节点(Node):比如Conv、Relu、Add这些算子
- 张量(Tensor):节点之间的数据流动
- 图结构(Graph):节点怎么连在一起的
- 元数据(Metadata):模型版本、作者、opset版本等
这里有个小细节,opset版本特别重要。我在项目中遇到过好几次,PyTorch导出的ONNX用了opset 17,但TensorRT只支持到opset 13,结果解析直接报错。所以导出时我一般会指定opset版本,比如:
torch.onnx.export(
model,
dummy_input,
"model.onnx",
opset_version=13, # 我习惯用13,兼容性好
input_names=["input"],
output_names=["output"]
)
4.2 使用ONNX Runtime验证模型
模型导出成ONNX之后,第一件事不是扔给TensorRT,而是先用ONNX Runtime跑一遍。为什么?因为你要确认导出的ONNX本身没问题。我曾经有一次,模型导出时dynamic axes设置错了,结果TensorRT解析一直报shape mismatch,查了两天才发现是ONNX本身就有问题。
验证代码其实很简单:
import onnxruntime as ort
import numpy as np
# 创建推理会话
sess = ort.InferenceSession("model.onnx")
# 获取输入输出信息
input_name = sess.get_inputs()[0].name
output_name = sess.get_outputs()[0].name
# 构造假数据
dummy_input = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 跑一次推理
output = sess.run([output_name], {input_name: dummy_input})
print("ONNX Runtime推理成功,输出shape:", output[0].shape)
这里我特别强调一点:输入数据的dtype一定要和ONNX模型要求的一致。很多人在这一步翻车,明明模型要求float32,你塞了个float64进去,ONNX Runtime不会报错,但TensorRT会。
4.3 TensorRT解析ONNX的流程
好,现在ONNX验证通过了,咱们来看看TensorRT是怎么吃下这个ONNX的。流程其实不复杂,我拆成几步讲:
- 读取ONNX文件:TensorRT调用ONNX解析器(onnxparser)读取.proto文件
- 构建计算图:解析器把ONNX的节点映射成TensorRT的ILayer
- 优化与编译:TensorRT对图进行融合、精度校准、内存优化
- 生成引擎:最终生成一个序列化的TensorRT引擎文件(.trt或.engine)
代码实现大概是这样的:
import tensorrt as trt
# 创建logger和builder
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
# 创建网络定义
network = builder.create_network(
1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
)
# 创建ONNX解析器
parser = trt.OnnxParser(network, logger)
# 解析ONNX文件
with open("model.onnx", "rb") as f:
if not parser.parse(f.read()):
for error in range(parser.num_errors):
print(parser.get_error(error))
raise RuntimeError("ONNX解析失败")
# 构建引擎
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30) # 1GB
engine = builder.build_serialized_network(network, config)
4.4 TensorRT解析ONNX的限制
说到限制,这才是本章的重点。TensorRT不是万能的,它解析ONNX时有几个硬伤:
| 限制类型 | 具体表现 | 我的建议 |
|---|---|---|
| 算子支持 | 某些ONNX算子TensorRT不支持 | 用自定义插件(后面章节会讲) |
| 动态shape | 部分动态操作解析失败 | 尽量固定shape,或用优化profile |
| 控制流 | If、Loop等节点解析困难 | 导出时禁用控制流,或用onnx-simplifier |
| 数据类型 | 不支持double、int64等类型 | 导出时强制转float32 |
我举个例子。有一次我导出一个BERT模型,里面有个Gather操作,ONNX Runtime跑得好好的,但TensorRT解析时报错:"Unsupported ONNX node type: Gather"。后来查了半天,发现是Gather的indices是int64类型,TensorRT只支持int32。解决办法就是在导出前把indices转成int32。
python -m onnxsim model.onnx model_sim.onnx。建议每次导出后都跑一遍。
4.5 实战经验总结
最后,我分享几个实战中总结的经验:
- 先跑ONNX Runtime:这是最低成本的验证方式
- 用Netron可视化:导出ONNX后,用Netron打开看看图结构,一眼就能发现异常节点
- 关注opset版本:TensorRT 8.x一般支持opset 13-16,太新或太旧都可能出问题
- 不要迷信一键转换:很多模型需要手动调整才能被TensorRT正确解析
嗯,这一章就到这里。ONNX解析是TensorRT开发的第一道坎,跨过去之后,后面的优化工作才会顺利。下一章咱们聊聊自定义插件,那才是真正展示技术的地方。