4、TensorRT 核心 API 解析:Builder、Config、Network、Engine、Context 五大核心对象的作用与生命周期
说实话,刚接触 TensorRT 的时候,我也被这一堆对象搞晕过。Builder、Config、Network、Engine、Context……名字都挺像,到底谁管谁?
今天我就把这五个核心对象掰开揉碎讲清楚。你只要记住一句话:Builder 是总指挥,Config 是参数表,Network 是蓝图,Engine 是成品,Context 是执行器。
4.1 Builder —— 整个流程的入口
Builder 说白了就是 TensorRT 的「总指挥」。你所有的工作,都得从它开始。
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);
我个人习惯把 Builder 的生命周期控制得尽量短。为什么?因为它占用的资源不少,而且一旦 Engine 构建完成,它就没什么用了。
核心职责:
- 创建 Network 对象
- 创建 Config 对象
- 调用 buildSerializedNetwork() 或 buildEngineWithConfig()
我记得刚入行时,有个同事把 Builder 一直留着不释放,结果内存泄漏查了两天……嗯,后来我们都养成了用完就删的习惯。
4.2 Config —— 构建时的参数调节器
Config 是 Builder 的「副手」。它负责告诉 Builder:你要怎么构建这个网络。
nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();
config->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 1 << 20); // 设置工作空间
config->setFlag(nvinfer1::BuilderFlag::kFP16); // 开启 FP16
你想想看,Config 里能调的东西可多了:
| 配置项 | 作用 | 我踩过的坑 |
|---|---|---|
| 工作空间大小 | 控制中间张量内存上限 | 设太小会导致某些层无法融合 |
| 精度模式 | FP32/FP16/INT8 | INT8 需要校准数据集,别偷懒 |
| DLA 核心 | 启用 Jetson 的深度学习加速器 | 不是所有层都支持 DLA |
| 稀疏权重 | 利用结构化稀疏加速 | 需要 2:4 稀疏模式,普通模型无效 |
我的小技巧:Config 的生命周期和 Builder 绑定。我一般会在构建完 Engine 后立刻销毁 Config,省内存。
4.3 Network —— 网络结构的定义者
Network 就是你的模型「蓝图」。它定义了有哪些层、层怎么连、输入输出是什么。
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(1 << int(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH));
这里有个关键点:显式 Batch 模式。我建议你一直用这个模式。为什么?
- 老版本是隐式 Batch,网络定义里不写 Batch 大小
- 新版本显式 Batch,第一维就是 Batch,更直观
- 隐式模式在 TensorRT 8.x 之后已经 deprecated 了
Network 的生命周期很有意思。它只在构建阶段存在。一旦 Engine 生成,Network 就可以释放了。
注意:Network 里添加的权重数据是拷贝进去的。所以你可以在添加完权重后,立刻释放原始权重数组。我曾经见过有人把几百 MB 的权重在内存里保留了两份……没必要。
4.4 Engine —— 优化后的推理引擎
Engine 是 TensorRT 的「成品」。它包含了优化后的网络结构、权重、以及各种 kernel 的调度信息。
nvinfer1::ICudaEngine* engine = builder->buildSerializedNetwork(*network, *config);
Engine 有两种存在形式:
- 内存中的对象:直接用于推理
- 序列化的文件:保存为 .engine 或 .trt 文件,下次直接加载
我个人习惯把 Engine 序列化保存。这样下次启动时,直接从文件加载,省去了重新优化的时间。尤其是大模型,优化一次可能要十几分钟。
// 序列化保存
nvinfer1::IHostMemory* serializedModel = engine->serialize();
// 保存到文件...
// 反序列化加载
nvinfer1::IRuntime* runtime = nvinfer1::createInferRuntime(logger);
nvinfer1::ICudaEngine* engine = runtime->deserializeCudaEngine(serializedModel->data(), serializedModel->size());
生命周期要点:Engine 一旦创建,可以跨进程、跨机器使用(通过序列化文件)。它的生命周期可以很长,甚至可以和你的应用一样长。
4.5 Context —— 真正的执行者
Context 是「执行器」。Engine 只是定义了「怎么做」,Context 才是真正干活的。
nvinfer1::IExecutionContext* context = engine->createExecutionContext();
这里有个很重要的概念:一个 Engine 可以创建多个 Context。每个 Context 有自己的推理状态,可以独立执行。
为什么会这样?你想想看,如果你的服务要同时处理多个请求,每个请求一个 Context,互不干扰。这就是并行推理的基础。
// 设置输入
context->setBindingDimensions(0, nvinfer1::Dims4{1, 3, 224, 224});
// 执行推理
context->enqueueV2(buffers, stream, nullptr);
避坑指南:我曾经犯过一个错误——多个线程共用一个 Context。结果推理结果时对时错,排查了两天才发现是线程安全问题。记住:Context 不是线程安全的。每个线程请用自己的 Context。
4.6 生命周期总结
好了,五个对象都讲完了。我们来个总结:
| 对象 | 创建时机 | 销毁时机 | 能否复用 |
|---|---|---|---|
| Builder | 程序启动时 | Engine 构建完成后 | 一般不用复用 |
| Config | Builder 创建后 | Engine 构建完成后 | 可以复用,但没必要 |
| Network | Config 创建后 | Engine 构建完成后 | 不能复用 |
| Engine | 构建完成后 | 应用退出时 | 可以长期持有 |
| Context | Engine 创建后 | 推理结束后 | 每个线程一个 |
最后说一句:记得用智能指针或者 RAII 管理这些对象的生命周期。TensorRT 的对象都是 raw pointer,忘记 delete 就是内存泄漏。我刚开始用的时候,至少因为这个问题重启过十几次服务……
下一章我们聊聊怎么用这些对象搭建一个完整的推理流水线。到时候我会手把手带你写一个可用的 demo。