犀牛鸟 2026 研究笔记 estelledc.github.io

Ch10: 量化入门——用精度换速度的艺术

Part 2: 移动端推理基础(从零开始) 前置章节:Ch09: 模型格式与转换 后续章节:Ch11: ncnn——零依赖的手工匠人(Part 3 开篇)


从高清照片到 JPEG

你用手机拍了一张照片,RAW 格式有 50MB。你要发微信给朋友——50MB 太大了,手机流量扛不住。于是你把它压缩成 JPEG,只有 3MB。朋友收到后放在手机屏幕上看,画质和原图几乎没有区别——日常拍摄的场景下,JPEG 压缩造成的画质损失人眼根本分辨不出来。

但如果你拍的是一张夜空星轨长曝光照片,JPEG 压缩后可能会出现明显的色阶断裂(banding)——在深色区域的渐变处出现突兀的色块。这是因为 JPEG 把色彩信息从”连续的”压缩成”离散的”,在大部分场景下这个离散化足够精细看不出差别,但在需要极其细腻的渐变过渡的场景下,精度损失就暴露了。

神经网络的量化和 JPEG 压缩本质上做的是同一件事——把高精度的连续数值映射到低精度的离散数值。fp32(32位浮点)就像 RAW 格式,范围大精度高但体积大;int8(8位整数)就像 JPEG,只有 256 个可能的值但体积只有原来的四分之一。大多数推理场景下,int8 的精度损失对最终结果的影响微乎其微——就像普通照片的 JPEG 压缩不影响观看。但在某些困难任务(如细粒度分类、语义分割的边界区域)上,精度损失可能变得明显——就像暗部的色阶断裂。


为什么量化有效

量化能有效工作,根本原因是神经网络对微小的数值扰动非常鲁棒

训练好的神经网络的权重值,如果你给每个权重加上一点点随机噪声(比如万分之一的扰动),模型的输出几乎不变。这是因为神经网络本质上是一个高度冗余的系统——几百万个参数共同决定输出,单个参数的微小偏差被其他参数”稀释”了。

量化做的就是给每个数值加上一个小的”舍入误差”——把 0.3782 舍入成 0.375,把 1.2456 舍入成 1.25。只要这个舍入误差足够小(相对于数值本身),网络的行为就基本不变。

三种精度的对比

精度 位宽 可表示的值 数值范围 体积 (相对) 计算速度 (相对)
fp32 32 bit ~40亿种 +/-3.4x10^38 1x (基准) 1x (基准)
fp16 16 bit ~65000种 +/-65504 0.5x ~2x
int8 8 bit 256种 -128 ~ +127 0.25x ~2-4x

fp32(单精度浮点)是训练的标准精度,也是推理的”安全选择”。每个数占 4 字节,可以精确表示约 40 亿个不同的值。对推理来说,这个精度通常远超实际需要。

fp16(半精度浮点)用 2 字节表示一个数,范围和精度都比 fp32 小。但对绝大多数推理任务来说够用了——模型权重和激活值很少超过 fp16 的范围。fp16 的计算速度在支持 fp16 指令的硬件上(ARM v8.2+ 的 fp16 NEON、移动 GPU)通常是 fp32 的 2 倍,因为 SIMD 寄存器可以装下双倍的数据(Ch06 讲过:128-bit 寄存器装 4 个 fp32 或 8 个 fp16)。

int8(8位整数)用 1 字节表示一个数,只有 256 个可能的值。这是量化的主要目标精度。int8 的计算速度可以达到 fp32 的 2-4 倍(NEON 的 vdotq_s32 指令每次处理 16 个 int8 乘加),模型体积缩小到原来的 25%。代价是精度损失——但在大多数场景下可以接受。

量化带来的三重收益

量化不仅仅是”用更少的位数表示数字”。它在三个维度上都带来了收益。

第一,模型体积缩小。一个 25.6M 参数的 ResNet-50 模型,fp32 时约 102MB,int8 时只有约 25.6MB。对手机来说,安装包小 75% 意味着用户下载速度更快、占用存储更少。

第二,内存占用降低。不仅权重占内存少了,中间激活值也可以用 int8 存储。Ch08 讲过,内存是移动端最紧迫的约束——量化直接缓解了这个压力。

第三,计算速度提升。这一点需要硬件支持。ARM NEON 的 int8 并行度是 fp32 的 4 倍(128-bit 寄存器装 16 个 int8 vs 4 个 fp32),计算吞吐量直接翻 4 倍。加上 ARMv8.2 的 vdotq_s32(4路 int8 点积)指令,int8 卷积的理论速度可以达到 fp32 的 4 倍以上。实际加速比通常在 2-4 倍之间,取决于具体模型和硬件。


