假设你经营一家快递公司,设计了一款“智能伸缩箱”:底面长宽固定,高度可以拉伸。客户寄一本书,把高度压到 5 厘米;寄一床被子,拉到 30 厘米。箱子没换、打包流程没变,变的只有“高度”这一个数字。ONNX 模型里的动态轴(Dynamic Axes),就是这口箱子上那扇“会伸缩的门”。 神经网络模型被导出成 ONNX 格式后,输入张量有多个维度。通道数、宽、高这些维度跟网络结构深度绑定,一旦写死就不能轻易改动,是箱子的“固定底”;而“一次处理几张图”(batch)却时时在变:手机端刚来 1 张图,服务器下一秒可能涌进 32 张。如果把 batch 也写死,模型就成了一只永远只能装固定数量货物的箱子,来 1 件浪费、来 33 件装不下。ONNX 的动态轴机制,正是给模型预留的“可伸缩高度”:同一个 .onnx 文件,既能服务 1 张图的低延迟请求,也能吞吐 32 张图的高并发流量。

岗位关联

掌握动态轴,你就能用同一个 ONNX 模型应对千变万化的输入形状,把模型从“实验室演示”推向“生产环境弹性部署”——这是算法部署、推理引擎等 AI 工程岗位的核心技能,也是面试中的高频考点。

目录
  1. 可视化演示
  2. 动态轴初识
  3. 符号维度机制
  4. 序列化格式
  5. 运行时绑定
  6. 性能优化
  7. 常见问题
  8. 工程实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示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 是静态轴,可调高度就是动态轴。包装员只需在箱子上写一行字:“高度=客户说了算”,无论客户寄书还是寄被子,都不需要重新设计箱子。这正是动态轴的第一课:一次导出,随处可变。

Sstatic=[1,3,224,224],Sdynamic=[N,3,224,224],N∈Z+\displaystyle S_{\text{static}} = [1, 3, 224, 224], \qquad S_{\text{dynamic}} = [N, 3, 224, 224], \quad N \in \mathbb{Z}^{+}
静态形状把每个维度的长度都写成固定数字;动态形状把批次维替换成符号 N,N 的具体值留到运行时再决定。
  1. 静态形状 S_static这是导出时写死的输入规格,四个数字从左到右依次表示批次、通道、高、宽,全部为固定正整数。
    Sstatic=[1,3,224,224]S_{\text{static}} = [1, 3, 224, 224]
  2. 动态形状 S_dynamic把第一个数字替换为符号 N,表示批次大小这一维留白,等待运行时填入。
    Sdynamic=[N,3,224,224]S_{\text{dynamic}} = [N, 3, 224, 224]
  3. N 的取值约束N 必须是正整数,常见取值是 1、8、16、32 等,具体由用户传入张量的实际形状决定。
    N∈Z+N \in \mathbb{Z}^{+}

符号维度机制

“高度可调”不能只是口头承诺,必须变成一张所有人都读得懂的标签。ONNX 的解法是:给每个维度预留两个字段——dim_value 和 dim_param,二者互斥,只能填一个。就像快递箱的侧边有一块铭牌,上面要么刻死一个数字,要么刻一个“可调”的标记。

dim_value 填一个具体的正整数,比如 224,表示这一维永远锁定;dim_param 填一个字符串,比如 “batch_size“,表示这一维是”符号化“的。那个字符串叫符号名,相当于一个占位符变量:它本身不携带数值,只承诺”运行时会来一个数”。真正把数字代入的动作,发生在推理引擎收到用户输入的那一刻,而不是模型导出时。

符号名有一些设计约束:必须是非空字符串,建议在同一个模型内保持唯一,否则运行时可能产生绑定歧义;它还只存在于输入/输出张量的形状描述中,ONNX 的算子图内部并没有“batch_size 变量”这个概念。换句话说,动态轴是模型边界的“声明”,不是计算图内部的“变量”。

就像标签上要写清楚“谁负责调、怎么调”,dim_param 也是一份开放接口:ONNX 只负责声明“这是可变量”,具体数值由推理引擎和使用方在运行时“签约”。标签写得越规范,后续打包就越省心。

