4. TensorRT API入门:创建Runtime和Logger,使用trtexec工具快速转换模型,解析engine文件的序列化与反序列化
好,咱们进入正题。这一章我打算带你亲手摸一摸TensorRT的“门把手”——Runtime、Logger、trtexec工具,还有那个让人又爱又恨的engine文件序列化。
说实话,我刚开始接触TensorRT时,最头疼的就是搞不清这些基础组件到底怎么配合。你想想看,一个推理框架,总得有个“管家”来管日志、有个“调度员”来管执行吧?TensorRT里,Logger就是那个管家,Runtime就是调度员。
4.1 创建Logger和Runtime——你的第一个TensorRT程序
先别急着跑模型,咱们从最基础的开始。创建一个Logger,说白了就是告诉TensorRT:“嘿,你把日志信息往哪儿吐?”
#include <NvInfer.h>
// 创建Logger
class MyLogger : public nvinfer1::ILogger {
void log(Severity severity, const char* msg) noexcept override {
// 只打印WARNING及以上级别的日志
if (severity <= Severity::kWARNING) {
std::cout << "[TensorRT] " << msg << std::endl;
}
}
};
MyLogger gLogger;
// 创建Runtime
nvinfer1::IRuntime* runtime = nvinfer1::createInferRuntime(gLogger);
if (!runtime) {
std::cerr << "Runtime创建失败!" << std::endl;
return -1;
}
这里有个细节要注意:createInferRuntime返回的是一个裸指针。嗯,记得用完要delete,或者直接用智能指针包一下。我见过不少新手在这漏内存,跑一次推理内存涨几MB,跑一天服务器直接挂了。
4.2 trtexec——模型转换的瑞士军刀
trtexec这个工具,说白了就是TensorRT官方给的“一键转换”神器。你不用写一行代码,就能把ONNX、Caffe、UFF这些模型转成TensorRT的engine文件。
我个人习惯在开发阶段先用trtexec快速验证模型能不能转成功,再写C++/Python的部署代码。这样能省下大量调试时间。
4.2.1 基本用法
# 最简单的转换:ONNX -> TensorRT engine
trtexec --onnx=model.onnx --saveEngine=model.engine
# 指定精度和batch size
trtexec --onnx=model.onnx \
--saveEngine=model_fp16.engine \
--fp16 \
--minShapes=input:1x3x224x224 \
--optShapes=input:8x3x224x224 \
--maxShapes=input:16x3x224x224
# 只做推理测试,不保存engine
trtexec --onnx=model.onnx --loadInputs=input.bin
--minShapes和--maxShapes,结果转出来的engine只能跑固定batch size=1。线上服务一来突发流量,batch size一变大,直接报错。所以,动态shape一定要显式指定范围!
4.2.2 常用参数速查表
| 参数 | 作用 | 我的建议 |
|---|---|---|
--onnx |
指定ONNX模型路径 | 最常用,优先用ONNX格式 |
--saveEngine |
保存engine到文件 | 部署时必用 |
--fp16 |
启用FP16推理 | 边缘端强烈推荐,速度翻倍 |
--int8 |
启用INT8量化 | 需要校准数据集,精度有损失 |
--minShapes/optShapes/maxShapes |
动态shape范围 | 动态batch必填 |
--workspace |
设置工作空间大小(MB) | 显存不够时调大 |
--verbose |
输出详细日志 | 调试时打开,部署时关掉 |
4.3 Engine文件的序列化与反序列化——把模型“存”起来
你想想看,每次部署都重新跑一遍模型转换,那得多慢?尤其是大模型,转一次可能十几分钟。所以TensorRT允许你把转换好的engine“序列化”成一个二进制文件,部署时直接“反序列化”加载,几秒钟搞定。
说白了,序列化就是把内存里的模型结构、权重、优化信息打包成一个文件;反序列化就是把这个文件再读回内存。
4.3.1 序列化:从Runtime到文件
// 假设你已经有了一个builder构建好的engine
nvinfer1::IHostMemory* serializedModel = engine->serialize();
// 写入文件
std::ofstream file("model.engine", std::ios::binary);
file.write(reinterpret_cast<const char*>(serializedModel->data()),
serializedModel->size());
file.close();
// 释放序列化内存
serializedModel->destroy();
4.3.2 反序列化:从文件到Runtime
// 读取engine文件
std::ifstream file("model.engine", std::ios::binary);
file.seekg(0, std::ios::end);
size_t size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
file.read(buffer.data(), size);
file.close();
// 反序列化
nvinfer1::ICudaEngine* engine = runtime->deserializeCudaEngine(
buffer.data(), size, nullptr);
if (!engine) {
std::cerr << "Engine反序列化失败!" << std::endl;
return -1;
}
这里要注意,deserializeCudaEngine的第三个参数是IPluginFactory。如果你的模型用了自定义插件,这里要传对应的工厂类。否则,嗯,你会看到一个漂亮的空指针崩溃。
4.4 完整流程:从ONNX到推理
咱们把上面这些串起来,画个图你就明白了。
你看,整个流程其实就三步:
- 转换阶段:用trtexec把ONNX转成engine文件(序列化)
- 加载阶段:用Runtime反序列化engine文件
- 推理阶段:创建ExecutionContext执行推理
4.5 避坑指南
- 版本不匹配:序列化和反序列化用的TensorRT版本必须一致。我曾经把TensorRT 8.4转的engine拿到8.2的环境上跑,结果加载时直接报“incompatible version”。
- 动态shape的坑:如果模型用了动态shape,反序列化后必须调用
engine->getBindingDimensions()确认输入输出的维度范围是否正确。 - 显存泄漏:每次反序列化都会分配显存,记得在程序退出时
destroy掉engine和runtime。我习惯用RAII封装一下,省心。 - 文件完整性:engine文件是二进制格式,传输时一定要用二进制模式,别用文本模式。否则文件会损坏,加载时各种诡异错误。
嗯,这一章的内容就到这儿。你把这些基础组件搞明白了,后面讲构建器、网络定义、优化策略时,就不会觉得云里雾里了。动手试试吧,用trtexec转一个自己的模型,再写代码反序列化跑一下推理——相信我,跑通的那一刻,你会觉得这一切都值得。