2、深度学习模型量化基础:量化的数学原理(对称/非对称量化)、量化精度与性能的权衡、INT8与FP16对比
好,咱们正式开始聊量化。说实话,很多同学一听到「量化」就觉得是玄学,其实没那么复杂。你想想看,深度学习模型本质上就是一堆矩阵乘法,而矩阵里的数字,我们通常用 FP32 来存。但 FP32 太「胖」了,一个数占 4 个字节。我们能不能用更瘦的数据类型来存?这就是量化的核心动机。
2.1 量化的数学原理:对称 vs 非对称
量化的本质,就是做一个映射。把 FP32 的数值范围,映射到 INT8 的数值范围(-128 到 127,或者 0 到 255)。这个映射怎么做?有两种主流方案。
2.1.1 对称量化(Symmetric Quantization)
对称量化,说白了就是让零点对齐。FP32 的 0.0,映射到 INT8 的 0。正负范围是对称的。
公式很简单:
scale = max(|x_fp32|) / 127
x_int8 = round(x_fp32 / scale)
这里有个关键点:scale 是一个浮点数,也叫缩放因子。它决定了每个 INT8 步长代表多少 FP32 的值。
重要:对称量化不需要零点偏移(zero-point),实现起来更简单。硬件实现也更快。我见过很多部署框架,默认就用对称量化。
举个例子。假设你的权重范围是 [-2.0, 2.0],那么 scale = 2.0 / 127 ≈ 0.0157。FP32 的 1.0 就变成 round(1.0 / 0.0157) = 64。你看,计算很直接。
2.1.2 非对称量化(Asymmetric Quantization)
非对称量化,允许零点偏移。FP32 的 0.0 不一定映射到 INT8 的 0。它更适合那些数值分布不对称的情况,比如 ReLU 之后的激活值(全是非负数)。
公式稍微复杂一点:
scale = (max - min) / 255
zero_point = round(-min / scale)
x_int8 = round(x_fp32 / scale) + zero_point
这里 zero_point 就是零点偏移。它让量化范围可以「平移」,从而更精确地覆盖原始数据分布。
我的经验:我在项目中遇到过,对于权重,对称量化通常就够了。但对于激活值,尤其是经过 ReLU 后的值,非对称量化往往能多保留 0.5-1% 的精度。你想想看,ReLU 把所有负数截断成 0,数值分布全挤在正半轴,这时候对称量化会浪费一半的 INT8 范围(负数部分全空着)。非对称量化就能把整个 INT8 范围都利用起来。
2.2 量化精度与性能的权衡
量化不是免费的午餐。你省了内存和带宽,但可能会损失精度。这个权衡,是部署工程师每天都要面对的。
2.2.1 精度损失从哪来?
精度损失主要来自两个地方:
- 截断误差:FP32 的数值范围比 INT8 大得多。超出 INT8 范围的值,会被截断(clamp)到边界值。比如 FP32 的 3.0,如果 scale 是 0.02,那么 3.0 / 0.02 = 150,但 INT8 最大是 127,所以只能截断成 127。这 23 的差值就丢了。
- 舍入误差:量化后的值必须是整数。round 操作会引入舍入误差。比如 1.3 / 0.02 = 65,但 1.31 / 0.02 = 65.5,四舍五入后也是 65。这 0.5 的误差就丢了。
这两种误差叠加,就会导致模型输出和原始 FP32 模型有偏差。
2.2.2 性能提升从哪来?
性能提升是实打实的:
| 指标 | FP32 | INT8 | 提升比例 |
|---|---|---|---|
| 内存占用 | 4 字节/参数 | 1 字节/参数 | 4x |
| 带宽需求 | 高 | 低 | 约 4x |
| 计算吞吐 | 基础 | 2-4x | 取决于硬件 |
| 功耗 | 高 | 低 | 显著降低 |
我建议你记住这个表。面试的时候经常被问到。
注意:INT8 的加速效果高度依赖硬件。在 NVIDIA T4 上,INT8 的 Tensor Core 吞吐是 FP32 的 4 倍。但在一些老旧的 GPU 上,可能只有 2 倍甚至更少。我曾经在 Jetson Nano 上踩过坑,它的 INT8 支持有限,加速比远不如预期。
2.3 INT8 与 FP16 对比
很多同学会问:既然 FP16 也是 2 字节,INT8 是 1 字节,为什么不用 FP16?
嗯,这里要分清楚。FP16 和 INT8 是两种完全不同的数据类型。
2.3.1 数值表示范围
| 数据类型 | 位数 | 范围 | 精度 |
|---|---|---|---|
| FP32 | 32 | ~±3.4e38 | 约 7 位有效数字 |
| FP16 | 16 | ~±65504 | 约 3-4 位有效数字 |
| INT8 | 8 | -128 到 127 | 整数精度 |
FP16 虽然只有 16 位,但它是指数+尾数的结构,能表示很大的范围(最大 65504)。INT8 只能表示 256 个离散的整数值。
2.3.2 什么时候用 FP16?
FP16 适合那些对数值范围敏感、但对精度要求不极端的情况。比如:
- 训练时的混合精度训练(AMP),用 FP16 做前向和反向,用 FP32 做权重更新。
- 推理时,如果模型对量化敏感(比如一些 NLP 模型),FP16 比 INT8 更安全。
2.3.3 什么时候用 INT8?
INT8 适合那些对带宽和延迟极度敏感的场景。比如:
- 边缘设备部署(Jetson、手机等),内存和带宽都是稀缺资源。
- 高吞吐服务,比如在线推荐系统,需要同时处理大量请求。
核心对比:FP16 是「降精度但保范围」,INT8 是「降范围但保速度」。你想想看,FP16 的计算单元和 FP32 是共享的(在 Tensor Core 里),而 INT8 有独立的计算单元。所以 INT8 的吞吐可以做到 FP32 的 4 倍,而 FP16 通常只有 2 倍。
2.3.4 我的选择策略
我个人习惯这样选:
- 先试 INT8。 如果精度损失在 1% 以内,直接上 INT8。省下来的内存和带宽,可以让你部署更大的模型。
- INT8 精度不够? 试试量化感知训练(QAT)。如果 QAT 还不行,再退回到 FP16。
- FP16 是保底方案。 它比 FP32 快,比 INT8 准。但说实话,在 TensorRT 里,FP16 的加速比往往不如 INT8 明显。
避坑指南:我曾经在一个目标检测模型上,INT8 量化后 mAP 掉了 3 个点。我以为是量化方法不对,折腾了两天。后来发现是某个卷积层的权重分布太宽,导致截断误差太大。解决办法很简单:对该层单独做 per-channel 量化,而不是 per-tensor 量化。精度就回来了。
好了,这一章的内容就到这里。量化不是银弹,但它是目前部署领域最实用的技术之一。下一章我们会聊量化感知训练(QAT)的具体实现,到时候会手把手带你写代码。
公众号:蓝海资料掘金营,微信deep3321