量化的数学原理

量化的数学核心是一个线性映射——把浮点数的连续范围映射到整数的离散范围。

基本公式

浮点值 = scale x (整数值 - zero_point)

或者反过来:

整数值 = round(浮点值 / scale) + zero_point

其中 scale(缩放因子)和 zero_point(零点偏移)是量化参数,它们决定了浮点和整数之间的映射关系。确定了这两个参数,就确定了量化方案。

用一个具体的例子来理解。假设我们要把范围 [-1.0, 1.0] 内的浮点数量化到 int8(范围 [-128, 127]):

scale = (1.0 - (-1.0)) / (127 - (-128)) = 2.0 / 255 = 0.00784
zero_point = 0(对称量化,后面会解释)

量化过程:
  浮点值 0.5  -> round(0.5 / 0.00784) + 0 = round(63.8) = 64
  浮点值 -0.3 -> round(-0.3 / 0.00784) + 0 = round(-38.3) = -38
  浮点值 0.0  -> round(0.0 / 0.00784) + 0 = 0

反量化过程:
  整数值 64  -> 0.00784 x (64 - 0) = 0.50176  (误差 0.00176)
  整数值 -38 -> 0.00784 x (-38 - 0) = -0.29792 (误差 0.00208)

可以看到,量化再反量化后的值和原始值有微小的差别——这就是量化误差。误差的大小取决于 scale 的值:scale 越小,精度越高,但可表示的范围也越小。

对称量化 vs 非对称量化

对称量化:zero_point = 0。浮点值的零被精确映射到整数的零。这意味着量化范围关于零对称:[-max, +max] 映射到 [-128, +127]。好处是计算更简单(不需要处理 zero_point),坏处是如果数据分布不对称(比如 ReLU 的输出全是非负数),会浪费一半的表示范围。

对称量化: [-max_val, +max_val] -> [-128, +127]
  scale = max_val / 127
  zero_point = 0

非对称量化:zero_point 不为零。可以更精确地匹配数据的实际分布范围。比如 ReLU 输出的范围是 [0, max_val],非对称量化会把 0 映射到 -128,max_val 映射到 127——充分利用了 int8 的全部 256 个值。

非对称量化: [min_val, max_val] -> [-128, +127]
  scale = (max_val - min_val) / 255
  zero_point = round(-128 - min_val / scale)

实际中,权重通常使用对称量化(权重分布通常接近零对称),激活值(特别是 ReLU 后)通常使用非对称量化。

量化计算的实际过程

量化后的卷积运算完全在整数域进行——这就是速度提升的来源。简化流程如下:

输入激活 (fp32) -> 量化 -> int8 激活
                            |
权重 (fp32) -> 预量化 -> int8 权重
                            |
int8 激活 x int8 权重 = int32 累加结果    <-- 整数乘法,硬件快
                            |
int32 结果 x (scale_input x scale_weight) -> fp32 输出
                            |
                     下一层的输入

关键点:乘法和累加都在整数域完成(int8 x int8 = int16,累加到 int32),只在最后一步用一次浮点乘法把结果缩放回浮点范围。这一次浮点运算的开销和整个卷积的几百万次乘加相比微不足道。


两种量化方式

确定 scale 和 zero_point 有两种截然不同的方式,对应两种量化流程。

PTQ(Post-Training Quantization,训练后量化)

PTQ 不需要重新训练模型——在训练好的 fp32 模型上直接进行。流程是:

  1. 准备一个小的”校准数据集”(calibration dataset),通常几十到几百张代表性图片就够
  2. 用 fp32 模型对校准数据集做前向传播,收集每一层激活值的统计分布(最大值、最小值、直方图)
  3. 根据统计分布,为每一层确定最优的 scale 和 zero_point
  4. 把 fp32 权重用确定好的 scale 量化为 int8 权重
  5. 导出量化后的模型

PTQ 的最大优势是简单快速——不需要训练环境,不需要训练数据(校准数据集可以很小),不需要 GPU,几分钟就能完成。所有六个推理引擎都支持 PTQ。

PTQ 的劣势是对量化敏感的模型(如非常小的模型、低比特量化)可能有明显的精度下降。原因是 PTQ 完全基于统计信息来确定量化范围——如果校准数据不够代表性,确定的范围可能不是最优的。

