4、动态形状核心概念:优化维度(Optimization Profile)、最小/最优/最大尺寸(min/opt/max)、Profile的绑定与切换
好,咱们进入动态形状最核心的部分了。
说实话,很多同学一听到「动态形状」就觉得头大,觉得TensorRT这玩意儿怎么这么麻烦。其实你想想看,如果模型只处理固定尺寸的输入,那跟静态图有什么区别?动态形状才是真正让TensorRT在工业界落地的大杀器。
我个人习惯把动态形状拆成三个东西来理解:优化维度(Profile)、三个尺寸(min/opt/max)、以及Profile怎么绑定和切换。搞懂这三样,动态形状你就拿下了七成。
4.1 优化维度(Optimization Profile)到底是什么?
说白了,Optimization Profile就是一张「性能调优的配方表」。
TensorRT在构建引擎时,需要知道你的输入尺寸大概在什么范围。它不可能为每一种尺寸都生成最优的kernel,那样引擎会爆炸。所以它采用了一种折中方案:你告诉它三个关键尺寸,它针对最优尺寸(opt)做深度优化,然后保证在最小(min)到最大(max)这个区间内都能正确运行。
我在项目中遇到过这样一个场景:一个检测模型,输入图片从640x640到1920x1080不等。如果我只设一个Profile,那性能在极端尺寸下会掉得很厉害。后来我用了多个Profile,分别覆盖小图、中图、大图三个区间,推理速度直接提升了30%。
4.2 最小/最优/最大尺寸(min/opt/max)
这三个尺寸,是动态形状的命根子。咱们一个一个说。
4.2.1 最小尺寸(min)
这是你模型实际运行中可能遇到的最小输入尺寸。注意,是「可能遇到」,不是「理论最小」。我见过有人把min设成1x1,结果引擎构建时间翻倍,性能还差。为什么?因为TensorRT要为这个极端尺寸预留资源。
我的建议:min设为你业务场景中95%情况下的最小尺寸,不要为了「万一」去设一个极值。
4.2.2 最优尺寸(opt)
这是最关键的尺寸。TensorRT会针对opt尺寸做最激进的优化,包括算子融合、内存布局、kernel选择等等。说白了,你希望模型在哪个尺寸上跑得最快,就把opt设成那个尺寸。
我曾经犯过一个错误:把opt设成了训练时的默认尺寸,但实际部署时大部分请求都是另一个尺寸。结果推理速度还不如不用动态形状。后来我改了opt,让它匹配实际流量分布,效果立竿见影。
4.2.3 最大尺寸(max)
这是你模型能接受的上限。注意,max设得越大,TensorRT为它预留的显存就越多。如果你设了一个不切实际的max,比如4K分辨率,但实际只用1080p,那显存浪费会非常严重。
4.3 三个尺寸的关系——一张表说清楚
| 尺寸 | 作用 | 对性能的影响 | 对显存的影响 |
|---|---|---|---|
| min | 定义下限,保证正确性 | 影响较小 | 预留最小资源 |
| opt | 定义优化目标,追求极致性能 | 最大 | 中等 |
| max | 定义上限,保证可用性 | 影响中等 | 最大(决定显存峰值) |
4.4 Profile的绑定与切换
好,现在你有了多个Profile,怎么用?
4.4.1 绑定Profile到输入张量
在构建引擎时,你需要把Profile绑定到具体的输入张量上。一个输入张量可以绑定多个Profile,但每个Profile只能对应一组[min, opt, max]。
// C++示例:创建两个Profile
IBuilder* builder = ...;
INetworkDefinition* network = ...;
IBuilderConfig* config = builder->createBuilderConfig();
// Profile 1:小图场景
IOptimizationProfile* profile1 = builder->createOptimizationProfile();
profile1->setDimensions(input->getName(),
OptProfileSelector::kMIN, Dims4{1, 3, 640, 640});
profile1->setDimensions(input->getName(),
OptProfileSelector::kOPT, Dims4{1, 3, 1280, 720});
profile1->setDimensions(input->getName(),
OptProfileSelector::kMAX, Dims4{1, 3, 1920, 1080});
config->addOptimizationProfile(profile1);
// Profile 2:大图场景
IOptimizationProfile* profile2 = builder->createOptimizationProfile();
profile2->setDimensions(input->getName(),
OptProfileSelector::kMIN, Dims4{1, 3, 1920, 1080});
profile2->setDimensions(input->getName(),
OptProfileSelector::kOPT, Dims4{1, 3, 2560, 1440});
profile2->setDimensions(input->getName(),
OptProfileSelector::kMAX, Dims4{1, 3, 3840, 2160});
config->addOptimizationProfile(profile2);
4.4.2 运行时切换Profile
引擎构建好之后,运行时怎么切换?很简单,通过setOptimizationProfileAsync()或者setOptimizationProfile()接口。
// 运行时切换Profile
IExecutionContext* context = ...;
int profileIdx = 0; // 切换到Profile 0
// 异步切换(推荐)
context->setOptimizationProfileAsync(profileIdx, stream);
// 或者同步切换
context->setOptimizationProfile(profileIdx);
// 然后绑定对应的输入张量
context->setBindingDimensions(0, Dims4{1, 3, 1280, 720});
4.5 多Profile的最佳实践
嗯,这里要注意几个点:
- Profile数量不是越多越好。每个Profile都会增加引擎构建时间和显存占用。我一般控制在2-4个。
- Profile的opt要覆盖实际流量热点。比如你的业务中80%的请求是1080p,那opt就设成1080p。
- min和max要留有余量,但别太大。我习惯在真实数据的基础上,上下浮动10%-20%。
好了,这一节的内容就到这儿。下一节咱们会深入讲动态形状下的内存管理,那才是真正考验工程能力的地方。