老陈开了一家“AI 私房菜”,后厨墙上贴着一张祖传魔法菜谱,名字叫 ONNX。菜谱上每一道菜都画着专用餐盘:接客的盘子标“1 人份”,装图的盘子标“224×224”,上菜的托盘标“1 层”。盘子尺寸全是固定数字。客人敢拿一张 512 宽的图片来,后厨直接摆手:您的菜盘子不对,做不了。 有一天,一位熟客下了一单特殊的需求:“我家吃饭人数不固定,照片尺寸也不固定,您能不能让餐盘自己伸缩?”老陈盯着菜谱琢磨半天,最后发现 ONNX 允许把某些尺寸数字换成“伸缩标签 X”——这就是动态轴(Dynamic Axes)。盘子只要贴着 X,就能根据客人的需求自动拉长缩短:人数变、宽高变、序列长度变,统统不在话下。 本文会从“固定餐盘”讲起,带你认识动态轴的本质、符号维度、形状推理,再讲怎样写导出配置、避开算子约束、优化推理性能,最后用一段真机代码演示完整的动态轴导出与推理。读完你就能让手里的模型像伸缩餐桌一样“因客而变”。

岗位关联

掌握动态轴,你就能把一张“死板”的模型菜单变成能接百样客人的“弹性菜单”,从此告别上线前最让人头痛的 shape 报错。

目录
  1. 可视化演示
  2. 静态轴:固定尺寸的餐盘
  3. 动态轴:贴上伸缩标签的盘子
  4. 形状推理:后厨的流水线账本
  5. 导出配置:写菜单时标注伸缩盘
  6. 算子约束:固定管道的传菜口
  7. 推理优化:厨房的排程烦恼
  8. 工程实践:有的放矢的伸缩桌
  9. 问题排查:对不上号的传菜口
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态展示ONNX动态轴的可变维度业务需求、dim_param定义与表示、形状推理传播机制及各类算子的动态轴兼容性(Elementwise 100%)。

本节配有交互动效(进入视口后自动加载,约 3KB)。

静态轴:固定尺寸的餐盘

老陈的菜谱最初像一张“填空题”:每个装食材的容器都写死了尺寸。你拿来的图片必须恰好装进 [1, 3, 224, 224] 的盘子,差一点都不行。这个 [1, 3, 224, 224] 就是张量的“形状”,而形状里的每个数字都代表一个轴的长度。

在 ONNX 世界里,数据以张量(Tensor)形式流动,模型的本质是一张计算图:节点是算子,边是张量。对于图片,常用 NCHW 布局表示:N 是图片张数(批次),C 是通道数,H 是高,W 是宽。模型导出时如果没有特别说明,这些维度就会按照参考样本的尺寸被写死。

ONNX 文件里的形状并不是“画”在盘上的花纹,而是存在结构化的字段中。图输入的描述体叫 ValueInfoProto,其中的 type -> tensor_type -> shape 记录着每个维度的具体数值,这个数值的载体就是 dim_value。序列化成 .onnx 文件后,这一串固定数字就成了模型不可退让的“规矩”。

固定餐盘的好处是规格统一、便于提前备菜;坏处是只要任何一个维度对不上,模型直接“拒单”。这也是静态轴在实际部署中最让人头疼的地方:上线前总会有用户传来各种尺寸的图,而模型只认那一套数字。

shape(X)=[d0,d1,…,dn−1]\text{shape}(X) = [d_0, d_1, \ldots, d_{n-1}]
  1. shape 的含义$X$ 是任意一个张量,$\text{shape}(X)$ 返回该张量所有轴的长度所组成的数组,例如 $[1, 3, 224, 224]$。
    shape(X)=[d0,d1,…,dn−1]\text{shape}(X) = [d_0, d_1, \ldots, d_{n-1}]
  2. 每个轴的长度$d_i$ 是第 $i$ 个轴的长度,$n$ 是轴的数量(也叫张量的阶),每个 $d_i$ 都是正整数,等价于 ONNX 里的 dim_value。
    di∈Z≥1,i=0,1,…,n−1d_i \in \mathbb{Z}_{\ge 1}, \quad i = 0, 1, \ldots, n-1

动态轴:贴上伸缩标签的盘子

