4、动态shape概念:什么是动态shape、静态shape与动态shape的区别、动态shape的应用场景(如NLP变长序列、目标检测)
在PyTorch模型导出ONNX并部署到推理引擎(如ONNX Runtime、TensorRT)的过程中,动态shape(Dynamic Shape)是决定模型通用性与部署复杂度的核心分水岭。许多开发者初次接触ONNX时,往往使用固定尺寸的输入(如 torch.randn(1, 3, 224, 224))导出模型,这在处理单张图片分类时没有问题,但一旦遇到NLP中的变长句子、目标检测中的多尺度输入或批量推理,就会遇到维度不匹配或推理失败。本章将系统梳理动态shape的本质、与静态shape的差异、典型应用场景,并给出判断与处理的方法论。
上图展示了动态shape在模型部署中的核心位置:静态shape与动态shape的差异决定了后续导出策略、推理引擎配置以及性能调优方向。下面我们深入拆解每个关键点。
4.1 核心原理:什么是动态shape
动态shape指的是模型输入或输出的一个或多个维度在运行时是可变的,而不是在导出时被固定为常量。在PyTorch中,张量的shape是运行时属性,但ONNX协议要求每个张量维度要么是静态值(如 1、224),要么被标记为动态符号(如 batch、seq_len)。当导出ONNX时,PyTorch默认会将所有输入维度固化为导出时使用的具体数值,除非显式通过 dynamic_axes 参数声明哪些维度是动态的。
从底层实现看,ONNX模型中的每个张量维度都有一个维度参数(dim_param)。静态shape的维度参数为空字符串,而动态shape的维度参数是一个字符串标识符(如 "batch"、"seq")。推理引擎在加载模型时,会为这些动态维度分配运行时实际值。例如,一个用于文本分类的BERT模型,输入 input_ids 的shape为 [batch_size, seq_len],其中 seq_len 在不同样本间可能从10变化到512。如果不声明动态,导出时固定为 [1, 128],那么推理时输入 [1, 200] 就会报错。
4.2 静态shape与动态shape的区别
静态shape与动态shape的差异贯穿模型导出、推理性能和部署灵活性三个层面。下表从多个维度进行对比:
| 对比维度 | 静态Shape | 动态Shape |
|---|---|---|
| 维度定义 | 所有维度在导出时固定为常量 | 部分或全部维度使用符号变量 |
| 导出方式 | 直接使用 torch.onnx.export 默认参数 | 需显式指定 dynamic_axes 参数 |
| 推理性能 | 高(可预分配内存、算子融合优化) | 中等(需运行时shape推断,可能动态分配内存) |
| 内存占用 | 固定,可精确预估 | 随输入变化,峰值可能更高 |
| 灵活性 | 低(输入尺寸必须与导出时一致) | 高(可处理变长、多尺度输入) |
| 部署复杂度 | 低(配置简单) | 高(需配置动态轴范围、优化profile) |
| 适用场景 | 固定尺寸图像分类、固定batch推理 | NLP变长序列、目标检测、多尺度推理 |
| TensorRT支持 | 直接支持,无需额外配置 | 需设置Optimization Profile(min/opt/max) |
从工程实践角度,静态shape适合对延迟极度敏感且输入尺寸固定的场景(如工业质检中固定分辨率图像),而动态shape适合需要处理自然语言或真实世界图像尺寸变化的场景。值得注意的是,动态shape并非全有或全无——你可以只将batch维度设为动态,而保持图像尺寸固定;也可以将batch和seq_len都设为动态,但限制其范围。
4.3 动态shape的应用场景
动态shape在真实AI应用中几乎无处不在,下面分析两个最典型的场景。
4.3.1 NLP变长序列处理
在自然语言处理中,句子长度天然是变化的。以BERT模型为例,输入通常包括 input_ids、attention_mask 和 token_type_ids,它们的shape均为 [batch_size, seq_len]。如果采用静态shape,必须将所有句子padding到固定长度(如128或512),这会导致:
- 计算浪费:短句子(如“Hello”)被padding到512,大量无效计算消耗GPU资源。
- 性能下降:padding部分虽然被attention_mask屏蔽,但矩阵乘法仍会执行。
- 灵活性受限:如果部署时遇到超过预设长度的文本,模型直接崩溃。
使用动态shape后,每个batch可以按该batch内最长句子的长度进行padding(动态padding),甚至支持batch内不同样本长度不同(需推理引擎支持)。实际部署中,通常将 seq_len 设为动态,并设置合理范围(如min=1, opt=64, max=512),这样既保证灵活性,又给TensorRT等引擎提供优化依据。
4.3.2 目标检测中的多尺度输入与动态输出
目标检测模型(如YOLO、Faster R-CNN)的输入通常是任意尺寸的图像,且输出检测框数量随图像内容和模型置信度阈值变化。这里存在两个层面的动态性:
- 输入动态:图像宽高可变。许多检测模型采用多尺度训练,推理时也期望支持不同分辨率输入。
- 输出动态:检测框数量
num_boxes不是固定的。例如一张空无一物的图片可能输出0个框,而拥挤的街道可能输出100个框。ONNX模型中,输出boxes的shape为[num_boxes, 4],scores的shape为[num_boxes],num_boxes必须声明为动态维度。
对于输出动态,处理起来比输入动态更复杂。因为输出维度取决于模型内部的计算结果(如NMS后的保留框数),ONNX导出时无法预知。此时必须将 num_boxes 设为动态轴,并在推理引擎中配置允许的最大输出框数(如max=300),超出部分会被截断或忽略。
max_output_boxes_per_class 参数会被固化为常量。如果该值设置过小,推理时可能丢失检测框;设置过大,则动态范围过宽,影响TensorRT优化效果。建议根据实际场景统计最大目标数,设置合理的上限。
4.4 判断方法与操作流程
判断一个模型是否需要动态shape,可以遵循以下流程:
- 分析输入数据特性:检查训练和推理时输入尺寸是否固定。如果存在变长文本、不同分辨率图像或可变batch,则需要动态shape。
- 检查输出结构:如果输出包含可变长度的张量(如检测框列表、序列生成结果),则必须声明动态输出轴。
- 评估部署目标:确认推理引擎是否支持动态shape。ONNX Runtime支持,但TensorRT需要额外配置Optimization Profile。
- 确定动态轴范围:为每个动态维度设置合理的min/opt/max值。opt值应接近实际推理中最常见的尺寸,以获得最佳性能。
在PyTorch中导出动态shape模型的代码示例如下:
import torch
import torch.onnx
# 假设模型输入为 [batch, seq_len]
model = BertModel()
dummy_input = torch.randn(1, 128, dtype=torch.long)
torch.onnx.export(
model,
dummy_input,
"bert_dynamic.onnx",
input_names=["input_ids", "attention_mask"],
output_names=["logits"],
dynamic_axes={
"input_ids": {0: "batch", 1: "seq_len"},
"attention_mask": {0: "batch", 1: "seq_len"},
"logits": {0: "batch"}
},
opset_version=17
)
上述代码中,dynamic_axes 字典的键是输入/输出名称,值是一个字典,映射“维度索引”到“符号名称”。符号名称可以任意起,但建议使用有意义的名称如 batch、seq_len、height、width。
4.5 案例拆解:BERT模型动态shape导出与验证
下面通过一个完整的案例,展示从静态shape到动态shape的转换过程,以及如何验证导出结果。
背景与约束:某NLP团队需要将BERT-base模型部署到ONNX Runtime,要求支持batch size为1~8、序列长度最大为256。初始导出时使用了固定shape [1, 128],导致线上推理时遇到长度超过128的文本直接报错。
初始现象:线上服务日志显示 Shape mismatch: expected [1,128], got [1,200],且无法处理batch size大于1的请求。
分析判断:问题根源在于导出时未声明动态轴。修复方案是重新导出,将batch和seq_len均设为动态,并设置合理范围。
操作步骤:
- 准备动态导出脚本,设置
dynamic_axes如上节代码所示。 - 设置动态范围:batch从1到8(opt=1),seq_len从1到256(opt=128)。
- 导出后使用
onnxruntime验证:分别测试[1, 50]、[4, 200]、[8, 256]三种输入,确认输出正确。 - 对比静态与动态模型的推理延迟:在CPU上,动态模型比静态模型慢约5%~10%,但在处理短文本时节省了约40%的计算时间。
结果对比:
| 指标 | 静态Shape([1,128]) | 动态Shape(batch:1-8, seq:1-256) |
|---|---|---|
| 最长支持序列 | 128 | 256 |
| batch支持 | 仅1 | 1~8 |
| 短文本(seq=20)延迟 | 12ms | 7ms |
| 长文本(seq=256)延迟 | 不支持 | 35ms |
| 模型文件大小 | 420MB | 420MB(不变) |
风险与代价:动态模型在TensorRT上部署时,需要额外设置Optimization Profile,且首次推理时会有shape推断开销。此外,如果动态范围设置过大(如seq_len max=1024),可能导致TensorRT编译时间显著增加。
复盘经验:动态shape的声明应基于真实业务数据分布,而不是盲目设置最大范围。建议先统计线上数据的长度分布,将opt值设为最常见的长度(如128),max值设为可接受的最大长度(如256),这样在性能和灵活性之间取得平衡。
4.6 常见误区与总结
在动态shape处理中,开发者常犯以下错误:
- 误区一:认为动态shape会大幅降低性能。实际上,对于NLP变长序列,动态shape配合动态padding往往比静态shape+固定padding更快,因为减少了无效计算。
- 误区二:将所有维度都设为动态。不必要的动态维度会增加推理引擎的优化难度。例如,图像分类中只有batch需要动态,宽高可以固定。
- 误区三:忽略输出动态。检测模型输出
num_boxes是动态的,但有些开发者只设置输入动态,导致导出时报错或推理时输出被截断。 - 误区四:动态范围设置不合理。min值过小或max值过大会导致TensorRT优化效果差,甚至编译失败。
onnxruntime 或 onnx.checker 验证模型,并测试多种输入尺寸,确保没有维度错误。