一次送入 32 行却被拒绝 — 救活死掉的批次轴
目标
用相同的权重,生成批次轴为符号的 /root/onnxq-shape/dyn.onnx 和固定为 1 的 /root/onnxq-shape/fixed.onnx,并编写读取轴并真正放入数值的工具 /root/onnxq-shape/axes.py。最后找出常量形状被写死、批次轴已经失效的模型,把它修复过来。
为什么重要
ONNX 中的轴,要么固定为整数,要么以符号名保持开放,要么干脆为空。三者含义完全不同,但粗略看文件时分辨不出来。想增大批次来测吞吐量的实验在第一步就被挡住,原因多半在这里。 同样重要的是,拒绝发生在放入数值时,而不是打开会话时。若因为会话打开了就断定不是模型的问题,就会跑去挖别的地方。 如果同一个符号名用在两个输入上,那就不是“两者都是动态的”,而是“两者的行数必须相等”这一约束。违反时会被拒绝,但运行时不会点出符号名,而是报出优化之后的节点名称。原因与现象之间隔着一层。 最安静的事故是 Reshape 中写死了常量形状。输入以符号保持开放,但从那个位置起批次轴变成了整数。没有错误也没有警告,形状推断原样相信该常量,给出前后一致的答案。 评分器不会相信你写下的文字。它会在临时目录里摆出自己亲手搭建的文件,真正运行你的工具,把结果与评分器自己读取并运行同一文件得到的答案进行核对。轴名称、形状和写死的行数每次运行都不同。
步骤
- 创建并运行 /root/onnxq-shape/build_shapes.py,生成 /root/onnxq-shape/dyn.onnx 和 /root/onnxq-shape/fixed.onnx。
- 在 /root/onnxq-shape/axes.py 中实现
dims,读出输入输出的轴是整数还是符号。 - 加入
feed,按给定的行数生成数值并真正放入,记录结果或拒绝。 - 加入
infer,区分onnx.shape_inference填上的与没能填上的。 - 加入
symbols,汇总同一个符号名用在哪些位置。 - 加入
scan,找出常量形状被写死的 Reshape。 - 加入
repair,把写死的形状中的批次位置改回去,并给输出轴重新加上符号名。 - 生成 /root/onnxq-shape/shape_report.json 和 /root/onnxq-shape/shape_report.md 作为报告。
参考
- Python 解释器是 /opt/onnx-lab/bin/python。系统自带的
python3中既没有 onnx 也没有 numpy。运行示例:/opt/onnx-lab/bin/python /root/onnxq-shape/axes.py dims /root/onnxq-shape/dyn.onnx - 这个 Pod 没有网络。材料需要自己搭建。
- 模型约定:两个文件的输入名称都是
x,输出名称都是y,输入的第 1 轴是 5,输出的第 1 轴是 3。dyn.onnx的第 0 轴是符号名,fixed.onnx的第 0 轴是整数 1。两个文件的 initializer 名称和值必须完全相同——只有轴声明不同的同一个模型才能用来比较。 - 运行约定:
axes.py <명령> ...(占位符为命令名)。答案以一个 JSON 对象输出到标准输出。成功时退出码为 0,未知命令时为 2。运行时拒绝的情况 不是失败而是答案,所以要捕获异常并以 JSON 记录,退出码保持为 0。 dims <모델>(占位符为模型文件)的响应:{"inputs": {이름: 축목록}, "outputs": {이름: 축목록}}。轴固定时为整数,符号时为字符串,没有时为 null;如果形状声明本身不存在,则轴列表为 null。由 initializer 填充的名称不放入 inputs。feed <모델> <행수...>(占位符依次为模型文件与各输入的行数)按运行时输入顺序,每个输入接收一个行数。每个输入从声明的第 1 轴起原样使用,只把第 0 轴填为给定的行数(数值随意)。响应:{"status": "ok"|"error", "error_type": 예외 클래스 이름 또는 null, "message": 문자열, "shapes": 출력 모양 목록 또는 null}。infer <모델>(占位符为模型文件)的响应:{"value_info": {중간 텐서 이름: 축목록}, "unknown": [이름...]}。unknown 是按名称排序的列表,其中的名称由节点产生、但不是计算图输出,并且推断没能填上它们的形状。symbols <모델>(占位符为模型文件)的响应:{심볼 이름: ["텐서이름:축번호", ...]}。从运行时输入和计算图输出中汇总。scan <모델>(占位符为模型文件)的响应:{"frozen_reshape": [노드 이름 정렬], "shapes": {노드 이름: 상수 형}}。只收录目标形状是 initializer 且其第 0 位为正数的 Reshape。第 0 位为 -1 或 0 时,轴是活的,不收录。节点名称为空时,使用该节点的第一个输出名称。repair <모델> <출력>(占位符依次为模型文件与输出文件)把被收录的 Reshape 的常量形状中第 0 位改为 -1,把计算图输出的第 0 轴重新声明为第一个运行时输入的第 0 轴的符号,然后保存为新文件。不要动权重和节点。响应为{"out", "fixed", "batch_symbol"}。- 第 6 步和第 7 步想亲眼看到现象,需要一个出了事故的文件。用 /root/onnxq-shape/make_frozen.py 生成 /root/onnxq-shape/frozen.onnx,对它运行
scan和feed,修复后的结果放到 /root/onnxq-shape/healed.onnx。评分器不会读取这些文件,而是另外搭建自己的文件来运行你的工具。 shape_report.json中写入dyn_input、fixed_input(各为输入的轴列表)和trials(每一项有model、rows、status,4 项以上)。必须同时有 dyn.onnx 接受的记录和 fixed.onnx 拒绝的记录。shape_report.md用## 어떤 축이 열려 있나、## 굳은 축에 무엇을 넣었나、## 추론이 못 채운 곳、## 보내는 쪽에 요청할 것四节来写(四个标题为韩文,依次意为“哪些轴是开放的”“往固定轴里放了什么”“推断没能填上的地方”“要向发送方提出的请求”),并在正文中写出批次轴的符号名。- 官方文档:ONNX Concepts · onnx.shape_inference · ONNX IR · ORT Python API
- 常见错误:以为会话打开了形状就一定没问题;把拒绝当作异常抛出而不留记录;看到
value_info为空就认为推断失败了;在 Reshape 的批次位置写入实际行数。
做出只有轴声明不同的两个版本
创建并运行 /root/onnxq-shape/build_shapes.py,生成 /root/onnxq-shape/dyn.onnx 和 /root/onnxq-shape/fixed.onnx。两个文件的权重完全相同,只有第 0 轴的声明不同。
在 make_tensor_value_info 的形状列表中,第 0 位放入字符串就是符号,放入整数就固定为该值。权重只创建一次,把同一个对象放入两个计算图——值不同的话,后面就无法比较这两个模型。
读出轴是开放的还是固定的
在 /root/onnxq-shape/axes.py 中实现 dims <모델>(占位符为模型文件),读出运行时输入和输出的轴。整数就记为整数,符号就记为字符串,没有就记为 null。
一个轴要么有 dim_param(符号)或 dim_value(整数)中的一个,要么两者都没有。请把这三种情况区分开来记录。另外,由 initializer 填充的名称不是运行时输入,要从 inputs 中去掉。
真正放入数值试试
加入 feed <모델> <행수...>(占位符依次为模型文件与各输入的行数),为每个运行时输入按该行数生成数值并放入,输出 {"status", "error_type", "message", "shapes"}。即使被拒绝,退出码也是 0。
后面的轴原样使用声明的整数,只把第 0 轴换成给定的行数。拒绝不是失败而是答案——捕获异常,记下类名和第一行,以后那段文案会告诉你在哪个轴期望什么。把打开会话的步骤和放入数值的步骤分开包裹。二者在不同的地方失败。
推断填上的与没能填上的
加入 infer <모델>(占位符为模型文件),区分 onnx.shape_inference 填上的中间张量和没能填上的名称。响应为 {"value_info", "unknown"}。
infer_shapes 会返回新的 ModelProto,填上的结果放在 graph.value_info 中。unknown 是由节点产生、但不是计算图输出的名称中没有被填上的那些。在完全没有中间张量的计算图里,value_info 会以空的状态返回——那不是失败,而是没有东西可填。
相同的名称就是相同的值
加入 symbols <모델>(占位符为模型文件),为每个符号名汇总它出现的位置,写成 "텐서이름:축번호"(韩文占位符,依次意为张量名称与轴编号)。然后向使用同一符号的两个输入放入不同的行数,用 feed 确认被拒绝。
如果两个输入的第 0 轴写着相同的名称,就意味着运行时要求两个行数必须相同。违反时会被拒绝,但运行时不会点出符号名,而是报出优化之后的节点名称。所以预先建好这张表,才能解读那个错误。
找出写死常量形状的 Reshape
加入 scan <모델>(占位符为模型文件),找出目标形状是 initializer 且其第 0 位为正数的 Reshape。响应为 {"frozen_reshape", "shapes"}。
第 0 位为 -1 时由其余轴计算,所以轴是活的。0 表示原样使用输入的那个轴,所以同样是活的。只能挑出正数。如果目标形状不是 initializer 而是其他节点的输出,那就是运行时才确定的,不在这项检查的对象之内。
让已死的批次轴复活
加入 repair <모델> <출력>(占位符依次为模型文件与输出文件),把被写死的常量形状的第 0 位改为 -1,把计算图输出的第 0 轴重新声明为第一个运行时输入的符号名,并保存为新文件。
想就地修改 initializer,可以新建张量并用 CopyFrom 覆盖。输出轴要清除 dim_value,并把名称放入 dim_param。不要动权重和节点——修复后的文件在原来能放入的行数上必须给出相同的值,才能说修复了。
把两个版本并排摆放后做报告
在 /root/onnxq-shape/shape_report.json 中写入 dyn_input、fixed_input 和 trials,并把 /root/onnxq-shape/shape_report.md 写成 ## 어떤 축이 열려 있나、## 굳은 축에 무엇을 넣었나、## 추론이 못 채운 곳、## 보내는 쪽에 요청할 것 四节(四个标题为韩文,依次意为“哪些轴是开放的”“往固定轴里放了什么”“推断没能填上的地方”“要向发送方提出的请求”)。
trials 是记录对哪个模型放入多少行、得到什么结果。评分器会亲自放入同样的组合来核对,所以要实际测量后再写。报告正文中要写出批次轴的符号名,接收方才能用这个名称来交流。