问题描述
我先把 YOLOv8-pose 转 K230 kmodel 精度损失问题 和我这一路的研究过程系统总结一下。
一、最终结论
目前针对这个模型
best.onnx
我们已经基本确认:
- float32 路线是通的
float32 kmodel 可以正常工作,说明:
预处理口径基本对
ONNX 导出本身没坏
K230 / nncase 的基础编译链路是通的
所以问题不是模型本身不能上 K230,而是量化后 score/conf 分支精度坍塌。
- 量化后的主要病灶不是 box,也不是 keypoint,而是 score/conf
量化后最典型现象是:
box 范围基本还在
keypoint 范围基本还在
conf / score 被压到接近 0
最早在 CONVERSION_SUMMARY.md 里就已经观察到:
ONNX conf max ≈ 0.9278
PTQ kmodel conf max ≈ 0.023~0.025
这会直接导致:
conf_thres=0.25 下全部过不了阈值
端到端表现就是 0 detections
3. cosine 高,不代表端到端可用
多个量化模型都出现了这种情况:
output cosine 看起来还不错,常在 0.99 左右
但检测结果仍然是 0/18
这说明:
整体张量相似度不能反映 score 分支这种小幅值但决定生死的信号是否被保住
对 YOLO pose 来说,score 分支比全局 cosine 更关键
二、问题是怎么一步步定位出来的
阶段 1:先确认不是预处理问题
我们先沿着 02_to_kmodel.py 和 05_inference_batch.py 检查了整条链。
确认了:
输入是 1x3x320x320
letterbox 逻辑一致
/255.0 一致
NCHW 一致
推理侧没有多做一次 sigmoid
校准数据也是按同样 letterbox + /255 生成
所以可以排除一个常见怀疑:
量化是否因为输入口径错了 255 倍
结论:不是这个问题。
阶段 2:确认症状是 conf 通道被压垮
在 05_inference_batch.py 的端到端验证中,始终看到:
ONNX 正常出框
PTQ kmodel 全部 no detections
进一步做单图数值检查时看到:
ONNX conf max ≈ 0.9278
kmodel conf max ≈ 0.0248
但与此同时:
box max 仍接近原值
keypoint max 仍接近原值
所以第一次明确定位到:
量化主要伤的是 score/conf 分支,而不是整个输出都坏了。
阶段 3:尝试普通 PTQ 与高精度 PTQ
我们先后试了很多 PTQ 组合,集中在 02_to_kmodel.py 上加参数:
包括:
uint8
int8
int16 activation + uint8 weight
NoClip
Kld
UseSquant
NoFineTuneWeights
还专门从用户给的
calibration_photo
生成了新的校准集:
calib_from_calibration_photo_320/
并重新编出了:
best_ptq_act_i16_w_u8_calibphoto.kmodel
结果仍然是:
cosine 依旧不错
conf max 依旧只有 0.02478
检测仍然 0/18
这说明:
换校准集、提升 activation 精度、调整 calibrate method,都没有触到真正根因。
阶段 4:尝试 MixQuant,想直接保住 head
随后围绕 QuantScheme 做了多轮实验,核心文件包括:
QuantScheme_head_f16.json
QuantScheme_output_f16.json
QuantScheme_head_last6_f16.json
QuantScheme_last6_plus_concat5_f16.json
QuantScheme_model22_all_f32.json
思路是:
把 model.22 头部相关节点改成 f16 / f32
重点保护最后几层 conv、concat、输出附近节点
结果出现两种情况:
情况 A:数值几乎不变
例如:
即使把 model.22 整段几乎都标成 f32
conf max 还是在 0.023 左右
情况 B:改得太激进直接爆掉
比如某些输出侧保护会导致:
conf max 异常大
kpt max = inf
所以这一步得到的核心认识是:
我们改到了“看起来重要”的 head 节点,但并没有真正改到“决定 score 生死”的量化边界。
阶段 5:证明不是最终 output0 拼接导致的
一个自然怀疑是:
最终输出 [1,38,2100] 混合了 box / score / kpts,是否因为统一量化尺度把 score 淹没了?
于是我先写了:
01b_split_onnx_outputs.py
把最终输出拆成:
output_box
output_score
output_kpts
生成:
best_split_outputs.onnx
验证结果:
ONNX 切片完全一致,diff = 0
但 PTQ 后,output_score 仍然只有 0.02478
这一步很关键,它证明了:
问题不是最终 output0 这个 graph output 混在一起才坏的。
score 在到达最终输出前就已经坏了。
阶段 6:把内部节点挂成 Graph Outputs,真正定位到 score 分支坍塌点
这是整个研究里最关键的一步。
我写了:
01c_expose_internal_outputs.py
把这些内部节点挂出来:
head_box_internal <- /model.22/Mul_2_output_0
head_score_logits <- /model.22/Split_output_1
head_score_internal <- /model.22/Sigmoid_output_0
head_kpts_internal <- /model.22/Reshape_7_output_0
然后直接比较 ONNX 和 kmodel 的 mean/std。
关键统计
head_score_logits:
ONNX mean/std: -16.8456 / 3.4032
kmodel mean/std: -14.1022 / 2.1587
kmodel max: -3.7489
head_score_internal:
ONNX mean/std: 0.004157 / 0.060332
kmodel mean/std: -0.003023 / 0.001598
kmodel max: 0.023132
这两组数据直接构成了病理链:
logits 最大值在 kmodel 中只能到 -3.7489
sigmoid 后最大值自然就是 sigmoid(-3.7489) ≈ 0.0231
与实际测得的 head_score_internal max = 0.023132 完全一致
所以最终可以明确说:
不是 sigmoid 本身坏了,而是 sigmoid 前的 score logits 已经失去正向激活能力。
也就是说:
原本应该接近 0 或大于 0 的那些关键 score logits
在量化后被整体压回了负区间
导致 sigmoid 后永远上不去
这是我们对根因的最强证据。
阶段 7:围绕最后 score conv 建“f32 护城河”,但仍无效
在锁定 logits 崩坏后,又继续往前追:
定位到三尺度 score 最后卷积:
/model.22/cv3.0/cv3.0.2/Conv_output_0
/model.22/cv3.1/cv3.1.2/Conv_output_0
/model.22/cv3.2/cv3.2.2/Conv_output_0
对应权重:
model.22.cv3.0.2.weight
model.22.cv3.1.2.weight
model.22.cv3.2.2.weight
于是又做了更窄更狠的一版 QuantScheme,只保护:
最后 score conv 的 weight
最后 score conv 的 output
Split
Sigmoid
再加部分上游输入
例如:
QuantScheme_score_lastconv_kld_f32.json
QuantScheme_score_lastconv_with_bias_input_f32.json
并且改成:
calibrate_method=Kld
quant_scheme_strict=true
NoFineTuneWeights
结果是:
logits 和 score 的统计完全不变,一字不差。
这说明:
即使我们已经知道病灶在 score logits 这一路,
通过当前 QuantScheme 能控制到的这些名字,也仍然救不回来。
更大的可能是:
nncase 内部真正生效的量化边界发生在更早/更隐蔽的融合重写之后
而这些边界并不对应我们能在 JSON 里改到的名字
阶段 8:开始尝试“更早导出 raw head”,绕开尾部融合链
在认识到“最终输出太晚、内部 score too late、QuantScheme 也控不住”之后,又继续尝试本质不同的一条路:
直接导出三尺度更早的 raw head 输出,而不是让 nncase 继续处理 tail fusion。
对应脚本:
01d_expose_raw_head_outputs.py
挂出的节点:
head_raw_p3 <- /model.22/Concat_1_output_0
head_raw_p4 <- /model.22/Concat_2_output_0
head_raw_p5 <- /model.22/Concat_3_output_0
实际 shape 是:
P3: [1, 65, 40, 40]
P4: [1, 65, 20, 20]
P5: [1, 65, 10, 10]
这一步说明:
这三个节点比最终 output0 更早
后面还有一整段 tail fusion / reshape / sigmoid / concat / decode 链
这条路技术上是可编译的
并且 raw-head PTQ 的统计显示:
三个 raw head 输出并没有像最终 score 那样明显崩成 0.02
说明问题更可能集中在尾部解耦/重排/切分那段之后
不过这一块还没完整做完“图外 decode + 端到端恢复”,所以目前结论还停在:
raw-head 方向是目前唯一还保留希望的“本质不同路线”。
硬件板卡
庐山派230
软件版本
nncase 2.9.0