4、TensorRT基础:TensorRT简介、推理引擎构建流程、Engine文件序列化与反序列化
各位同学,欢迎来到第四章。前面我们聊了Occupancy网络的理论和模型设计,但说实话,模型再牛,跑不起来也是白搭。今天咱们就进入实战环节的核心——TensorRT。
TensorRT是什么?说白了,它就是NVIDIA给自家GPU量身定做的一套推理加速引擎。我最早接触它是在一个自动驾驶的落地项目上,当时模型在PyTorch里跑得挺欢,一上Jetson Orin就卡成PPT。后来用了TensorRT,推理速度直接翻了3倍。嗯,从那天起我就知道,这玩意儿是嵌入式部署的必修课。
4.1 TensorRT简介:它到底干了什么?
TensorRT不是一个训练框架,它只负责推理。它的核心思想就四个字:极致优化。
你想想看,一个训练好的模型,里面有很多冗余计算。比如某些层的权重是0,或者某些算子可以合并。TensorRT就是把这些没用的东西砍掉,把能合并的合并,再配合GPU的硬件特性,让模型跑得飞快。
具体来说,它做了这几件事:
- 层融合(Layer Fusion):把连续的卷积、BN、ReLU合并成一个算子。我在项目中遇到过,一个ResNet50的模型,光这一步就能减少30%的显存访问。
- 精度校准(Precision Calibration):支持FP16和INT8推理。FP16基本无损,INT8需要做校准,但速度提升非常明显。
- 内存优化:TensorRT会复用显存,减少频繁申请释放的开销。
- 内核自动调优:它会根据你的GPU型号,自动选择最优的CUDA kernel。
重要提醒:TensorRT只支持推理,不支持训练。你需要在PyTorch或TensorFlow里把模型训好,然后导出成ONNX或直接使用TensorRT的API来构建引擎。
4.2 推理引擎构建流程:从模型到引擎
构建一个TensorRT引擎,流程其实不复杂。我习惯把它分成三步:模型解析 → 网络定义 → 引擎构建。咱们一步步来看。
4.2.1 第一步:模型解析
这一步是把你的模型文件读进来。TensorRT支持两种方式:
- ONNX解析:最推荐的方式。几乎所有主流框架都能导出ONNX,通用性最强。
- TensorRT的Parser:比如CaffeParser、UffParser。现在用得少了,除非你维护老项目。
我个人习惯用ONNX。为什么呢?因为ONNX就像模型界的“普通话”,不管你是PyTorch还是TensorFlow,都能转成ONNX,然后TensorRT一把梭。我曾经踩过一个坑,用PyTorch直接转TensorRT,结果某些算子不支持,折腾了两天。后来老老实实走ONNX路线,半小时搞定。
4.2.2 第二步:网络定义
解析完成后,TensorRT会生成一个INetworkDefinition对象。这个对象里包含了网络的所有层、权重、输入输出信息。
这里有个关键点:你需要显式地设置输入输出的名称和维度。尤其是动态batch的情况,一定要在构建时指定好优化profile。
// 伪代码示例:构建网络定义
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
// 解析ONNX模型
parser->parse(modelFile, *network);
// 设置输入维度(动态batch示例)
auto input = network->getInput(0);
input->setDimensions(Dims4{-1, 3, 224, 224}); // -1表示动态维度
小技巧:如果你不确定网络定义是否正确,可以用network->getNbLayers()打印层数,跟原始模型对比一下。我经常这么干,能快速发现解析问题。
4.2.3 第三步:引擎构建
这是最耗时的一步。TensorRT会根据你的网络定义和配置,生成一个优化后的引擎。
配置项里最重要的几个:
| 配置项 | 说明 | 我的建议 |
|---|---|---|
| 工作空间大小 | 构建时允许使用的最大显存 | 设成1GB左右,太小会导致构建失败 |
| 精度模式 | FP32、FP16、INT8 | 嵌入式设备优先FP16,性能与精度平衡 |
| 动态形状 | 支持不同输入尺寸 | Occupancy网络一般固定输入,不用开 |
// 伪代码示例:构建引擎
IBuilderConfig* config = builder->createBuilderConfig();
config->setMemoryPoolLimit(MemoryPoolType::kWORKSPACE, 1 << 30); // 1GB
config->setFlag(BuilderFlag::kFP16); // 开启FP16
IHostMemory* serializedModel = builder->buildSerializedNetwork(*network, *config);
构建时间取决于模型大小和GPU性能。我见过一个Occupancy网络,在RTX 3090上构建花了3分钟,在Jetson Orin上花了15分钟。所以,千万不要在设备上现场构建,一定要提前在PC上构建好,然后序列化保存。
4.3 Engine文件序列化与反序列化
为什么要序列化?原因很简单:构建引擎太慢了。你想想看,如果每次启动程序都要等15分钟,用户不骂娘才怪。
序列化就是把构建好的引擎保存成一个文件(通常是.engine或.trt)。反序列化就是把这个文件读回来,直接使用。
4.3.1 序列化:保存引擎
构建引擎时,我们调用了buildSerializedNetwork,它返回一个IHostMemory对象。这个对象里就是序列化后的二进制数据。我们把它写到文件里就行。
// 保存引擎到文件
std::ofstream engineFile("model.engine", std::ios::binary);
engineFile.write(static_cast<const char*>(serializedModel->data()),
serializedModel->size());
engineFile.close();
这里有个细节:序列化后的引擎是跟GPU型号绑定的。你在RTX 3090上生成的引擎,不能直接拿到Jetson Orin上用。我曾经犯过这个错误,在PC上生成引擎,拷到Jetson上直接崩溃。排查了半天才发现是架构不兼容。
避坑指南:序列化引擎只能在相同GPU架构上使用。比如Ampere架构(RTX 30系列)的引擎不能用于Turing架构(RTX 20系列)。建议在目标设备上重新构建,或者使用TensorRT的兼容性工具检查。
4.3.2 反序列化:加载引擎
反序列化就简单多了。把文件读进来,传给runtime->deserializeCudaEngine就行。
// 从文件加载引擎
std::ifstream engineFile("model.engine", std::ios::binary);
engineFile.seekg(0, std::ios::end);
size_t fileSize = engineFile.tellg();
engineFile.seekg(0, std::ios::beg);
std::vector<char> engineData(fileSize);
engineFile.read(engineData.data(), fileSize);
engineFile.close();
// 反序列化
IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(engineData.data(), fileSize);
反序列化很快,基本是毫秒级的。加载完成后,你就可以创建IExecutionContext来执行推理了。
4.3.3 序列化与反序列化的最佳实践
根据我的经验,这里有几个要点:
- 版本一致性:构建和反序列化用的TensorRT版本必须一致。我见过有人用TensorRT 8.5构建,用8.4加载,结果报错。升级版本后一定要重新构建。
- 文件校验:建议在保存引擎时,同时保存一个MD5校验值。加载时先校验,防止文件损坏。我在一个量产项目上遇到过SD卡坏道导致引擎文件损坏的情况,加了校验后问题立刻定位。
- 多设备部署:如果你要部署到多台设备上,建议每台设备单独构建引擎。虽然麻烦,但能避免兼容性问题。
4.4 本章小结
好了,这一章的内容就这些。咱们回顾一下:
- TensorRT是NVIDIA的推理加速引擎,核心是层融合、精度校准和内存优化。
- 构建引擎分三步:解析模型、定义网络、构建引擎。构建过程很慢,要提前做。
- 序列化保存引擎文件,反序列化快速加载。注意引擎跟GPU架构绑定,不能混用。
下一章,我们会把Occupancy网络完整地走一遍部署流程。从ONNX导出到TensorRT推理,每一步我都会带着你实操。到时候你会发现,前面这些基础打好了,后面就顺风顺水了。
有什么问题,欢迎在课程群里交流。咱们下章见!