B 当你把模型从16位砍到4位

当你把模型从16位砍到4位

May 19, 2025
ai

第一次在本地跑 Llama 的时候,下载页面那一排 Q4_0、Q4_K_M、Q5_1 让我愣了半天。

选了最大的文件,跑不起来。显存不够。选了最小的文件,跑起来了,但回答像喝了假酒。后来才知道,这一排名字背后是一整套妥协方案——在模型的"胖瘦"和"聪明程度"之间找平衡。

这篇文章讲最早的那套方案:legacy quants。GGUF 现在已经有 K-quants 和 I-quants 了,但 legacy 是地基。搞懂了它,后面那些花里胡哨的变体无非是在这个地基上换砖头。

FP16 和 INT4

在聊量化之前,先搞懂两样东西:模型权重长什么样,以及我们想把它变成什么样。

FP16 是半精度浮点数。16 个 bit(2 个字节),存一个带小数点的数。C 语言里没有 FP16,但你想象一个比 float(32 位)更省空间的浮点数就行。大模型的权重——那些矩阵里的每一个数字——出厂的时候就是 FP16。一个 70 亿参数的模型,70 亿个 FP16,也就是 14 GB。不大不小,刚好塞不进一张 12 GB 的消费级显卡。

INT4 是 4 位整数。4 个 bit,能表示 $2^4 = 16$ 个不同的值。如果你只写过 C,你熟悉的整数是 int(32 位)、short(16 位)、char(8 位)。4 位的整数在 C 里没有——太小了,小到没法单独寻址,必须打包存储。

打个比方:FP16 像女生的口红色号——豆沙、枫叶、烂番茄,每一格都有名字。INT4 是男人的衣柜——黑、白、灰、深蓝,能数的就那几种。

量化的核心问题就是:怎么用男人的衣柜里那几种颜色,还原出口红色号的全部渐变?

这是有损压缩。丢掉的信息永远捡不回来。

假装天平是平的

GGUF 的量化不是把每个 FP16 单独转成 INT4。那样做的话,每个权重需要一个 scale(缩放系数)来告诉你怎么"还原",而 scale 本身就是 FP16——16 位。用 16 位存 scale 去压缩一个 16 位的权重?没有意义。

GGUF 的做法是把一组权重打包,共享一个 scale。这一组叫一个 block。我们先用一个 10 个权重的小 block 来走一遍完整的计算——把 block 设这么小是为了手算方便,实际 GGUF 的 block size 是 32。

假设某层神经网络里有这么 10 个权重(FP16):

$$ \mathbf{w} = [-0.8,\ 0.3,\ 1.5,\ -0.2,\ -1.0,\ 0.7,\ -0.5,\ 2.0,\ -1.2,\ 0.1] $$

Type 0 叫对称量化。它假设权重的分布是围绕 0 对称的:最小值是 $-d$,最大值是 $+d$。

第一步,找出 $d$。这个 block 里绝对值最大的数是 2.0,所以 $d = 2.0$。对称区间就是 $[-2.0,\ 2.0]$。

第二步,确定 INT4 的桶。4 位能表示 16 个整数——但 Type 0 故意扔掉最小的那个桶,只用 15 个($-7$ 到 $+7$)。为什么?因为 15 是奇数,正中间的那个桶 $0$ 当零点,左右各 7 个桶对称分布。

第三步,算 scale。scale 的意思是:原来 FP16 区间里的"1 个单位",对应 INT4 区间里的多少个单位。

$$ S = \frac{\beta - \alpha}{\text{max\_int} - \text{min\_int}} = \frac{2.0 - (-2.0)}{7 - (-7)} = \frac{4}{14} = \frac{2}{7} \approx 0.286 $$

读法:FP16 每变动 $0.286$,INT4 就跳一格。

第四步,逐一把每个权重"扔进"最近的桶。量化函数很简单:

$$ \text{quant}(r) = \text{round}\left(\frac{r}{S}\right) $$
原始值 $r$$r / S$$\text{round}(r/S)$
$-0.8$$-0.8 / 0.286 = -2.80$$-3$
$0.3$$0.3 / 0.286 = 1.05$$1$
$1.5$$1.5 / 0.286 = 5.25$$5$
$-0.2$$-0.2 / 0.286 = -0.70$$-1$
$-1.0$$-1.0 / 0.286 = -3.50$$-4$
$0.7$$0.7 / 0.286 = 2.45$$2$
$-0.5$$-0.5 / 0.286 = -1.75$$-2$
$2.0$$2.0 / 0.286 = 7.00$$7$
$-1.2$$-1.2 / 0.286 = -4.20$$-4$
$0.1$$0.1 / 0.286 = 0.35$$0$

