零基础搞懂 SNPE 模型量化:INT8 精度损失 = 0% 的秘密
- 引言:模型减肥
- 量化到底是什么
- SNPE 量化工具和参数
- 校准数据集怎么准备
- 看看量化后的 DLC 长啥样
- x86 CPU 上验证 INT8 推理
- 为什么 MNIST 没掉精度
- 骁龙真机实测
- 踩坑记录
- 量化对车机意味着什么
- 总结
- 系列文章
这是 AI 实操第 7 天。第 6 天把 MNIST 模型转成了 DLC,今天给 DLC 做 INT8 量化:压缩 3.46 倍、精度零损失,并在骁龙真机上实测四个后端。 本文首发地址 https://h89.cn/archives/673.html 项目地址 https://gitee.com/chenjim/cockpit-ai-from-zero
引言:模型减肥
上一篇我把第 1 天训练的 MNIST CNN 模型转成了 DLC 格式,439KB,在开发机上用 CPU 后端跑通了推理。
但 439KB 只一个模型。车机里要同时跑语音识别、语音合成、驾驶员监测、乘员监测、导航指令理解……十几个模型,加起来就是好几 MB。FP32(32 位浮点数)不光占空间,运算也比 INT8(8 位整数)慢得多。
所以有了这一篇:模型量化(Model Quantization),把 FP32 的权重和激活值压缩到 INT8。
实际结果:
| 指标 | FP32 DLC | INT8 DLC |
|---|---|---|
| 模型大小 | 439 KB | 127 KB |
| 压缩比 | — | 3.46x |
| x86 CPU 200张准确率 | 96.00% | 96.00% |
| 骁龙 CPU 50张准确率 | 98.00% | 98.00% |
| 骁龙 GPU 50张准确率 | 98.00% | 98.00% |
| 精度损失 | — | 0%(4个后端一致) |
说明:96%、97.62%、98% 都是抽样结果(50~200 张),受样本选择影响。PyTorch / ONNX Runtime 在 10,000 张全量测试集上的基准是 99.12%(与 day01 一致)。量化是否掉精度,关键看 FP32 与 INT8 在同一份样本上是否一致。
不过零损失靠的是 MNIST 太简单 + per-channel 量化效果好。真实模型多少会掉点。
DSP/HTP 后端在消费者手机上不可用(固件签名限制),车载/工业平台的骁龙才开放。
量化到底是什么
用大白话说:FP32 一个数占 4 个字节,INT8 一个数占 1 个字节。模型参数从 float 变 int8,直接省 75% 空间。不仅省内存,整数乘法也比浮点乘法快(CPU 有专门的 SIMD 指令——单指令多数据,一条指令同时算多个数;DSP/NPU 更是只支持 INT8)。
但问题是:不能直接把 float 砍成 int。比如 0.0039 如果存成 int8,值就是 0,信息没了。
所以需要一个「编码」——把浮点区间 [min, max] 映射到整数区间 [Qmin, Qmax]。
对称量化
浮点值 x = scale × (q - zero_point)
其中:
- q 是量化后的整数值
- scale = (max - min) / (Qmax - Qmin) ← 每个间隔代表多少
- zero_point 确保浮点 0 映射为整数 0(对称量化时 zero_point = 0)
INT8 的范围是 [-128, 127](signed)或 [0, 255](unsigned),scale 越小精度越高,但区间覆盖越窄。SNPE 默认权重用 signed、激活值用 unsigned。
为什么需要校准数据集
权重的 min/max 可以离线算(权重存在 DLC 里),但激活值的 min/max 不知道——你得实际跑几百张图,收集每层的输出值范围。这些用于收集范围的图片就叫「校准数据集」(Calibration Dataset)。
SNPE 量化工具跑校准的流程:
- 用 FP32 DLC 在 CPU 后端跑校准数据集的每一张图
- 记录每一层输出的 min/max
- 用 min/max 算出 scale 和 zero-point
- 把编码信息写进新的 DLC
校准数据不参与训练,只是走一遍前向传播收集统计信息。
SNPE 量化工具和参数
SNPE SDK 提供两个量化工具:
| 工具 | 说明 |
|---|---|
snpe-dlc-quantize |
传统 SNPE 量化命令,参数稳定 |
qairt-quantizer |
新版本(对应 QAIRT 品牌),参数更丰富 |
两个底层逻辑相同,我用的是 snpe-dlc-quantize。
核心参数
snpe-dlc-quantize \
--input_dlc mnist_cnn.dlc \
--output_dlc mnist_cnn_int8.dlc \
--input_list calibration_list.txt \
--weights_bitwidth 8 \ # 权重用 INT8
--act_bitwidth 8 \ # 激活值用 INT8
--use_per_channel_quantization \ # 每个通道独立量化
--bias_bitwidth 32 # 偏置用 32bit,防止精度损失
各参数说明:
-
--weights_bitwidth 8/--act_bitwidth 8:设为 8 就是 INT8 量化。设为 16 就是 INT16(省一半空间但精度更低)。SNPE 还支持 INT4 权重(要加--pack_4_bit_weights),但需要硬件支持。 -
--use_per_channel_quantization:这是「精度无损」的关键。普通的 per-tensor(逐张量)量化对整个卷积核用一套 scale/zero-point,per-channel(逐通道)给卷积核的每个输出通道单独一套。有什么区别?卷积核不同通道的权重范围差异很大,一个 scale 管不过来,per-channel 粒度高几十倍,精度好得多。 -
--bias_bitwidth 32:偏置(bias)的值很小(例如 0.01),用 INT8 根本存不下。32 位给它足够精度,因为 bias 总共才几百字节,不占地方。
其他可选参数
| 参数 | 作用 |
|---|---|
--act_quantizer_calibration mse |
用 MSE 方法算 scale(精度更好但慢) |
--algorithms cle |
跨层均衡(Cross-Layer Equalization),处理长尾分布层 |
--param_quantizer_schema symmetric |
强制权重对称量化 |
--use_per_row_quantization |
对全连接层用逐行量化 |
MNIST 模型简单,不需要这些高级选项,默认 min-max 校准就够用。
校准数据集怎么准备
SNPE 要求校准数据必须是单个 raw 二进制文件,每行一个文件路径写进 calibration_list.txt。raw 文件的格式是 NHWC float32 小端序字节流。
我从 MNIST 训练集的 60000 张里取了前 1000 张做校准:
# MNIST raw 格式是 uint8 [0-255],需要转 float32 [0-1]
img_float = img_uint8.astype(np.float32) / 255.0
# DLC 输入是 NHWC [1, 28, 28, 1],28x28=784 个 float32 = 3136 字节
img_nhwc = img_float.reshape(28, 28, 1)
# 写入 raw 文件
img_nhwc.astype(np.float32).tofile("calib_0000.raw")
calibration_list.txt 长这样:
/vol1/1000/Project/audi-ai-hands-on/day07-H-snpe-quantize/calibration/calib_0000.raw
/vol1/1000/Project/audi-ai-hands-on/day07-H-snpe-quantize/calibration/calib_0001.raw
...共 1000 行
关键点:raw 文件必须和 DLC 模型预期的输入格式一模一样(NHWC + float32 + [28,28,1])。如果格式不对,量化工具不会报错,但收集的激活值范围是错的,量化后的模型精度就烂了。
看看量化后的 DLC 长啥样
跑完量化后,用 snpe-dlc-info 看 INT8 DLC 的结构,和 FP32 版本完全不同:
| Id | Name | Type | Inputs (data type) | Runtimes |
|----|---------------|---------|--------------------------------------|----------|
| 0 | /conv1/Conv | Conv2d | input: uFxp_8 [1,28,28,1] | A D G C |
| | | | conv1.weight: sFxp_8 [3,3,1,16] | |
| | | | conv1.bias: sFxp_32 [16] | |
关键信息:
uFxp_8:unsigned 8-bit 定点数,用于输入和激活值sFxp_8:signed 8-bit 定点数,用于权重sFxp_32:signed 32-bit 定点数,用于偏置- 每层都带了 encoding:例如
conv1.weight encoding for channel_0: min=-0.333, max=0.330, scale=0.0026, offset=0
scale=0.0026 就是这层每个 INT8 单位代表的浮点值。offset=0 说明是对称量化。
再看这个:「Runtimes: A D G C」——量化后的模型仍然支持全部四个后端,直接可以推到骁龙 AIP/DSP 上走 INT8 推理。
x86 CPU 上验证 INT8 推理
没骁龙手机也能先在开发机上验证。SNPE 的 CPU 后端有个 --enable_cpu_fxp 标志,强制它用定点(fixed-point)模式跑推理:
snpe-net-run \
--container mnist_cnn_int8.dlc \
--input_list input_list.txt \
--output_dir ./output_int8 \
--enable_cpu_fxp # 关键:启用量化编码,模拟 DSP INT8 行为
不带 --enable_cpu_fxp 的话,CPU 后端默认还是用 FP32 算——即使 DLC 里有量化编码也会被忽略。加上这个标志后,CPU 会按 INT8 编码来算,和真实的 DSP/NPU INT8 推理行为一致。
验证脚本跑 200 张 MNIST 测试图:
============================================================
量化结果汇总(200 张抽样)
============================================================
ONNX Runtime(200张抽样):97.62%
FP32 DLC 准确率(200张) :96.00%
INT8 DLC 准确率(200张) :96.00%
INT8 精度损失 :0.00% (0/200)
模型压缩比 :3.46x (439KB → 127KB)
INT8 与 FP32 一致 :200/200
200 张里 INT8 和 FP32 每一张的预测都一模一样。零损失。
为什么 MNIST 没掉精度
MNIST 模型太小了,小到量化几乎伤不到它:
- 28x28 的灰度手写数字,10 个类,信息量极低
- 总共 2 个卷积层 + 2 个全连接层,10 万参数
- Per-channel 量化给 16 个通道各一套 scale,每个通道第一层只有 3x3x1=9 个参数
- 9 个参数用一套 scale,足够精确了
换成大模型(ResNet、YOLO、LLaMA),量化通常会掉 0.5%~3%。但值不值要看场景。语音识别模型用 INT8 可能只掉 0.1% WER(词错误率),这点损失换 4 倍空间和 2 倍速度,很划算。
什么时候量化会翻车:模型里有层激活值分布极不均匀的——少数值很大、多数值很小。min-max 校准的范围被大值拉宽,scale 太小,大部分值弱到变 0。这时候要用 MSE 校准或跨层均衡(--algorithms cle)。
骁龙真机实测
后来把手机连上了,跑了一轮真机 Benchmark。设备信息:
- 型号:24122RKC7C(Redmi)
- 芯片:骁龙
sun(Snapdragon 8 Gen 2 级别) - 系统:Android 16,arm64-v8a
推送文件、跑推理、拉回结果的过程写成了 phone_benchmark.py,一键执行。
============================================================
骁龙手机 SNPE Benchmark 结果(50 张)
============================================================
手机: 24122RKC7C (sun/arm64-v8a)
后端 正确 准确率
------------------------------------------------------------
FP32 CPU 49/50 98.0%
INT8 CPU(fxp) 49/50 98.0%
FP32 GPU 49/50 98.0%
INT8 GPU 49/50 98.0%
DSP / HTP 不可用 错误3110
几个点:
-
四个可用后端(FP32/INT8 × CPU/GPU)的 50 张预测逐张完全一致。INT8 量化在 Adreno GPU 上也没掉精度。
-
DSP 和 HTP 报错 3110——
No backend could validate Op=/conv1/Conv Type=Conv2d。这不是我模型的问题,是消费级手机的 DSP 固件受签名保护,SNPE 通用版 SDK 无权加载自定义模型到 Hexagon DSP。车载/工业场景的骁龙平台会开放这个权限,普通的零售手机就别想了。 -
GPU 后端不需要额外配置,
--use_gpu直接用 Adreno GPU 加速。而且 INT8 DLC 在 GPU 上同样跑得通——GPU 直接吃 DLC 里的量化编码,不需要--enable_cpu_fxp。
踩坑记录
坑一:校准数据格式不对,量化工具一声不吭
量化工具不会检查 raw 文件的内容(float32 还是 uint8、NHWC 还是 NCHW)。我一开始用 NCHW [1,1,28,28] 的格式写 raw,量化成功跑完了,DLC 也正常生成——但推理准确率 10%,跟瞎猜没区别。
DLC 模型输入是 NHWC,你给它 NCHW 的数据,激活值范围全算错了。编码后的 INT8 推理自然不准。
坑二:snpe-dlc-quantize 的 bash 包装脚本有个语法 bug
工具本身是个 bash wrapper,启动时会报:
snpe-dlc-quantize: line 224: [: too many arguments
不影响工具运行,但看起来吓人。原因是脚本里某处用 [ 比较字符串时没加引号。SDK bug,没修。
坑三:校准数据太少,准确率直接跪
一开始我只用了 5 张图校准,出来的 INT8 模型准确率 80% 不到。加到 1000 张(覆盖了 0-9 十个数字)就正常了。
规律很简单:校准数据不用多到和训练集一样,但要覆盖输入空间的多样性。MNIST 1000 张够了,ImageNet 类别的模型得几千张。
量化对车机意味着什么
回到车载 AI。车机和手机不一样:
- 手机没电了充就行;车机不怕没电,但芯片温度要控制在 85℃ 以内
- 手机 DSP 降频了顶多卡一点;车机降频会连累导航和倒车影像
- 车载 AI 模型单个不大,但数量多——十几个同时跑,加起来几十 MB
INT8 量化在这场景下的好处:十几个模型全量化后总体积降到几 MB,内存占用砍半,DSP 延迟比 FP32 低 2-4 倍。
如果要做端侧大模型(比如 LLM 跑语音对话),INT8 基本是硬性要求。几 GB 参数的模型,FP32 根本塞不进 NPU 的片上 SRAM。
总结
这次搞清楚了:
- 量化不是玄学:float 值通过 scale + zero-point 映射到 int,公式就一行
- SNPE 量化工具:
snpe-dlc-quantize核心参数是 bitwidth、per-channel 和校准集 - 校准数据:raw 浮点格式,格式跟模型输入必须一致,几百到一千张就够
- 验证方法:x86 CPU +
--enable_cpu_fxp模拟 INT8 行为,精度 OK 了再推真机 - 实测算力:3.46x 压缩,FP32/INT8 × CPU/GPU 四个组合精度完全一致
- 真机限制:DSP/HTP 被消费级手机锁了,车载/工业平台才能用
下篇是第 8 天 Python 工程化:给训练脚本加类型标注、写 pytest 用例、配 pre-commit 质量门禁。
系列文章
本系列「车载端侧 AI 工程化从零上手」共 10 篇,建议按序阅读:
- 零基础用 PyTorch 识别手写数字:MNIST 实战入门
- 把 PyTorch 模型变成 ONNX:导出、验证和可视化一次学会
- MLflow 实验追踪:从入门到上手
- 用 MNN 在 CPU 上跑 AI 推理:阿里端侧推理框架上手记
- 小白用 TensorRT 给模型加速:ONNX 转 Engine 踩坑实录
- 零基础上手:高通 SNPE 模型转换实战(从 ONNX 到 DLC)
- 零基础搞懂 SNPE 模型量化:INT8 精度损失 = 0% 的秘密
- Python 工程化重构:从 print 脚本到 pytest 项目
- 车载语音指令识别:从 44% 到 96% 的调优之路
- 车载语音指令识别:SNPE 转换与真机部署实测记录
本文链接:零基础搞懂 SNPE 模型量化:INT8 精度损失 = 0% 的秘密 - https://h89.cn/archives/673.html
版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。