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;
}
我的小习惯:我一般会把Logger设成全局单例,这样整个程序生命周期里日志输出都统一。曾经有个项目,因为不同模块各自创建了Logger,结果日志格式乱七八糟,排查问题差点没把我逼疯。

这里有个细节要注意: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();
关键点:序列化后的engine文件是平台相关的。你在X86机器上转出来的engine,不能直接放到Jetson Orin上跑。因为底层优化的指令集、CUDA版本、TensorRT版本都可能不同。我见过有人把PC上转的engine拷到嵌入式设备上,结果加载直接崩——血的教训。

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到推理

咱们把上面这些串起来,画个图你就明白了。

TensorRT Engine 序列化与反序列化流程 ONNX模型 trtexec 转换 model.engine 反序列化 Runtime推理 ⚠ 注意: 序列化后的engine文件与CUDA版本、TensorRT版本、GPU架构绑定。换平台必须重新转换。

你看,整个流程其实就三步:

  1. 转换阶段:用trtexec把ONNX转成engine文件(序列化)
  2. 加载阶段:用Runtime反序列化engine文件
  3. 推理阶段:创建ExecutionContext执行推理
我的经验:在实际项目中,我通常把转换阶段放在开发机上做,生成engine文件后,连同部署代码一起打包发给边缘设备。边缘设备只做反序列化和推理,不做转换。这样既节省了边缘端的算力,也避免了在设备上装一堆转换依赖库。

4.5 避坑指南

  • 版本不匹配:序列化和反序列化用的TensorRT版本必须一致。我曾经把TensorRT 8.4转的engine拿到8.2的环境上跑,结果加载时直接报“incompatible version”。
  • 动态shape的坑:如果模型用了动态shape,反序列化后必须调用engine->getBindingDimensions()确认输入输出的维度范围是否正确。
  • 显存泄漏:每次反序列化都会分配显存,记得在程序退出时destroy掉engine和runtime。我习惯用RAII封装一下,省心。
  • 文件完整性:engine文件是二进制格式,传输时一定要用二进制模式,别用文本模式。否则文件会损坏,加载时各种诡异错误。

嗯,这一章的内容就到这儿。你把这些基础组件搞明白了,后面讲构建器、网络定义、优化策略时,就不会觉得云里雾里了。动手试试吧,用trtexec转一个自己的模型,再写代码反序列化跑一下推理——相信我,跑通的那一刻,你会觉得这一切都值得。