本节摘要:量化的本质是用「一个缩放因子 + 一组小整数」近似一串浮点数。缩放因子负责拉开量程,分块负责适配权重的局部分布,两者组合让 4 比特整数保留大部分模型质量。本节把这套机制讲到可以亲手验算的程度,并给出误差从何而来、被什么控制的判断框架。
一个 16 位浮点数的取值空间有六万多个刻度,一个 4 位整数只有十六个刻度。直接把浮点数四舍五入到最近的整数刻度,误差会大到模型直接报废。量化之所以可行,靠的不是更强的舍入,而是换了一个思路:不纠结绝对刻度,只关心相对关系。权重张量里的数值大多聚在一个很窄的区间里,与其用固定的十六个刻度去覆盖整个浮点空间,不如让刻度的宽窄跟着数据自己伸缩——这个伸缩的比例,就是缩放因子。
llama.cpp 的量化以「块」为单位。经典布局把 32 个相邻权重划成一块,每块独立计算缩放因子。以对称量化 Q4_0 为例,一个块的处理流程是:
给定块内 32 个浮点权重 w1..w32: 1. 求块内绝对值最大者 d = max(|wi|) 2. 缩放因子 s = d / 7 # 4 位有符号整数最大刻度是 7 3. 每个权重编码为 qi = round(wi / s),落在 -8..7 4. 解码时 wi ≈ s × qi 块的实际存储:32 个 4 位整数(16 字节)+ 1 个 16 位缩放因子(2 字节) 等效位宽 = (16 + 2) × 8 / 32 = 4.5 比特每参数
两个关键观察。第一,缩放因子让量程自适应:无论这块权重的数值聚在 0.001 还是 3.0 附近,十六个刻度总能被拉开到恰好覆盖本地分布,等效于每个块都享受「量身定制」的精度。第二,元数据开销摊到块内:2 字节的缩放因子除以 32 个权重,只把等效位宽从 4.0 拉高到 4.5——这就是第 1 章心算公式里「加一成元数据余量」的来历。
非对称版本 Q4_1 再引入一个零点偏移 m,用「s × q + m」仿射映射覆盖不跨零的分布,精度略好、速度略慢。K 系量化(下一节)则把这套块结构再向上封装一层超块,此处先有个预期:块结构是层层嵌套的洋葱,核心思想始终是「局部自适应」。
下面的脚本对一个模拟权重块执行完整的 Q4_0 量化与反量化,并统计误差。跑一遍胜过读十遍:
import random random.seed(7) # 模拟一块 32 个权重:大多聚在 0 附近,少数离群 block = [random.gauss(0, 0.08) for _ in range(31)] block.append(0.63) # 一个较大的离群值 d = max(abs(w) for w in block) # 块内最大绝对值 s = d / 7 # 缩放因子 q = [round(w / s) for w in block] # 编码为 -8..7 的整数 r = [s * qi for qi in q] # 解码近似 err = sum(abs(a - b) for a, b in zip(block, r)) / 32 print(f"缩放因子 s = {s:.4f}") print(f"平均绝对误差 = {err:.4f}") # 典型输出:缩放因子 s ≈ 0.09,平均绝对误差 ≈ 0.018
结果值得琢磨:平均误差大约是缩放因子的五分之一,而最大误差出现在离群值附近——多数靠近零的权重被压成了 0 或 ±1 个刻度,它们的相对误差不小,但对最终输出的影响也小,因为乘上它们本来就近似乘零。真正的精度战场在那些「数值大、影响大」的权重上,这正是下一节 K 系量化与重要性矩阵要解决的问题。
量化误差有两个来源、两个控制旋钮。来源一:刻度稀疏。十六个刻度之间的间隙由缩放因子决定,块内数值跨度越大,间隙越宽,单一权重的误差越大——所以块越小、块内分布越集中,误差越小;块越小则元数据开销越大,32 是 llama.cpp 权衡后的经典值。来源二:离群值绑架量程。一个 0.63 的离群值把缩放因子撑大,其他三十一个权重的有效刻度就变粗了。控制手段也对应两条:位宽上探(Q5、Q6 把刻度加密一档到两档)或重要性感知(把精度预算优先分给影响大的层与张量)。
由此可以建立一条选档直觉:模型越大,权重分布相对越平缓,同样位宽的相对损伤越小——这就是为什么 70B 模型敢用 Q2 档位还大体可用,而 3B 小模型掉到 Q4 以下就明显变笨。把这个直觉与第 1 章的体积心算合起来用,选档位就不再是玄学。
Q4_0 与 Q4_1 的差异值得单独拆开,因为它揭示了「分布形状决定格式选择」的思想。对称格式假设块内数值围绕零对称分布,只用缩放因子,零点自动落在零——计算时省一次加法,硬件实现最简。非对称格式多一个零点偏移,能把「整体偏正」或「整体偏负」的块完美贴合,代价是每个块多存一个参数、解码多一次运算。实测里两者的困惑度差异很小,速度差异也不大,llama.cpp 的 K 系量化实际采用了混合策略:值本身走对称思路,偏移信息以更经济的方式编进子块因子。给学习者的启示是:格式之间没有全面碾压,只有权衡点的移动——这个判断框架比背格式表重要得多。
另一个值得建立的直觉是误差的「不可见性」。单看某个权重,量化误差可能高达百分之几;但模型输出是数千个权重共同作用的统计结果,误差在求和中大量抵消。这解释了为什么 4 比特模型的输出「大体正常、偶尔犯错」——统计平均掩盖了个体误差,而那些没被抵消的残差就表现为偶发的细节错误。理解这一点,你就理解了为什么长链推理任务比短回答更挑位宽:误差沿推理链累积,没有机会被平均掉。
这是 2022 年定下的经验值,恰好匹配当时主流硬件的向量寄存器宽度与缓存线:一个块的字节数落在高效搬运的区间,块内统计量又足够稳定。后来的 K 系引入超块也没动这个内核,兼容惯性加性能够用,32 就固定了下来。
推理时的激活通常以 16 位甚至更高精度参与计算,量化的主要是权重。这是「权重是搬运大头、激活是计算临时值」的物理地位决定的——本书的带宽公式(5.1 节)也建立在这个事实上。
理论上存在二值化方案,实践里通用语言模型的质量损失不可接受,社区仅在极小的实验性模型上尝试过。1 到 2 比特区间目前是「有产物、不可用」的状态,把它当作量化地图的边界标记即可。
亲手跑一次量化时(第 3.2 节的命令),留意输出里逐张量滚过的日志行:每行一个张量名、一个选定的格式。你会看到输出层与嵌入相关张量被保留为高精度——这是工具在执行「关键路径少损」的策略,与本节的误差理论完全互证。养成看量化日志的习惯还有个实际收益:哪天模型行为异常,第一反应就是回查这份日志,确认某个敏感张量没有被异常量化。理论、日志、行为,三者从此闭环。