4、TensorRT基础入门:推理流程与核心API

好,咱们进入TensorRT的世界。说实话,很多做模型部署的朋友,一开始都被TensorRT那套Builder、Engine、Runtime的概念搞晕过。我当年也一样,看了半天文档,感觉每个单词都认识,连起来就不知道它们在干嘛。

这一章,我就带你把这些概念彻底捋清楚。你跟着我走一遍,保证能明白TensorRT到底是怎么把模型“吃”进去,又怎么“吐”出推理结果的。

4.1 TensorRT推理流程概览

先看整体流程。TensorRT的推理,说白了就两大阶段:构建(Build)和推理(Inference)。

  • 构建阶段:你把训练好的模型(比如ONNX格式)喂给TensorRT,它帮你做优化、做量化、生成一个高度优化的推理引擎。这个过程比较慢,但只做一次。
  • 推理阶段:你加载那个优化好的引擎,用它来跑实际的推理任务。这个过程非常快,而且可以反复用。

我画了张图,你一看就明白。

TensorRT 推理流程概览 构建阶段 (Build Phase) ONNX 模型文件 Builder + Config + Parser 优化后的 Engine (序列化) 序列化 推理阶段 (Inference Phase) Engine (反序列化) Runtime + ExecutionContext 推理结果 (Output)

你看,左边是构建,右边是推理。中间那个箭头,就是序列化和反序列化的过程。我个人习惯把构建好的引擎保存成一个文件,下次直接用,省得每次都重新优化一遍。

4.2 核心API:Builder / Config / Engine / Runtime

这四个API,是TensorRT的灵魂。我一个个给你讲清楚。

API 职责 一句话理解
Builder 创建引擎的“总指挥” 负责整个构建流程的调度
Config 设置构建参数 告诉Builder你要怎么优化
Engine 优化后的推理“发动机” 包含所有优化后的网络和权重
Runtime 加载和执行引擎 在推理阶段使用Engine

举个例子你就懂了。Builder就像是一个建筑公司的项目经理,Config就是设计图纸,Engine是盖好的大楼,Runtime就是物业公司,负责让大楼正常运转。

我的经验: 刚开始接触时,最容易搞混的是Engine和Runtime。记住:Engine是“静态的”优化结果,Runtime是“动态的”执行环境。你可以在不同机器上用Runtime加载同一个Engine文件。

4.3 ONNX解析器构建Engine

这是最常用的方式。你把模型导出成ONNX格式,然后用TensorRT的ONNX解析器(ONNX Parser)来构建Engine。

代码其实不复杂,核心就几步:

// 1. 创建Builder
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);

// 2. 创建Config
nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();

// 3. 创建ONNX解析器
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(flags);
nvonnxparser::IParser* parser = nvonnxparser::createParser(*network, logger);

// 4. 解析ONNX文件
parser->parseFromFile("model.onnx", 
                       nvinfer1::ILogger::Severity::kINFO);

// 5. 构建Engine
nvinfer1::ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);

嗯,这里要注意一点:buildEngineWithConfig这个调用,在TensorRT 8.x之后是推荐方式。老版本用的是buildCudaEngine,但那个已经过时了。我刚开始迁移时还踩过这个坑。

避坑指南: 我曾经在解析ONNX时遇到算子不支持的情况。TensorRT不是所有ONNX算子都支持,尤其是自定义算子。解决办法有两个:一是用TensorRT的插件(Plugin)扩展,二是修改模型结构,避开不支持的算子。

4.4 Engine序列化与反序列化

构建Engine很耗时,尤其是做INT8量化时,可能需要跑几百张图片做校准。所以,我们通常会把Engine保存成文件,下次直接加载。

序列化(保存Engine):

// 获取序列化后的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();

反序列化(加载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();

// 创建Runtime并反序列化
nvinfer1::IRuntime* runtime = nvinfer1::createInferRuntime(logger);
nvinfer1::ICudaEngine* engine = runtime->deserializeCudaEngine(buffer.data(), size);

你看,反序列化只需要Runtime,不需要Builder和Config。这就是为什么部署机器上可以只装TensorRT Runtime,不装完整的TensorRT库。

关键点: 序列化后的Engine文件是平台相关的。你在A机器上构建的Engine,不一定能在B机器上跑。比如,A机器是RTX 3090,B机器是A100,那Engine就不能通用。必须重新构建。

4.5 trtexec命令行工具使用

trtexec是TensorRT自带的命令行工具,非常实用。我个人觉得,它是调试和验证模型的首选工具,没有之一。

基本用法:

# 从ONNX构建Engine
trtexec --onnx=model.onnx --saveEngine=model.engine

# 测试推理性能
trtexec --loadEngine=model.engine --shapes=input:1x3x640x640

# 查看网络层信息
trtexec --onnx=model.onnx --verbose --dumpLayerInfo

几个常用参数:

参数 作用 我的使用场景
--onnx 指定ONNX模型路径 构建Engine时必用
--saveEngine 保存构建好的Engine 构建完成后保存
--loadEngine 加载已有的Engine 测试性能时用
--shapes 指定输入张量形状 动态batch时必用
--best 启用所有优化 追求极致性能时用
--verbose 打印详细信息 调试时必开

举个例子,我想看看YOLOv8在FP16下的性能:

trtexec --onnx=yolov8n.onnx --fp16 --saveEngine=yolov8n_fp16.engine
trtexec --loadEngine=yolov8n_fp16.engine --shapes=images:1x3x640x640

输出里会显示latency、throughput等关键指标。我一般重点关注GPU Compute Time和End-to-End Host Latency。

小技巧: 用--verbose参数可以看到每个层的耗时,方便定位性能瓶颈。我曾经用这个参数发现某个卷积层用了float32而不是float16,一查才知道是算子不支持FP16。

4.6 本章小结

这一章,我们走通了TensorRT的完整推理流程。从Builder构建,到Config配置,再到Engine序列化,最后用Runtime加载推理。trtexec工具可以帮你快速验证和调试。