这是 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 量化工具跑校准的流程:

  1. 用 FP32 DLC 在 CPU 后端跑校准数据集的每一张图
  2. 记录每一层输出的 min/max
  3. 用 min/max 算出 scale 和 zero-point
  4. 把编码信息写进新的 DLC

校准数据不参与训练,只是走一遍前向传播收集统计信息。

SNPE INT8 量化流程

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

几个点:

  1. 四个可用后端(FP32/INT8 × CPU/GPU)的 50 张预测逐张完全一致。INT8 量化在 Adreno GPU 上也没掉精度。

  2. DSP 和 HTP 报错 3110——No backend could validate Op=/conv1/Conv Type=Conv2d。这不是我模型的问题,是消费级手机的 DSP 固件受签名保护,SNPE 通用版 SDK 无权加载自定义模型到 Hexagon DSP。车载/工业场景的骁龙平台会开放这个权限,普通的零售手机就别想了。

  3. 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。

总结

这次搞清楚了:

  1. 量化不是玄学:float 值通过 scale + zero-point 映射到 int,公式就一行
  2. SNPE 量化工具:snpe-dlc-quantize 核心参数是 bitwidth、per-channel 和校准集
  3. 校准数据:raw 浮点格式,格式跟模型输入必须一致,几百到一千张就够
  4. 验证方法:x86 CPU + --enable_cpu_fxp 模拟 INT8 行为,精度 OK 了再推真机
  5. 实测算力:3.46x 压缩,FP32/INT8 × CPU/GPU 四个组合精度完全一致
  6. 真机限制:DSP/HTP 被消费级手机锁了,车载/工业平台才能用

下篇是第 8 天 Python 工程化:给训练脚本加类型标注、写 pytest 用例、配 pre-commit 质量门禁。


系列文章

本系列「车载端侧 AI 工程化从零上手」共 10 篇,建议按序阅读:

  1. 零基础用 PyTorch 识别手写数字:MNIST 实战入门
  2. 把 PyTorch 模型变成 ONNX:导出、验证和可视化一次学会
  3. MLflow 实验追踪:从入门到上手
  4. 用 MNN 在 CPU 上跑 AI 推理:阿里端侧推理框架上手记
  5. 小白用 TensorRT 给模型加速:ONNX 转 Engine 踩坑实录
  6. 零基础上手:高通 SNPE 模型转换实战(从 ONNX 到 DLC)
  7. 零基础搞懂 SNPE 模型量化:INT8 精度损失 = 0% 的秘密
  8. Python 工程化重构:从 print 脚本到 pytest 项目
  9. 车载语音指令识别:从 44% 到 96% 的调优之路
  10. 车载语音指令识别:SNPE 转换与真机部署实测记录

本文链接:零基础搞懂 SNPE 模型量化:INT8 精度损失 = 0% 的秘密 - https://h89.cn/archives/673.html

版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。

标签: MNIST, INT8, SNPE, DLC, 骁龙, 量化, 校准数据集, per-channel

🎓 呈言英语 智能英语学习平台
📚单词学习 🎧听说练习 📖阅读理解 ✏️拼写练习 🌟 AI智能推荐 · 科学记忆曲线
🚀 立即开始免费学习

添加新评论