想象你是一位工厂厂长,手里拿着一张刚刚画好的流水线蓝图:几十个工位整齐排列,每个工位只负责一道工序;传送带把半成品从上一个工位送到下一个;仓库里堆着已经称量好的原料;大门处标着“原材料入口”和“成品出口”。这张蓝图不关心工位上的工人具体姓什么,只关心“谁在哪道工序”“原料怎么流”“按什么顺序开工”。ONNX 的图结构(Graph),就是这样一张 AI 模型的通用蓝图——它用统一的语言描述神经网络里“计算怎么组织、数据怎么流动”,让 PyTorch、TensorFlow、飞桨训练出的模型,都能被同一个推理引擎读懂并运行。接下来,我们走进这张蓝图,看懂它的每一个零件。

岗位关联

对 AI 从业者而言,掌握 ONNX 图结构,意味着你能看懂模型转换时的报错、能亲手优化推理速度、能在部署环节游刃有余——这是实现“一次训练、到处部署”的看家本领。

目录
  1. 可视化演示
  2. 图的基本组成
  3. 张量与形状
  4. 属性与算子集
  5. 序列化机制
  6. 执行语义
  7. 子图与嵌套
  8. 优化与调试
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

通过计算图DAG、图生命周期与子图匹配替换,直观展示AI编译器以图结构描述与优化模型计算过程。

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

计算图DAG 节点表示算子 边表示张量依赖 G = (V, E) 有向无环图刻画计算流程。

图的基本组成

走进这座“AI 工厂”,你最先看到的是挂在墙上的总平面图:一排工位(节点)、连接工位之间的传送带(张量)、仓库里预先配好的原料(初始器)、以及标注清楚的大门(输入输出)。ONNX 里的图(Graph)就是这张平面图的数字化表达。

在 ONNX 中,每一个圆角方块都是一个 Node(节点),它代表一次具体的算子调用,比如 MatMul(矩阵乘法)、Relu(激活)、Add(加法)。节点不直接存储数据,它只有三个关键信息:op_type(干哪类活)、inputs(从哪些传送带取货)、outputs(把结果放到哪条传送带)。数据本身是 Tensor(张量),它通过字符串名字被节点引用——就像物流单据上写着“把编号为 A 的箱子送到工位 B”。

常量权重并不会显示成工位,而是被放进 initializer(初始器)列表,表示“这些是工厂仓库里预先称好的原料”;模型的入口与出口则由 graph.input 和 graph.output 描述,对应工厂大门上的“原材料入口”与“成品出口”。把这几样东西合在一起,就得到了一个完整的图:

有了这张“平面图”,部署工程师就能清楚地知道:哪些环节是必须的、哪些原料是提前备好的——这就是后续一切优化和调试的起点。

G=(N, E, I, In, Out)\displaystyle G = (N,\ E,\ I,\ In,\ Out)
  1. 节点集合 N图中的每个圆角方块,也就是一次算子调用,共同组成节点集合。
    N={n1,n2,…,nk}\displaystyle N = \{ n_1, n_2, \dots, n_k \}
  2. 边集合 E节点之间依靠张量名字产生数据依赖,形成有方向的传送带。
    E={(u,v)∣u 的输出被 v 消费}\displaystyle E = \{ (u, v) \mid u \text{ 的输出被 } v \text{ 消费} \}
  3. 初始器集合 I图中预置的常量张量,如权重和偏置,它们不参与执行却随时待命。
    I={t1,t2,…,tm}\displaystyle I = \{ t_1, t_2, \dots, t_m \}
  4. 输入输出 In 与 Out图对外暴露的接口,指定了模型吃什么、吐出什么。
    In={x1,x2,… },Out={y1,y2,… }\displaystyle In = \{ x_1, x_2, \dots \}, \quad Out = \{ y_1, y_2, \dots \}

张量与形状

传送带上的每一箱货物,都要贴上一张标签:尺寸(形状)和材质(数据类型)。否则到了下一个工位,工人根本不知道该拿多大的夹具、按什么温度加工。ONNX 里描述张量的标签叫 ValueInfo(张量值信息),它记录两件事:元素的 dtype(数据类型)和 shape(形状)。

类型决定了每个数字占几个字节,比如 float32 占 4 字节、int64 占 8 字节;形状则说明张量是几维的、每一维多大,比如 [1, 3, 224, 224] 代表一张 224×224 的彩色图片。更有意思的是,某些维度可以不是固定数字,而是一个“参数名”,例如用 dim_param 表示 batch(批大小)。这样同一个图就能接受不同批次大小的输入,而不用重新导出模型——就像传送带设计成“可伸缩”,来多少箱货都能接住。

