16. 模型压缩与体量门槛
15. 端侧 NPU 加速导读 里的编码器导出后是一张 617 MB 的 FP32 图。把它交给在线通路,设备加载时回了一句 Authentication failed, input model cannot run by npu,没有节点名,也没有说大了多少。这一篇解决的是这个阶段的问题:怎么判断拦路的到底是不是体量,以及四种压缩手段里哪一种真的能让模型过去。
结论先摆出来——同一批实验里出现过 169 MB 被拒、168 MB 通过的情况。差 1 MB 不可能是门槛,所以「文件变小了」和「能放进去」从一开始就不是同一件事。
先确认拦路的是不是体量
报错不给数字时,唯一能用的办法是做对照实验:固定其他变量,只改体量,看拒绝行为变不变。
真正把结论钉住的是一组耗时对照。把 FP32 的图直接送上去,设备 185 ms 就拒了;把同一张图做权重量化、得到一个 169 MB 的文件再送,设备花了 4272 ms 才拒。两次都是拒,但差了 20 多倍的时间。
这个时间差本身就是证据:秒拒意味着还没真正展开模型就按文件体量否掉了;慢拒意味着设备先把权重摆开、算完实际占用,才发现超限。两种拒绝走的不是同一道门。顺着这个线索继续夹逼,上限落在 201—268 MB 之间,而且作用在反量化之后的字节数上,不是磁盘上的文件大小。
这个区间是在这台设备、这个系统版本、这条在线通路上量出来的。换一条通路它就不成立——离线通路最后收下了 150 MiB 的模型,却在别的地方失败,原因完全无关。排查时不要把两条路的证据混用。
四种压缩手段,两种结局
图里的横轴是体量,琥珀色带子是上面量出来的上限区间。五根条形里有两根通过,三根被拒,而被拒的三根各有各的原因。
FP32 原始权重(617 MB):明显超限,185 ms 秒拒。这一根只是用来确定秒拒的基线行为。
转换器 FP16(336 MB → 168 MB):通过。这是在线通路上唯一真正有效的手段。原因是它改的是计算精度本身——加载之后权重就是 16 位,运行时不需要还原成别的东西,所以摆开之后也还是这么大。
权重量化(169 MB):被拒,而且是慢拒。文件确实变小了,但这种压缩只改变权重在文件里的存法,加载后运行时会把它反量化回原精度参与计算。门槛量的是摆开之后的字节数,所以这一级压缩在门槛面前等于没做。168 MB 通过、169 MB 被拒的矛盾到这里才讲得通:前者摆开还是 168 MB,后者摆开回到了 600 MB 量级。
全量量化:连门槛都没走到。转换器在图里插入的量化转换节点不被设备侧识别,直接报 Unrecognized node type 113。这是算子集的问题,不是体量的问题,属于 17. 图转换与算子适配 的范畴。
离线后量化 INT8(150.3 MiB):通过,但走的是另一条通路。离线编译器把量化后的指令和权重一起固化进模型文件,运行时不存在"还原"这一步,所以文件多大、摆开就多大。
从这里能提炼出的判断方法
压缩手段的有效性,取决于它压缩的是存储还是计算。
| 手段 | 压的是什么 | 运行时会不会还原 | 对体量门槛有没有用 |
|---|---|---|---|
| 权重量化 | 存储 | 会 | 没用 |
| 降低计算精度(FP16) | 计算 | 不会 | 有用 |
| 全量量化 | 计算 | 不会 | 看算子集是否接受 |
| 离线编译 + 后量化 | 计算,并固化成指令 | 不会 | 有用 |
这张表在别的硬件上也值得先过一遍:拿到一个"模型太大"的报错,先问运行时会不会把权重还原回去,再决定压哪一层。只盯着文件大小做优化,很容易做完一轮发现什么也没变。
同样值得固化进排查流程的是记录拒绝的耗时量级。多数人只记"通过/失败",但秒拒和慢拒是两个不同的故障,耗时差异往往是唯一免费拿到的线索。
走离线通路时压缩是另一套做法
换到离线通路之后,压缩的入口从转换器的命令行开关变成了一个独立的后量化工具,过程也更长:
- 先把图固定成静态形状——后量化工具不接受动态维度,一个动态批次的 joiner 直接会被算成
[0,512]这种空形状。 - 准备校准集:用真实音频跑一遍推理,把每一路输入的张量落成文件。
- 跑后量化,权重按绝对值最大定标、激活按滑动极值定标,纯 CPU 完成,不需要训练。
- 把量化参数交给离线编译器,产出最终的模型文件。
这条路上 FP32 模型编译出来是 614,205,325 B,后量化之后是 157,611,404 B——150.3 MiB,压缩 3.90 倍。设备侧兼容性查询返回 0,117 路输入输出全部对上,构建耗时 3.67 s。模型终于进去了。
有一个开关是无效的:给离线编译器加上"权重按 FP16 存"的参数,产出字节数和不加时完全相同。查下来原因是编译器默认就把矩阵乘的权重存成了 FP16(产出文件恰好是 FP32 版本的一半),这个开关没有额外空间。遇到"改了参数但产物字节数一模一样"的情况,先怀疑这个参数本来就是默认行为,不要假设它生效了。
校准集是这一步最容易做错的地方
后量化的质量取决于校准集是否覆盖真实的激活范围,而流式模型的校准集比一般模型难做——它有 116 路缓存状态,这些状态不是随便填的数,是上一拍算出来的。
实际踩到的几个坑:
校准数据的样本数不是写在参数里的。 工具用 校准文件首维 ÷ 模型输入首维 反推有多少组样本。如果按直觉在张量前面加一个"样本数"维度,首维会被算成一个不整除的数,直接报 Too few input data!。正确做法是:rank 和模型输入保持一致,样本数体现为首维乘以 N。我一开始误判成"样本数不够",把 32 组加到 208 组仍然报错,这个判断是错的,后来撤回了。
校准文件要带一个 20 字节的头。 这类格式细节以工具自带的预处理脚本为准,照着文档手写容易漏。
做批量校准时不能把一路音频复制 16 份。 后面为了填满批次做过一版 16 路的校准集,正确做法是取 16 段错开起点的真实音频并行递推,各自带各自的状态。复制 16 份会让 16 路的激活完全一致,统计出的极值范围偏窄,量化参数会过紧。
环境依赖要对齐到补丁版本。 这套工具的文档写的是 protobuf 3.20.0,但同环境里的 onnx 1.14.1 要求 >=3.20.2,而量化工具自己生成的序列化代码又要求 <=3.20.x,三个约束的交集只有 3.20.3。另外文档没有列出 cv2 和 tqdm 两个依赖,缺了会在运行中途才报错。容器内存要给到 16 GB 以上,8 GB 时第一组校准就会被内核杀掉。
体量不是唯一的资源约束
把模型塞进去只是第一关,内存是另一条线,而且两者会互相顶。
走在线通路时,一个备选方案是把编码器从中间切成两段,分别用 FP16 放上去——按节点 2050 切开,两段分别是 145 MiB 和 148 MiB,都在门槛以内,跨段只需要传 7 到 9 个张量、合计约 60 KB,切面代价很小。听起来可行,但算下内存就否掉了:INT8 时这部分占 162 MB,换成两段 FP16 之后变成 293 MB,多出 131 MB。而带说话人分离的场景本来就已经逼近内存预算。
「过了体量门槛」和「内存放得下」是两个独立的约束,用降精度换门槛时,必须把内存一起算。具体怎么把这 500 多 MB 拆开归因,在 18. 时延、功耗与内存怎么测 里。
总结
模型放不进去时,先分清压缩的是存储还是计算:只改存储格式的压缩会被运行时还原,对体量门槛无效。判断门槛位置可以不依赖报错信息——用对照实验夹逼区间,并记录拒绝的耗时量级,秒拒和慢拒指向不同的门。换到离线通路之后压缩能做得更彻底(617 MB 到 150.3 MiB),但代价是多出校准集、静态形状和一长串环境约束,其中校准集的构造方式最容易悄悄做错。
模型进去了,下一个问题是设备认不认识这张图里的每一个算子。
下一篇:17. 图转换与算子适配。