QAT(Quantization-Aware Training,量化感知训练)

QAT 在训练过程中”模拟”量化的效果——在前向传播时插入模拟量化节点(fake quantization),让网络在训练时就”适应”量化带来的噪声。反向传播时用直通估计器(Straight-Through Estimator, STE)绕过量化节点的不可导问题。

普通训练: input -> conv -> relu -> output
                                    | 反向传播

QAT训练:  input -> [量化->反量化] -> conv -> [量化->反量化] -> relu -> output
                                                                 | 反向传播(STE 绕过量化节点)

QAT 的优势是精度更高——网络在训练时已经学会了”在量化噪声下也能给出好的输出”。对于精度敏感的任务(如人脸识别、医学图像),QAT 通常比 PTQ 好 1-3 个百分点的精度。

QAT 的劣势是需要完整的训练环境和训练数据,需要几个小时甚至几天的额外训练。对于”拿到一个开源模型想快速部署”的场景,QAT 的门槛太高。

方式 需要训练? 需要训练数据? 耗时 精度
PTQ 只需小量校准数据 分钟级 够用(大多数场景)
QAT 需要完整训练集 小时-天 最优

实际建议:先试 PTQ,如果精度不满足要求再上 QAT。大多数情况下 PTQ 的精度已经够用。


校准(Calibration)过程

PTQ 的核心步骤是校准——确定每一层的 scale 和 zero_point。校准的关键是准确估计每一层激活值的分布范围。

三种校准方法

MinMax 校准:直接取校准数据集上观察到的最小值和最大值作为量化范围。

激活值: [0.1, 0.3, 0.8, 2.1, 0.5, 1.2, 0.9, 3.7]
min = 0.1, max = 3.7
scale = (3.7 - 0.1) / 255 = 0.0141

优点:简单。缺点:容易受异常值(outlier)影响——如果有一个激活值是 100.0(异常大),整个量化范围就被拉到 [0.1, 100.0],大部分正常值(0-4 之间)只能用很少的整数值表示,精度严重下降。

KL 散度校准(Entropy Calibration):通过计算 fp32 分布和 int8 分布之间的 KL 散度(Kullback-Leibler divergence)来找到最优的截断阈值。基本思路是:允许截掉一小部分极端值(少量溢出),换取中间大部分值的更高精度。

激活值直方图:
  |
  |    ####
  |  ########
  |############
  |################
  +--------------------> 值
   0  1  2  3  4  100  (100是异常值)

KL校准: 截断到 [0, 4],丢弃100这个异常值
  好处: 0-4之间的值获得更高精度
  代价: 超过4的值被截断到4

NVIDIA 的 TensorRT 率先使用了 KL 散度校准,效果通常优于 MinMax。ncnn 的 ncnn2table 工具也支持这种方法。

Percentile 校准:取分布的某个百分位数(如 99.99%)作为截断阈值——忽略最极端的 0.01% 的值。比 MinMax 鲁棒,比 KL 散度简单。

ncnn 的校准工具:ncnn2table

ncnn 提供了 ncnn2table 工具来执行 PTQ 校准。使用流程如下:

# Step 1: 准备校准图像列表
find calibration_images/ -name "*.jpg" > image_list.txt

# Step 2: 运行校准
ncnn2table \
    model.param model.bin \
    image_list.txt \
    model.table \
    mean=[104,117,123] \
    norm=[1,1,1] \
    shape=[224,224,3] \
    thread=4

# Step 3: 使用量化表进行推理(C++ 代码中)
# ncnn::Net net;
# net.opt.use_int8_inference = true;
# net.load_param("model.param");
# net.load_model("model.bin");
# // 加载量化校准表即可启用 INT8 推理

ncnn2table 的输出是一个 .table 文件,包含每一层的 scale 值。推理时 ncnn 读取这个表,在运行时动态完成量化和反量化。


混合精度量化

并非模型中的每一层都同样适合量化。某些层对量化非常敏感——量化后精度显著下降;另一些层则很鲁棒——量化后几乎无影响。

混合精度量化(Mixed Precision Quantization)的思路是:敏感层保留高精度(fp32 或 fp16),不敏感层使用低精度(int8)

模型的各层量化敏感度:
  Conv1:  低  -> int8  (体积大、计算量大、不敏感 -> 最大收益)
  Conv2:  低  -> int8
  Conv3:  中  -> int8  (还能接受)
  Conv4:  高  -> fp16  (量化后精度掉太多 -> 保留高精度)
  FC:     低  -> int8