当两个张量做逐元素运算(如相加)时,引擎会先做“广播”(broadcast):把形状不同的张量按对齐规则补成一样的尺寸。

如果标签贴错了,比如两个形状完全不相容的张量被送到同一个加法工位,传送带立刻“卡死”,推理引擎会直接报 shape mismatch 错误。

out_dimi=max⁡(dimiA, dimiB)\displaystyle out\_dim_i = \max(dim_i^A,\ dim_i^B)
  1. 取输入张量维度张量 A 和 B 分别有各自第 i 维的尺寸。
    dimiA, dimiB\displaystyle dim_i^A,\ dim_i^B
  2. 逐维比较广播规则要求两个维度要么相等、要么其中一个是 1,才能对齐。
    dimiA=dimiB  或  min⁡(dimiA,dimiB)=1\displaystyle dim_i^A = dim_i^B \ \text{ 或 } \ \min(dim_i^A, dim_i^B) = 1
  3. 输出取较大值形状不匹配的部分被“拉伸”到较大的一边,得到输出形状。
    out_dimi=max⁡(dimiA, dimiB)\displaystyle out\_dim_i = \max(dim_i^A,\ dim_i^B)

属性与算子集

每个工位上除了传送带,还有一排旋钮和一块铭牌。旋钮是 Attribute(属性),比如卷积算子的 strides(步长)、pads(填充)、kernel_shape(卷积核尺寸);铭牌上写着这台设备遵循的“国标版本”,也就是 Opset(算子集版本)。

为什么需要版本号?因为同一个算子在不同版本里可能有不同的默认行为和参数格式。最典型的例子是 Pad(填充)算子:旧版本把填充值写在属性里,新版本把它挪到了输入张量中。如果模型声明 opset=11,推理引擎就必须按 11 版的说明书来理解这个算子。导出的模型必须满足“算子的最低要求版本 ≤ 模型声明的 opset 版本”这一条件,否则引擎会大喊“不支持的算子”。

旋钮拧错了顶多产品不合格,版本选错了可是整条流水线直接停工——这也是模型转换中报错频率最高的原因之一。

0≤min_opset(op)≤model_opset\displaystyle 0 \leq min\_opset(op) \leq model\_opset
  1. 算子的最低版本要求每个算子 schema 都标注了自己从哪个 opset 开始可用。
    min_opset(op)\displaystyle min\_opset(op)
  2. 模型声明的算子集版本文件头部通过 opset_import 字段声明本模型遵循哪个版本。
    model_opset\displaystyle model\_opset
  3. 不等式约束模型版本必须不低于算子要求,否则引擎无法按正确语义执行。
    min_opset(op)≤model_opset\displaystyle min\_opset(op) \leq model\_opset

序列化机制

蓝图绘好之后,需要存档。你不会把图纸直接打印成文本文件贴在墙上,而是把它压缩成一个二进制工程文件。ONNX 选用的“压缩格式”是 Google 的 Protobuf(Protocol Buffers)。.onnx 文件看起来是一堆乱码,但它内部结构井井有条:最外层是 ModelProto(模型外壳),里面包含一个 GraphProto(图主体),再往里是节点、张量、属性等更小的消息。

Protobuf 的厉害之处在于“字段编号 + 类型 + 载荷”的紧凑编码:每个字段都有固定编号,解码器按编号取数据,因此协议升级时新增字段不影响老程序读取——就像档案柜每个抽屉编了号,即使后来加了新抽屉,老员工也知道去哪几个柜子拿旧资料。

理解了序列化机制,你就不再害怕“模型文件损坏”或“版本不兼容”——用 onnx.load() 把它读成结构体,任何异常都能追溯到具体某个字段。

Bytes=∑field( key(field_no,wire_type) ∥ payload )\displaystyle Bytes = \sum_{field} (\, key(field\_no, wire\_type) \ \| \ payload \,)
  1. 字段键每个字段用 (字段编号, 线类型) 组合成一个 key 字节段。
    key(field_no,wire_type)\displaystyle key(field\_no, wire\_type)
  2. 载荷根据线类型,后面跟着定长值或长度前缀 + 实际数据。
    payload=length ∥ data\displaystyle payload = length \ \| \ data
  3. 拼接所有字段按消息定义顺序把多个字段的编码串接起来,就是一段二进制。
    Bytes=∑field( key ∥ payload )\displaystyle Bytes = \sum_{field} (\, key \ \| \ payload \,)