第五步,“还原”。使用时把 INT4 乘以 scale:

$$ \tilde{r} = \text{quant}(r) \times S $$
$\text{quant}$$\tilde{r}$原始 $r$绝对误差
$-3$$-3 \times 0.286 = -0.857$$-0.8$$0.057$
$1$$1 \times 0.286 = 0.286$$0.3$$0.014$
$5$$5 \times 0.286 = 1.429$$1.5$$0.071$
$-1$$-1 \times 0.286 = -0.286$$-0.2$$0.086$
$-4$$-4 \times 0.286 = -1.143$$-1.0$$0.143$
$2$$2 \times 0.286 = 0.571$$0.7$$0.129$
$-2$$-2 \times 0.286 = -0.571$$-0.5$$0.071$
$7$$7 \times 0.286 = 2.000$$2.0$$0.000$
$-4$$-4 \times 0.286 = -1.143$$-1.2$$0.057$
$0$$0 \times 0.286 = 0.000$$0.1$$0.100$

平均绝对误差约 $0.073$。

存储方面:10 个 INT4 权重(40 bit)+ 1 个 FP16 scale(16 bit)= 56 bit。原始 10 个 FP16(160 bit)→ 压缩到 35%。

看起来不错。但这个例子里藏了一个问题。

注意看我们的原始权重:最小值 $-1.2$,最大值 $2.0$。分布是偏的——正向的权重更多、更大。但 Type 0 强行把区间对称化为 $[-2.0,\ 2.0]$。在 $[-2.0,\ -1.2]$ 这一段,并没有任何原始权重落在这里,但系统仍然为它分配了桶。

想象一条数轴:

$$ \underbrace{[-2.0 \quad\cdots\quad -1.2]}_{\text{空着的桶}}\quad [-1.2 \quad\cdots\quad 0 \quad\cdots\quad 2.0] $$

对称量化以为左边会有跟右边一样多的权重,结果左边空了一大截,右边的桶又不够细。

这就像给你的衣柜分配空间——左边归冬天衣服,右边归夏天衣服。结果你冬天只有两件外套,左边的柜子空了一半,夏天的 T 恤却挤得不行。

这就是对称问题。

承认天平是歪的

Type 1 用的是非对称量化。不假装权重对称于零了——实际的最小值就是最小值,实际的最大值就是最大值。区间是 $[-1.2,\ 2.0]$,不是 $[-2.0,\ 2.0]$。

而且 Type 1 不扔桶。16 个桶全部用上:$-8$ 到 $+7$。

$S$ 更精细了——因为区间更窄,但桶数更多:

$$ S = \frac{\beta - \alpha}{\text{max\_int} - \text{min\_int}} = \frac{2.0 - (-1.2)}{7 - (-8)} = \frac{3.2}{15} = \frac{16}{75} \approx 0.213 $$

多了一个关键参数:零点 $Z$。$Z$ 回答的问题是:原始 FP16 的 $0$,应该被映射到 INT4 的哪个桶?

如果区间是完美对称的,零自然落在桶 $0$。但区间不对齐,零就会偏。$Z$ 的公式来自简单的线性映射:

$$ Z = \text{round}\!\left(-\frac{\alpha}{S}\right) + \text{min\_int} = \text{round}\!\left(\frac{1.2}{0.213}\right) + (-8) = 6 - 8 = -2 $$

所以零被映射到桶 $-2$——不是 $0$。

量化函数也多了一步平移:

$$ \text{quant}(r) = Z + \text{round}\!\left(\frac{r}{S}\right) $$
原始值 $r$$r / S$$\text{round}(r/S)$$\text{quant} = Z + \text{round}$
$-0.8$$-0.8 / 0.213 = -3.75$$-4$$-6$
$0.3$$0.3 / 0.213 = 1.41$$1$$-1$
$1.5$$1.5 / 0.213 = 7.03$$7$$5$
$-0.2$$-0.2 / 0.213 = -0.94$$-1$$-3$
$-1.0$$-1.0 / 0.213 = -4.69$$-5$$-7$
$0.7$$0.7 / 0.213 = 3.28$$3$$1$
$-0.5$$-0.5 / 0.213 = -2.34$$-2$$-4$
$2.0$$2.0 / 0.213 = 9.38$$9$$7$
$-1.2$$-1.2 / 0.213 = -5.63$$-6$$-8$
$0.1$$0.1 / 0.213 = 0.47$$0$$-2$