Dimension:{dim_value=v,v≥1  (static)dim_param=s,s≠∅  (dynamic)\displaystyle \text{Dimension}: \begin{cases} \text{dim\_value} = v, & v \ge 1 \;(\text{static}) \\ \text{dim\_param} = s, & s \ne \emptyset \;(\text{dynamic}) \end{cases}
一个 ONNX 维度只有两种合法状态:填固定数字(dim_value)表示静态轴;填非空字符串(dim_param)表示动态轴。
  1. 维度二选一ONNX 的每个轴用一个 Dimension 消息描述,它只有两种合法状态:填数字或填字符串,不能同时填。
    Dimension:{dim_value}  or  {dim_param}\text{Dimension}: \{\text{dim\_value}\} \;\text{or}\; \{\text{dim\_param}\}
  2. 静态分支 dim_value填写正整数 v,且要求 v ≥ 1,0 和负数都不是合法的张量维度。
    dim_value=v,v≥1\text{dim\_value} = v, \quad v \ge 1
  3. 动态分支 dim_param填写非空字符串 s,例如 "batch_size",表示这一维留待运行时绑定。
    dim_param=s,s≠∅\text{dim\_param} = s, \quad s \ne \emptyset

序列化格式

导出的 .onnx 文件,本质是一份用 protobuf 序列化的“标准打包合同”,必须把“哪些维度可伸缩”白纸黑字地写进去。合同的骨架是 ModelProto → GraphProto → 各种 Proto,而输入输出的形状声明存放在 graph.input 和 graph.output 的 ValueInfoProto 中。

在 ValueInfoProto 里,每个张量形状是一个 TensorShapeProto,里面由一个一个 Dimension 组成。动态轴的秘密就藏在 Dimension 的 dim_param 字段中:只要它是字符串,这个维度就“悬空”,等待运行时填入。因此,动态轴不是某个算子的属性,而是“边界声明”——它精确地贴在模型入口和出口的形状上,告诉所有下游工具哪些维度可以伸缩。

有一个常被误解的点:动态轴并不会让 .onnx 文件变大多少。符号名序列化后只占几十字节的字符串存储,跟权重参数动辄几百 MB 相比微不足道。模型压缩的真正大头是算子定义和权重数据,所以“文件小=没用动态轴”的判断是错的。真正运行时占用的内存,要等符号名被绑定成具体数字后,才能按形状连乘算出。

合同上写“高度可调”不会让合同变厚,但签合同时双方必须现场确认“这次调到多少”——序列化阶段只负责把规则写进文件,真正量尺寸要等运行时刻。这也是理解“文件大小 vs 运行时灵活性”两回事的关键。

Memory(X)=(∏i=0n−1di)×size(elem),di={σ(dim_parami),dynamicdim_valuei,static\displaystyle \text{Memory}(X) = \left( \prod_{i=0}^{n-1} d_i \right) \times \text{size}(\text{elem}), \qquad d_i = \begin{cases} \sigma(\text{dim\_param}_i), & \text{dynamic} \\ \text{dim\_value}_i, & \text{static} \end{cases}
张量占用的内存等于所有维度长度连乘,再乘以单个元素字节数;动态轴维度要先用绑定函数 σ 换算成实际长度。
  1. 总内存公式 Memory张量占用的字节数,是所有维度的长度连乘,再乘以单个元素占用的字节数。
    Memory(X)=(∏i=0n−1di)×size(elem)\text{Memory}(X) = \left( \prod_{i=0}^{n-1} d_i \right) \times \text{size}(\text{elem})
  2. 静态维代入静态轴直接使用声明值 dim_value 参与连乘,数值在导出时就已经知道。
    di=dim_valueid_i = \text{dim\_value}_i
  3. 动态维代入动态轴需要用绑定函数 σ 把符号名映射为本次实际长度,再参与连乘。
    di=σ(dim_parami)d_i = \sigma(\text{dim\_param}_i)

运行时绑定

合同签完、货物送到,打包机拿出卷尺现场量高度。推理引擎(如 ONNX Runtime)收到输入张量后,做的第一件事同样是“量尺寸”:形状校验。引擎按顺序检查轴的数量是否与图一致、所有静态维度的长度是否完全相等、动态维度是否为正整数。

校验全部通过后,引擎把 dim_param 绑定为本次输入的实际数值,并沿着整个计算图逐层推断每个中间张量的形状,据此分配内存、挑选合适的 kernel。这一过程叫做“形状绑定”或“动态轴解析”。如果某个中间算子的输出形状依赖于 batch,引擎必须逐个节点地完成形状传播,代价比静态图高不少。

关键点在于:绑定是“逐次”的。下一次调用传一个不同 batch 的张量,引擎会重新绑定、重新分配内存。由于形状在编译期未知,引擎无法像静态图那样“一次编译、永久复用”,只能走更通用的 kernel 或在运行时做条件分支——这正是动态轴性能开销的根源。

打包机每次都要拉一次卷尺,但如果客户永远寄同一高度的箱子,聪明的打包员会记住刻度、下次直接照做。这恰好预告了下一节的主题:把“总是相同的刻度”锁死,能带来多大的性能收益。

σt:dim_param↦vt∈Z+,shape(Xt)=(vt,3,224,224)\displaystyle \sigma_t: \text{dim\_param} \mapsto v_t \in \mathbb{Z}^{+}, \qquad \text{shape}(X_t) = (v_t, 3, 224, 224)
每次推理 t 都建立一次符号名到实际数值的绑定;绑定后符号 N 被替换为一个正整数,得到本次真实的输入形状。
  1. 绑定函数 σ_t第 t 次推理时,引擎把符号名映射为一个正整数 v_t,形成本次调用的绑定关系。
    σt:dim_param↦vt\sigma_t: \text{dim\_param} \mapsto v_t
  2. 形状代入绑定完成后,形状描述中的符号被替换成 v_t,得到本次实际形状。
    shape(Xt)=(vt,3,224,224)\text{shape}(X_t) = (v_t, 3, 224, 224)
  3. 值域约束v_t 必须是正整数,引擎会拒绝非法或与图结构冲突的形状。
    vt∈Z+v_t \in \mathbb{Z}^{+}

性能优化

假如你的业务里 batch 永远是 16,那“可调高度”就变成了一根多余的铰链。干脆把高度锁死在 16,定制专用模具,效率必然更高。固定形状的 ONNX 图也有同样的红利:推理引擎能在编译期拿到全部形状信息,做更激进的优化。

三大收益最为明显。第一,算子融合更彻底:静态形状下,引擎敢把 Conv+BatchNorm+ReLU 融成一个 kernel,省去中间张量的反复读写。第二,内存可复用:引擎能精确计算最大占用,一次性分配一块缓冲池反复使用;而动态形状要么反复 malloc,要么预留倍增的内存,两者都浪费。第三,kernel 选择更精准:cuDNN、oneDNN 这类算子库对特定形状往往有手写优化 kernel,动态形状只能退回到通用实现。

于是业界形成一条黄金准则:动态轴是“按需开启”的能力,不是“默认全开”的配置。生产部署通常只让 batch 或序列长度一个维度动态,通道、高、宽尽量保持静态,在灵活性与性能之间取平衡。

标准化模具快而专一,伸缩箱万能但每次都要调节。选“专一的快”还是“兼容的慢”,取决于业务是流水线还是杂货铺——这正是部署工程师每天要做的判断题。

Tdyn(B)=αB+β+γ(B),Tfixed(B)=αB+β,γ(B)≥0\displaystyle T_{\text{dyn}}(B) = \alpha B + \beta + \gamma(B), \qquad T_{\text{fixed}}(B) = \alpha B + \beta, \qquad \gamma(B) \ge 0
动态形状的耗时等于固定形状耗时加上额外开销 γ(B),γ(B) 来自形状解析、内存重分配和通用 kernel 的代价。
  1. 固定形状耗时 T_fixed当 batch=B 且形状预先固定时,耗时近似为 αB+β,其中 α 是单样本边际耗时,β 是固定开销。
    Tfixed(B)=αB+βT_{\text{fixed}}(B) = \alpha B + \beta
  2. 动态形状耗时 T_dyn动态形状在固定耗时之上多出一块额外开销 γ(B)。
    Tdyn(B)=Tfixed(B)+γ(B)T_{\text{dyn}}(B) = T_{\text{fixed}}(B) + \gamma(B)
  3. 额外开销 γ(B)它包含形状解析、内存重新分配、通用 kernel 的选择代价,非负且通常随 B 增大而增大。
    γ(B)≥0\gamma(B) \ge 0

常见问题

打包现场永远有意外。动态轴最经典的事故是形状不匹配:图期望 [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 的推理引擎实例。

理解引擎的校验逻辑本身也是一道防线。几乎所有推理引擎在绑定前都会执行一套形状检查:秩必须等于图的秩,静态轴必须精确匹配,动态轴必须是正整数。这套规则就是打包台前的检查清单。

打包事故大多源于“标签没看全”或“尺寸量错”。把引擎的校验规则当作打包台前的检查清单,出错率自然下降——这也是成熟团队会给动态轴模型配一套“形状冒烟测试”的原因。

valid(X)  ⟺  (rank(X)=r)  ∧  (∀i:diX∈Z+)  ∧  (∀i∈static:diX=dim_valuei)\displaystyle \text{valid}(X) \iff \big(\text{rank}(X) = r\big) \;\land\; \big(\forall i: d_i^{X} \in \mathbb{Z}^{+}\big) \;\land\; \big(\forall i \in \text{static}: d_i^{X} = \text{dim\_value}_i\big)
一个输入合法,当且仅当:秩等于图的秩、所有维度都是正整数、所有静态轴与声明值完全相等。
  1. 秩一致性 rank(X)=r引擎先核对轴的数量,图的秩是 r,输入也必须恰好 r 根轴,多了少了都报错。
    rank(X)=r\text{rank}(X) = r
  2. 正整数约束每个维度的长度都必须是正整数,0 或负数不可能成为合法的张量维度。
    ∀i:diX∈Z+\forall i: d_i^{X} \in \mathbb{Z}^{+}
  3. 静态轴精确匹配凡是被 dim_value 锁定的静态轴,输入形状必须与声明值完全相等;动态轴则只受正整数约束。
    ∀i∈static:diX=dim_valuei\forall i \in \text{static}: d_i^{X} = \text{dim\_value}_i

工程实践

最后,把前面的知识收拢成一套可落地的标准作业流程(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:(tensor_name,axis_index)↦symbol_name\displaystyle \text{dynamic\_axes}: \big(\text{tensor\_name}, \text{axis\_index}\big) \mapsto \text{symbol\_name}
dynamic_axes 是一个从“张量名 × 轴下标”到“符号名”的映射,输入输出对称配置才能保证形状推断一致。
  1. 映射定义dynamic_axes 本质是一个从“张量名 × 轴下标”到“符号名”的映射表。
    dynamic_axes:(tensor_name,axis_index)↦symbol_name\text{dynamic\_axes}: (\text{tensor\_name}, \text{axis\_index}) \mapsto \text{symbol\_name}
  2. 输入示例把输入张量 input 的第 0 轴映射为符号名 batch_size。
    ("input",0)↦"batch_size"(\text{"input"}, 0) \mapsto \text{"batch\_size"}
  3. 输出对称输出张量最好也标上同一符号,保证输出形状推断一致,避免被误判为固定维度。
    ("output",0)↦"batch_size"(\text{"output"}, 0) \mapsto \text{"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 演示

python41 行
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()
目的:手工构建一个带动态轴 batch_size 的 ONNX 模型(MatMul),并用 onnxruntime 以 1、4、16 三种 batch 运行,验证同一个模型能弹性处理不同形状的输入。
  1. X.type.tensor_type.shape.dim[0].dim_param = "batch_size"把输入第 0 维的维度标记改为符号名,相当于在箱子上贴“高度可调”标签。
  2. Y.type.tensor_type.shape.dim[0].dim_param = "batch_size"输出也对称标记动态轴,防止输出形状推断被误判为固定。
  3. model.ir_version = 8设定 IR 版本为 8,配合 opset 13,保证动态轴语义被工具链正确解析。
  4. onnx.checker.check_model(model)用官方校验器做质检,结构非法会直接抛错,相当于发货前的验货环节。
  5. 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 动态轴的含义,下列说法正确的是?

单选题 · easy

  • A.模型的结构可以随意调整
  • B.张量的某维度长度运行时可变← 正确答案
  • C.网络的层数可以动态增减
  • D.权重数值随着输入自动更新

答案:B。动态轴只允许张量某个维度的长度在运行时变化,模型结构(算子图、层数)和权重形状都保持不变。A 和 C 是“结构动态”的误解,D 属于权重更新范畴,都不是动态轴。

Q2. ONNX 中标记某个维度为动态轴,靠的是哪个字段?

单选题 · easy

  • A.采用 dim_value 填写数字
  • B.采用 data_type 指定类型
  • C.采用 dim_param 命名符号← 正确答案
  • D.采用 initializer 保存初值

答案:C。dim_param 保存一个字符串符号名,例如 "batch_size",表示该维度等待运行时绑定。dim_value 是固定数字,data_type 描述数据类型,initializer 保存权重初值,三者都与动态轴标记无关。

Q3. 使用动态轴时,以下哪种变化是被允许的?

单选题 · mid

  • A.改变动态维度的长度数值← 正确答案
  • B.改变张量的轴个数数量
  • C.改变算子的执行顺序
  • D.改变权重张量的形状大小

答案:A。动态轴的含义就是“某个维度的长度可伸缩”,因此 A 是它唯一允许的变化。轴的个数(秩)、算子执行顺序、权重形状在 ONNX 图里都是固定的。

Q4. 推理引擎处理带动态轴的模型时,首先执行的动作是?

单选题 · mid

  • A.重新训练一次网络的权重
  • B.修改模型计算图的结构
  • C.校验并绑定实际的形状← 正确答案
  • D.重新序列化一次模型文件

答案:C。引擎先做形状校验(秩、静态轴是否匹配、动态轴是否为正整数),然后把 dim_param 绑定为本次输入的实际数值。它不会改图、不会重训、也不会重新序列化。

Q5. 相比动态轴,固定形状的主要优势在于?

单选题 · mid

  • A.便于算子融合与内存复用← 正确答案
  • B.能够支持的算子数量更多
  • C.不再需要任何推理引擎
  • D.模型的文件体积会更小

答案:A。静态形状下引擎能提前完成算子融合、精确分配并复用内存。算子支持范围、是否需要推理引擎、模型文件大小都与动静轴的选择没有直接关系。

Q6. torch.onnx.export 的 dynamic_axes 参数在结构上是?

单选题 · mid

  • A.一个普通字符串列表结构
  • B.一个嵌套的字典映射结构← 正确答案
  • C.一个布尔掩码的数组结构
  • D.一个算子的名字典结构

答案:B。dynamic_axes 是嵌套字典,形如 {"input": {0: "batch_size"}},外层 key 是张量名,内层 key 是轴下标,value 是符号名。它精确描述“哪个张量的哪个轴可变”。

Q7. 关于 dim_value 与 dim_param 的说法,正确的有?

多选题 · hard

  • A.两者必须同时被填写设置
  • B.两者互斥只能选一个填写← 正确答案
  • C.dim_param 用来保存符号名← 正确答案
  • D.dim_value 用来保存固定大小← 正确答案

答案:BCD。ONNX 的 Dimension 中,dim_value(固定数字)与 dim_param(符号名)互斥二选一:填了数字就是静态轴,填了符号名就是动态轴。A 的错误在于“同时填写”,正确关系是互斥。

Q8. 动态轴的典型应用场景有哪些?

多选题 · mid

  • A.推理批次大小可变的场景← 正确答案
  • B.变长文本序列的 NLP 模型← 正确答案
  • C.训练时动态增加网络层数
  • D.运行时替换模型的权重

答案:AB。批大小(batch)和序列长度(seq_len)是最典型的两类动态轴。C 是“结构可变”(属于动态图范畴),D 是权重替换(属于模型更新范畴),都不是动态轴解决的问题。

Q9. 开启动态轴可能带来哪些代价?

多选题 · hard

  • A.推理引擎无法预分配固定内存← 正确答案
  • B.算子融合等优化机会明显减少← 正确答案
  • C.ONNX 文件的体积显著变大
  • D.权重的精度会自动地降低

答案:AB。动态形状下,引擎不知道下一次 batch 是多少,只能动态分配内存,且大量依赖形状的优化(算子融合、专用 kernel)无法展开。动态轴不影响模型文件大小,也不会改变权重精度。

Q10. 以下哪个维度通常不建议设为动态轴?

单选题 · easy

  • A.batch 批次维度
  • B.序列长度维度
  • C.点云点数维度
  • D.图像通道维度← 正确答案

答案:D。通道维度与卷积核结构强耦合,改变通道数意味着权重形状不匹配,因此通常保持静态。batch、序列长度、点云点数都是“输入数量”维度,适合动态化。