上次算到,160 亿参数的模型用 Q4_0 量化后,光 scale 和 $\alpha$ 这些"说明书"就要吃掉 2 GB。
2 GB 不小。一台 16 GB 显存的显卡,模型权重压缩后大概 8 GB,结果说明书自己占了四分之一。这就像你去宜家买了一张桌子,包装盒里一半是螺丝和安装图纸。
K-quants 要解决的就是这个问题。思路很直白——既然权重可以量化,那说明书也可以量化。
套娃
K-quants 的核心结构叫 super-block。做法是把 8 个普通 block 打包成一组,然后对 8 个 scale 再做一次量化——从 FP16(16 bit)砍到 INT8(8 bit):
super-block: 256 个 INT4 权重
├── block 0: scale = 0.10 (FP16 → INT8)
├── block 1: scale = 0.14 (FP16 → INT8)
├── block 2: scale = 0.11 (FP16 → INT8)
├── block 3: scale = 0.18 (FP16 → INT8)
├── block 4: scale = 0.08 (FP16 → INT8)
├── block 5: scale = 0.16 (FP16 → INT8)
├── block 6: scale = 0.13 (FP16 → INT8)
├── block 7: scale = 0.09 (FP16 → INT8)
└── super-scale = 0.00142 (FP16) ← 新加的,用来还原上面那些 INT8
来走一遍完整的计算。假设这 8 个 block 里 256 个权重已经量化完了——每个 block 的 scale 是上面那 8 个 FP16 值。现在的任务是把这 8 个 scale 自己也量化掉。
第一步,把这 8 个 FP16 scale 当作 8 个"小权重"。最大值 $d = 0.18$,对称区间就是 $[-0.18,\ 0.18]$。
第二步,确定 INT8 的桶。INT8 有 256 个桶——跟 Type 0 一样的对称套路,扔掉最左边的一个,用 $-127$ 到 $+127$,共 254 个桶。正中间 $0$ 当零点。
第三步,算这一层(meta 层)的 scale——我们就叫它 super-scale:
$$ S_{\text{meta}} = \frac{0.18 - (-0.18)}{127 - (-127)} = \frac{0.36}{254} \approx 0.00142 $$第四步,把 8 个 scale 逐一量化:
| scale(FP16) | $s / S_{\text{meta}}$ | $\text{round}$ → INT8 |
|---|---|---|
| $0.10$ | $0.10 / 0.00142 = 70.4$ | $70$ |
| $0.14$ | $0.14 / 0.00142 = 98.6$ | $99$ |
| $0.11$ | $0.11 / 0.00142 = 77.5$ | $78$ |
| $0.18$ | $0.18 / 0.00142 = 126.8$ | $127$ |
| $0.08$ | $0.08 / 0.00142 = 56.3$ | $56$ |
| $0.16$ | $0.16 / 0.00142 = 112.7$ | $113$ |
| $0.13$ | $0.13 / 0.00142 = 91.5$ | $92$ |
| $0.09$ | $0.09 / 0.00142 = 63.4$ | $63$ |
第五步,验证还原精度。使用时把 INT8 乘以 $S_{\text{meta}}$:
| INT8 | $\tilde{s}$ | 原始 scale | 误差 |
|---|---|---|---|
| $70$ | $70 \times 0.00142 = 0.0994$ | $0.10$ | $0.0006$ |
| $99$ | $99 \times 0.00142 = 0.1406$ | $0.14$ | $0.0006$ |
| $78$ | $78 \times 0.00142 = 0.1108$ | $0.11$ | $0.0008$ |
| $127$ | $127 \times 0.00142 = 0.1803$ | $0.18$ | $0.0003$ |
| $56$ | $56 \times 0.00142 = 0.0795$ | $0.08$ | $0.0005$ |
| $113$ | $113 \times 0.00142 = 0.1605$ | $0.16$ | $0.0005$ |
| $92$ | $92 \times 0.00142 = 0.1306$ | $0.13$ | $0.0006$ |
| $63$ | $63 \times 0.00142 = 0.0895$ | $0.09$ | $0.0005$ |
误差在 $0.0003$ 到 $0.0008$ 之间——非常小,因为这些 scale 本身就小。
看看存储省了多少。这 8 个 scale,量化前后:
| 8 个 scale | super-scale | 合计 | |
|---|---|---|---|
| 量化前 | $8 \times 16 = 128\ \text{bit}$ | — | 128 bit |
| 量化后 | $8 \times 8 = 64\ \text{bit}$ | 16 bit | 80 bit |
每个 super-block(256 个权重)省了 48 bit。一个 block 有 32 个权重——上面这 256 个权重如果还用 legacy 方式,per-weight 是 $4 + 16/32 = 4.5\ \text{bit}$。K-quant 把说明书也压了,per-weight 降到 $4 + 80/256 = 4.3125\ \text{bit}$。
单看这 $0.1875\ \text{bit}$ 的差距不明显。放大到 160 亿参数。
$$ 16\text{B} / 32 = 5\text{ 亿个 block} $$每个 block 省 6 bit 的 scale 开销($16 \to 10$),总共 $5\text{亿} \times 6 = 30\text{ 亿 bit} \approx 375\ \text{MB}$。如果用的是 Type 1(每个 block 还有 $\alpha$),$\alpha$ 的 FP16 也压缩到 INT8——每 block 再省 6 bit,又是 375 MB。加上 super-block 的额外开销抵消一小部分,常量的总开销从约 2 GB 砍到约 1 GB。
1 GB 对于消费级显卡来说,可能就是跑得动和跑不动的区别。
这套双重量化的方案最早来自 QLoRA 的论文。当时 QLoRA 用它来压缩 LoRA 适配器的存储——思路是一样的:辅助信息太多,那就把辅助信息也量化一遍。
顺带一提,网上说 K-quants 的"K"是指 K-means 聚类。不是的。K-quants 用的还是同样的仿射量化(把浮点数线性映射到整数),不是向量量化。K 来自开发者 Kawrakow 的名字,也可能是"kernel"——因为 super-block 结构需要新的 CPU kernel 来支持。
不只是省空间
super-block 带来的好处不止存储。还有一个更隐蔽的优势:内存访问。
CPU 读取内存的时候不是按需取几个字节——它一次读一整条 cache line,通常是 64 字节。256 个 INT4 权重连起来正好是 $256 \times 4 / 8 = 128$ 字节,刚好两条 cache line。CPU 一口气读完,不用满世界跳到不同的内存地址。
legacy quants 的 block 只有 32 个权重。每个 block 16 字节,散落在内存各处。读完 block 0 跳到 block 1,中间隔了很远——每次都是 cache miss,CPU 空转着等内存。
super-block 把 8 个 block 的权重在内存里排成连续的 128 字节。读一个 super-block 只需要两次 cache line 加载,而不是 8 次跳跃。一次推理要扫过整个模型——几亿个 block 的 cache miss 变成几分之一,这个差别在 CPU 推理上很明显。
这是一个典型的"顺便解决"型优化。当时做 super-block 是为了省存储,结果发现连推理速度也变快了。
重要的人多吃点
K-quants 还做了一件事:不是所有权重一视同仁。
以 Llama 7B 为例。32 层 transformer,每层 4 个权重矩阵(Q、K、V、O),加上 2 个 feed-forward 矩阵和 2 个 LayerNorm。总共大约 $32 \times (4+2+2) = 256$ 个权重矩阵。其中 LayerNorm 的 64 个权重矩阵(每层 2 个)加起来才几十万个参数——占模型总量的万分之一不到——但它们的精度直接影响每一层的输出缩放。
K-quants 给这些敏感层分配更高的精度:
- LayerNorm 的权重保持 FP16,不量化。反正参数少,不差这点空间
- 注意力层的 Q、K、V、O 权重用更高 bit(比如 Q5、Q6 替代 Q4),因为"看对"上下文比什么都重要
- 输出层也用更高精度,直接决定下一个 token 到底是谁
这就是下载页面里 Q4_K_S、Q4_K_M、Q4_K_L 的含义——同一个基础精度(Q4),用不同的"敏感层保护策略":
| 后缀 | 策略 | 文件大小 |
|---|---|---|
| S (Small) | 激进压缩,敏感层也尽量压 | 最小 |
| M (Medium) | 折中,大部分场景的最佳选择 | 中等 |
| L (Large) | 保守,敏感层给足精度 | 最大 |
选 S 能跑但可能不太聪明,选 L 最聪明但可能跑不动。M 是大多数人的答案。
这不是什么理论突破。就是一个工程判断:既然有些层对误差更敏感,那就把有限的 bit 预算倾斜给它们。一个 7B 模型有约 70 亿个参数,其中真正"敏感"的可能只有几百万——给这几百万多配点 bit,其他几十亿省着用。跟团队里给核心成员配更好的电脑是一个道理——资源总是有限的,关键是放在哪里。
所以下载页那一排 K
回到最初下载页面让人愣住的那一排名字。Q4_0、Q4_K_M、Q5_1。
Q4_0 是 legacy 对称量化——桶少、空桶多、说明书占地方。
Q4_K_M 是 K-quant——两层套娃压缩了说明书,M 档给敏感层留了余量。同样的 4 bit 基础精度,效果比 Q4_0 好是应当的,因为桶用得更聪明、关键层吃得更好。
Q5_1 是 legacy 非对称量化换成 5 bit——基础精度更高了,但说明书还是一样膨胀。
搞懂了 legacy 和 K-quants,这一排名字就是一套密码。Q 后面的数字告诉你基础的桶有多大(bit 数),下划线后面的字母告诉你说明书怎么压缩、资源怎么分配。_\0 是朴素方案,_K_M 是套娃折中,_I 是更激进的重要性加权——但那是下一篇的事了。
理解 K-quants 记住一件事就够了:所有能被量化的东西,最后都会被量化。
diet 是没有尽头的。