为了接住“人数不固定、尺寸不固定”的客人,老陈想出个办法:在餐盘上贴一张伸缩标签 X。客人说“我这桌 8 人”,标签 X 就变成 8;说“3 人”,X 就变成 3。这就是动态轴的直觉:把固定的数字,换成一个可被赋值的符号。

ONNX 的维度描述 Dimension 里除了 dim_value,还有一个字段叫 dim_param。dim_param 是一个字符串,比如 "batch"、"height"、"width"。当某个轴写成 dim_param 时,它就是动态的;也可以写成空字符串,表示“完全未知、完全动态”。动态轴的本质,就是“把形状里的某个数字替换成符号”。

要特别注意的是,动态轴并不是“随便动”。同一个符号如果在模型里出现多次,这些位置就必须取同一个值。比如输入图片的批次维度叫 "batch",输出张量的批次维度也叫 "batch",那么输入批次是 8 时,输出批次也必须是 8。这就像同一张伸缩桌在前后厅共用一个编号 X,前面的桌坐了几个人,后面上菜就必须按几个人准备。

有了伸缩标签,老陈的菜谱终于能接客。但这只是第一步:标签贴上去之后,后厨还得会算——不同客人点同一道菜,到底该上多少份、多大份?这就轮到形状推理登场了。

d_i \in \mathbb{Z}_{\ge 1} \cup \{\ast\}, \quadd_i^{(A)} = d_j^{(B)} \iff \operatorname{sym}_i^{(A)} = \operatorname{sym}_j^{(B)}
  1. 维度的合法取值每个轴要么是一个固定的正整数,要么是星号(用 $\ast$ 表示“可伸缩”),这就是“固定数字”与“伸缩标签”两种状态的数学写法。
    di∈Z≥1∪{∗}d_i \in \mathbb{Z}_{\ge 1} \cup \{\ast\}
  2. 符号共享绑定如果张量 $A$ 的第 $i$ 维上的符号名与张量 $B$ 的第 $j$ 维上的符号名相同,那么这两个轴在推理时必须取相同数值。这就是 ONNX 用来保证“输入输出批次一致”的规则。
    di(A)=dj(B)  ⟺  sym⁡i(A)=sym⁡j(B)d_i^{(A)} = d_j^{(B)} \iff \operatorname{sym}_i^{(A)} = \operatorname{sym}_j^{(B)}

形状推理:后厨的流水线账本

客人坐下后,后厨要拿着伸缩标签推算整条流水线上每一道菜的量。ONNX 中也有同样的机制,叫形状推理(Shape Inference):给定输入张量的形状和算子属性,推导出输出张量的形状。形状推理让模型在只有符号维度时,依然能知道中间结果长什么样。

最典型的例子是卷积层。想象后厨有一块“配菜网格”,一个固定大小的网兜(卷积核)在网格上滑动,每走一步捞起一撮菜。输出网格的大小由输入网格大小、网兜大小、滑动步长和边缘填充共同决定。数学上就是经典的卷积输出尺寸公式:

更妙的是,这个公式完全兼容符号维度:如果输入 $H_{\text{in}}$ 是动态的 $\ast$,输出 $H_{\text{out}}$ 也会被推导成动态的 $\ast$ 或一个符号表达式。动态信息就像流水线上的传单,一张接一张往下传,中间结果的形状记录在 value_info 里,供优化器和调试工具查阅。

Hout=⌊Hin+2P−KS⌋+1H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} + 2P - K}{S} \right\rfloor + 1
  1. 输入高度$H_{\text{in}}$ 是输入特征图的高度;$H_{\text{out}}$ 是输出特征图的高度。
    Hin∈Z≥1H_{\text{in}} \in \mathbb{Z}_{\ge 1}
  2. 填充与核大小$P$ 是边缘填充的层数,$K$ 是卷积核的大小。分子中的 $2P$ 表示上下各填充一层,$K$ 表示网兜本身占据的格子数。
    Hin+2P−KH_{\text{in}} + 2P - K
  3. 步长与向下取整$S$ 是滑动步长,$\lfloor \cdot \rfloor$ 表示向下取整,因为网格不能切出半个格子;最后还要加 1,因为起点本身算一格。
    ⌊Hin+2P−KS⌋+1\left\lfloor \frac{H_{\text{in}} + 2P - K}{S} \right\rfloor + 1

导出配置:写菜单时标注伸缩盘

