4、ONNX解析与模型导入:ONNX格式简介、使用ONNX Parser导入模型、处理动态输入形状、调试ONNX导入失败问题。
好,咱们进入第四章。这一章聊的是ONNX解析和模型导入。说实话,这是整个TensorRT插件开发里最磨人的环节之一。我见过太多人模型写好了,一跑ONNX解析就崩,然后就开始怀疑人生。别急,咱们一步步来。
4.1 ONNX格式简介:它到底是个啥?
ONNX,全称Open Neural Network Exchange。说白了,它就是深度学习模型的“通用语言”。你想想看,PyTorch训练出来的模型,TensorFlow训练出来的模型,它们各自有各自的格式。ONNX就是那个中间人,让不同框架的模型能互相串门。
我个人习惯把ONNX理解成一个计算图。它里面包含了:
- 节点(Node):每个节点代表一个算子,比如Conv、Relu、Add。
- 张量(Tensor):节点之间的数据流动,就是张量。
- 图(Graph):所有节点和张量组成的网络结构。
举个例子,一个简单的卷积网络,ONNX文件里大概长这样:
graph(%input : Float(1, 3, 224, 224)) {
%conv1_weight : Float(64, 3, 3, 3)
%conv1_bias : Float(64)
%conv1_out = Conv(%input, %conv1_weight, %conv1_bias)
%relu1_out = Relu(%conv1_out)
%pool1_out = MaxPool(%relu1_out)
return %pool1_out
}
嗯,这里要注意。ONNX的算子集是标准化的,但不同框架导出的ONNX可能会有细微差别。我在项目中遇到过,PyTorch导出的ONNX里有些算子名字跟TensorRT的不完全一样,这就得靠后面的插件来兜底了。
4.2 使用ONNX Parser导入模型
TensorRT里有个好东西叫ONNX Parser。它的任务就是把ONNX文件“翻译”成TensorRT能理解的网络结构。用法其实挺简单的:
// 创建builder和network
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
// 创建ONNX parser
nvinfer1::IONNXParser* parser = nvonnxparser::createParser(*network, gLogger);
// 解析ONNX文件
if (!parser->parseFromFile("model.onnx", static_cast<int>(ILogger::Severity::kWARNING))) {
// 解析失败,获取错误信息
std::cout << "Failed to parse ONNX file!" << std::endl;
return -1;
}
你看,核心就三步:建网络、建解析器、解析文件。但这里有个坑——parseFromFile返回false不代表所有算子都失败了。它可能只是某个算子没找到,但其他部分已经解析成功了。我刚开始做的时候,一看到返回false就直接放弃,后来才发现可以继续用已经解析好的部分。
kWARNING或更低。这样你能看到哪些算子被跳过了,哪些被成功转换了。我一般用kINFO,信息更全。
4.3 处理动态输入形状
动态输入形状,说白了就是你的模型输入尺寸不固定。比如你训练时用224x224,但部署时可能遇到192x192或256x256的图片。TensorRT默认是静态的,你得告诉它:“嘿,我这输入是可变的。”
怎么做呢?在ONNX导出时就要设置动态轴:
# PyTorch导出ONNX时设置动态轴
dynamic_axes = {
'input': {0: 'batch_size', 2: 'height', 3: 'width'},
'output': {0: 'batch_size', 2: 'height', 3: 'width'}
}
torch.onnx.export(
model,
dummy_input,
"model_dynamic.onnx",
dynamic_axes=dynamic_axes,
opset_version=11
)
然后在TensorRT里,你得用优化配置文件(Optimization Profile)来指定输入的范围:
// 创建优化配置文件
IOptimizationProfile* profile = builder->createOptimizationProfile();
// 设置输入的最小、最优、最大尺寸
profile->setDimensions("input", OptProfileSelector::kMIN, Dims4(1, 3, 192, 192));
profile->setDimensions("input", OptProfileSelector::kOPT, Dims4(1, 3, 224, 224));
profile->setDimensions("input", OptProfileSelector::kMAX, Dims4(1, 3, 256, 256));
// 把配置文件加到config里
config->addOptimizationProfile(profile);
这里有个关键点:最优尺寸(OPT)决定了TensorRT做算子融合和内存优化的基准。你想想看,如果你设的最优尺寸是224x224,但实际跑的是192x192,性能可能会打折扣。我建议你把最常用的尺寸设成OPT。
4.4 调试ONNX导入失败问题
ONNX导入失败,是每个TensorRT开发者都会遇到的噩梦。我总结了几种常见情况:
| 错误类型 | 典型表现 | 解决方法 |
|---|---|---|
| 算子不支持 | 日志里出现"Unsupported operator XXX" | 写插件,或者用ONNX-Simplifier简化模型 |
| 形状不匹配 | "Tensor shape mismatch" | 检查ONNX导出时的动态轴设置 |
| 版本冲突 | "ONNX opset version too high" | 降低opset版本,一般用11或13比较稳 |
| 常量折叠失败 | "Constant folding failed" | 用onnxruntime跑一遍,看能不能正常推理 |
调试时,我一般会这么做:
- 看日志:把日志级别调到
kVERBOSE,看看到底卡在哪个节点上。 - 用Netron可视化:把ONNX文件拖进Netron,看看结构对不对。我经常发现导出的ONNX里多了一些莫名其妙的节点。
- 分段测试:把模型切成几段,分别导入。这样能快速定位问题出在哪一段。
- 用onnxruntime验证:先用onnxruntime跑一遍ONNX,确认模型本身没问题。如果onnxruntime都跑不通,那肯定是ONNX导出有问题。
避坑指南:我曾经遇到过一个特别诡异的问题——ONNX解析成功,但推理结果全是错的。查了两天,最后发现是输入数据的预处理跟训练时不一致。说白了,ONNX只负责网络结构,不负责数据预处理。你得自己保证输入数据跟训练时一模一样。
嗯,这一章的内容差不多就这些。ONNX解析看起来简单,但实际坑不少。我的建议是:先跑通一个简单的模型,比如一个只有卷积和ReLU的网络,然后再逐步增加复杂度。这样出了问题,你至少知道是哪个新加的算子搞的鬼。
下一章咱们聊聊插件开发的核心——如何写一个自定义算子。到时候我会拿一个实际案例来演示,保证你能跟着做出来。