执行语义

蓝图有了,下一步是排产。工厂不可能按图纸上编号的顺序傻乎乎地开工,而是要遵循一条铁律:某个工位开工前,它需要的所有半成品必须已经生产完毕。ONNX 图本身不规定执行顺序,推理引擎接到图后,会先做一次“排产”——拓扑排序(Topological Sort),确定一个合法的执行序列。

数据流图的特点是“有向无环”:张量从生产者流向消费者,形成一条条单向的传送带。引擎维护每个节点的“原料缺口数”(入度),先让原料齐全的节点开工,每完工一个节点,就把它的产出送给下游,减少下游的缺口数;当缺口数归零,下游节点也就具备了开工条件。如果图中存在环形依赖,所有节点都会陷入“我等你、你等我”的僵局,引擎直接报错。

排产排对了,数据流才能像传送带一样顺畅地从入口流到出口;排错了,后道工序拿到空箱子,整个推理就崩了。

∀ (u→v)∈E:order(u)<order(v)\displaystyle \forall\ (u \to v) \in E:\quad order(u) < order(v)
  1. 有向边从上游节点 u 指向下游节点 v,表示 v 需要 u 的输出。
    (u→v)∈E\displaystyle (u \to v) \in E
  2. 执行位置拓扑序为每个节点分配一个次序编号。
    order(u), order(v)\displaystyle order(u),\ order(v)
  3. 依赖保证任何边的起点 u 都必须排在终点 v 之前执行。
    order(u)<order(v)\displaystyle order(u) < order(v)

子图与嵌套

有些大型工厂里,主车间内还会嵌套一个“子车间”:比如质检站内部,有自己的小流水线,把零件送进去,经过几道小工序,再送回主线。ONNX 用子图(Subgraph)实现同样的嵌套逻辑,最典型的是控制流算子 If 和 Loop。If 节点的属性 then_branch 和 else_branch 本身就是两个完整的图——条件为真走甲子图,为假走乙子图。

子图不是孤立的,它可以“捕获”外层图里的张量:子图里的节点能直接读取父图已经算出的中间结果,就像子车间可以直接领取主线传送带上的零件,不必重新生产。另一个嵌套机制是 FunctionProto(函数原型):把一串常见操作封装成一个自定义算子,供整个模型反复调用,能显著减小文件体积。

子图让复杂的条件分支和循环在“一张图”里优雅表达,代价是执行时需要递归处理——但每个子图内部依然遵循拓扑排序,秩序无处不在。

Gsub=(Nsub, Esub, Cin)\displaystyle G_{sub} = (N_{sub},\ E_{sub},\ C_{in})
  1. 子图节点集合子车间内部的小工位们。
    Nsub\displaystyle N_{sub}
  2. 子图内部连接子车间内部的传送带布局。
    Esub\displaystyle E_{sub}
  3. 捕获变量集合从外层图读入但不在子图中定义的张量名。
    Cin={父图中被本子图引用的张量}\displaystyle C_{in} = \{ \text{父图中被本子图引用的张量} \}

优化与调试

工厂运营久了,总会有咨询顾问来提建议:把能提前预制的零件提前做好、把能合并的工序合成一道、把空转的环节直接拆掉。ONNX 生态里也有一群“顾问”:形状推断(Shape Inference)、常量折叠(Constant Folding)、算子融合(Operator Fusion)。

形状推断先依据输入形状,推算出每个中间张量的形状,把信息补全,让后续优化有据可依。常量折叠则是把“输入全是常量”的固定计算提前算好,例如 Add(1, 2) 直接变成常量 3。算子融合如把 Conv 和 BatchNorm 合成一个算子,减少中间结果的搬运和显存开销——就像两道工序合并到一台机器上,工件不再需要离开机器。调试时,你也可以手动加载模型,遍历每个节点,打印输出张量的形状与数值,快速定位哪一段数据流出现了异常。

优化不是换一台更快的机器,而是把流水线排得更聪明、更省力——这也是为什么同一个模型在 ONNX Runtime 里跑得比原始框架更快的秘密之一。

