4、TensorRT基础API:创建Builder与Config、构建Network与Engine、序列化与反序列化
好,咱们进入第四章。这一章是TensorRT的“地基”。
说白了,你后面所有花里胡哨的优化技巧,都得靠这几个API搭起来。我刚开始接触TensorRT时,觉得这玩意儿不就是个推理引擎嘛,直接扔个模型进去不就完了?结果一上手,发现Builder、Config、Network、Engine这几个概念绕得我头晕。
嗯,今天咱们就把它们彻底捋清楚。
4.1 创建Builder与Config:一切从这里开始
Builder,翻译过来就是“构建者”。它是TensorRT的入口点。你想想看,你要盖一栋楼,总得先有个施工队吧?Builder就是这个施工队的队长。
创建Builder的代码很简单:
#include <NvInfer.h>
using namespace nvinfer1;
// 创建Builder
IBuilder* builder = createInferBuilder(gLogger);
这里有个细节——gLogger。它是一个日志记录器,负责输出TensorRT的日志信息。我个人习惯用Logger类继承ILogger,然后重写log()方法。这样你能控制哪些日志需要显示,哪些可以忽略。
有了Builder,下一步就是创建Config。Config是Builder的配置对象,它决定了Builder怎么干活。
// 创建Config
IBuilderConfig* config = builder->createBuilderConfig();
Config能配置的东西很多,比如:
- 工作空间大小:
setMemoryPoolLimit(),控制显存使用上限 - 精度模式:FP32、FP16、INT8
- 动态形状:允许输入尺寸变化
- DLA加速:如果你有NVIDIA的DLA硬件
举个例子,设置FP16模式:
if (builder->platformHasFastFp16()) {
config->setFlag(BuilderFlag::kFP16);
std::cout << "FP16模式已开启" << std::endl;
}
这里要注意:platformHasFastFp16()会检查你的GPU是否支持FP16加速。我曾经在T4上跑得好好的,换到K80上直接报错——因为K80不支持FP16。所以,别偷懒,一定要加这个检查。
setMemoryPoolLimit()。建议设置为显存总量的70%-80%,留点余量给其他进程。
4.2 构建Network与Engine:模型的核心
Network,就是你的神经网络。它由一系列层(Layer)和张量(Tensor)组成。Engine,则是优化后的可执行对象。你可以把Network理解成“设计图”,Engine理解成“盖好的房子”。
创建Network的代码:
// 创建Network
INetworkDefinition* network = builder->createNetworkV2(0U);
参数0U表示使用默认的网络标志。如果你想支持动态形状,需要传1U << static_cast<int>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH)。这个后面会细讲。
有了Network,你就可以往里面添加层了。比如添加一个卷积层:
// 添加输入张量
ITensor* input = network->addInput("input", DataType::kFLOAT, Dims4{1, 3, 224, 224});
// 添加卷积层
auto conv = network->addConvolutionNd(*input, 64, DimsHW{3, 3}, weightMap["conv.weight"], weightMap["conv.bias"]);
conv->setStrideNd(DimsHW{1, 1});
conv->setPaddingNd(DimsHW{1, 1});
// 标记输出
network->markOutput(*conv->getOutput(0));
嗯,这里要注意:TensorRT的API是C++风格的,但很多操作符(比如ReLU、池化)都有对应的addXxx()方法。你不需要手动实现前向传播,TensorRT会帮你搞定。
Network构建完成后,就该生成Engine了:
// 构建Engine
IHostMemory* serializedModel = builder->buildSerializedNetwork(*network, *config);
IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(serializedModel->data(), serializedModel->size());
这里我用了buildSerializedNetwork()而不是buildEngineWithConfig()。为什么?因为前者返回序列化后的模型,方便保存到磁盘。后者直接返回Engine,但无法持久化。
4.3 序列化与反序列化:让模型“活”起来
序列化,就是把Engine保存到磁盘。反序列化,就是从磁盘加载Engine。这俩操作,说白了就是“存”和“取”。
序列化的代码:
// 序列化Engine到文件
std::ofstream file("model.engine", std::ios::binary);
file.write(static_cast<const char*>(serializedModel->data()), serializedModel->size());
file.close();
反序列化的代码:
// 从文件反序列化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);
IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(buffer.data(), size);
这里有个坑:序列化后的Engine是平台相关的。什么意思?你在T4上生成的Engine,拿到A100上可能跑不了。因为TensorRT会针对特定GPU架构生成最优的CUDA内核。
kSAFE标志生成跨平台Engine(但性能会打折扣)。
另外,序列化后的Engine文件大小通常比原始模型小很多。比如一个ResNet-50的ONNX模型大约100MB,序列化后可能只有30MB。这是因为TensorRT做了权重压缩和层融合。
4.4 完整流程:从Builder到Engine
咱们把整个流程串起来,看看一个完整的构建过程:
// 1. 创建Builder
IBuilder* builder = createInferBuilder(gLogger);
// 2. 创建Config
IBuilderConfig* config = builder->createBuilderConfig();
config->setMemoryPoolLimit(MemoryPoolType::kWORKSPACE, 1 << 30); // 1GB
// 3. 创建Network
INetworkDefinition* network = builder->createNetworkV2(0U);
// 4. 添加网络结构(这里省略具体层)
// ...
// 5. 构建序列化模型
IHostMemory* serializedModel = builder->buildSerializedNetwork(*network, *config);
// 6. 创建Runtime并反序列化
IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(serializedModel->data(), serializedModel->size());
// 7. 清理资源
serializedModel->destroy();
network->destroy();
config->destroy();
builder->destroy();
嗯,这里要注意资源管理。TensorRT的API是C风格的,没有RAII。所以你得手动调用destroy()。我建议用智能指针封装一下,或者写个RAII包装类。否则内存泄漏会让你头疼。
std::unique_ptr,自定义删除器。这样即使异常抛出,资源也能自动释放。代码看起来也更干净。
4.5 总结
这一章咱们把TensorRT的四个核心API捋了一遍:
- Builder:施工队长,负责统筹全局
- Config:施工方案,控制精度、内存等
- Network:设计图,定义网络结构
- Engine:盖好的房子,可执行推理
序列化和反序列化,就是让Engine能存能取,方便部署。
下一章,咱们会深入Network的构建细节,看看如何从ONNX模型导入,以及如何手动搭建网络。到时候,你会发现自己已经能掌控TensorRT的“命脉”了。