光会推理还不够,老陈得在写菜单时就告诉后厨:哪个盘子是伸缩盘、哪个维度允许伸缩。这个“写菜单”的动作,对应到工程里就是模型导出阶段的配置。PyTorch 训练好的模型默认只认固定输入,需要我们在导出时显式声明动态轴。

核心参数是 torch.onnx.export 里的 dynamic_axes。它接收一个字典,结构是 {张量名: {维度索引: 符号名}}。举个例子:

这段配置的意思是:input 的第 0 维绑定符号 batch,第 2、3 维绑定 height、width;output 的第 0 维也绑定 batch。由于输入输出共享同一个 batch 符号,运行时它们的批次一定一致。导出后,ONNX 文件里对应的 dim_value 会变成 dim_param。

导出配置是动态轴落地最关键的一步:菜单上没写清楚 X 在哪里,后厨就只能按固定盘子出菜。但只要这一步写对,模型文件里就会留下清晰的符号标记,供后续任何推理引擎读取。

s~i={si,i∉Δ∗,i∈Δ\tilde{s}_i =\begin{cases}s_i, & i \notin \Delta \\\ast, & i \in \Delta\end{cases}
  1. 固定形状$s_i$ 是导出参考输入(dummy_input)的第 $i$ 个维度的长度,例如 $s_0 = 1$。
    s=[s0,s1,…,sn−1]s = [s_0, s_1, \ldots, s_{n-1}]
  2. 动态轴集合$\Delta$ 是我们要改成动态轴的维度索引集合,比如 $\Delta = \{0, 2, 3\}$ 表示 batch 和高宽都动态。
    Δ={i∣第 i 维被声明为动态}\Delta = \{i \mid \text{第 } i \text{ 维被声明为动态}\}
  3. 替换规则属于 $\Delta$ 的维度在模型中写成 $\ast$,不属于 $\Delta$ 的维度保留原来的固定数字。这就是“只在需要伸缩的位置贴标签”。
    s~i=∗,∀i∈Δ\tilde{s}_i = \ast, \quad \forall i \in \Delta
python7 行
dynamic_axes = {    "input": {0: "batch", 2: "height", 3: "width"},    "output": {0: "batch"}}torch.onnx.export(model, dummy, "model.onnx",                  input_names=["input"], output_names=["output"],                  dynamic_axes=dynamic_axes)

算子约束:固定管道的传菜口

盘子可以伸缩,但后厨的传菜管道不是所有位置都能伸缩。老陈发现,有些算子天生灵活,有些算子则要求某个维度必须固定。这也是动态轴最常见的“坑”:你给输入声明了动态宽高,结果中间某个全连接层直接报错。

卷积和池化是“灵活派”。它们对空间维度没有硬性长度要求,输出尺寸可以随输入尺寸变化,因此很适合配合动态 H/W。全连接层(Gemm / MatMul)则是“固定派”,因为矩阵乘法的本质要求两个矩阵的“接缝”维度完全对齐。这个接缝维度一旦写成动态,形状推理就无法确定结果形状,部署时很容易崩溃。

此外,Reshape 也需要小心。它把张量重新排列成新形状,但总元素个数必须不变;一个维度动态时,另一个维度常常必须写成 -1,让推理引擎自动计算。正确理解每个算子的“伸缩边界”,才能避免菜单上标了 X、实际上菜时却卡在传菜口的尴尬。

Cm×n=Am×p×Bp×n  ⟹  pA=pBC_{m \times n} = A_{m \times p} \times B_{p \times n} \implies p_A = p_B
  1. 矩阵乘法$A$ 的形状是 $m \times p$,$B$ 的形状是 $p \times n$,结果 $C$ 的形状是 $m \times n$。
    Cm×n=Am×p×Bp×nC_{m \times n} = A_{m \times p} \times B_{p \times n}
  2. 中间维度相等两个矩阵能够相乘的必要条件是 $A$ 的“宽”等于 $B$ 的“高”,即 $p$ 必须一致。这个 $p$ 就是传菜管道的接口直径,不能一个写 16、另一个写动态 $\ast$。
    pA=pBp_A = p_B

推理优化:厨房的排程烦恼

动态轴让模型接客更灵活,但老陈渐渐发现一个新问题:厨房每次遇到新人数,都得重新排桌子。固定 shape 时,推理引擎可以提前做好内存布局、算子融合、自动调优;可 shape 一变,之前那些优化成果可能全部作废,需要重新规划。

在 ONNX Runtime、TensorRT 等推理引擎中,固定 shape 往往能触发更深度的图优化:预分配内存、选择最优 kernel、展开循环。而动态 shape 会迫使引擎在运行时遇到新形状时重新做这些决策。第一次遇到 8 人桌时排一次,第一次遇到 3 人桌时又要排一次。

因此,理性做法是限制动态范围:如果能用“固定几个档位”解决,就尽量不要让 shape 无限变化。给伸缩桌装上限位器,既保住了灵活性,又避免了厨房反复重排。

Ttotal=∑k=1M(Topt(k)+Trun(k))T_{\text{total}} = \sum_{k=1}^{M} \left( T_{\text{opt}}^{(k)} + T_{\text{run}}^{(k)} \right)
  1. 形状种类数$M$ 是生产环境中出现的不同输入形状种类数,例如图片服务可能出现 224、416、512 三种尺寸。
    M∈Z≥1M \in \mathbb{Z}_{\ge 1}
  2. 优化时间$T_{\text{opt}}^{(k)}$ 是为第 $k$ 种新 shape 重新做图优化、内存规划和自动调优所花费的时间。
    Topt(k)≥0T_{\text{opt}}^{(k)} \ge 0
  3. 推理时间$T_{\text{run}}^{(k)}$ 是第 $k$ 种 shape 真正跑一次推理的时间。总耗时是“重新排桌”与“正式上菜”的叠加。
    Ttotal=∑k=1M(Topt(k)+Trun(k))T_{\text{total}} = \sum_{k=1}^{M} \left( T_{\text{opt}}^{(k)} + T_{\text{run}}^{(k)} \right)

工程实践:有的放矢的伸缩桌

明白了动态轴的能力与代价,老陈开始给不同客人定制不同的伸缩方案。工程上最常见的三种动态轴需求是:动态批次、多分辨率输入和动态序列长度。

动态批次适合在线推理服务。请求往往零散到达,如果模型只支持 batch=1,GPU 算力浪费严重;如果固定 batch=16,又得等请求凑齐。把 batch 维度设为动态,系统就能按当前窗口内的请求数灵活打包,兼顾延迟和吞吐。多分辨率输入则适合目标检测与分割任务——图片来自不同设备,原图直接送入网络往往比强行 resize 更保真。NLP 模型的序列长度也一样:每个请求的 token 数天然不同,把序列维度做成动态,能省去大量 padding 带来的浪费。

工程实践的原则可以总结成一句话:动态轴是给部署松绑的工具,但不是越多越好。只让必要的轴动态,其余维度保持固定,才能同时拿到灵活性与性能。

M=⌈NB⌉,Ttotal=M⋅t(B)M = \left\lceil \frac{N}{B} \right\rceil, \qquad T_{\text{total}} = M \cdot t(B)
  1. 请求总数与批大小$N$ 是待处理的请求总数,$B$ 是每个批次包含的样本数。二者单位都是“个”,所以相除结果无量纲。
    NB\frac{N}{B}
  2. 向上取整$\lceil \cdot \rceil$ 向上取整,因为即使最后一批不满,也要完整跑一次推理。$M$ 就是总共要跑的批次数。
    M=⌈NB⌉M = \left\lceil \frac{N}{B} \right\rceil
  3. 总耗时$t(B)$ 是每批推理耗时。增大 $B$ 时,$M$ 变小,虽然单批时间略增,但总吞吐通常会更好。这就是动态批次在工程上“凑满座位”的价值。
    Ttotal=M⋅t(B)T_{\text{total}} = M \cdot t(B)

问题排查:对不上号的传菜口

动态轴真正上线后,最常见的报错不再是“模型不会推理”,而是“传菜口对不上号”。你可能收到类似这样的报错:Actual input shape [4,3,512,512] differs from expected [1,3,224,224]。这通常说明模型根本没有被导出成动态轴,或者某个维度的符号没有绑定成功。

排查分三步走。第一,用 onnx.load 读取模型,打印 graph.input 里每个 ValueInfoProto 的 shape 字段,看 dim 是 dim_value(固定数字)还是 dim_param(符号)。第二,用 Netron 打开模型,可视化地检查各条边上的 shape 标注,找到“哪根线断了”。第三,准备多种形状的测试输入,分别用 onnxruntime 跑一遍,观察是哪一种 shape 触发异常。如果问题出在算子内部,还要检查是否满足广播条件。

排查动态轴问题,本质上是在确认“伸缩标签是否真正传递到了每一个需要的节点”。标签断了,模型就退回静态;标签通了,再大的盘子也能稳稳接住。

∀k,dk(A)=dk(B) 或 dk(A)=1 或 dk(B)=1\forall k,\quad d_k^{(A)} = d_k^{(B)} \ \text{或} \ d_k^{(A)} = 1 \ \text{或} \ d_k^{(B)} = 1
  1. 逐维检查$d_k^{(A)}$ 表示张量 $A$ 在第 $k$ 个轴的长度,$d_k^{(B)}$ 同理。广播要求每个维度都满足条件。
    ∀k\forall k
  2. 相等或为 1两个张量能直接运算的充分条件是该维长度相等;若某一方为 1,则可以自动扩展成另一方长度。这就是“传菜口尺寸要么相同、要么有一边是万能接口”的数学表达。
    dk(A)=dk(B) 或 dk(A)=1 或 dk(B)=1d_k^{(A)} = d_k^{(B)} \ \text{或} \ d_k^{(A)} = 1 \ \text{或} \ d_k^{(B)} = 1

AI 应用场景

在线图像检测:任意尺寸图片直接推理

阶段:模型部署与推理服务

解决的问题:传统目标检测服务要求输入固定尺寸(如 608×608),用户上传的监控截图、手机照片比例各异。强行 resize 会压缩小目标,导致漏检;用动态轴让宽高维度可变化,模型可以直接接收原始图像分辨率。

具体案例:某智慧安防平台上线车牌识别模型,输入尺寸从 640×640 扩展到动态 H/W 后,同一模型同时服务抓拍机、手持设备和高空相机,省去了多份固定尺寸模型副本的维护成本。

大语言模型服务:动态序列长度

阶段:模型推理与生成

解决的问题:对话场景中每个用户的 prompt token 数不同,固定长度意味着要么大量 padding 浪费算力,要么截断丢失信息。把序列长度设为动态轴后,模型按实际 token 数计算,显著降低无效计算。

具体案例:某企业私有化 LLM 服务把 sequence_length 作为动态轴,长文档问答的平均首 token 延迟下降约 40%,同时避免了超长输入被暴力截断的问题。

推荐系统召回:动态 Batch 打包

阶段:在线特征计算与模型推理

解决的问题:推荐服务流量有高峰低谷,固定 batch 要么延迟高、要么 GPU 利用率低。动态 batch 轴让推理服务根据当前排队请求数自动凑批,在延迟边界内最大化吞吐。

具体案例:某电商推荐服务将召回模型导出为动态 batch 的 ONNX 模型,配合请求队列调度,在双 11 高峰 GPU 利用率从 42% 提升到 76%,P99 延迟保持稳定。

Python 演示

python52 行
import numpy as npimport onnximport onnxruntime as ortimport torchimport torch.nn as nn  class FlexibleNet(nn.Module):    def __init__(self):        super().__init__()        self.features = nn.Sequential(            nn.Conv2d(3, 8, kernel_size=3, padding=1),            nn.ReLU(),            nn.MaxPool2d(2),            nn.Conv2d(8, 16, kernel_size=3, padding=1),            nn.ReLU(),            nn.AdaptiveAvgPool2d((4, 4))        )        self.head = nn.Linear(16 * 4 * 4, 10)     def forward(self, x):        x = self.features(x)        x = x.flatten(1)        return self.head(x)  model = FlexibleNet().eval()dummy = torch.randn(2, 3, 32, 32)dynamic_axes = {    "input": {0: "batch", 2: "height", 3: "width"},    "output": {0: "batch"}}onnx_path = "flexible.onnx"torch.onnx.export(    model, dummy, onnx_path,    input_names=["input"], output_names=["output"],    dynamic_axes=dynamic_axes, opset_version=17) onnx.checker.check_model(onnx_path)loaded = onnx.load(onnx_path)g = loaded.graphfor vi in list(g.input) + list(g.output):    dims = [d.dim_param if d.dim_param else d.dim_value            for d in vi.type.tensor_type.shape.dim]    print(vi.name, dims) session = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])for b, h, w in [(1, 16, 16), (4, 64, 64), (7, 48, 80)]:    x = np.random.randn(b, 3, h, w).astype(np.float32)    y = session.run(["output"], {"input": x})[0]    print("input", x.shape, "->", "output", y.shape)
目的:训练一个能接受任意宽高的分类网络,导出为带动态轴的 ONNX 模型,再用 ONNX Runtime 验证多种输入尺寸都能正常推理。
  1. dynamic_axes = { "input": {0: "batch", 2: "height", 3: "width"}, ... }声明输入张量的批次、高、宽三个维度都是伸缩标签,输出批次也绑定同一个 batch 符号。
  2. torch.onnx.export(model, dummy, onnx_path, ..., dynamic_axes=dynamic_axes)执行导出,动态轴配置会写入 ONNX 文件,替代默认的固定形状。
  3. onnx.checker.check_model(onnx_path)校验导出文件的语法和结构,相当于检查菜单格式是否合规。
  4. d.dim_param if d.dim_param else d.dim_value逐维度打印形状信息:有符号名打印符号,否则打印固定值,用于确认动态轴是否真正写入。
  5. session.run(["output"], {"input": x})用不同的 (batch, height, width) 组合驱动同一个模型,验证动态轴确实生效。

