4. 量化粒度:从粗到细的精度博弈
聊完量化原理,咱们得面对一个现实问题:到底量化到多细才算合适?
量化粒度,说白了就是「共享一个缩放因子(scale)和零点(zero point)的数据范围有多大」。范围越大,计算越省事,但精度损失也越大。范围越小,精度保得越好,但计算开销也跟着涨。
我个人习惯把量化粒度分成三个层次:逐层、逐通道、逐组。咱们一个一个来看。
4.1 逐层量化(Per-Tensor)—— 最粗犷的方式
这是最原始的量化方式。整个张量(比如一个卷积层的权重或激活值)只用一个 scale 和 zero point。
优点很明显:
- 实现简单,几乎不增加额外计算
- 硬件友好,很多老芯片只支持这种模式
缺点也很致命:
- 精度损失大。你想想看,一个 4096 维的向量,最大值和最小值可能差几十倍。用一个 scale 去覆盖整个范围,小数值的精度基本就丢了。
⚠️ 避坑指南: 我曾经在一个 7B 模型上试过纯逐层量化,结果 perplexity 直接炸了,从 8.5 飙到 15 以上。后来发现是某些 outlier channel 把整个 scale 拉大了,导致大部分数值被压缩到几个离散值上。
代码实现也很直白:
# 逐层量化示例
import torch
def per_tensor_quantize(tensor, num_bits=8):
min_val = tensor.min()
max_val = tensor.max()
scale = (max_val - min_val) / (2**num_bits - 1)
zero_point = -min_val / scale
quantized = torch.round(tensor / scale + zero_point)
quantized = torch.clamp(quantized, 0, 2**num_bits - 1)
return quantized, scale, zero_point
4.2 逐通道量化(Per-Channel)—— 工业界的主流选择
逐通道量化,就是每个输出通道单独算一个 scale 和 zero point。对于卷积层来说,就是每个卷积核有自己的量化参数。
为什么效果好?
因为不同通道的数值分布差异很大。比如一个卷积层有 64 个输出通道,有的通道数值集中在 [-1, 1],有的集中在 [-100, 100]。逐通道量化能各自适配,精度自然就上去了。
💡 我的经验: 在 LLaMA 系列模型上,逐通道量化通常能把精度损失控制在 1% 以内。我建议所有 7B 以上的模型,至少用逐通道量化。
实现上稍微复杂一点:
def per_channel_quantize(tensor, num_bits=8, dim=0):
# tensor shape: [out_channels, in_channels, ...]
min_vals = tensor.min(dim=dim, keepdim=True).values
max_vals = tensor.max(dim=dim, keepdim=True).values
scale = (max_vals - min_vals) / (2**num_bits - 1)
zero_point = -min_vals / scale
quantized = torch.round(tensor / scale + zero_point)
quantized = torch.clamp(quantized, 0, 2**num_bits - 1)
return quantized, scale, zero_point
4.3 逐组量化(Per-Group)—— 精度与效率的折中
逐组量化是最近几年才流行起来的。它把通道再细分,比如每 32 个或 64 个元素为一组,每组独立量化。
为什么需要它?
因为有些模型(比如 GPTQ、AWQ 量化后的模型)对精度极其敏感。逐通道量化还不够,得再细一点。但逐元素量化又太贵,所以就有了逐组量化这个折中方案。
🔧 实用建议: 我个人习惯在 4-bit 量化时用逐组量化,组大小设为 128。这样精度基本能追上 8-bit 逐通道量化,但内存占用直接减半。
实现上需要处理分组逻辑:
def per_group_quantize(tensor, num_bits=8, group_size=128):
# 将 tensor 重塑为 [num_groups, group_size]
orig_shape = tensor.shape
tensor_flat = tensor.flatten()
num_groups = tensor_flat.numel() // group_size
tensor_groups = tensor_flat[:num_groups * group_size].reshape(num_groups, group_size)
min_vals = tensor_groups.min(dim=1, keepdim=True).values
max_vals = tensor_groups.max(dim=1, keepdim=True).values
scale = (max_vals - min_vals) / (2**num_bits - 1)
zero_point = -min_vals / scale
quantized = torch.round(tensor_groups / scale + zero_point)
quantized = torch.clamp(quantized, 0, 2**num_bits - 1)
# 恢复原始形状
quantized = quantized.reshape(orig_shape)
return quantized, scale, zero_point
4.4 三种粒度的权衡对比
咱们用一张表来直观对比:
| 量化粒度 | 精度损失 | 计算开销 | 内存占用 | 适用场景 |
|---|---|---|---|---|
| 逐层(Per-Tensor) | 高(2-5% perplexity 上升) | 低(几乎无额外开销) | 低(只存 2 个参数) | 小模型、对精度不敏感的任务 |
| 逐通道(Per-Channel) | 中(0.5-1% perplexity 上升) | 中(每个通道多一次乘加) | 中(存 C_out 组参数) | 大多数大模型的首选 |
| 逐组(Per-Group) | 低(0.1-0.3% perplexity 上升) | 高(每组独立计算,访存压力大) | 高(存 N/group_size 组参数) | 4-bit 量化、对精度要求极高的场景 |
4.5 知识体系总览
下面这张图能帮你快速理解三种粒度的关系:
4.6 实际项目中的选择策略
说了这么多理论,到底该怎么选?我分享几个实战经验:
- 8-bit 量化:直接用逐通道量化。精度损失小,计算开销也能接受。
- 4-bit 量化:必须用逐组量化。我试过逐通道 4-bit,perplexity 直接掉了 3 个点,换成逐组 128 后只掉了 0.5 个点。
- 混合精度:有些层对精度敏感(比如 attention 的 QKV 投影),可以用更细的粒度;其他层用粗粒度。我曾在某个 13B 模型上,把 attention 层用逐组 128,FFN 层用逐通道,整体精度只损失 0.2%,但速度提升了 15%。
📌 核心结论: 量化粒度没有银弹。你得根据模型大小、目标精度、硬件能力来权衡。我的建议是:从逐通道开始,精度不够再往逐组走,永远别用逐层(除非你在做 demo 或者模型极小)。
公众号:蓝海资料掘金营,微信deep3321