3、静态形状与动态形状:什么是静态形状、什么是动态形状、为什么需要动态形状
好,咱们今天聊一个非常核心的概念——静态形状和动态形状。
说实话,我刚开始接触ONNX时,也被这两个词搞得有点晕。明明都是张量,怎么还分静态动态?后来踩了几个坑才明白,这其实是模型部署中最容易忽略、也最容易出问题的地方。
3.1 什么是静态形状
静态形状,说白了就是——模型在导出时,输入输出的尺寸就固定死了。
举个例子。你训练了一个分类模型,输入图片是224x224。导出ONNX时,输入张量的形状就是 [1, 3, 224, 224]。这个形状里,每个维度都是确定的数字。1是batch size,3是通道数,224是宽和高。
一旦固定,就不能改了。你传一张448x448的图进去,模型直接报错。你传一张128x128的图,也不行。
- 所有维度都是具体数值,没有-1或None
- 推理时输入必须严格匹配这个形状
- 内存分配在模型加载时就确定了,效率高
- 适合生产环境,因为行为可预测
我在项目中遇到过这样一个场景:一个OCR模型,导出时固定了输入尺寸为640x640。结果上线后发现,用户上传的图片五花八门,有横屏的、有竖屏的、有长条的。每次都要先resize到640x640,导致文字被压扁,识别率直线下降。这就是静态形状的局限性。
3.2 什么是动态形状
动态形状就灵活多了。它允许某些维度在推理时变化。
还是那个分类模型。如果导出时把batch size设为动态,输入形状就是 [-1, 3, 224, 224]。这里的-1表示「这个维度可以变」。你可以一次传1张图,也可以一次传32张图,模型都能处理。
更灵活一点,连宽高也可以动态。比如 [-1, 3, -1, -1],这样任意尺寸的图片都能输入。当然,这需要模型本身支持可变尺寸,比如全卷积网络。
-1或"batch"这样的字符串来表示。不同框架写法略有不同,但意思一样——这个维度是可变的。
你想想看,动态形状就像是一个「可伸缩的容器」。你塞多少东西进去,它都能装下。而静态形状是一个「固定大小的盒子」,只能装固定数量的东西。
3.3 为什么需要动态形状
这个问题其实很实际。我总结了三个核心原因:
- 业务场景多变——用户上传的图片尺寸不可能统一。你让用户先resize再上传?体验太差了。
- batch size不确定——线上服务的并发量是波动的。高峰期可能一次处理64张图,低峰期可能只有1张。静态形状只能固定一个batch size,要么浪费资源,要么处理不过来。
- 模型本身需要可变输入——比如目标检测模型,输入图片的原始尺寸就是不一样的。强行resize会丢失信息,影响检测精度。
我记得有一次做视频分析项目。模型输入是视频帧序列,每段视频的帧数都不一样。如果用静态形状,只能把所有视频都补齐到最大帧数,结果内存爆了。后来改成动态形状,按实际帧数处理,问题就解决了。
- 推理引擎需要做更多的内存管理,性能可能下降10%-20%
- 某些算子不支持动态形状,导出时可能会报错
3.4 静态 vs 动态:一张表说清楚
| 对比维度 | 静态形状 | 动态形状 |
|---|---|---|
| 形状定义 | 所有维度固定,如[1,3,224,224] | 部分维度可变,如[-1,3,-1,-1] |
| 推理性能 | 高,内存预分配 | 略低,需要动态分配 |
| 灵活性 | 低,输入必须匹配 | 高,支持多种输入 |
| 部署难度 | 简单,几乎无坑 | 中等,需处理边界情况 |
| 适用场景 | 生产环境、固定输入 | 多变输入、研究原型 |
嗯,这里要注意一点。很多初学者觉得「动态形状更高级,所以应该都用动态」。其实不是的。我曾经在一个推理服务里全部用了动态形状,结果上线后QPS上不去,排查了半天才发现是动态内存分配拖了后腿。后来改成静态形状,性能直接翻倍。
3.5 代码示例:看看形状长什么样
咱们用Python代码直观感受一下。假设你用PyTorch导出一个ONNX模型:
import torch
import torch.onnx
class SimpleModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv = torch.nn.Conv2d(3, 16, 3)
def forward(self, x):
return self.conv(x)
model = SimpleModel()
# 静态形状导出
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy_input, "static_model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes=None) # 不设置动态轴
# 动态形状导出(batch和宽高都动态)
torch.onnx.export(model, dummy_input, "dynamic_model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size", 2: "height", 3: "width"},
"output": {0: "batch_size", 2: "height", 3: "width"}
})
你看,区别就在 dynamic_axes 这个参数。不设置就是静态,设置了就是动态。就这么简单。
3.6 什么时候该用哪种?
我个人的经验是这样的:
- 纯分类模型——用静态形状。输入尺寸固定,batch size也固定,性能最好。
- 目标检测/分割模型——用动态形状。因为输入图片尺寸多变,而且模型本身通常支持可变输入。
- NLP模型——用动态形状。句子长度不可能一样,padding太多会浪费算力。
- 视频处理模型——用动态形状。帧数不确定,batch size也不确定。
说白了,就是看你的输入「变不变」。变就动态,不变就静态。别为了炫技而用动态,也别为了省事而用静态。
好,这一节就到这里。下一节咱们聊聊如何在ONNX中具体配置动态形状,包括不同框架的导出技巧。到时候我会分享一些我踩过的坑,保证让你少走弯路。