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分别服务不同流量。
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。
优化策略小结
动态Shape下的显存与计算图优化,核心是让tactic选择与真实输入分布对齐。具体做法包括:
- 用真实流量统计确定opt形状,而不是拍脑袋填中间值。
- workspace先宽后紧,观察峰值后再收紧,避免tactic过度激进。
- 对双峰分布拆Profile,减少单Profile内的性能方差。
- 用
trtexec在min/opt/max三点做基准,确认波动在可接受范围内。 - 必要时开启
--useCudaGraph或--useSpinWait降低调度开销,但需评估对动态Shape的兼容性。
这些手段的共同目标是:在动态Shape带来的灵活性下,把性能波动控制在可预测、可接受的范围内,而不是追求单一形状下的极致延迟。