混合精度: 大部分层 int8 获得速度收益,少数敏感层 fp16 保住精度

怎么判断一层是否敏感?最直接的方法是逐层量化测试:每次只量化一层(其他层保持 fp32),然后在验证集上测精度。精度下降最多的那些层就是敏感层。

TNN 的 NC8HW8 布局是专门为 fp16 推理设计的——把 8 个 channel 打包在一起(而非 NC4HW4 的 4 个),恰好匹配 fp16 在 128-bit NEON 寄存器中的容量(128 / 16 = 8)。这种设计让 TNN 在混合精度推理中可以为 fp16 层使用最优的数据布局。


量化在六个引擎中的支持

六个引擎对量化的支持范围和深度各不相同。

引擎 PTQ QAT fp16 INT8 INT4 特色
ncnn 支持 - 支持 支持 - ncnn2table 校准工具,elempack 自适应
MNN 支持 支持 支持 支持 支持 端侧训练 + QAT,LLM INT4 推理
ORT 支持 支持 支持 支持 支持 丰富的量化工具链,onnxruntime-quantization
TNN 支持 - 支持 (NC8HW8) 支持 - fp16 专用 NC8HW8 布局
Paddle-Lite 支持 支持 支持 支持 - PaddleSlim 量化训练框架
MACE 支持 - 支持 支持 - Hexagon DSP INT8 整图下推

几个值得注意的细节:

ncnn 的量化流程是最清晰简洁的。ncnn2table 校准 -> .table 文件 -> 推理时加载。没有复杂的配置选项——符合 ncnn “极简”的设计哲学。

MNN 的量化能力最全面。除了标准 PTQ,MNN 还支持 QAT(通过端侧训练功能实现)和 INT4 量化(主要用于 LLM 推理)。MNN 的量化工具集成在 MNNConvert 中,可以在模型转换的同时完成量化。

TNN 的 NC8HW8是一个有趣的设计——这是专门为 fp16 推理设计的数据布局。128-bit 的 NEON 寄存器可以装 8 个 fp16 值(而非 4 个 fp32),所以 TNN 把 channel 按 8 个一组打包(NC8HW8),让 fp16 SIMD 利用率达到 100%。这和 ncnn 的 elempack 思路一致——根据数据精度调整打包数——但 TNN 是固定选择而非动态调整。

MACE 的 Hexagon 量化走的是完全不同的路线。MACE 把整个量化后的模型下推到 Hexagon DSP 执行——DSP 的 HVX 指令集原生支持 INT8 运算,每条指令可以同时处理 128 个 int8 值(1024-bit 向量宽度)。这种”整图下推”模式让量化推理的效率比逐层在 CPU 上做 INT8 更高。

Paddle-Lite 的 PaddleSlim 提供了完整的 QAT 工具链——在 PaddlePaddle 训练框架中做量化感知训练,然后通过 opt 工具导出量化后的 .nb 模型。整个流程在百度的生态内闭环完成。


实际效果:YOLOv8n fp32 vs INT8

用具体数字来感受量化的实际收益。以下数据综合各引擎公开 benchmark 的典型值(实际表现因设备和实现而异)。

指标 fp32 fp16 INT8 (PTQ) INT8 (QAT)
模型体积 ~12.8 MB ~6.4 MB ~3.2 MB ~3.2 MB
推理延迟 (ARM CPU) ~80 ms ~45 ms ~25 ms ~25 ms
mAP (COCO val) 37.3% ~37.2% ~36.5% ~36.9%
峰值内存 ~30 MB ~18 MB ~10 MB ~10 MB

几个关键观察:

fp16 几乎无损。mAP 从 37.3% 降到 ~37.2%——0.1% 的差异在测试误差范围内,可以认为没有精度损失。推理速度提升约 1.8 倍,模型体积减半。如果你的硬件支持 fp16(大多数现代 ARM 都支持),fp16 是一个”没有理由不用”的优化。

INT8 PTQ 有可接受的精度损失。mAP 从 37.3% 降到 ~36.5%——0.8% 的下降在大多数应用场景中可以接受。速度提升约 3.2 倍,体积和内存占用都降到原来的四分之一。

QAT 收回了大部分精度。mAP 从 36.5%(PTQ)提升到 ~36.9%——通过额外的训练,精度损失从 0.8% 收窄到 0.4%。代价是需要重新训练。

