第4章:量化基础——从浮点到整数的“压缩艺术”
大家好,我是你们的老朋友。今天我们来聊聊量化。
量化这个词,听起来挺高大上的。说白了,就是把模型里的浮点数(FP16、FP32)变成整数(INT8、INT4)。为什么要这么干?因为整数运算快、省显存。你想想看,一个大模型动辄几十GB,不压缩一下,消费级显卡根本跑不动。
我个人习惯把量化比作“照片压缩”。原始照片是RAW格式,细节丰富但体积大。量化就是转成JPEG,体积小了,但画质会损失一点点。我们的目标就是:在能接受的画质下,把体积压到最小。
4.1 什么是量化?
量化的数学定义很简单:
Q = round( (x - zero_point) / scale )
其中:
- x 是原始浮点值
- scale 是缩放因子
- zero_point 是零点偏移
- Q 是量化后的整数值
反量化就是逆过程:
x_approx = Q * scale + zero_point
嗯,这里要注意:量化是有损的。round操作会丢失精度。我在项目中遇到过,有些模型对精度极其敏感,量化后直接崩了。所以量化不是万能的,需要选对方法。
核心思想:用更少的比特数表示数值范围,牺牲精度换取速度和存储。
4.2 对称量化 vs 非对称量化
这是量化里最基础的选择题。我刚开始做量化时,也纠结过到底用哪个。
4.2.1 对称量化
对称量化,zero_point固定为0。公式简化成:
Q = round( x / scale )
它的特点是:量化后的整数范围是对称的。比如INT8,范围是[-128, 127]。这意味着,如果你的浮点数值分布不对称(比如全是正数),那有一半的整数范围就浪费了。
优点:计算简单,硬件友好。很多GPU的Tensor Core对对称量化有优化。
缺点:如果数据分布不对称,量化精度会下降。
我的经验:对于权重(weights),我一般用对称量化。因为训练好的权重分布通常接近对称。但对于激活值(activations),尤其是经过ReLU后的值,全是非负的,这时候对称量化就不太合适了。
4.2.2 非对称量化
非对称量化,zero_point可以不为0。公式就是最原始的那个:
Q = round( (x - zero_point) / scale )
它的好处是:可以充分利用整数范围。比如你的数据范围是[0.0, 10.0],非对称量化可以把整个INT8范围[-128, 127]都映射过去,精度更高。
优点:对数据分布适应性强,精度损失更小。
缺点:计算稍微复杂一点,需要额外处理zero_point。
| 对比项 | 对称量化 | 非对称量化 |
|---|---|---|
| zero_point | 固定为0 | 可调整 |
| 计算复杂度 | 低 | 中 |
| 硬件支持 | 优秀(Tensor Core优化) | 一般 |
| 适用场景 | 权重、对称分布数据 | 激活值、非对称分布数据 |
避坑指南:我曾经在一个项目里,对激活值用了对称量化,结果模型精度掉了5个点。后来换成非对称量化,精度只掉了0.5个点。所以,别偷懒,该用非对称就用非对称。
4.3 量化粒度:per-tensor, per-channel, per-group
量化粒度,说白了就是“量化参数(scale和zero_point)共享的范围”。粒度越细,精度越高,但计算开销也越大。
4.3.1 Per-tensor 量化
整个张量(tensor)共享一组scale和zero_point。这是最简单的量化方式。
# 伪代码示例
scale = max(abs(tensor)) / 127
quantized_tensor = round(tensor / scale)
优点:实现简单,计算量小。
缺点:如果张量内部数值分布差异大,精度损失严重。
举个例子:一个卷积层的权重,有些通道数值大,有些通道数值小。用per-tensor量化,小数值通道的细节会被“淹没”在量化噪声里。
4.3.2 Per-channel 量化
每个通道(channel)有自己的scale和zero_point。这是目前最常用的量化粒度。
# 伪代码示例
for c in range(num_channels):
scale[c] = max(abs(tensor[c])) / 127
quantized_tensor[c] = round(tensor[c] / scale[c])
优点:精度比per-tensor好很多,尤其适合卷积层。
缺点:计算量增加,但通常可以接受。
我的建议:对于权重,我习惯用per-channel量化。对于激活值,如果模型不大,也可以用per-channel。但大模型里,激活值用per-tensor更常见,因为计算量太大。
4.3.3 Per-group 量化
这是最近比较火的一种粒度。把通道分成若干组,每组共享量化参数。它是per-tensor和per-channel的折中方案。
# 伪代码示例
group_size = 128 # 每组128个元素
for g in range(0, tensor.numel(), group_size):
group = tensor.flatten()[g:g+group_size]
scale = max(abs(group)) / 127
quantized_group = round(group / scale)
优点:精度接近per-channel,但计算量更小。
缺点:实现稍微复杂,需要处理分组逻辑。
为什么per-group会火?因为大模型里,权重矩阵动辄几千行几千列。per-channel要算几千个scale,per-tensor只算1个。per-group算几十个,精度和效率的平衡点找得比较好。
| 粒度 | 精度 | 计算开销 | 适用场景 |
|---|---|---|---|
| Per-tensor | 低 | 低 | 小模型、激活值 |
| Per-channel | 高 | 中 | 权重、中等模型 |
| Per-group | 中高 | 中低 | 大模型、LLM |
4.4 量化知识体系总览
为了让大家更直观地理解,我画了一张图:
4.5 实战中的选择建议
说了这么多理论,到底怎么选?我给大家一个实战清单:
- 小模型(<1B参数):权重用per-channel对称量化,激活值用per-tensor非对称量化。
- 中等模型(1B-7B):权重用per-channel对称量化,激活值用per-channel非对称量化(如果显存够)。
- 大模型(>7B):权重用per-group对称量化(group_size=128),激活值用per-tensor非对称量化。
一个小技巧:如果你不确定选什么粒度,先从per-channel开始。精度够就留着,不够就换per-group或per-tensor。我一般这么试,省时间。
好了,这一章的内容就到这里。量化是模型部署的基石,理解清楚对称/非对称、per-tensor/per-channel/per-group的区别,后面学TensorRT-LLM的量化工具时会轻松很多。