4. 模型量化入门:从FP32到INT8,为什么量化是裁剪的第一步?
做嵌入式AI,尤其是Cortex-M系列,你很快会撞上一堵墙——内存墙。
模型在PC上跑得飞起,一放到MCU上,Flash不够,RAM爆炸。这时候你第一个想到的可能是剪枝、蒸馏,但我个人的经验是:先做量化。为什么?因为量化带来的收益最直接、最稳定,而且几乎不损失太多精度。
说白了,量化就是把模型里那些“娇贵”的FP32小数,换成“皮实”的INT8整数。你想想看,一个参数从4字节变成1字节,体积直接缩到四分之一。这还不算完,整数运算在Cortex-M上比浮点快得多,很多M4/M7内核甚至没有硬件浮点单元,跑FP32全靠软件模拟,那速度……嗯,我当年调一个语音识别模型,用FP32推理一次要2秒,量化到INT8后直接降到200毫秒,效果立竿见影。
4.1 为什么FP32在MCU上“水土不服”?
先看一组数据,你就明白了:
| 数据类型 | 位宽 | 表示范围 | 精度 | Cortex-M硬件支持 |
|---|---|---|---|---|
| FP32 | 32 bit | ±3.4×10³⁸ | ~7位小数 | 仅M7/M55有FPU,其余需软模拟 |
| INT8 | 8 bit | -128 ~ 127 | 整数 | 所有Cortex-M原生支持 |
| INT16 | 16 bit | -32768 ~ 32767 | 整数 | 所有Cortex-M原生支持 |
看到没?FP32的表示范围大得离谱,但MCU根本不需要。一个神经网络权重,大部分值都在[-1, 1]之间,你用32位去存它,纯粹是浪费。我曾经在一个项目里,模型权重分布集中在[-0.5, 0.5],FP32的高位全是0,白白占着Flash。
更关键的是,Cortex-M的Flash和RAM都是按KB算的。一个100KB的FP32模型,量化后只有25KB。这25KB的差距,往往决定了你的芯片能不能装下这个模型。
4.2 量化原理:从浮点到整数的“映射”
量化不是简单的四舍五入。它背后有一套数学映射关系。
我们用一个公式就能说清楚:
// 量化公式:浮点 → 整数
q = round(r / scale) + zero_point
// 反量化公式:整数 → 浮点
r = (q - zero_point) * scale
这里:
- r:原始FP32值
- q:量化后的INT8值
- scale:缩放因子,决定每个整数步长代表多少浮点数
- zero_point:零点偏移,保证浮点0能精确映射到整数
举个例子。假设你有一组权重,范围在[-1.0, 1.0]。INT8的范围是[-128, 127]。那么:
scale = (1.0 - (-1.0)) / (127 - (-128)) = 2.0 / 255 ≈ 0.00784
zero_point = round(-(-1.0) / 0.00784) + (-128) = round(127.55) - 128 = 128 - 128 = 0
所以浮点0.5量化后就是:q = round(0.5 / 0.00784) + 0 = round(63.78) = 64。
反推回来:r = (64 - 0) * 0.00784 = 0.50176,误差只有0.00176,完全可以接受。
4.3 两种主流量化方式:对称 vs 非对称
实际工程中,我们主要用两种量化策略:
| 特性 | 对称量化 | 非对称量化 |
|---|---|---|
| zero_point | 固定为0 | 根据数据分布计算 |
| 表示范围 | [-127, 127](牺牲一个值) | [-128, 127](全利用) |
| 适用场景 | 权重(通常对称分布) | 激活值(通常非对称,如ReLU后全正) |
| 计算复杂度 | 低(无需零点偏移) | 中等(需要处理零点) |
| 精度损失 | 对对称分布友好 | 对非对称分布更精确 |
我个人在Cortex-M上做量化时,权重用对称,激活用非对称。为什么?因为权重经过训练后,正负值分布通常比较对称,用对称量化可以省掉zero_point的计算,推理时少一次减法操作。而激活值经过ReLU后全是非负数,用非对称量化能把[0, 6]这个范围映射到[0, 255],精度更高。
4.4 量化后的推理:整数运算才是王道
量化不只是为了省内存。更重要的是,INT8的乘加运算在Cortex-M上比FP32快一个数量级。
看一个典型的卷积层计算:
// FP32卷积(无硬件FPU时)
for (int i = 0; i < N; i++) {
sum += a_fp32[i] * b_fp32[i]; // 每条指令都要调用软浮点库
}
// INT8卷积(原生支持)
for (int i = 0; i < N; i++) {
sum += a_int8[i] * b_int8[i]; // 单条指令完成,32位累加器防溢出
}
在Cortex-M4上,一次INT8乘法只需要1个时钟周期,而FP32软模拟需要几十甚至上百个周期。你想想看,一个百万参数的模型,这个差距有多大。
但这里有个坑:累加器溢出。INT8乘法结果最大是127×127=16129,如果累加很多次,16位累加器会溢出。所以Cortex-M的SMLABB指令用32位累加器来存结果,安全得多。
- 输入数据量化:将FP32输入转为INT8
- 整数卷积/全连接:用INT8权重和INT8输入做乘加
- 累加结果反量化:将INT32累加结果转回FP32
- 激活函数:在FP32域做ReLU等操作
- 输出再量化:将激活结果量化回INT8,传给下一层
嗯,这里要注意,不是所有层都适合量化。比如Batch Normalization层,它涉及除法和开方,量化后精度损失很大。我通常的做法是:把BN层的参数融合到卷积层里,再做量化。这样既保留了BN的效果,又避免了量化误差。
4.5 量化精度损失:如何评估与补救?
量化不是无损的。你可能会问:损失多少算正常?
根据我的经验:
- 分类任务:Top-1准确率下降<1%算优秀,<3%可接受
- 检测任务:mAP下降<2%算正常
- 语音/信号处理:信噪比下降<1dB算良好
如果精度损失过大,可以试试这些方法:
- 逐通道量化:每个输出通道单独算scale,而不是整个层共用一个。我试过,能挽回1-2%的精度。
- 量化感知训练(QAT):在训练时模拟量化误差,让模型自己适应。效果最好,但需要重新训练。
- 混合精度:敏感层用INT16或FP32,不敏感层用INT8。适合关键任务。
4.6 量化在Cortex-M上的落地流程
最后,我画了一张流程图,帮你理清整个量化落地的步骤:
这个流程我用了很多次,每次都能在1-2天内完成从FP32到INT8的转换。关键是要先跑通,再优化。不要一开始就追求极致精度,先让模型在MCU上跑起来,看看内存和速度是否达标,再回头调精度。
- 模型体积:减少75%(FP32→INT8)
- 推理速度:提升2-4倍(取决于内核和算子优化)
- 内存带宽:降低75%,减少总线瓶颈
- 功耗:降低50%以上(整数运算比浮点省电)
所以,为什么量化是裁剪的第一步?因为它用最小的改动,换来了最大的收益。你不需要改模型结构,不需要重新训练,只需要做一次数学变换,就能让模型在Cortex-M上跑起来。这就是我为什么每次做嵌入式AI,第一件事就是量化。