还原时先减 $Z$ 再乘 $S$:

$$ \tilde{r} = (\text{quant}(r) - Z) \times S $$
$\text{quant}$$\tilde{r}$原始 $r$绝对误差
$-6$$(-6 + 2) \times 0.213 = -0.853$$-0.8$$0.053$
$-1$$(-1 + 2) \times 0.213 = 0.213$$0.3$$0.087$
$5$$(5 + 2) \times 0.213 = 1.493$$1.5$$0.007$
$-3$$(-3 + 2) \times 0.213 = -0.213$$-0.2$$0.013$
$-7$$(-7 + 2) \times 0.213 = -1.067$$-1.0$$0.067$
$1$$(1 + 2) \times 0.213 = 0.640$$0.7$$0.060$
$-4$$(-4 + 2) \times 0.213 = -0.427$$-0.5$$0.073$
$7$$(7 + 2) \times 0.213 = 1.920$$2.0$$0.080$
$-8$$(-8 + 2) \times 0.213 = -1.280$$-1.2$$0.080$
$-2$$(-2 + 2) \times 0.213 = 0.000$$0.1$$0.100$

平均绝对误差约 $0.062$,比 Type 0 的 $0.073$ 低了 15%。

看一看桶的使用情况就清楚了。Type 0 用 15 个桶覆盖 $[-2.0,\ 2.0]$,每格宽 $0.286$,有 3 个桶落在空区 $[-2.0,\ -1.2]$。Type 1 用 16 个桶覆盖 $[-1.2,\ 2.0]$,每格宽 $0.213$——桶更密,还全部用在刀刃上。

代价是存储:Type 1 多存一个 $\alpha$(也叫 min 或 offset)。10 个权重的情况下,存储是 40 bit(权重)+ 16 bit(scale)+ 16 bit($\alpha$)= 72 bit,压缩率从 35% 掉到 45%。block size 到 32 时,这点额外开销就显得微不足道了——常量开销只多 16 bit,但精度提升 15%,划算。

块有多大,精度就有多细

你可能注意到了一个关键点:scale 和 $\alpha$ 是共享的。

block size 就是一个旋钮。拧大一点(比如 64),常量开销摊得更薄,但 block 里的权重分布差异更大,用一个 scale 去覆盖可能顾此失彼。拧小一点(比如 16),精度更高,但常量占比变大,压缩效果打折扣。

GGUF 的 legacy quants 用 block size 32。Q4_0 的 block 结构:32 个 INT4 权重 + 1 个 FP16 scale。

$$ \text{Q4\_0 每权重} = \frac{32 \times 4 + 16}{32} = 4.5\ \text{bit} $$

Q4_1 多一个 FP16 的 $\alpha$:

$$ \text{Q4\_1 每权重} = \frac{32 \times 4 + 32}{32} = 5\ \text{bit} $$

放大了看,一个 160 亿参数的模型用 Q4_0 量化后,量化常量(scale 和 $\alpha$)额外吃掉大约 2 GB——这是在量化权重之上的额外开销。

顺便说一句,Q5_0、Q8_0 和 Q5_1、Q8_1 就是同理的——只是把 4 换成 5 或 8,桶更多,精度更高,文件更大。

所以选哪个

Type 0 和 Type 1 没有谁绝对更好。取决于你的权重分布。

如果你的权重真的比较对称——比如某些归一化层——Type 0 用更少的存储拿到差不多的精度。如果权重分布明显偏斜——比如 transformer 的 feed-forward 层——Type 1 多花一点存储换精度是值的。

现实是大部分权重其实不太对称。所以 GGUF 后来的 K-quants 和 I-quants 在 legacy 的基础上做了更聪明的块划分和重要性加权——但那又是另一个故事了。

理解 legacy 的要点不在记住公式,而在抓住那个本质问题:你只有 $N$ 个桶,要装下连续空间里的一堆点。桶怎么摆,决定了哪些信息被留下,哪些被抛弃。

你可以把它类比成采样定理。奈奎斯特频率告诉你至少需要多密的采样点才能不失真。量化没有这种保证——你注定失真。你能控制的只是误差的分布方式。

搞懂了这一点,后面看 K-quants 的 super-block 和 I-quants 的 importance matrix,无非是在问:能不能让重要的权重分到更宽的区间,让不重要的权重挤一挤?

思路简单。实现很脏。


参考资料

TouchingFish.top