常见误区

误区我把 `dynamic_axes` 写上之后,模型就百无禁忌了,任何尺寸都能高效跑,不需要再做任何处理。

正解动态轴解决的是“维度不固定”的合法性问题,不代表每个算子都支持任意形状。全连接层、Reshape、部分自定义算子仍有内部约束;而且新 shape 首次出现时往往有额外的优化开销。动态轴更像是给了盘子伸缩能力,但传菜管道的接口仍然是固定的。

为何常见:初学者看到「动态」二字,容易联想到“万能”,忽略了 ONNX 图中每个算子都有各自的形状约束。

误区在 ONNX 文件里,只要把 `dim_value` 改得大一点,比如从 224 改成 512,就得到动态轴了。

正解dim_value 表示固定数值,改大只是把固定值从 224 变成 512,模型依然只认 512。动态轴必须使用 dim_param(符号名)或将维度留空,让形状在推理时按实际输入决定。

为何常见:人们容易从“改数字”的角度理解配置,而动态轴的本质是“把数字换成符号”,这是两种完全不同的机制。

误区输入 tensor 的第 0 维叫 `"batch"`,输出 tensor 的第 0 维也叫 `"batch"`,那我输入 8 张图,输出是不是可以随意返回 16 条结果?

正解同一个符号名在 ONNX 表示“同一个伸缩标签”,所有挂着这个标签的维度在任何一次推理中都必须取同一个数值。想让输出形状与输入不同,就不要给它们绑定相同的符号名。

