假设你经营一家快递公司,设计了一款“智能伸缩箱”:底面长宽固定,高度可以拉伸。客户寄一本书,把高度压到 5 厘米;寄一床被子,拉到 30 厘米。箱子没换、打包流程没变,变的只有“高度”这一个数字。ONNX 模型里的动态轴(Dynamic Axes),就是这口箱子上那扇“会伸缩的门”。 神经网络模型被导出成 ONNX 格式后,输入张量有多个维度。通道数、宽、高这些维度跟网络结构深度绑定,一旦写死就不能轻易改动,是箱子的“固定底”;而“一次处理几张图”(batch)却时时在变:手机端刚来 1 张图,服务器下一秒可能涌进 32 张。如果把 batch 也写死,模型就成了一只永远只能装固定数量货物的箱子,来 1 件浪费、来 33 件装不下。ONNX 的动态轴机制,正是给模型预留的“可伸缩高度”:同一个 .onnx 文件,既能服务 1 张图的低延迟请求,也能吞吐 32 张图的高并发流量。
掌握动态轴,你就能用同一个 ONNX 模型应对千变万化的输入形状,把模型从“实验室演示”推向“生产环境弹性部署”——这是算法部署、推理引擎等 AI 工程岗位的核心技能,也是面试中的高频考点。
可视化演示
演示AI编译器动态轴机制:类型分类展示batch/序列/空间三类动态轴;符号shape展示符号变量与静态维度混合的shape表达式;变长应用对比padding与动态轴在变长文本推理中的维度差异。
本节配有交互动效(进入视口后自动加载,约 3KB)。
动态轴三类 batch/序列/空间 batch轴最常用 维度需运行期才能确定
动态轴初识
想象你是一家快递公司的打包专家。公司最得意的发明是“智能伸缩箱”:底面长宽永远固定,高度却可以伸缩。客户寄一本书,把高度压到 5 厘米;寄一床被子,把高度拉到 30 厘米。箱子没换、流程没变,变的只是“高度”这一个数字。在 ONNX 的世界里,输入张量的每个维度就是箱子的一根尺寸,其中有的尺寸写死,有的尺寸待定。
在 ONNX 的坐标系里,张量的每个维度都叫“轴”。以图像分类模型为例,输入通常长这样 [1, 3, 224, 224]:1 是批次大小(一次看几张图),3 是 RGB 通道,224 是宽和高。通道数和宽高一旦定死,卷积核的结构就固定了,它们像箱子的底面挡板,属于静态轴;而“一次处理几张图”完全取决于调用方,下一秒可能来 1 张,也可能来 32 张,这个维度就该是动态轴。
ONNX 为此设计了动态轴机制:导出模型时,允许开发者把某个维度的“数字”换成“符号”,告诉所有下游工具“这个位置是可变的”。但要注意,动态轴只允许“某个维度的长度”伸缩,不允许改变轴的数量——箱子可以拉高,但不能凭空多出一根提手。在专业术语里,这就是“秩(rank)必须固定”。
回到快递箱:底面 30×40 是静态轴,可调高度就是动态轴。包装员只需在箱子上写一行字:“高度=客户说了算”,无论客户寄书还是寄被子,都不需要重新设计箱子。这正是动态轴的第一课:一次导出,随处可变。
- 静态形状 S_static这是导出时写死的输入规格,四个数字从左到右依次表示批次、通道、高、宽,全部为固定正整数。
- 动态形状 S_dynamic把第一个数字替换为符号 N,表示批次大小这一维留白,等待运行时填入。
- N 的取值约束N 必须是正整数,常见取值是 1、8、16、32 等,具体由用户传入张量的实际形状决定。
符号维度机制
“高度可调”不能只是口头承诺,必须变成一张所有人都读得懂的标签。ONNX 的解法是:给每个维度预留两个字段——dim_value 和 dim_param,二者互斥,只能填一个。就像快递箱的侧边有一块铭牌,上面要么刻死一个数字,要么刻一个“可调”的标记。
dim_value 填一个具体的正整数,比如 224,表示这一维永远锁定;dim_param 填一个字符串,比如 “batch_size“,表示这一维是”符号化“的。那个字符串叫符号名,相当于一个占位符变量:它本身不携带数值,只承诺”运行时会来一个数”。真正把数字代入的动作,发生在推理引擎收到用户输入的那一刻,而不是模型导出时。
符号名有一些设计约束:必须是非空字符串,建议在同一个模型内保持唯一,否则运行时可能产生绑定歧义;它还只存在于输入/输出张量的形状描述中,ONNX 的算子图内部并没有“batch_size 变量”这个概念。换句话说,动态轴是模型边界的“声明”,不是计算图内部的“变量”。
就像标签上要写清楚“谁负责调、怎么调”,dim_param 也是一份开放接口:ONNX 只负责声明“这是可变量”,具体数值由推理引擎和使用方在运行时“签约”。标签写得越规范,后续打包就越省心。
- 维度二选一ONNX 的每个轴用一个 Dimension 消息描述,它只有两种合法状态:填数字或填字符串,不能同时填。
- 静态分支 dim_value填写正整数 v,且要求 v ≥ 1,0 和负数都不是合法的张量维度。
- 动态分支 dim_param填写非空字符串 s,例如 "batch_size",表示这一维留待运行时绑定。
序列化格式
导出的 .onnx 文件,本质是一份用 protobuf 序列化的“标准打包合同”,必须把“哪些维度可伸缩”白纸黑字地写进去。合同的骨架是 ModelProto → GraphProto → 各种 Proto,而输入输出的形状声明存放在 graph.input 和 graph.output 的 ValueInfoProto 中。
在 ValueInfoProto 里,每个张量形状是一个 TensorShapeProto,里面由一个一个 Dimension 组成。动态轴的秘密就藏在 Dimension 的 dim_param 字段中:只要它是字符串,这个维度就“悬空”,等待运行时填入。因此,动态轴不是某个算子的属性,而是“边界声明”——它精确地贴在模型入口和出口的形状上,告诉所有下游工具哪些维度可以伸缩。
有一个常被误解的点:动态轴并不会让 .onnx 文件变大多少。符号名序列化后只占几十字节的字符串存储,跟权重参数动辄几百 MB 相比微不足道。模型压缩的真正大头是算子定义和权重数据,所以“文件小=没用动态轴”的判断是错的。真正运行时占用的内存,要等符号名被绑定成具体数字后,才能按形状连乘算出。
合同上写“高度可调”不会让合同变厚,但签合同时双方必须现场确认“这次调到多少”——序列化阶段只负责把规则写进文件,真正量尺寸要等运行时刻。这也是理解“文件大小 vs 运行时灵活性”两回事的关键。
- 总内存公式 Memory张量占用的字节数,是所有维度的长度连乘,再乘以单个元素占用的字节数。
- 静态维代入静态轴直接使用声明值 dim_value 参与连乘,数值在导出时就已经知道。
- 动态维代入动态轴需要用绑定函数 σ 把符号名映射为本次实际长度,再参与连乘。
运行时绑定
合同签完、货物送到,打包机拿出卷尺现场量高度。推理引擎(如 ONNX Runtime)收到输入张量后,做的第一件事同样是“量尺寸”:形状校验。引擎按顺序检查轴的数量是否与图一致、所有静态维度的长度是否完全相等、动态维度是否为正整数。
校验全部通过后,引擎把 dim_param 绑定为本次输入的实际数值,并沿着整个计算图逐层推断每个中间张量的形状,据此分配内存、挑选合适的 kernel。这一过程叫做“形状绑定”或“动态轴解析”。如果某个中间算子的输出形状依赖于 batch,引擎必须逐个节点地完成形状传播,代价比静态图高不少。
关键点在于:绑定是“逐次”的。下一次调用传一个不同 batch 的张量,引擎会重新绑定、重新分配内存。由于形状在编译期未知,引擎无法像静态图那样“一次编译、永久复用”,只能走更通用的 kernel 或在运行时做条件分支——这正是动态轴性能开销的根源。
打包机每次都要拉一次卷尺,但如果客户永远寄同一高度的箱子,聪明的打包员会记住刻度、下次直接照做。这恰好预告了下一节的主题:把“总是相同的刻度”锁死,能带来多大的性能收益。
- 绑定函数 σ_t第 t 次推理时,引擎把符号名映射为一个正整数 v_t,形成本次调用的绑定关系。
- 形状代入绑定完成后,形状描述中的符号被替换成 v_t,得到本次实际形状。
- 值域约束v_t 必须是正整数,引擎会拒绝非法或与图结构冲突的形状。
性能优化
假如你的业务里 batch 永远是 16,那“可调高度”就变成了一根多余的铰链。干脆把高度锁死在 16,定制专用模具,效率必然更高。固定形状的 ONNX 图也有同样的红利:推理引擎能在编译期拿到全部形状信息,做更激进的优化。
三大收益最为明显。第一,算子融合更彻底:静态形状下,引擎敢把 Conv+BatchNorm+ReLU 融成一个 kernel,省去中间张量的反复读写。第二,内存可复用:引擎能精确计算最大占用,一次性分配一块缓冲池反复使用;而动态形状要么反复 malloc,要么预留倍增的内存,两者都浪费。第三,kernel 选择更精准:cuDNN、oneDNN 这类算子库对特定形状往往有手写优化 kernel,动态形状只能退回到通用实现。
于是业界形成一条黄金准则:动态轴是“按需开启”的能力,不是“默认全开”的配置。生产部署通常只让 batch 或序列长度一个维度动态,通道、高、宽尽量保持静态,在灵活性与性能之间取平衡。
标准化模具快而专一,伸缩箱万能但每次都要调节。选“专一的快”还是“兼容的慢”,取决于业务是流水线还是杂货铺——这正是部署工程师每天要做的判断题。
- 固定形状耗时 T_fixed当 batch=B 且形状预先固定时,耗时近似为 αB+β,其中 α 是单样本边际耗时,β 是固定开销。
- 动态形状耗时 T_dyn动态形状在固定耗时之上多出一块额外开销 γ(B)。
- 额外开销 γ(B)它包含形状解析、内存重新分配、通用 kernel 的选择代价,非负且通常随 B 增大而增大。
常见问题
打包现场永远有意外。动态轴最经典的事故是形状不匹配:图期望 [N, 3, 224, 224],用户却传了 [N, 4, 224, 224](通道轴被改)或 [3, 3, 224, 224](忘了批次维)。这类错误只在运行时暴露,所以调用前用输入张量的 shape 对照模型声明做断言,是最廉价的安全网。
第二类是导出配置失误:torch.onnx.export 的 dynamic_axes 参数是嵌套字典,key 必须与张量名完全一致,下标必须准确对应轴的位置。写错 key 的典型结果是“导出成功但维度悄悄被固定”——不跑不同形状根本发现不了。第三类是性能骤降:同一个模型在固定 batch 下测 5ms,开动态轴后涨到 20ms,原因是引擎走了通用 kernel,这时应做“半固定”处理,或构建多个固定 shape 的推理引擎实例。
理解引擎的校验逻辑本身也是一道防线。几乎所有推理引擎在绑定前都会执行一套形状检查:秩必须等于图的秩,静态轴必须精确匹配,动态轴必须是正整数。这套规则就是打包台前的检查清单。
打包事故大多源于“标签没看全”或“尺寸量错”。把引擎的校验规则当作打包台前的检查清单,出错率自然下降——这也是成熟团队会给动态轴模型配一套“形状冒烟测试”的原因。
- 秩一致性 rank(X)=r引擎先核对轴的数量,图的秩是 r,输入也必须恰好 r 根轴,多了少了都报错。
- 正整数约束每个维度的长度都必须是正整数,0 或负数不可能成为合法的张量维度。
- 静态轴精确匹配凡是被 dim_value 锁定的静态轴,输入形状必须与声明值完全相等;动态轴则只受正整数约束。
工程实践
最后,把前面的知识收拢成一套可落地的标准作业流程(SOP)。我们以 PyTorch → ONNX → ONNX Runtime 为例,目标是让一个分类模型支持动态 batch。
具体分五步走。第一步,在 torch.onnx.export 中传入 dynamic_axes 嵌套字典,例如 {"input": {0: "batch_size"}, "output": {0: "batch_size"}}。第二步,用 onnx.checker 校验模型合法性,再用 shape_inference 推断形状,确认符号维没有被意外消除。第三步,用 onnxruntime 分别以 batch=1 和 batch=16 跑两遍,验证输出形状正确。第四步,做性能回归,对比静态/动态两种导出的延迟与吞吐,决定是否真的需要动态轴。第五步,给入口加最大 batch 上限,防止动态轴 + 超大请求触发内存暴涨(OOM)。
dynamic_axes 的映射语义是“(张量名,轴下标)→ 符号名”。输入设了动态轴,输出通常也要对称设置,否则输出形状推断可能失败;符号名只是字符串,不要求与任何 Python 变量名一致,但建议与业务语义对齐,比如 batch_size、seq_len。
标准流程就像打包手册:贴标签(dynamic_axes)、验货(checker)、试装(Runtime 冒烟测试)、压测(性能回归)、限重(batch 上限)。把五步执行到位,你手里那只“会伸缩的快递箱”才能真正安全地送上生产线的传送带。
- 映射定义dynamic_axes 本质是一个从“张量名 × 轴下标”到“符号名”的映射表。
- 输入示例把输入张量 input 的第 0 轴映射为符号名 batch_size。
- 输出对称输出张量最好也标上同一符号,保证输出形状推断一致,避免被误判为固定维度。
AI 应用场景
医院影像弹性批处理
阶段:模型部署与推理服务
解决的问题:影像科的请求量忽高忽低,固定 batch 的模型要么单张延迟高,要么小请求也要排队凑批。动态轴让同一个 ONNX 模型按需接收 1~32 张 CT 图,低峰期保延迟、高峰期保吞吐,相当于把“智能伸缩箱”搬进了阅片室。
具体案例:某三甲医院 AI 辅助诊断平台,X 光片读片模型在 torch.onnx.export 时配置 {0: "batch_size"},运行时 ONNX Runtime 根据队列积压动态调整 batch,GPU 利用率提升约 40%。
变长文本序列处理
阶段:模型导出与格式转换
解决的问题:NLP 模型输入句子长度不一,若固定为最长长度,短句也会浪费大量计算。把序列长度(seq_len)设为动态轴,模型只需为每条真实输入分配恰好够用的计算量。
具体案例:语音助手意图识别模型,导出 ONNX 时将 input 的第 1 维设为动态,上线后平均输入长度下降 60%,单次推理耗时降低 35%,且无需为不同长度维护多份模型。
多框架互操作迁移
阶段:跨框架模型转换
解决的问题:PyTorch 训练、TensorRT 部署,中间经由 ONNX 转换时,动态轴保证“哪些维度可变”这一信息在格式转换中不丢失,避免在目标框架里重新手工声明。
具体案例:自动驾驶团队的 3D 检测模型,点云点数动态变化,团队用 ONNX 动态轴统一描述边界形状,成功从 PyTorch 迁移到 TensorRT,省去了为每个典型点数维护模板模型的重复工作。
Python 演示
import numpy as npimport onnximport onnxruntime as ortfrom onnx import helper, TensorProto def build_dynamic_model(path): # 输入 X: [batch_size, 4], 第 0 维是动态轴, 符号名为 batch_size X = helper.make_tensor_value_info("X", TensorProto.FLOAT, [None, 4]) X.type.tensor_type.shape.dim[0].dim_param = "batch_size" Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, [None, 2]) Y.type.tensor_type.shape.dim[0].dim_param = "batch_size" # 固定权重 W: [4, 2], 相当于快递箱的固定底 W = helper.make_tensor( "W", TensorProto.FLOAT, [4, 2], [1.0, 0.5, -0.5, 1.0, 0.2, 0.8, -1.0, 0.3], ) node = helper.make_node("MatMul", ["X", "W"], ["Y"]) graph = helper.make_graph([node], "dynamic_axis_demo", [X], [Y], [W]) model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)]) model.ir_version = 8 onnx.checker.check_model(model) onnx.save(model, path) return path def main(): path = build_dynamic_model("dynamic_axis_model.onnx") graph = onnx.load(path).graph dim0 = graph.input[0].type.tensor_type.shape.dim[0] print("dynamic dim_param:", dim0.dim_param) sess = ort.InferenceSession(path, providers=["CPUExecutionProvider"]) for batch in [1, 4, 16]: x = np.random.rand(batch, 4).astype(np.float32) y = sess.run(None, {"X": x})[0] print("batch = {} -> output shape = {}".format(batch, y.shape)) if __name__ == "__main__": main()
X.type.tensor_type.shape.dim[0].dim_param = "batch_size"把输入第 0 维的维度标记改为符号名,相当于在箱子上贴“高度可调”标签。Y.type.tensor_type.shape.dim[0].dim_param = "batch_size"输出也对称标记动态轴,防止输出形状推断被误判为固定。model.ir_version = 8设定 IR 版本为 8,配合 opset 13,保证动态轴语义被工具链正确解析。onnx.checker.check_model(model)用官方校验器做质检,结构非法会直接抛错,相当于发货前的验货环节。y = sess.run(None, {"X": x})[0]同一个推理会话换不同 batch 的输入,验证“一次导出,随处可变”。
常见误区
误区既然动态轴能提升灵活性,那就把所有维度都设成动态轴,batch、通道、宽、高全都可调,这样模型最万能。
正解每个动态维度都会让推理引擎失去优化机会:无法预分配固定内存、无法做算子融合、kernel 选择退化为通用实现。通道维度一旦动态化,卷积核权重形状就无法匹配,模型直接不可用。生产环境通常只让 batch 或序列长度一个维度动态,其余保持静态。
为何常见:初学者容易把“动态”和“灵活”画等号,且在小模型上性能差异不明显,误以为动态轴没有代价。
误区模型支持了动态轴,.onnx 文件里要存各种可能的形状,文件体积一定会变大很多。
正解动态轴只是把维度数字换成字符串占位符,dim_param 最多占几十字节,.onnx 文件大小几乎不变。模型体积主要由算子定义和权重数据决定,与是否使用动态轴没有直接关系。
为何常见:误以为“可调”意味着要存储多份变体模型,混淆了“运行时形状”与“文件存储”两个层面。
误区ONNX 支持动态轴,说明 ONNX 模型的结构可以像 PyTorch 的 eager 模式一样随意变化,层数、分支都能动态增减。
正解动态轴只允许“某个维度的长度”变化,轴的个数、算子图结构、权重形状全部固定。ONNX 本质是静态图格式,动态轴只是静态图上的“边界可调参数”,与动态图完全是两回事。
为何常见:术语里都带“动态”二字,加上部分框架宣传语模糊,容易把两个层次的概念混为一谈。
误区只要在 torch.onnx.export 里写了 dynamic_axes,导出成功就代表模型一定能处理任意形状,不需要额外测试。
正解dynamic_axes 的 key 写错、下标对错轴、忘记给输出配置,都可能导致“导出成功但维度被悄悄固定”或运行时形状推断失败。必须用 onnxruntime 至少跑两种不同形状做回归验证,最好再配合 onnx.shape_inference 检查符号维是否保留。
为何常见:导出成功只说明模型结构合法,不说明运行时形状语义正确;而形状错误往往只在特定 batch 下才暴露,常规单形状测试发现不了。
面试题
Q1. 关于 ONNX 动态轴的含义,下列说法正确的是?
- A.模型的结构可以随意调整
- B.张量的某维度长度运行时可变← 正确答案
- C.网络的层数可以动态增减
- D.权重数值随着输入自动更新
答案:B。动态轴只允许张量某个维度的长度在运行时变化,模型结构(算子图、层数)和权重形状都保持不变。A 和 C 是“结构动态”的误解,D 属于权重更新范畴,都不是动态轴。
Q2. ONNX 中标记某个维度为动态轴,靠的是哪个字段?
- A.采用 dim_value 填写数字
- B.采用 data_type 指定类型
- C.采用 dim_param 命名符号← 正确答案
- D.采用 initializer 保存初值
答案:C。dim_param 保存一个字符串符号名,例如 "batch_size",表示该维度等待运行时绑定。dim_value 是固定数字,data_type 描述数据类型,initializer 保存权重初值,三者都与动态轴标记无关。
Q3. 使用动态轴时,以下哪种变化是被允许的?
- A.改变动态维度的长度数值← 正确答案
- B.改变张量的轴个数数量
- C.改变算子的执行顺序
- D.改变权重张量的形状大小
答案:A。动态轴的含义就是“某个维度的长度可伸缩”,因此 A 是它唯一允许的变化。轴的个数(秩)、算子执行顺序、权重形状在 ONNX 图里都是固定的。
Q4. 推理引擎处理带动态轴的模型时,首先执行的动作是?
- A.重新训练一次网络的权重
- B.修改模型计算图的结构
- C.校验并绑定实际的形状← 正确答案
- D.重新序列化一次模型文件
答案:C。引擎先做形状校验(秩、静态轴是否匹配、动态轴是否为正整数),然后把 dim_param 绑定为本次输入的实际数值。它不会改图、不会重训、也不会重新序列化。
Q5. 相比动态轴,固定形状的主要优势在于?
- A.便于算子融合与内存复用← 正确答案
- B.能够支持的算子数量更多
- C.不再需要任何推理引擎
- D.模型的文件体积会更小
答案:A。静态形状下引擎能提前完成算子融合、精确分配并复用内存。算子支持范围、是否需要推理引擎、模型文件大小都与动静轴的选择没有直接关系。
Q6. torch.onnx.export 的 dynamic_axes 参数在结构上是?
- A.一个普通字符串列表结构
- B.一个嵌套的字典映射结构← 正确答案
- C.一个布尔掩码的数组结构
- D.一个算子的名字典结构
答案:B。dynamic_axes 是嵌套字典,形如 {"input": {0: "batch_size"}},外层 key 是张量名,内层 key 是轴下标,value 是符号名。它精确描述“哪个张量的哪个轴可变”。
Q7. 关于 dim_value 与 dim_param 的说法,正确的有?
- A.两者必须同时被填写设置
- B.两者互斥只能选一个填写← 正确答案
- C.dim_param 用来保存符号名← 正确答案
- D.dim_value 用来保存固定大小← 正确答案
答案:BCD。ONNX 的 Dimension 中,dim_value(固定数字)与 dim_param(符号名)互斥二选一:填了数字就是静态轴,填了符号名就是动态轴。A 的错误在于“同时填写”,正确关系是互斥。
Q8. 动态轴的典型应用场景有哪些?
- A.推理批次大小可变的场景← 正确答案
- B.变长文本序列的 NLP 模型← 正确答案
- C.训练时动态增加网络层数
- D.运行时替换模型的权重
答案:AB。批大小(batch)和序列长度(seq_len)是最典型的两类动态轴。C 是“结构可变”(属于动态图范畴),D 是权重替换(属于模型更新范畴),都不是动态轴解决的问题。
Q9. 开启动态轴可能带来哪些代价?
- A.推理引擎无法预分配固定内存← 正确答案
- B.算子融合等优化机会明显减少← 正确答案
- C.ONNX 文件的体积显著变大
- D.权重的精度会自动地降低
答案:AB。动态形状下,引擎不知道下一次 batch 是多少,只能动态分配内存,且大量依赖形状的优化(算子融合、专用 kernel)无法展开。动态轴不影响模型文件大小,也不会改变权重精度。
Q10. 以下哪个维度通常不建议设为动态轴?
- A.batch 批次维度
- B.序列长度维度
- C.点云点数维度
- D.图像通道维度← 正确答案
答案:D。通道维度与卷积核结构强耦合,改变通道数意味着权重形状不匹配,因此通常保持静态。batch、序列长度、点云点数都是“输入数量”维度,适合动态化。