f(c1,c2)⇒c′\displaystyle f(c_1, c_2) \Rightarrow c'
  1. 固定输入若某个算子的所有输入都是编译期已知的常量。
    c1,c2∈常量\displaystyle c_1, c_2 \in \text{常量}
  2. 预计算把函数 f 在常量上先执行一次。
    f(c1,c2)=v\displaystyle f(c_1, c_2) = v
  3. 子图替换用计算出的常量 c' 替换整个子图,运行时不再重复计算。
    子图⇒c′\displaystyle \text{子图} \Rightarrow c'

AI 应用场景

跨框架模型转换与部署

阶段:模型部署

解决的问题:传统做法是 PyTorch 训练的模型只能留在 PyTorch 推理,服务端想用 C++ 高效部署往往需要手写推理代码,极易出错。AI 通过 torch.onnx.export 把模型转成 ONNX 图,再用 ONNX Runtime 或 TensorRT 加载,一份模型多端运行。

具体案例:某广告推荐团队的 CTR 模型在 PyTorch 中训练完毕,导出 ONNX 后需要用 TensorRT 加速。转换时报 Unsupported operator,工程师检查 opset_import 与算子版本,将 opset 从 12 升到 13 后顺利部署。

推理性能优化与图改写

阶段:模型推理优化

解决的问题:纯 PyTorch 推理慢、显存占用高。AI 利用 ONNX Runtime 的图优化 pass,对 ONNX 图做算子和布局优化,减少中间张量落地的开销。

具体案例:某视觉团队的 ResNet 分类服务在迁移到 ONNX Runtime 后,开启 graph_optimization_level=ORT_ENABLE_ALL,通过算子融合把推理时延降低约 20%,QPS 显著提升。

中间张量可视化与问题排查

阶段:模型调试

解决的问题:模型输出错误时难以定位是哪一层出了偏差。AI 通过遍历 ONNX 图的节点,提取并打印中间张量的 shape 与数值,快速定位“数据流断裂点”。

具体案例:某 NLP 模型在导出后推理结果全为 NaN,工程师用 Python 脚本逐节点检查张量,发现某个 Reshape 节点把形状写错导致广播异常,修正后恢复正常。

Python 演示

python72 行
import onnxfrom onnx import helper, TensorProto, numpy_helperimport numpy as np  def build_demo_model():    """构建 Y = Relu(X @ W + b) 的小型 ONNX 模型, 包含 3 个节点"""    X = helper.make_tensor_value_info('X', TensorProto.FLOAT, [1, 3])    Y = helper.make_tensor_value_info('Y', TensorProto.FLOAT, [1, 2])    W = numpy_helper.from_array(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32), name='W')    b = numpy_helper.from_array(np.array([0.5, -1.0], dtype=np.float32), name='b')    matmul = helper.make_node('MatMul', ['X', 'W'], ['mm_out'], name='mm')    add = helper.make_node('Add', ['mm_out', 'b'], ['add_out'], name='add')    relu = helper.make_node('Relu', ['add_out'], ['Y'], name='relu')    graph = helper.make_graph([matmul, add, relu], 'tiny_graph', [X], [Y], initializer=[W, b])    model = helper.make_model(graph, opset_imports=[helper.make_opsetid('', 13)])    model.ir_version = 8    return model  def topo_sort(graph):    """对图中节点做拓扑排序 (Kahn 算法)"""    producer = {}    for n in graph.node:        for out in n.output:            producer[out] = n    indeg = {n.name: 0 for n in graph.node}    succ = {n.name: [] for n in graph.node}    for n in graph.node:        for inp in n.input:            if inp in producer:                parent = producer[inp]                succ[parent.name].append(n.name)                indeg[n.name] += 1    q = [n.name for n in graph.node if indeg[n.name] == 0]    order = []    while q:        current = q.pop(0)        order.append(current)        for nxt in succ[current]:            indeg[nxt] -= 1            if indeg[nxt] == 0:                q.append(nxt)    if len(order) != len(graph.node):        raise RuntimeError('graph has cycle')    return order  def inspect_model(model):    g = model.graph    print('=== Graph 基本信息 ===')    print('graph name:', g.name)    print('input:', [i.name for i in g.input])    print('output:', [o.name for o in g.output])    print('node count:', len(g.node))    print('initializer:', [t.name for t in g.initializer])    print()    print('=== 节点列表 ===')    for n in g.node:        print(n.op_type, ':', n.input, '->', n.output)    print()    print('=== 拓扑排序 ===')    print(topo_sort(g))  def main():    model = build_demo_model()    inspect_model(model)  if __name__ == '__main__':    main()
目的:用 onnx 库构建一个 3 节点的小型计算图,然后手动实现 Kahn 拓扑排序,验证图的可执行顺序。
  1. X = helper.make_tensor_value_info('X', TensorProto.FLOAT, [1, 3])定义输入张量 X 的名字、类型和形状,相当于在门口挂上“原材料规格牌”
  2. numpy_helper.from_array(np.array(...), name='W')把权重数组包装成 ONNX 初始器 TensorProto,存入仓库
  3. helper.make_node('MatMul', ['X', 'W'], ['mm_out'], name='mm')创建一个矩阵乘法工位,声明取货口 X、W 和放货口 mm_out
  4. graph = helper.make_graph([matmul, add, relu], 'tiny_graph', [X], [Y], initializer=[W, b])把节点、大门、仓库原料组合成图对象
  5. model = helper.make_model(graph, opset_imports=[helper.make_opsetid('', 13)])给图套上模型外壳,声明算子集版本为 13
  6. for out in n.output: producer[out] = n记录“哪条传送带由哪个工位产出”,用于还原依赖关系
  7. if inp in producer:若某节点的输入是内部节点的输出,就建立一条依赖边
  8. q = [n.name for n in graph.node if indeg[n.name] == 0]找出所有“原料齐备”的工位,作为排产起点
  9. order.append(current)每处理完一个节点就记入执行序列,最终得到合法的拓扑序
  10. if len(order) != len(graph.node): raise RuntimeError('graph has cycle')如果节点数对不上,说明存在环,推理引擎也无法执行