为何常见:大家习惯把名字当成普通字符串,忽略了 ONNX 中符号名本身就是“数值一致性约束”的载体。

误区既然动态轴这么好用,那我把所有维度都设成动态,这样以后再也不怕形状变化了。

正解动态轴需要在推理时处理可变的内存布局、可能在每次新 shape 到来时重新做图优化与 kernel 选择,GPU 场景尤其明显。工程上通常只让真正需要变化的轴动态,其余维度全部固定。

为何常见:教程里往往强调动态轴带来的灵活性,却很少画等号讲清“灵活也是要买单的”,导致初学者默认零成本。

误区模型一旦导出成 ONNX,动态轴就只能重训重导,代码丢了就等于判死刑。

正解ONNX 是开放格式,可以用 onnx 库直接修改 ValueInfoProto 里的 dim 字段,例如把 dim_value 改成 dim_param,再 onnx.save 写回。虽然手工改图需要谨慎,但绝不是“一锤定音”。

为何常见:多数人只熟悉 PyTorch 导出流程,不了解 ONNX 本身的图结构编辑能力,从而误以为导出即不可逆。

面试题

Q1. ONNX 动态轴的根本作用是什么?

单选题 · easy

  • A.修改模型权重数值
  • B.让输入维度灵活可变← 正确答案
  • C.动态增减神经网络层
  • D.自动选择推理后端

