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%。

核心要点:一个Profile = 一组[min, opt, max]的绑定。你可以有多个Profile,每个Profile对应不同的尺寸策略。

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,那显存浪费会非常严重。

避坑指南:我曾经把一个视频模型的max设成了1920x1080,结果显存占用比预期多了2GB。后来发现,实际业务中最大只用到1280x720。所以,max一定要基于真实数据来设,别拍脑袋。

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是有开销的。我建议你在处理一批相同尺寸的请求时再切换,不要每张图都切一次。比如视频流中,场景变化时才切,而不是每帧都切。
// 运行时切换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%。
一句话总结:Profile是TensorRT动态形状的「调优开关」。min保证下限,opt追求极致,max兜住上限。用好这三个尺寸,你的动态形状模型就能又快又稳。

好了,这一节的内容就到这儿。下一节咱们会深入讲动态形状下的内存管理,那才是真正考验工程能力的地方。