第一次在本地跑 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,无非是在问:能不能让重要的权重分到更宽的区间,让不重要的权重挤一挤?
思路简单。实现很脏。