把模型文件标记为INT8并不能证明整个推理链都在执行整数计算。权重、输入、输出和中间激活可能采用不同数据类型,某些量化方式仍需要浮点输入或浮点内核。
MCU部署最常关注全整数量化:权重和激活使用整数表示,输入输出也具有明确Scale与Zero Point。是否获得速度收益还取决于运行时和目标处理器的优化内核。
|---------------------------------------|
| 检查量化模型时,要逐项确认权重、输入、输出、中间激活和算子内核的数据类型。 |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 完成本篇后,你应该能够 1. 说明INT8量化对权重、激活、输入输出和算子内核的改变。 2. 计算FP32与INT8张量的存储差异并识别饱和。 3. 运行一个整数全连接层,比较反量化输出与浮点基线。 INT8用-128到127共256个整数格子近似一段浮点范围。模型仍在做同类数学运算,但数值表示和内核发生改变。 |

图1:全整数量化涉及输入、权重、中间激活和输出映射
一、先把核心关系连起来
权重量化主要减少模型存储
FP32权重通常每个占4字节,INT8权重每个占1字节。仅权重量化可以显著缩小权重部分,但推理时可能将权重还原为浮点,输入和激活仍为浮点。
因此文件变小不等于整数内核已经生效。需要查看模型张量类型和目标运行时执行路径。
中间激活决定整数计算能否贯穿网络
全整数量化为每个激活张量保存量化参数,算子在整数域完成乘加、偏置和重缩放。乘法结果通常在更宽的累加类型中保存,再映射到下一层int8范围。
某个算子缺少整数实现时,转换器可能失败,或模型中出现浮点回退。MCU项目应明确禁止意外浮点算子。
表1:常见量化方案的差异
| 方案 | 数据类型路径 | MCU意义 |
|---|---|---|
| 仅权重量化 | 权重INT8;输入和激活通常FP32 | 主要减小文件 |
| 动态范围量化 | 权重INT8;激活由运行时动态处理 | 需确认运行时支持 |
| 全整数量化 | 权重、输入和激活均使用整数路径 | 适合整数内核与MCU部署 |
二、输入输出契约随量化改变
int8输入需要把预处理后的实数按照Scale和Zero Point量化。模型输出也要反量化,或在整数域重新计算业务阈值。
量化参数来自部署模型,而非训练脚本中的经验常量。模型更新后,输入输出Scale可能变化,固件必须读取或同步新参数。
Python:要求转换器只生成全整数INT8算子
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| import tensorflow as tf converter = tf.lite.TFLiteConverter.from_saved_model("saved_model") converter.optimizations = tf.lite.Optimize.DEFAULT converter.representative_dataset = representative_dataset converter.target_spec.supported_ops = tf.lite.OpsSet.TFLITE_BUILTINS_INT8 converter.inference_input_type = tf.int8 converter.inference_output_type = tf.int8 tflite_model = converter.convert() |
三、收益要在目标固件上闭环
量化前后统一比较测试集指标、模型文件、最终Flash、Arena、推理时间和能量。模型文件缩小约四倍只是权重层面的理论关系,元数据和运行时代码不会按同比例缩小。
Cortex-M上的速度收益依赖CMSIS-NN等优化内核、算子形状和编译配置。板端测量是最终证据。
整数推理仍要表达实数尺度
量化张量由整数数组、scale和zero point共同表达。整数q对应实数r≈scale×(q-zero_point)。权重、输入、每层激活和输出可以拥有不同scale,卷积累加通常使用int32以容纳多个int8乘积。
一次整数全连接先计算 `(qx-zx)×(qw-zw)` 的int32累加,再结合输入、权重和输出尺度做requantize,最后限制到int8范围。高效内核把乘尺度转换成整数乘法与移位,避免每个元素执行浮点除法。
只有权重量化时,模型文件可能变小,输入和激活仍为浮点,MCU未必获得完整整数加速。全整数模型要求算子路径都支持INT8,并把输入输出类型明确设为int8。
|-----------------------------------------------------------------------------------|
| r = scale×(q-zero_point) int32累加:acc = Σ(qx-zx)(qw-zw) 再按 `(Sx×Sw)/Sy` 映射到输出整数。 |
量化误差集中在舍入、裁剪和异常范围
实数落到有限网格时发生舍入;超出校准范围时被裁剪到-128或127。少量极端值把scale拉大,会让大多数常见值只使用少数整数码;校准范围过窄又会在现场饱和。
比较FP32与INT8不能只看模型字节。要在同一独立测试集上比较混淆矩阵,统计输入与关键激活的饱和比例,并在目标板测Flash、Arena和延迟。
输入契约也改变。固件写入int8张量前要使用模型实际scale与zero point;直接把原始传感器整数截成int8会破坏物理尺度。输出分数需要反量化后再应用阈值,或把阈值预先映射到同一整数域。
INT8链路中的数值对象
| 对象 | 常用类型 | 检查 |
|---|---|---|
| 输入 | int8+量化参数 | 范围与饱和 |
| 权重 | int8/per-channel | 每通道scale |
| 累加 | int32 | 溢出与偏置尺度 |
| 输出 | int8或int16 | 反量化与阈值 |
动手:逐项执行一个INT8全连接层
脚本先计算浮点输出,再分别量化输入和两行权重,执行int32累加并反量化。它省略高效内核的定点requantize,但完整展示误差来自哪里。
实验环境与输入
- Python 3标准库。
- 保存为 `int8_dense.py` 并运行。
- 参数为教学示例,不代表某个已训练模型。
按顺序完成实验
- 运行并比较float与dequant输出。
- 打印输入和权重整数码。
- 把输入2.4改为20,观察饱和及误差。
- 缩小输入scale,比较分辨率与范围冲突。
可直接运行:量化、int32累加与反量化
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| def q8(value, scale, zero=0): return max(-128, min(127, round(value/scale)+zero)) x = 0.35, -0.80, 1.20, 2.40 w = \[0.50, -0.25, 0.10, 0.35, -0.40, 0.70, -0.20, 0.15] bias = 0.05, -0.10 sx, sw = 0.02, 0.005, 0.006 float_y = sum(a\*b for a, b in zip(x, row))+bias\[i for i, row in enumerate(w)] qx = q8(v, sx) for v in x dequant_y = \[\] for i, row in enumerate(w): qw = q8(v, sw\[i) for v in row] acc = sum(a*b for a, b in zip(qx, qw)) # 偏置在真实整数模型中按 sx*swi 量化为int32 restored = acc*sx*swi + biasi dequant_y.append(restored) print("row", i, "qx", qx, "qw", qw, "acc", acc) print("float :", float_y) print("int8->fp:", dequant_y) print("error :", b-a for a, b in zip(float_y, dequant_y)) |
先读懂代码中的关键路径
- qx与每行qw使用不同scale,复现输入per-tensor、权重per-channel。
- 乘积先累加为宽整数,再乘Sx×Sw恢复实数。
- 偏置保留浮点只为教学;真实模型按Sx×Sw量化为int32。
- 输入超过scale覆盖范围时q8裁剪,信息不可恢复。
你应该观察到什么
- 反量化输出接近浮点输出但不完全相等。
- 累加值超出int8范围却安全保存在Python整数,对应MCU的int32累加。
- 极端输入被裁剪后误差明显增加。
成功标准
- 能用scale和zero point完成量化与反量化。
- 能区分权重量化和全整数推理。
- 效果、资源和饱和统计同时验收。
失败时从哪里查起
INT8输出异常
| 现象 | 原因 | 检查 |
|---|---|---|
| 输出全为边界值 | 输入缩放或校准范围错 | 统计q=-128/127比例 |
| 分类索引正确但分数怪 | 输出未反量化 | 读取输出scale/zp |
| 模型仍含浮点算子 | 转换未限制INT8集合 | 检查算子列表和I/O dtype |
理解这段手工计算后,再阅读CMSIS-NN或TFLM内核中的乘法器、移位和饱和会更容易。
把实验迁移到真实MCU项目
TFLM固件直接向int8输入张量写码,量化前先执行完整物理量和标准化链。记录-128与127占比,现场饱和增多时触发输入漂移。
输出若用于阈值,可以反量化到浮点,也可以把阈值转换为整数码;两种方式必须使用输出张量自己的scale与zero point。
整数内核的最终输出还会经历定点乘法器、右移、舍入和饱和。手工浮点恢复用于理解尺度,若要逐码复现TFLM,需要从模型量化参数生成与内核相同的定点乘法器,并用边界向量验证。
还要检查类别阈值位于哪个数值域。若验证集阈值是浮点概率,固件使用整数输出时先按输出scale映射,并对阈值前后一个整数码做边界测试。
整数模型的测试向量要覆盖零点附近、正负最大常见值和饱和边界。对每条向量同时保存浮点输入、量化输入、浮点输出和整数输出,才能区分模型误差与固件量化错误。
模型文件缩小比例与固件Flash缩小比例通常不同,因为运行时和业务代码不随权重位宽等比例变化。资源报告应把两者分栏。
把结果再向前推进一步
- 计算600元素FP32与INT8输入张量的字节差异。
- 说明为什么偏置常用int32。
- 为真实输入增加饱和比例日志。
**收束:**INT8量化可能只改变权重,也可以贯穿输入、激活和输出。MCU项目需要明确使用全整数链路,并以模型检查和板端测量验证。
参考资料: