3. TensorRT基础:TensorRT简介、构建引擎、推理流程、性能分析工具
3.1 TensorRT核心设计思想
TensorRT是NVIDIA推出的高性能深度学习推理优化器,其核心目标是在保持模型精度的前提下,最大化GPU上的推理吞吐量并降低延迟。与训练框架(如PyTorch、TensorFlow)不同,TensorRT不负责反向传播,而是将训练好的模型转化为经过深度优化的推理引擎。其优化手段包括:层融合(将卷积、偏置、激活合并为单一算子)、精度校准(FP16/INT8量化)、内核自动调优(选择最优的CUDA kernel)、以及显存复用等。
理解TensorRT的关键在于区分“构建期”与“运行期”。构建期完成模型解析、图优化和内核选择,生成序列化的引擎文件(.engine或.plan);运行期则直接加载引擎执行推理,不再进行任何图级优化。这种“一次构建、多次部署”的模式,使得TensorRT在服务端推理场景中极具优势。
3.2 构建引擎:从模型到序列化文件
构建引擎是TensorRT使用中最关键的一步,其流程可归纳为四个阶段:网络定义 → 配置设置 → 构建引擎 → 序列化保存。以下是一个基于ONNX模型的典型构建代码片段:
// C++ 示例:构建TensorRT引擎
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(1U << 0); // 显式批量
// 解析ONNX模型
auto parser = nvonnxparser::createParser(*network, logger);
parser->parseFromFile("model.onnx", 1);
// 创建构建配置
auto config = builder->createBuilderConfig();
config->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 1U << 30); // 1GB工作空间
config->setFlag(nvinfer1::BuilderFlag::kFP16); // 启用FP16
// 设置动态批量范围(关键!)
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);
// 构建并序列化
nvinfer1::IHostMemory* serialized = builder->buildSerializedNetwork(*network, *config);
// 保存到文件...
OptimizationProfile 指定批量大小的 最小、最优、最大 三个维度。运行时TensorRT会根据实际请求批量自动选择最优kernel,但超出最大范围会报错。
3.3 推理流程:上下文与执行
运行期推理流程相对简洁,但需要注意显存绑定和异步执行两个关键点。核心步骤如下:
- 反序列化引擎:从文件加载
.engine到内存,创建IRuntime对象。 - 创建执行上下文:每个上下文拥有独立的中间显存和CUDA流,多线程场景下建议每线程一个上下文。
- 绑定输入输出缓冲区:通过
setTensorAddress或enqueueV3指定GPU显存地址。 - 异步推理:调用
enqueueV3将任务提交到CUDA流,立即返回,通过事件或回调获取完成信号。
// 推理执行(伪代码)
auto context = engine->createExecutionContext();
void* buffers[2]; // 输入输出GPU指针
cudaStream_t stream;
cudaStreamCreate(&stream);
// 设置动态批量维度(每次推理前必须设置)
context->setInputShape("input", nvinfer1::Dims4{batchSize, 3, 224, 224});
// 异步提交
context->enqueueV3(stream, nullptr);
cudaStreamSynchronize(stream); // 等待完成
setInputShape 更新维度,否则TensorRT会沿用上次的shape,导致输出缓冲区越界或结果错误。此外,不要在多个线程间共享同一个 ExecutionContext,它内部状态非线程安全。
3.4 性能分析工具:trtexec与Nsight
TensorRT提供了官方命令行工具 trtexec,用于快速测量引擎性能并验证正确性。其常用参数如下表:
| 参数 | 作用 | 示例 |
|---|---|---|
--loadEngine |
加载已构建的引擎文件 | --loadEngine=model.engine |
--shapes |
指定动态批量输入维度 | --shapes=input:1x3x224x224 |
--useCudaGraph |
启用CUDA图捕获,减少kernel启动开销 | — |
--dumpProfile |
输出每层耗时分析 | — |
--streams |
设置并发流数量,测试吞吐 | --streams=4 |
对于更细粒度的性能剖析,推荐使用 Nsight Systems 和 Nsight Compute。Nsight Systems 适合查看CPU-GPU时间线、kernel启动间隙和显存拷贝瓶颈;Nsight Compute 则深入分析单个kernel的SM占用率、内存吞吐和指令效率。在动态批处理调优中,重点关注不同批量下kernel的SM占用率变化,若批量增大但SM占用率未提升,说明存在显存带宽或launch开销瓶颈。
3.5 案例:动态批量引擎的构建与验证
假设我们有一个ResNet-50 ONNX模型,需要支持1~16的批量范围。构建命令如下:
# 构建动态批量引擎(FP16)
trtexec --onnx=resnet50.onnx \
--minShapes=input:1x3x224x224 \
--optShapes=input:8x3x224x224 \
--maxShapes=input:16x3x224x224 \
--fp16 \
--saveEngine=resnet50_dynamic.engine
# 测试不同批量的延迟
trtexec --loadEngine=resnet50_dynamic.engine \
--shapes=input:1x3x224x224 \
--avgRuns=100
trtexec --loadEngine=resnet50_dynamic.engine \
--shapes=input:16x3x224x224 \
--avgRuns=100
通过对比不同批量的延迟-吞吐曲线,可以找到最佳工作批量。例如,批量1时延迟为0.8ms,批量16时延迟为4.2ms,但吞吐从1250 FPS提升至3800 FPS,说明GPU利用率随批量增大而显著提升。
3.6 误区与复盘
setMemoryPoolLimit 为显存容量的50%~70%。误区2:动态批量范围设置过宽。过宽的OPT范围会导致构建时间剧增,且可能因kernel选择过多而降低性能。建议OPT值设为实际最常见的批量。
误区3:混淆“最大批量”与“最优批量”。最大批量只是上限,不代表性能最优。实际调优时应通过profiling找到吞吐拐点。
复盘一个真实案例:某团队将动态批量范围设为1~64,OPT=32,结果构建耗时超过30分钟,且推理延迟比固定批量8的引擎高出15%。原因在于过大的OPT范围导致TensorRT为每个批量都尝试了多种tactic,最终选择了通用性最好但非最优的kernel。修正方案是将OPT设为8,并限制最大批量到16,构建时间降至4分钟,延迟恢复至预期水平。