4. 动态Shape下的显存与计算图优化

动态Shape并不是“把输入尺寸留空”这么简单。它会把TensorRT的优化决策从构建期推迟到运行期,而运行期每一次输入尺寸变化,都可能触发显存分配、kernel tactic选择、甚至计算图重排。理解这三者之间的耦合关系,是控制动态Shape部署性能波动的关键。

动态范围如何影响kernel选择

在构建Engine时,TensorRT会针对每个Profile中声明的min/opt/max形状,为每一层枚举候选kernel(tactic),并在opt形状附近做基准测试,选出在该范围内综合表现最好的tactic。注意:tactic是在构建期选定的,但它的实际执行效率取决于运行期真实输入落在范围中的哪个位置。

举例来说,某卷积层在min=(1,3,224,224)、opt=(8,3,224,224)、max=(32,3,224,224)的Profile下,TensorRT可能选中一个针对batch=8优化的tactic。当实际输入batch=1时,该tactic的并行度利用不足,延迟反而高于一个专为小batch设计的tactic;当batch=32时,又可能因寄存器/共享内存压力出现占用率下降。这就是动态Shape下“同一Engine、不同输入、性能波动”的根源之一。

因此,Profile的min/opt/max不是随便填的。opt应尽量贴近线上最常见的输入尺寸,让TensorRT的tactic搜索围绕真实热点展开;min和max则决定显存与tactic的边界。如果业务输入分布是双峰(例如batch=1和batch=16各占一半),单一Profile很难同时兼顾,此时应考虑拆分为两个Profile,或使用多个Engine分别服务不同流量。

动态Shape下tactic选择与性能波动关系 Profile形状范围 min (1,3,224,224) opt (8,3,224,224) max (32,3,224,224) 构建期:TensorRT在opt附近基准测试,选定tactic Tactic A (小batch优化) Tactic B (中batch优化) ★选中 Tactic C (大batch优化) 运行期实际延迟(示意) batch=1 延迟↑ batch=8 最优 batch=32 延迟↑

Workspace大小估算

Workspace是TensorRT在推理时用于存放中间张量、tactic临时缓冲的显存区域。动态Shape下,workspace需求不再是固定值,而是随输入尺寸变化的函数。TensorRT在构建期会为Profile范围内的最坏情况预留workspace,但实际运行时按需分配。

估算workspace的实用方法是:先设置一个较大的workspaceSize(例如1GB)构建Engine,然后用trtexec --dumpProfile或--verbose观察各层实际使用的临时显存峰值。更直接的方式是使用IExecutionContext::setOptimizationProfile后,通过getMaxWorkspaceSize()查询当前Profile下的最大workspace需求。

一个常见的误区是:把workspace设得越大越好。实际上,过大的workspace会让TensorRT倾向于选择更激进的tactic(例如需要大块临时缓冲的Winograd卷积),这些tactic在opt形状下可能更快,但在min/max形状下可能因显存碎片或分配失败而回退。建议的做法是:先给一个合理上限(如512MB),观察实际峰值,再逐步收紧到峰值的1.2~1.5倍。

# 示例:查询各Profile的workspace需求
import tensorrt as trt

engine = trt.Runtime(trt.Logger()).deserialize_cuda_engine(plan_bytes)
for i in range(engine.num_optimization_profiles):
    ctx = engine.create_execution_context()
    ctx.set_optimization_profile_async(i, stream)
    # 绑定输入后查询
    print(f"Profile {i} max workspace: {ctx.get_max_workspace_size()} bytes")

动态Shape导致的tactic重选与性能波动

需要明确一个关键事实:TensorRT不会在每次推理时重新选择tactic。tactic在构建期确定,运行期只是按选定的tactic执行。但为什么还会出现性能波动?原因有三:

  • tactic对形状敏感:同一个tactic在不同batch/分辨率下,GPU占用率、内存带宽利用率差异显著。例如一个针对batch=8调优的GEMM tactic,在batch=1时可能只用到30%的SM。
  • 显存分配策略变化:动态Shape下,TensorRT使用动态显存分配器。当输入尺寸变化时,中间张量的生命周期和复用模式改变,可能导致额外的cudaMalloc/cudaFree或显存碎片,引入不可预测的延迟尖峰。
  • 多Profile切换开销:如果使用多个Profile,每次切换Profile需要重新绑定输入输出、重新计算显存布局,这个开销在频繁切换时不可忽略。

要定位性能波动,推荐用trtexec --shapes分别跑min、opt、max三个点,对比GPU Compute Time和Host Walltime。如果opt点远快于min/max,说明tactic选择偏向opt,这是预期行为;如果min点异常慢,可以考虑为小batch单独建一个Profile或Engine。

实用建议:对于延迟敏感且输入分布已知的场景,优先使用“多Profile + 按需切换”而非“单Profile覆盖全范围”。例如batch=1和batch=16各建一个Profile,运行时根据实际batch选择,通常比单Profile的min/max跨度更稳。

优化策略小结

动态Shape下的显存与计算图优化,核心是让tactic选择与真实输入分布对齐。具体做法包括:

  1. 用真实流量统计确定opt形状,而不是拍脑袋填中间值。
  2. workspace先宽后紧,观察峰值后再收紧,避免tactic过度激进。
  3. 对双峰分布拆Profile,减少单Profile内的性能方差。
  4. 用trtexec在min/opt/max三点做基准,确认波动在可接受范围内。
  5. 必要时开启--useCudaGraph或--useSpinWait降低调度开销,但需评估对动态Shape的兼容性。

这些手段的共同目标是:在动态Shape带来的灵活性下,把性能波动控制在可预测、可接受的范围内,而不是追求单一形状下的极致延迟。