前一轮 OE-Skills 测评里,我看过 Agent 生成的 UCP 代码。其中一个问题是:代码拿到输出后,有没有按模型的量化参数把数据读对?
当时主要看代码和文档。这次,在 Codex 的协助下,我把实验推进到了实际的量化、编译和推理输出:用官方预训练 ResNet18,跑通 PTQ 到 HBM,再对着原始数据看结果。
这次最明显的现象是:同一份 HBM、同一批输入,30 张图片里有 9 张,只因为换了输出读法,最终选出的类别就变了。
模型没有换,图片没有换,推理也正常结束。差别出在最后几行后处理代码。
一、先把模型跑起来
这一轮在本地 OE CPU 容器里完成,用 HBDK 的本地模拟器运行 HBM。
项目 | 本次配置 |
|---|---|
模型 | OE 官方示例的预训练 ResNet18 ONNX |
环境 | OE 3.9.1、HMCT 2.8.4、HBDK 4.11.11 |
编译目标 | nash-e |
图片 | 官方包自带的 100 张 ImageNet 图片及标签 |
数据拆分 | 按文件名排序,前 70 张校准,后 30 张做诊断对照 |
输入方式 | DDR featuremap,主机侧完成图片预处理 |
拆分在评测前固定,校准集和测试集不重叠。这次关心的是同一批图片经过不同阶段、不同处理方式后,结果在哪里发生变化。
图片预处理沿用官方示例:RGB,短边缩放到 256,中心裁剪为 224×224,除以 255,再按通道做 mean/std 归一化,最后转为 NCHW。
完整路线是:
在定点转换完成后、编译 HBM 之前,我去掉了 I/O 边界的 Quantize、Dequantize 节点,把输入量化和输出反量化放到应用侧处理。因此,后面拿到的是整数张量,输出解码也成为需要单独对照的环节。
二、先留一份正确结果,再改一个环节
我先比较浮点 ONNX、校准后的 PTQ ONNX 和 HBM,得到这 30 张诊断图片的基线:
阶段 | Top-1 正确数 | Top-5 包含标签的图片数 |
|---|---|---|
浮点 ONNX | 25/30 | 27/30 |
PTQ ONNX | 24/30 | 28/30 |
HBM 本地仿真,按元数据解码 | 24/30 | 28/30 |
Top-1 看最高分的类别是否等于标签;Top-5 看分数最高的五个类别里是否有标签。
接下来固定模型和输入,只换输出读法。这样就能把量化本身带来的变化,与后处理造成的变化分开。
三、1000 个输出整数,不能直接比大小
如果只看到这个 shape,很容易直接写:
因此第 c 个类别的分数要先还原:
再对这些分数排序。
这一步针对带量化信息的整数分类输出。如果模型已经输出反量化后的浮点分数,就直接比较那些分数;如果各类别共用同一个正 scale 和同一个 zero point,整数排序与反量化排序一致。这次排序会变,原因就在于各类别的 scale 不同。
类别,索引从 0 开始 | 原始整数 q | scale | 反量化后的分数 |
|---|---|---|---|
424:barbershop | 42809 | 0.000371393107343 | 15.899 |
415:bakery | 69627 | 0.000208402707358 | 14.510 |
这两个类别的 zero point 都是 0。

我还试了给所有类别统一套用第一项 scale,结果仍然有 9 张不同。这份产物的 zero point 全为 0,乘同一个正数不会改变原始整数的顺序,自然也修不好这个问题。
先从自己的 HBM 取得量化参数
在已有 OE 环境里,把脚本与 HBM 放在同一目录,指定一个新的 JSON 文件名运行:
再用这些参数解码输出
四、4000 bytes 对了,类型也可能读错
还有一种错误更直接:把这份 INT32 文件当成 FLOAT32 读取。
INT32 和 FLOAT32 都是每个元素 4 bytes,所以文件长度依然是 4000 bytes。长度检查会通过,数据含义却已经变了。
这里需要分清两个操作:
文件大小回答的是“有多少字节”,dtype 回答的是“这些字节是什么意思”。前者对了,后者仍然可能错。
五、输入处理错了,会是什么样?
输出之外,我又固定权重和量化产物,主动改了三种输入处理:把 BGR 当 RGB、漏掉归一化、重复做一次 mean/std。每组处理完,再恢复正确流程重新推理。
先看 PTQ ONNX 在全部 30 张图上的结果:
输入处理 | Top-1 正确数 | 与正确基线选出不同类别的图片数 |
|---|---|---|
正确预处理 | 24/30 | 0 |
BGR 当 RGB | 20/30 | 8 |
漏掉归一化,直接输入 0~255 像素 | 0/30 | 30 |
在已归一化张量上再做一次 mean/std | 3/30 | 27 |
恢复正确预处理 | 24/30 | 0 |

图2|相同 PTQ 模型、相同 30 张图片,只改变推理时的预处理。“重复 mean/std”是在归一化张量上再次做通道均值与标准差处理。
通道顺序写反后,部分图片仍然能分对,仅凭几张图看起来正常,很容易漏过去。漏掉归一化或重复做 mean/std,影响则大得多。
恢复正确处理后,30 张 PTQ 输出逐元素回到了正常基线。我还把固定的前三张图拿到 HBM 里,做三类错误与恢复,共增加 12 次仿真;恢复后的整数输出同样与原基线一致。
这组对照给了一个很实用的定位办法:先保留正确输入和输出,再一次只改一个环节。结果异常时,可以顺着变化找原因,而不是把所有掉点都先归到 PTQ 上。
六、参数核对之外,还要比较实际数据
我也把自己的 I/O 检查器接到这次实验中,核对类型、shape、stride、文件长度和预处理声明。它能发现明确的参数矛盾,但实际输入已经处理错、声明仍写“正确 RGB 与归一化”时,检查会通过。填进来的信息一致,不能说明上游脚本确实按那套流程执行。
因此我同时保留源图片、正确预处理生成的张量和逐阶段输出。42 次输出解码与独立 NumPy 实现逐字节一致,定点 BC 与 HBM 的整数输出也逐元素一致;另开新容器复跑前三张基线图片,输入、原始输出和解码结果的哈希均与首次一致。
七、这次对照得到的排查顺序
这一轮之后,我会按数据流排查“能运行,但结果不对”:
从源图片开始,核对 RGB/BGR、缩放裁剪和归一化。
查看实际 HBM 输入要求,核对类型、shape、布局和输入量化。
按输出 dtype、stride 和量化参数还原分数,再做后处理。
对照浮点 ONNX、PTQ ONNX 和 HBM,找出差异开始出现的阶段。
模型转换和推理完成之后,还有一件事值得单独确认:程序读出来的结果,是否就是模型真正输出的含义?
这次 ResNet18 对照里,最具体的例子就是那 1000 个类别 scale。少用了一组参数,4000 bytes 没变,运行也没报错,最终的类别却变了。
这次对照把代码里容易忽略的问题落实到了具体输出上:同一份模型、同一批输入,仅改变输出读法,就有 9 张图片选出了不同的类别。按实际量化参数还原分数后,HBM 与 PTQ ONNX 在这 30 张图片上的 Top-1 类别全部一致。
因此,遇到“模型能运行,分类却不对”,沿着输入处理和输出解码检查,再判断是否需要调整量化。把每个阶段的数据读对,才能知道模型究竟在哪里发生了变化。