答案:B。动态轴是把形状中的固定数字换成符号(如 dim_param),解决的是“输入的 shape 维度不固定”的问题。它既不修改权重数值,也不改变网络层数,更不会自动切换推理后端。

Q2. torch.onnx.export 中 dynamic_axes={"input": {0: "batch"}} 表示什么?

单选题 · mid

  • A.把第 0 维固定为 1
  • B.把第 0 维绑定为 batch 符号← 正确答案
  • C.删除输入的第 0 个维度
  • D.把输出第 0 维改为 batch

答案:B。该写法将名为 input 的张量的第 0 维绑定为符号 batch,即“这个轴可以伸缩”。它不会固定、删除第 0 维,也不会影响输出张量的配置,除非你额外指定 output 的 dynamic_axes。

Q3. 张量形状 [1, 3, 224, 224] 中第 0 维通常表示什么?

单选题 · easy

  • A.通道数
  • B.批大小← 正确答案
  • C.图像高度
  • D.图像宽度

答案:B。NCHW 布局中第 0 维 N 是批大小(batch size)。第 1 维是通道数 C,第 2、3 维分别是高 H 和宽 W。

Q4. 关于 ONNX 符号维度,下列说法正确的有哪些?

多选题 · mid

  • A.dim_param 可以为维度起符号名← 正确答案
  • B.dim_value 表示可变符号
  • C.同名符号维度必须取同一数值← 正确答案
  • D.符号名只能写一个字母