常见误区

误区很多初学者以为 ONNX 文件里 `node` 数组的先后顺序就是推理引擎执行算子的顺序,于是手动调整节点顺序来“优化”模型。

正解ONNX 图是数据流图,执行顺序由张量依赖决定,而不是文件里的书写顺序。推理引擎会做拓扑排序,把节点顺序打乱重排完全合法。好比工厂开工不看图纸编号,而是看哪个工位原料先齐。

为何常见:因为一般序列化格式(如 JSON 数组)都有顺序语义,人们下意识把文件顺序等同于执行顺序。

误区有人把 `.onnx` 当成一种文本配置,用记事本打开看到乱码后,还想手动改几个“数字”来修改模型。

正解.onnx 是 Protobuf 二进制格式,不是人类可读的文本。正确做法是用 onnx.load() 读成 ModelProto 对象,修改后 onnx.save() 写回。这就像蓝图被压缩成二进制 PDF,你不能用文本编辑器去改 PDF 里的笔画。

为何常见:很多配置文件都是文本形式,容易形成“文件就该能看懂”的思维定式。

误区导出 ONNX 时选择最新版 opset 一定最好,因为新版本功能更全、缺陷更少。

正解opset 版本越高,要求推理引擎的支持版本也越高。若部署环境只支持 opset 12,而你导出 18,就会直接报“不支持的模型版本”错误。版本选择应当以“所有下游工具的最低支持版本”为准。

为何常见:新版本有更好用,但忽略了“兼容性”是双向的——模型旧引擎也得认。

误区看到 `W`、`b` 这些常量也有名字和数值,误以为它们也是 `node` 列表里的算子,会像工位一样“执行”。

正解initializer 是独立的常量存储区,不在 node 列表中,也不参与执行。它们只是被节点当作输入读取的“预置原料”。判断一个对象是不是节点,只看它是否出现在 graph.node 里。

为何常见:张量在图中确实扮演“货物流”的角色,容易与“动作”混淆。

误区模型经过图优化后,输出数值必须和原模型完全一致,否则就是优化器出了问题。

正解常量折叠、算子融合等优化在绝大多数情况下保持数值一致,但某些融合(如 Conv+BN)会把部分运算合并,浮点运算顺序变化可能导致极小误差;极少数优化还涉及精度策略调整。工程上应设置可接受的误差阈值来验证。

为何常见:大家总希望优化是“无损魔法”,忽略了浮点数运算本身就不满足完全结合律。

面试题

Q1. ONNX 模型文件底层使用的序列化格式是?

单选题 · easy

  • A.JSON 文本格式
  • B.XML 标签格式
  • C.Protobuf 二进制← 正确答案
  • D.CSV 表格数据

答案:C。ONNX 采用 Protobuf 作为底层序列化方案,.onnx 文件是紧凑的二进制格式。JSON、XML、CSV 都不是 ONNX 的官方存储格式,虽然可视化工具内部可能用 JSON 传输,但文件本身是 Protobuf。

