3、TensorRT核心API速览:Builder、Network、Config、Engine、Context、Runtime
好,咱们直接进入正题。TensorRT的API体系,说白了就是六个核心对象:Builder、Network、Config、Engine、Context、Runtime。这六个家伙各司其职,串联起从模型构建到推理执行的完整链路。我刚开始接触TensorRT时,也经常搞混它们之间的关系,后来画了一张图才彻底理清。
3.1 Builder:一切从这里开始
Builder是TensorRT的入口。你想想看,你要盖一栋楼,总得先有个施工队吧?Builder就是这个施工队的总指挥。它的职责很简单:创建Network和Config,然后调用build方法生成Engine。
核心职责:创建Network → 创建Config → 调用build() → 输出Engine
// 创建Builder
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);
// 创建Network
const auto explicitBatch = 1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(explicitBatch);
// 创建Config
nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();
// 构建Engine
nvinfer1::IHostMemory* serializedModel = builder->buildSerializedNetwork(*network, *config);
我个人习惯把Builder看作一次性的工具对象。用完就释放,别留着占内存。我在项目中遇到过有人反复创建Builder,结果内存泄漏查了半天——其实Builder创建一次就够了。
3.2 Network:定义你的计算图
Network就是你的模型结构。说白了,它是一张有向无环图,节点是层(Layer),边是张量(Tensor)。你往Network里添加各种层:卷积、全连接、激活函数、池化……TensorRT会帮你做图优化。
小技巧:Network有两种定义方式——隐式批量和显式批量。我建议你直接用显式批量(kEXPLICIT_BATCH),因为隐式批量在TensorRT 8.0之后已经被标记为废弃了。
// 添加输入张量
nvinfer1::ITensor* input = network->addInput("input", nvinfer1::DataType::kFLOAT, nvinfer1::Dims4{1, 3, 224, 224});
// 添加卷积层
auto conv = network->addConvolutionNd(*input, 64, nvinfer1::DimsHW{3, 3}, weightMap["conv.weight"], weightMap["conv.bias"]);
conv->setStrideNd(nvinfer1::DimsHW{1, 1});
// 添加激活层
auto relu = network->addActivation(*conv->getOutput(0), nvinfer1::ActivationType::kRELU);
// 标记输出
network->markOutput(*relu->getOutput(0));
嗯,这里要注意:Network只负责描述模型结构,不负责执行。它就像一张设计图纸,真正干活的是后面的Engine。
3.3 Config:调优参数的集中营
Config是Builder的配置对象。你可以在Config里设置各种优化参数:工作空间大小、精度模式、动态形状范围、INT8校准器……
| 配置项 | 说明 | 我的建议 |
|---|---|---|
| 工作空间 | Builder构建时可用显存 | 设大一点,比如1GB,构建更快 |
| 精度模式 | FP32/FP16/INT8 | FP16是性价比最高的选择 |
| 动态形状 | 支持可变输入尺寸 | 必须设置min/opt/max三个范围 |
| INT8校准器 | INT8量化需要 | 推荐使用EntropyCalibrator2 |
// 设置工作空间
config->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 1 << 30); // 1GB
// 开启FP16
config->setFlag(nvinfer1::BuilderFlag::kFP16);
// 设置动态形状
config->setProfileStream(stream);
auto profile = builder->createOptimizationProfile();
profile->setDimensions("input", nvinfer1::OptProfileSelector::kMIN, nvinfer1::Dims4{1, 3, 224, 224});
profile->setDimensions("input", nvinfer1::OptProfileSelector::kOPT, nvinfer1::Dims4{8, 3, 224, 224});
profile->setDimensions("input", nvinfer1::OptProfileSelector::kMAX, nvinfer1::Dims4{32, 3, 224, 224});
config->addOptimizationProfile(profile);
避坑指南:我曾经在Config里忘记设置动态形状的opt维度,结果推理时性能惨不忍睹。opt维度是TensorRT做内核自动调优的基准,一定要设成你最常用的batch size。
3.4 Engine:优化后的推理引擎
Engine是Builder的产出物。它包含了优化后的网络、权重、内核选择结果。Engine可以序列化到磁盘(.trt或.engine文件),也可以反序列化加载。
你想想看,Engine就像一辆组装好的赛车,Builder就是那个改装厂。改装厂花时间调校,但赛车一旦出厂,就能直接上赛道狂飙。
// 序列化Engine
nvinfer1::IHostMemory* serializedModel = builder->buildSerializedNetwork(*network, *config);
std::ofstream file("model.engine", std::ios::binary);
file.write(reinterpret_cast<const char*>(serializedModel->data()), serializedModel->size());
// 反序列化Engine
nvinfer1::IRuntime* runtime = nvinfer1::createInferRuntime(logger);
nvinfer1::ICudaEngine* engine = runtime->deserializeCudaEngine(serializedModel->data(), serializedModel->size());
小技巧:Engine序列化后是平台相关的。你在A100上生成的Engine,不能直接拿到T4上用。我建议你在目标硬件上重新构建,或者至少做一次兼容性检查。
3.5 Context:执行推理的上下文
Context是Engine的执行上下文。一个Engine可以创建多个Context,每个Context独立管理自己的推理状态。这在多线程推理时特别有用——每个线程持有一个Context,共享同一个Engine。
// 创建Context
nvinfer1::IExecutionContext* context = engine->createExecutionContext();
// 设置输入输出缓冲区
void* buffers[2];
cudaMalloc(&buffers[0], inputSize);
cudaMalloc(&buffers[1], outputSize);
// 执行推理
context->enqueueV2(buffers, stream, nullptr);
// 或者使用enqueueV3(TensorRT 10.0+)
context->enqueueV3(stream);
我个人习惯把Context看作「一次推理会话」。每次推理前,确保输入数据已经拷贝到GPU,推理完成后,及时从GPU取回结果。我在项目中遇到过有人忘记同步CUDA流,结果读到的全是脏数据——嗯,这个坑我踩过两次才长记性。
3.6 Runtime:反序列化的工具人
Runtime的作用很简单:把序列化的Engine反序列化回来。它不参与推理,只负责「拆包」。你想想看,Runtime就像一把钥匙,专门用来打开Engine这个保险箱。
// 创建Runtime
nvinfer1::IRuntime* runtime = nvinfer1::createInferRuntime(logger);
// 反序列化
nvinfer1::ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize);
// 用完释放
runtime->destroy();
避坑指南:Runtime和Builder一样,都是轻量级对象。但要注意:Runtime的destroy()必须在所有Engine销毁之后调用。我曾经在Engine还在使用时提前销毁了Runtime,结果程序直接崩溃——这个顺序问题,你一定要注意。
3.7 六者关系总结
好了,咱们把六个API串起来看:
- Builder 创建 Network 和 Config
- Builder 使用 Network + Config 构建 Engine
- Runtime 反序列化 Engine
- Engine 创建 Context
- Context 执行推理
说白了,这就是一条流水线:设计(Network)→ 调优(Config)→ 生产(Builder→Engine)→ 部署(Runtime→Engine→Context)。每个环节各司其职,缺一不可。
下一章,咱们会深入Network的细节,看看如何用代码搭建一个完整的网络。到时候我会手把手带你写一个自定义层的插件——嗯,那才是咱们这门课的重头戏。