答案:AC。dim_param 是符号名,dim_value 是固定数值。图中所有同名符号共享同一个“伸缩标签”,因此数值必须一致。符号名可以是任意字符串,不限于单字母。

Q5. 输入张量 batch 维标为 "batch",输出张量 batch 维也标为 "batch",输入 batch=8 时输出 batch 是多少?

单选题 · mid

  • A.1
  • B.4
  • C.8← 正确答案
  • D.不确定

答案:C。同名符号在 ONNX 中是等价的伸缩标签,一次推理中标签被赋成 8,所有挂着该标签的维度都会拿到 8。这是动态轴保证输入输出一致性最重要的机制。

Q6. 动态 shape 在 GPU 推理时可能额外付出什么代价?

单选题 · hard

  • A.每次新 shape 重排图优化← 正确答案
  • B.每个 batch 都降低模型精度
  • C.权重自动变大变小
  • D.输入必须全部 pad 到定长

答案:A。GPU 推理常提前做内存布局、算子融合和 kernel 自动调优。shape 变化后这些成果可能失效,需要重新优化。动态轴不改变模型精度,不改变权重,也不强制 pad 到定长。

Q7. 卷积输出高公式 H_out = floor((H_in + 2P - K) / S) + 1,若 H_in=32, P=1, K=3, S=2,则 H_out 是多少?

单选题 · mid

  • A.14
  • B.15
  • C.16← 正确答案
  • D.17

答案:C。代入公式得 (32 + 2 - 3) / 2 = 15.5,向下取整为 15,再加 1 得 16。这个计算在形状推理中频繁出现,也决定了动态空间维度的传播方式。

Q8. 全连接层(矩阵乘)对维度匹配的要求是什么?

单选题 · easy

  • A.中间维度必须相等← 正确答案
  • B.批次维度必须为 1
  • C.通道维必须为 3
  • D.所有维度都要动态

答案:A。矩阵乘法 A(m×p) × B(p×n) 要求 A 的列数与 B 的行数相等,即中间维度 p 必须相等。这也是全连接层对动态轴最不友好、最容易报错的位置。

Q9. 动态轴模型常用的排查手段有哪些?

多选题 · hard

  • A.Netron 查看输入输出 shape← 正确答案
  • B.打印图里的 value_info← 正确答案
  • C.多种 shape 跑同一个模型← 正确答案
  • D.修改训练时的损失函数

答案:ABC。Netron 可以可视化检查每条边的 shape 标注;打印 graph.input 的 dim 能确认是固定值还是符号;用多种 shape 实测能定位触发报错的具体维度。动态轴问题通常与训练损失函数无关。

Q10. 动态轴带来的主要性能风险包括哪些?

多选题 · hard

  • A.内存布局无法提前固定← 正确答案
  • B.算子融合要重新决策← 正确答案
  • C.可能引起重复自动调优← 正确答案
  • D.模型权重精度必然下降

答案:ABC。固定 shape 可以提前分配内存、预选 kernel 和静态融合算子;动态 shape 让这些优化无法一次到位,需要在新形状出现时重做。动态轴不会导致权重精度下降,精度问题应由量化或数值稳定性负责。