Q2. 在 ONNX 图结构中,表示“执行一次算子调用”的最小单元是?

单选题 · easy

  • A.Tensor 张量
  • B.Node 节点← 正确答案
  • C.Graph 图
  • D.Attribute 属性

答案:B。Node 节点代表一个算子实例,比如 MatMul、Relu,它声明了输入输出张量名和算子类型。Tensor 是流动的数据,Graph 是整张蓝图,Attribute 是节点的配置参数,它们都不是“执行动作”本身。

Q3. 模型中的预置权重常量通常存放在哪个字段?

单选题 · single

  • A.initializer 初始器← 正确答案
  • B.value_info 信息
  • C.opset_import 算子集
  • D.metadata_props 元数据

答案:A。graph.initializer 专门存放常量张量(权重、偏置),节点通过张量名引用它们。value_info 只描述张量形状类型,opset_import 声明算子集版本,metadata_props 存放模型元信息,都不是数据存储的位置。

Q4. 下列哪个字段标明模型使用的算子规范版本?

单选题 · single

  • A.model.version 版本
  • B.ir_version 版本
  • C.opset_import 算子集← 正确答案
  • D.graph.name 名字

答案:C。opset_import 声明了模型遵循的 ONNX 算子集版本(如 opset 13),引擎据此解释算子语义。ir_version 表示 ONNX 中间表示自身的版本,model.version 只是模型自定义版本号,graph.name 只是图的名字。

Q5. ONNX 推理引擎确定图执行顺序的依据是?

单选题 · single

  • A.文件里的书写顺序
  • B.节点名的字典顺序
  • C.张量依赖的拓扑关系← 正确答案
  • D.引擎随机调度策略

答案:C。ONNX 图是有向数据流图,执行顺序必须保证生产者先于消费者运行,即拓扑排序。文件书写顺序和节点名字都与执行无关,随机调度更是无法保证依赖关系正确。

Q6. PyTorch 模型导出 ONNX 时报 "Unsupported operator",最常见的原因是?

单选题 · single

  • A.图片解码库版本过低
  • B.本地显存容量不足
  • C.算子版本不被当前 opset 支持← 正确答案
  • D.Python 解释器版本不对

答案:C。报错通常意味着某个算子在当前声明的 opset 版本中不存在或语义不匹配,选用更高或合适的 opset,或替换为 ONNX 支持的等价算子即可解决。显存、解码库、解释器版本与算子支持校验无关。

Q7. 下列哪一项不是 ONNX 图中节点的常用字段?

单选题 · single

  • A.op_type 算子类型
  • B.inputs 输入列表
  • C.outputs 输出列表
  • D.tensor_data 数值← 正确答案

答案:D。节点只保存算子类型、名字、输入输出张量名及少量属性,本身不携带张量数值数据。数值存放在初始器(TensorProto)或图的输入中,所以 tensor_data 不是 NodeProto 的组成字段。

Q8. 关于 ONNX 的 ModelProto,下列说法正确的有哪些?

多选题 · mid

  • A.它是模型文件的根结构← 正确答案
  • B.内部包含一个 GraphProto← 正确答案
  • C.可以包含多个并列 graph
  • D.还携带版本与元数据← 正确答案

答案:ABD。ModelProto 是整个 .onnx 文件的根对象,内含一个 graph 字段、opset_import、ir_version 等。ONNX 规定每个模型只有一个主图,而不是多个并列 graph,因此 C 错误。

Q9. 关于 ONNX 中的子图,以下理解正确的有哪些?

多选题 · hard

  • A.常用于实现 If、Loop 控制流← 正确答案
  • B.子图可以引用外层张量← 正确答案
  • C.子图节点不计入模型参数
  • D.子图必须独立导出成文件

答案:AB。If 和 Loop 节点通过 attribute 内嵌子图来实现条件分支与循环;子图能捕获外层张量,如同子车间领取主线的零件。子图节点也是模型的一部分,不能说“不计入参数”;子图内嵌在父图中,无需独立文件。

Q10. 拓扑排序能保证下面哪一项性质?

单选题 · extreme

  • A.所有常量都在图开头初始化
  • B.每条边起点先于终点执行← 正确答案
  • C.输出节点总是最后被读取
  • D.同层算子并行度最大

答案:B。拓扑排序的核心性质是:对每条有向边 (u,v),u 的次序都小于 v,从而保证数据依赖成立。常量初始化、输出节点位置、并行度都不是拓扑排序本身的定义属性。