LLM 场景更极端。大语言模型的量化通常更激进——INT4 甚至 INT3 量化。LLaMA-7B 从 fp16(14GB)量化到 INT4(3.5GB)后,才能在手机上运行。INT4 的精度损失比 INT8 大,但对于对话类应用,用户通常不太敏感。


何时该用/不该用量化

量化不是万能药。以下是一些实际的决策建议。

推荐使用量化的场景

部署到移动端/嵌入式设备时——这是量化最主要的应用场景。体积和内存的缩减对移动端意义重大。几乎所有移动端部署都应该至少使用 fp16。

模型需要实时响应时——比如实时目标检测(要求 >30fps)、相机场景识别、AR 特效。INT8 量化的 2-4 倍加速可能意味着从”勉强可用”到”流畅体验”的质变。

边缘部署资源受限时——IoT 设备、无人机、车载系统等场景,设备算力和内存都很有限,量化是必备技术。

不推荐或谨慎使用的场景

精度极度敏感的任务——医疗影像诊断、金融风控等,1% 的精度下降可能带来严重后果。这类场景建议先用 fp16(精度损失极小),INT8 需要充分验证。

模型已经很小时——如果模型权重只有 1-2MB,量化到 INT8 省的那几百 KB 意义不大,反而引入了量化/反量化的额外开销和精度损失。

训练数据难以获取校准集时——PTQ 需要几百张代表性输入图片做校准。如果你的应用场景特殊(比如工业缺陷检测),难以收集足够多样的校准数据,量化效果可能不理想。

推荐的量化决策路径

1. 首先尝试 fp16 推理 -> 几乎无精度损失,简单有效
2. 如果 fp16 速度不够 -> 尝试 PTQ INT8
3. 评估 INT8 精度 -> 如果可接受就完成
4. 如果 INT8 精度不够 -> 尝试混合精度(敏感层保留 fp16)
5. 如果混合精度仍不够 -> 尝试 QAT(需要训练资源)
6. 如果以上都不满足需求 -> 考虑换更小的模型架构(如 MobileNet 系列)

量化的未来方向

量化技术还在快速演进。几个值得关注的方向:

更低比特量化(INT4/INT2):LLM 的端侧部署推动了 INT4 甚至更低比特量化的发展。GPTQ、AWQ 等方法在 INT4 精度下仍能保持可接受的对话质量。MNN 已经支持 INT4 量化推理。

动态量化:不是对所有输入使用固定的 scale,而是根据每次推理的实际输入动态计算 scale。好处是对不同输入分布都能保持较高精度,代价是需要运行时计算 scale(额外开销)。

硬件原生支持:越来越多的移动 SoC 在硬件层面支持 int8/int4 运算。ARMv9 的 SME(Scalable Matrix Extension)、高通的 Hexagon V73+ 都在向更低比特的原生支持演进。硬件进步会让量化的”速度收益/精度代价”比值越来越好。


本章小结

量化是移动端推理的核心优化技术,本质是用更少的比特位来表示数据,换取更小的体积、更快的速度和更低的内存占用。其数学基础是线性映射:float_value = scale x (int_value - zero_point)

两种主要量化方式:PTQ(训练后量化)简单快速但精度有一定损失,适合大多数场景;QAT(量化感知训练)在训练时模拟量化效果,精度更高但需要训练资源。校准过程是 PTQ 的关键——通过分析激活值分布来确定最优的 scale 和 zero_point。

fp16 推理是”免费午餐”——几乎不损失精度但速度翻倍,应该是移动端推理的默认选择。INT8 量化收益显著(体积减 75%、速度提升 2-4 倍)但有 1-3% 的精度损失,需要根据场景评估。混合精度量化是精度和速度之间的折中方案。

六个引擎都支持 INT8 量化,MNN 额外支持 QAT 和 INT4,TNN 有专门为 fp16 设计的 NC8HW8 布局。ncnn 的 ncnn2table 工具提供了最简洁的 PTQ 工作流程。

Part 2 的七章基础概念到此结束。你现在已经理解了推理的本质(Ch04)、移动端约束(Ch05)、CPU SIMD 优化(Ch06)、GPU 计算(Ch07)、内存管理(Ch08)、模型格式(Ch09)和量化技术(Ch10)。这些基础知识是阅读 Part 3(六大框架精讲)和 Part 4(核心抽象对比)的前提——后续章节的很多设计决策都是在回应本 Part 讲述的这些约束和优化方向。


导读目录:README.md 上一章:Ch09: 模型格式与转换——从 PyTorch 到手机的桥梁 下一章:Ch11: ncnn——零依赖的手工匠人