一、核心概念解析:计算图、引擎、上下文、绑定、优化策略
各位同学好,我是老张。今天咱们来聊聊TensorRT里最核心的几个概念。说实话,我刚开始接触TensorRT时,也被这些术语搞得晕头转向。什么引擎、上下文、绑定……听着就头大。但搞懂了它们,你才算真正入了门。
我习惯把TensorRT比作一个「翻译官」。它把你训练好的模型,翻译成能在GPU上跑得飞快的格式。而今天讲的这些概念,就是翻译过程中的关键环节。
1. 计算图:模型的骨架
计算图是什么?说白了,它就是模型的结构化表示。你训练好的PyTorch或TensorFlow模型,本质上是一堆算子的组合。TensorRT拿到这个模型后,第一件事就是把它解析成计算图。
我记得第一次用TensorRT时,直接拿一个ResNet-50的ONNX模型去转,结果报了一堆错。后来才发现,ONNX导出时有些算子没对齐。嗯,这里要注意:计算图的「干净程度」直接决定了后续优化的空间。
关键点:计算图是静态的。一旦构建完成,它的结构就固定了。动态图?TensorRT不支持。所以如果你的模型有动态控制流,得先把它「拍平」成静态图。
TensorRT会对计算图做三件事:
- 解析:把ONNX或UFF格式的模型读进来
- 融合:把相邻的小算子合并成一个大算子(比如Conv+Bias+ReLU合并成一个CBR)
- 优化:消除无用节点,常量折叠,内存复用
你想想看,一个ResNet-50原本有上百个节点,经过TensorRT一优化,可能只剩几十个。这就是它能跑得快的原因之一。
2. 引擎 (Engine):编译好的可执行文件
引擎是什么?它就是计算图经过TensorRT编译后生成的「可执行文件」。你可以把它理解成一个.so动态库,或者一个.pt模型文件。但它比这些更底层——它直接包含了CUDA kernel的二进制代码。
我习惯把引擎比作「预制菜」。计算图是菜谱,引擎就是已经做好的菜。你只需要加热(推理)就能吃。但预制菜有个问题:它只能针对特定的「口味」——也就是特定的GPU型号、特定的精度、特定的batch size。
个人经验:我建议你在部署时,把引擎序列化保存成.engine文件。下次加载时直接反序列化,省去重新编译的时间。一个ResNet-50的引擎,在T4上编译大概要3-5分钟,但加载只要几秒钟。
引擎有几个重要属性:
| 属性 | 说明 | 我的建议 |
|---|---|---|
| 序列化 | 可以保存为二进制文件 | 一定要保存,别每次都重新编译 |
| 平台相关 | 不同GPU架构不通用 | T4的引擎不能在A100上用 |
| 精度固定 | FP32/FP16/INT8一旦选定就定了 | 生产环境建议用FP16或INT8 |
3. 上下文 (Context):推理时的运行环境
上下文这个概念,我刚开始也觉得抽象。后来我把它理解为「引擎的一个实例」。一个引擎可以创建多个上下文,每个上下文都有自己的中间结果存储空间。
为什么会需要多个上下文?举个例子:你有一个服务,同时处理10个请求。如果只有一个上下文,就得排队。但如果你创建10个上下文,每个请求用一个,就能并行处理。当然,这需要显存够大。
避坑指南:我曾经在一个项目中,为了省显存只创建了一个上下文,结果并发一上来就卡死。后来改成每个请求一个上下文,问题就解决了。但要注意:上下文不是越多越好,每个上下文会占用额外的显存。
上下文的核心作用:
- 管理推理过程中的中间张量
- 维护执行状态
- 提供异步执行的能力
4. 绑定 (Binding):输入输出的接口
绑定,说白了就是引擎的输入输出接口。每个绑定对应一个张量,有名字、数据类型、形状这些属性。
我习惯把绑定比作「插头」。引擎是一个设备,绑定就是上面的插孔。你要把数据插到对应的插孔里,才能让引擎工作。
绑定的索引方式有两种:
- 按名称索引:通过张量名字找到绑定,可读性好
- 按索引索引:通过数字索引,性能更好
// 按名称索引(推荐用于调试)
int inputIdx = engine->getBindingIndex("input");
int outputIdx = engine->getBindingIndex("output");
// 按索引索引(推荐用于生产)
context->setBindingDimensions(inputIdx, nvinfer1::Dims4{1, 3, 224, 224});
context->enqueueV2(buffers, stream, nullptr);
关键点:绑定是零拷贝的。你给引擎一个显存地址,引擎直接读写这个地址,不会做额外的内存拷贝。所以你要确保输入输出的显存已经分配好。
5. 优化策略 (Optimization Profile):动态形状的解决方案
优化策略,这是TensorRT里最容易被忽视但又最重要的概念之一。它解决的是动态形状的问题。
你想想看,如果你的模型输入形状是固定的(比如224x224),那很简单。但现实场景中,输入形状往往是变化的——比如NLP里的变长序列,或者目标检测里不同尺寸的图片。
优化策略就是为这种情况设计的。它允许你指定输入形状的范围:最小值、最大值、以及一个「常见值」(优化点)。TensorRT会在这个范围内做优化。
| 参数 | 含义 | 示例 |
|---|---|---|
| kMIN | 输入的最小形状 | 1x3x224x224 |
| kMAX | 输入的最大形状 | 16x3x224x224 |
| kOPT | 优化点(最常见形状) | 4x3x224x224 |
个人经验:我建议你把kOPT设成实际生产中最常见的batch size。比如你的服务大部分时候处理4个请求,那就设成4。这样TensorRT会针对这个形状做深度优化,性能最好。
设置优化策略的代码示例:
// 创建优化策略
auto profile = builder->createOptimizationProfile();
profile->setDimensions("input", nvinfer1::OptProfileSelector::kMIN,
nvinfer1::Dims4{1, 3, 224, 224});
profile->setDimensions("input", nvinfer1::OptProfileSelector::kOPT,
nvinfer1::Dims4{4, 3, 224, 224});
profile->setDimensions("input", nvinfer1::OptProfileSelector::kMAX,
nvinfer1::Dims4{16, 3, 224, 224});
config->addOptimizationProfile(profile);
避坑指南:我曾经犯过一个错误——把kMIN和kMAX设得太宽泛。比如kMIN=1, kMAX=128。结果TensorRT编译了整整一个小时,而且生成的引擎性能很差。后来我把范围缩小到实际需要的范围(kMIN=1, kMAX=16),编译时间降到了5分钟,性能也上来了。
好了,这五个核心概念就讲完了。它们之间的关系是这样的:计算图是蓝图,引擎是编译好的程序,上下文是运行实例,绑定是数据接口,优化策略是动态形状的解决方案。搞懂了这些,你就能理解TensorRT的整个工作流程了。
下一章我们聊聊如何把PyTorch模型转换成TensorRT引擎,到时候会用到今天讲的概念。嗯,今天就到这里。