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 维度可变,运行时确定 区别 NLP变长序列 batch=1, seq_len=可变 需要动态seq_len轴 目标检测 输入尺寸可变(多尺度) 输出num_boxes动态变化 批量推理 batch_size可变 需动态batch维度 处理路径 1. 确定动态轴 → 2. 设置dynamic_axes → 3. 验证导出 → 4. 推理引擎配置 (如TensorRT需额外设置Optimization Profile)

上图展示了动态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] 就会报错。

关键理解:动态shape不是“没有shape”,而是“shape的某些维度由符号变量表示”。它允许同一个ONNX模型处理不同尺寸的输入,但代价是推理引擎无法在编译期完全优化内存布局和算子融合,可能带来少量性能损失。

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)的输入通常是任意尺寸的图像,且输出检测框数量随图像内容和模型置信度阈值变化。这里存在两个层面的动态性:

  1. 输入动态:图像宽高可变。许多检测模型采用多尺度训练,推理时也期望支持不同分辨率输入。
  2. 输出动态:检测框数量 num_boxes 不是固定的。例如一张空无一物的图片可能输出0个框,而拥挤的街道可能输出100个框。ONNX模型中,输出 boxes 的shape为 [num_boxes, 4],scores 的shape为 [num_boxes],num_boxes 必须声明为动态维度。

对于输出动态,处理起来比输入动态更复杂。因为输出维度取决于模型内部的计算结果(如NMS后的保留框数),ONNX导出时无法预知。此时必须将 num_boxes 设为动态轴,并在推理引擎中配置允许的最大输出框数(如max=300),超出部分会被截断或忽略。

常见陷阱:许多检测模型在导出ONNX时,NMS(非极大值抑制)操作中的 max_output_boxes_per_class 参数会被固化为常量。如果该值设置过小,推理时可能丢失检测框;设置过大,则动态范围过宽,影响TensorRT优化效果。建议根据实际场景统计最大目标数,设置合理的上限。

4.4 判断方法与操作流程

判断一个模型是否需要动态shape,可以遵循以下流程:

  1. 分析输入数据特性:检查训练和推理时输入尺寸是否固定。如果存在变长文本、不同分辨率图像或可变batch,则需要动态shape。
  2. 检查输出结构:如果输出包含可变长度的张量(如检测框列表、序列生成结果),则必须声明动态输出轴。
  3. 评估部署目标:确认推理引擎是否支持动态shape。ONNX Runtime支持,但TensorRT需要额外配置Optimization Profile。
  4. 确定动态轴范围:为每个动态维度设置合理的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均设为动态,并设置合理范围。

操作步骤:

  1. 准备动态导出脚本,设置 dynamic_axes 如上节代码所示。
  2. 设置动态范围:batch从1到8(opt=1),seq_len从1到256(opt=128)。
  3. 导出后使用 onnxruntime 验证:分别测试 [1, 50]、[4, 200]、[8, 256] 三种输入,确认输出正确。
  4. 对比静态与动态模型的推理延迟:在CPU上,动态模型比静态模型慢约5%~10%,但在处理短文本时节省了约40%的计算时间。

结果对比:

指标静态Shape([1,128])动态Shape(batch:1-8, seq:1-256)
最长支持序列128256
batch支持仅11~8
短文本(seq=20)延迟12ms7ms
长文本(seq=256)延迟不支持35ms
模型文件大小420MB420MB(不变)

风险与代价:动态模型在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优化效果差,甚至编译失败。
经验总结:动态shape是ONNX部署中“灵活性”与“性能”的平衡点。正确做法是:最小化动态维度数量(只声明必要的轴)、合理设置动态范围(基于业务统计)、验证所有边界情况(min/opt/max三种尺寸)。在导出后,务必使用 onnxruntime 或 onnx.checker 验证模型,并测试多种输入尺寸,确保没有维度错误。