博客算法工具链HBM 输出不能直接 argmax?一次 ResNet18 分类结果对照实验

HBM 输出不能直接 argmax?一次 ResNet18 分类结果对照实验

Sam Hui2026-10-07
8
0

前一轮 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。

mean 是 [0.485, 0.456, 0.406],std 是 [0.229, 0.224, 0.225]。我选了四张图片,把生成的张量与原始官方流程逐元素比较,最大绝对差小于 1e-6。

完整路线是:

在定点转换完成后、编译 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 看分数最高的五个类别里是否有标签。

HBM 和 PTQ ONNX 在这 30 张图上选出的 Top-1 类别全部一致。分数有数值差异,最大绝对差约为 0.894,逐图余弦相似度最低约为 0.9973。

接下来固定模型和输入,只换输出读法。这样就能把量化本身带来的变化,与后处理造成的变化分开。

三、1000 个输出整数,不能直接比大小

这份 HBM 的输入是 INT8 [1,3,224,224],输出是 INT32 [1,1000]。一个输出文件有 4000 bytes,存着 1000 个类别的整数值。

如果只看到这个 shape,很容易直接写:

但实际产物的量化信息里,输出 axis=1,有 1000 个 scale,每个类别使用自己的尺度。

因此第 c 个类别的分数要先还原:

再对这些分数排序。

这一步针对带量化信息的整数分类输出。如果模型已经输出反量化后的浮点分数,就直接比较那些分数;如果各类别共用同一个正 scale 和同一个 zero point,整数排序与反量化排序一致。这次排序会变,原因就在于各类别的 scale 不同。

拿文件编号为 00000076 的图片 ILSVRC2012_val_00000076.JPEG 来看,它是测试集的第 6 张,标签是类别 424:barbershop,理发店。与它竞争的一个类别是 415:bakery,面包店。

类别,索引从 0 开始

原始整数 q

scale

反量化后的分数

424:barbershop

42809

0.000371393107343

15.899

415:bakery

69627

0.000208402707358

14.510

这两个类别的 zero point 都是 0。

按原始整数比,69627 > 42809,选出面包店;乘上各自的 scale 后,15.899 > 14.510,选出的才是理发店。
alt text
图1|文件编号 00000076,测试集第 6 张。左侧比较原始整数,右侧比较反量化分数;两幅图分别使用自己的数值坐标。
在全部 30 张正确输入的图片里,直接整数 argmax 与正确解码的 Top-1 有 9 张不同。这里比较的是两种读法,不是重新计算模型相对标签的错误数。

我还试了给所有类别统一套用第一项 scale,结果仍然有 9 张不同。这份产物的 zero point 全为 0,乘同一个正数不会改变原始整数的顺序,自然也修不好这个问题。

先从自己的 HBM 取得量化参数

这些 scale 来自编译产物,不是手工填写。我通过 HBDK 读取输出类型、shape、字节 stride 和量化信息,再整理成 metadata.json。这个 JSON 是脚本生成的记录格式。
下面使用本次 OE 3.9.1 中的 HBDK 4.11.11 接口,导出第 0 个模型函数的所有输出。将代码保存为 export_output_metadata.py:

在已有 OE 环境里,把脚本与 HBM 放在同一目录,指定一个新的 JSON 文件名运行:

代码中的 _strides 是这次 HBDK 4.11.11 的字节步长字段。拿到 JSON 后,先看 dtype、shape、strides_bytes,再看 quantization 中的 axis 和 scale 数量。

再用这些参数解码输出

这次导出的第一个输出是紧凑排列的 [1,1000] INT32,字节 stride 为 [4000,4],量化 axis=1。对应的解码核心如下:
换模型时,要随实际元数据一起调整 dtype、shape、stride 和量化轴。关键是把模型输出与解释这些输出的参数放在一起处理,而不是只保存一个 .bin 文件。

四、4000 bytes 对了,类型也可能读错

还有一种错误更直接:把这份 INT32 文件当成 FLOAT32 读取。

INT32 和 FLOAT32 都是每个元素 4 bytes,所以文件长度依然是 4000 bytes。长度检查会通过,数据含义却已经变了。

我对 30 份基线输出做了这个对照。总计 30000 个元素里,错误的 FLOAT32 读法产生了 16770 个 NaN 或无穷值;单是文件编号 00000076 的图就有 553 个。

这里需要分清两个操作:

文件大小回答的是“有多少字节”,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

alt text

图2|相同 PTQ 模型、相同 30 张图片,只改变推理时的预处理。“重复 mean/std”是在归一化张量上再次做通道均值与标准差处理。

通道顺序写反后,部分图片仍然能分对,仅凭几张图看起来正常,很容易漏过去。漏掉归一化或重复做 mean/std,影响则大得多。

恢复正确处理后,30 张 PTQ 输出逐元素回到了正常基线。我还把固定的前三张图拿到 HBM 里,做三类错误与恢复,共增加 12 次仿真;恢复后的整数输出同样与原基线一致。

这组对照给了一个很实用的定位办法:先保留正确输入和输出,再一次只改一个环节。结果异常时,可以顺着变化找原因,而不是把所有掉点都先归到 PTQ 上。

六、参数核对之外,还要比较实际数据

我也把自己的 I/O 检查器接到这次实验中,核对类型、shape、stride、文件长度和预处理声明。它能发现明确的参数矛盾,但实际输入已经处理错、声明仍写“正确 RGB 与归一化”时,检查会通过。填进来的信息一致,不能说明上游脚本确实按那套流程执行。

因此我同时保留源图片、正确预处理生成的张量和逐阶段输出。42 次输出解码与独立 NumPy 实现逐字节一致,定点 BC 与 HBM 的整数输出也逐元素一致;另开新容器复跑前三张基线图片,输入、原始输出和解码结果的哈希均与首次一致。

七、这次对照得到的排查顺序

这一轮之后,我会按数据流排查“能运行,但结果不对”:

  1. 从源图片开始,核对 RGB/BGR、缩放裁剪和归一化。

  2. 查看实际 HBM 输入要求,核对类型、shape、布局和输入量化。

  3. 按输出 dtype、stride 和量化参数还原分数,再做后处理。

  4. 对照浮点 ONNX、PTQ ONNX 和 HBM,找出差异开始出现的阶段。

模型转换和推理完成之后,还有一件事值得单独确认:程序读出来的结果,是否就是模型真正输出的含义?

这次 ResNet18 对照里,最具体的例子就是那 1000 个类别 scale。少用了一组参数,4000 bytes 没变,运行也没报错,最终的类别却变了。

这次对照把代码里容易忽略的问题落实到了具体输出上:同一份模型、同一批输入,仅改变输出读法,就有 9 张图片选出了不同的类别。按实际量化参数还原分数后,HBM 与 PTQ ONNX 在这 30 张图片上的 Top-1 类别全部一致。

因此,遇到“模型能运行,分类却不对”,沿着输入处理和输出解码检查,再判断是否需要调整量化。把每个阶段的数据读对,才能知道模型究竟在哪里发生了变化。

参考资料

算法工具链
社区征文征程6
评论0
0/600