4. TensorRT基础:Builder/Config/Engine/Core概念解析、构建流程、序列化与反序列化
好,咱们进入TensorRT的核心环节。说实话,很多新手一上来就急着写代码,结果Builder、Config、Engine这几个概念搞得一团浆糊。我刚开始接触TensorRT时也踩过这个坑——明明模型导进去了,推理速度就是上不去,后来才发现是Config没配对。
今天咱们就把这几个核心概念掰开揉碎,讲清楚它们各自是干什么的,以及怎么配合工作。
4.1 四大核心概念:Builder / Config / Engine / Core
先给个直观的比喻,你想想看:
- Builder:就像建筑公司的总工头,负责统筹整个模型构建过程
- Config:是工头手里的施工图纸,告诉Builder怎么建——精度、内存限制、优化策略全写在这
- Engine:是建好的大楼,可以直接投入使用,但不能改了
- Core:是地基和框架,支撑整个TensorRT的运行环境
说白了,Builder拿着Config这张图纸,把模型盖成Engine这栋楼,然后Core负责让这栋楼能正常运转。
关键点:Engine一旦构建完成,就是只读的。你不能在推理时再改它的结构或精度。所以Config阶段一定要想清楚。
4.2 构建流程:从ONNX到Engine
我习惯把构建流程分成三步走:
- 创建Builder和Config:初始化环境,设置参数
- 解析模型:把ONNX或Caffe模型喂给Builder
- 构建Engine:Builder根据Config进行优化,生成最终的可执行引擎
这里贴一段我常用的构建代码,注释写得很清楚:
// 1. 创建Builder
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);
// 2. 创建Config
nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();
// 3. 设置Config参数
config->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 1 << 30); // 1GB
config->setFlag(nvinfer1::BuilderFlag::kFP16); // 启用FP16
// 4. 解析ONNX模型
const auto explicitBatch = 1U << static_cast<uint32_t>(
nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(explicitBatch);
nvinfer1::IONNXParser* parser = nvinfer1::createONNXParser(*network, logger);
parser->parseFromFile("model.onnx",
nvinfer1::OnnxParserFlags::kNONE);
// 5. 构建Engine
nvinfer1::IHostMemory* serializedModel = builder->buildSerializedNetwork(*network, *config);
nvinfer1::IRuntime* runtime = nvinfer1::createInferRuntime(logger);
nvinfer1::ICudaEngine* engine = runtime->deserializeCudaEngine(
serializedModel->data(), serializedModel->size());
我的经验:Config里的setMemoryPoolLimit别设太小。我曾经设成256MB,结果构建时直接OOM。建议至少1GB起步,除非你模型特别小。
4.3 序列化与反序列化:Engine的保存与加载
为什么要序列化?因为构建Engine是个耗时的过程。你想想看,每次启动程序都重新构建一遍,那得等到猴年马月。所以咱们把构建好的Engine保存成文件,下次直接加载。
序列化就是把Engine转成二进制数据,反序列化就是把这堆二进制数据还原成Engine。嗯,这里要注意:
- 序列化后的文件:通常以
.engine或.trt为后缀 - 平台相关性:序列化文件跟CUDA版本、TensorRT版本、GPU架构都绑定的。换个环境就得重新构建
看代码:
// 序列化:保存Engine到文件
void saveEngine(nvinfer1::ICudaEngine* engine, const std::string& path) {
nvinfer1::IHostMemory* serializedModel = engine->serialize();
std::ofstream file(path, std::ios::binary);
file.write(reinterpret_cast<const char*>(serializedModel->data()),
serializedModel->size());
serializedModel->destroy();
}
// 反序列化:从文件加载Engine
nvinfer1::ICudaEngine* loadEngine(const std::string& path,
nvinfer1::IRuntime* runtime) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
file.read(buffer.data(), size);
return runtime->deserializeCudaEngine(buffer.data(), size);
}
避坑指南:我曾经在A100上构建了Engine,然后直接拷贝到V100上用,结果反序列化失败。后来查文档才发现,不同架构的GPU生成的Engine不兼容。所以生产环境里,最好在目标机器上构建Engine,或者用TensorRT的兼容性模式。
4.4 知识体系总览
为了让你更直观地理解这些概念的关系,我画了张图:
4.5 实战中的注意事项
最后总结几个我踩过的坑,你记一下:
| 环节 | 常见问题 | 我的建议 |
|---|---|---|
| Builder创建 | Logger未正确初始化 | Logger一定要全局唯一,否则会崩溃 |
| Config设置 | 内存池设太小导致OOM | 先设1GB,不够再加 |
| 模型解析 | ONNX算子不支持 | 用trtexec --onnx先测试一下 |
| Engine构建 | 构建时间过长 | 用Config的setBuilderOptimizationLevel控制优化级别 |
| 序列化 | 跨平台不兼容 | 在目标机器上构建,或用兼容模式 |
小技巧:如果你不确定Config该怎么配,可以用trtexec命令行工具先跑一遍,它会打印出详细的构建参数。然后你照着抄就行。我刚开始就是这么干的,省了不少时间。
好了,Builder、Config、Engine、Core这几个概念,加上序列化反序列化,就是TensorRT的骨架。你把这些搞明白了,后面的INT8量化、动态形状什么的,学起来就顺了。