一个轴被固化,整个批次就过不去
一句话总结
ONNX 中的轴只有三种情形:固定为整数、以符号名保持开放、或者根本没有。无法增大批次的事故,绝大多数出在第一种而不是第三种——而且出在不报错就放行的地方。
为什么需要它
要测量量化后的模型快了多少,就得把批次调大。想一次放入 32 个样本,运行时却拒绝了。明明听说模型是按“动态批次”导出的,会话也成功打开了,可一放入数值就卡住。
这时常见的做法是去翻运行时选项。答案其实在文件里。会话能打开,只说明计算图成立,并不说明它会接受我们想放进去的形状。
轴有三种状态
ONNX Concepts 所说的张量形状,每个轴都是三种之一。
- 整数:写有
dim_value。该轴必须等于这个值。 - 符号名:
dim_param中有一个字符串。它在运行时确定,相同的名称表示相同的值。 - 为空:两者都没有。意思是不知道,推断也会在这里停下。
第二种里的“相同的名称表示相同的值”常被忽略。如果两个输入的第 0 轴都写着 batch,这就不只是“两个都是动态的”,而是 两者的行数必须相等 这一约束。一边放 5 行、另一边放 3 行,就会被拒绝。
但这种拒绝并不友好。运行时不会告诉你“符号 batch 分裂成了 5 和 3”。它报出的是计算图优化之后的节点名称,说该节点的输入形状不匹配。原因与现象之间隔了一层优化,初次见到时看不出原因。
선언 x [batch, 4] bias [batch, 3]
넣은 것 x 5행 bias 3행
런타임의 말 (융합된 노드 이름) 의 입력 모양이 맞지 않는다
실제 원인 같은 심볼에 다른 값을 넣었다
推断能填上的与填不上的
onnx.shape_inference 会遍历计算图,把中间张量的形状填入 value_info。输入为 [batch, 4]、权重为 4 x 6 时,中间张量会被填成 [batch, 6]。符号会以符号的形式传播下去。
有两种情况填不上。一种是 输入的形状为空 时。从未知出发,就会一直未知到底。另一种是 根本没有中间张量 时。如果只有一个节点,它的输出就是计算图的输出,那就没有可填的对象,value_info 会以空的状态返回。此时不能解读成“推断失败了”——只是没有东西可填。
还有更危险的情形:推断自信满满地填入了错误的值。如果 Reshape 的目标形状被写成了常量,推断就会原样相信这个常量。即使把输入开放成 [batch, 4],只要 Reshape 指向 [3, 6],其后所有张量都会固定为 3。不会报错。批次轴只在声明中留着,实际上已经死了。
在现场相遇的样子
第一,按批次 1 导出的模型。给导出工具一个示例输入,它的形状就会原样固化下来。如果不指定动态轴,[1, ...] 就会被写死,这个文件永远只能一次处理一条。测量吞吐量的实验就整个失去了意义。
第二,拒绝发生在放入数值时,而不是打开会话时。会话能顺利打开。于是得出“模型加载成功了,所以不是模型的问题”的结论,跑去挖别的地方。固定轴的报错信息会准确写明哪个输入的第几个轴期望什么、收到了什么,只读那一行就够了。
第三,写死了常量形状的 Reshape。这是最安静的事故。打开模型看,输入好端端地开放成 [batch, 4],推断结果也前后一致。只是批次轴从某个位置起已经变成了整数。于是“明明按动态轴导出了,为什么不行”这句话会不断冒出来。解决办法很简单——把批次位置设为 -1,它就会由其余轴计算出来,轴就复活了。
第四,量化虽然不会固定轴,却会掩盖原因。量化后的计算图节点名称变了,Q/DQ 也插了进来,变得难以阅读。所以 轴的问题在量化之前确认 要便宜得多。
实际工作中真正重要的事
- 先放进去试,再下结论。不要只读声明,要留下用多种行数真正放入的记录。
- 相同的符号就是相同的值。如果两个输入用了同一个名称,要把这条约束写进文档。
- 区分推断的空白与自信。为空与被错误填充,是完全不同的问题。
- 批次位置用 -1。不要在 Reshape 的目标形状里直接写批次值。
下一项实验要做什么
用相同的权重,做出批次轴为符号的版本和固定为 1 的版本并排摆放,再一步步扩展工具 axes.py。用同样的代码向两个文件放入多种行数,记录在哪里分道扬镳;区分推断填上的与没能填上的;向使用同一符号的两个输入放入不同的行数试试。最后找出写死了常量形状的 Reshape,把批次位置改回去,让轴复活。评分器每次都会用不同的轴名称、形状和写死的行数构建自己的文件,真正运行你的工具并核对答案。