2.1 量化的数学:从浮点到 4 个比特 上一章我们把「为什么量化」讲透了:显存墙把巨型模型挡在本地之外,而 INT4 提供的 4 倍压缩是把它们请回来的关键杠杆。这一节我们要做一件更硬核、也更重要的事——把「量化」翻译成数学。 为什么要先把数学讲清楚?因为后面你会在命令行里面对一堆参数(group size、校准样本数、clip 阈值……),如果你不知道这些数字背后到底在优化什么,就只能盲目抄别人配置、出了问题也不会调。而一旦你理解「scale 与 zeropoint 是怎么把浮点塞进 4 个比特的」,那些参数就不再是天书,而变成了你能主动驾驭的旋钮。
上一章我们把「为什么量化」讲透了:显存墙把巨型模型挡在本地之外,而 INT4 提供的 4 倍压缩是把它们请回来的关键杠杆。这一节我们要做一件更硬核、也更重要的事——把「量化」翻译成数学。
为什么要先把数学讲清楚?因为后面你会在命令行里面对一堆参数(group size、校准样本数、clip 阈值……),如果你不知道这些数字背后到底在优化什么,就只能盲目抄别人配置、出了问题也不会调。而一旦你理解「scale 与 zero_point 是怎么把浮点塞进 4 个比特的」,那些参数就不再是天书,而变成了你能主动驾驭的旋钮。
读完整节,你应该能用一句话回答「INT4 量化到底在算什么」:它用一对缩放因子,把一段连续浮点权重线性映射到 16 个整数格子上,而「怎么选这对因子才能尽量保住信息」就是全部难题。
最基础的量化叫做均匀量化(uniform quantization):它假设原始浮点值和量化后的整数之间,存在一个线性映射。这个映射只需要两个量就能描述。
对称量化(symmetric):以 0 为中心,不单独记录零点偏移。
scale = max(|w|) / (2^(b-1) - 1) q = round(w / scale) w' = q * scale
其中 b 是比特数,INT4 下 2^(b-1)-1 = 7,所以量化整数落在 [-7, 7](共 15 个非零值,加上 0 共 16 个)。对称量化的好处是反量化只需一次乘法,硬件极快;代价是它强制原始分布以 0 为中心,对偏态分布不友好。
非对称量化(affine / asymmetric):额外引入 zero_point,把量化空间平移,使原始的最小值映射到整数下限。
scale = (max(w) - min(w)) / (2^b - 1) zero_point = round(-min(w) / scale) q = round(w / scale) + zero_point w' = (q - zero_point) * scale
INT4 下 2^b - 1 = 15,量化整数落在 [0, 15]。非对称量化能把整个原始范围「贴」满这 16 个格子,对明显偏正或偏负的权重(比如 ReLU 之后的激活常是非负的)更省格子。
要点先记住:scale 决定每个格子的宽度,zero_point 决定格子整体平移多少。量化误差的大小,本质上由 scale 一家决定——scale 越大,格子越宽,四舍五入掉的信息越多。
下面这张图把两条路径放在一起对比,你能直观看到 zero_point 扮演的角色:
INT4 只有 4 个比特,能表示的整数总共 2^4 = 16 个。无论你用有符号的 [-8, 7] 还是无符号的 [0, 15],可用的离散值就是 16 个。
对比一下其他精度就明白 INT4 有多「挤」:
于是「量化」的全部艺术,就收敛成一个问题:怎么选 scale 和 zero_point,让这 16 个格子尽量覆盖原始权重的真实分布,同时把被压进同一个格子的信息损失降到最低?
这里有一个关键洞察,也是后面所有高级量化方法的出发点:神经网络权重通常近似零均值、近似对称的高斯分布,但末尾拖着几条长长的「尾巴」——少数极端值(离群点,outlier)的绝对值远大于大多数权重。 如果傻乎乎地用全局 min-max 去定 scale,这几个离群点会把 scale 撑得巨大,导致占绝大多数的正常权重全被挤进 [-1, 0] 或 [0, 1] 这样一个极窄区间,大量信息被「揉」成一个整数。这正是 RTN(四舍五入量化)在 INT4 下经常翻车的根因。
上面公式里的 min/max 是在「哪一段数据」上算的?这个「范围」的选择,就是量化粒度,直接决定 scale 的数量和精度。
用一个结构图看三者的嵌套关系:
一句话经验:粒度越细,精度越高、开销越大。 实战里 group size 128 是「质量与体积」的甜点,这也是为什么你后面会经常见到 q4_k_m 这类带 group 的量化档位。
现在我们把「离群点撑大 scale」这件事量化出来看。假设某层权重 99% 落在 [-1, 1],但有 1% 的离群点到了 ±8。
如果用全局 max 定 scale(对称):scale = 8 / 7 ≈ 1.14。那么正常的 [-1, 1] 区间全部映射到整数 [-1, 0, 1]——绝大多数权重被压成 3 个格子,精度崩塌。
如果改用 per-group(组大小 128),离群点大概率被单独关在某一组里,其他组用 scale ≈ 1/7 就能把 [-1,1] 细致地铺满 15 个格子。差距一目了然。
这就是为什么「方法选错」比「精度选低」更致命:同样是 INT4,per-tensor + RTN 和 per-group + 校准补偿 的效果可能天差地别。后面第 3 章会专门讲「离群点怎么处理」,这里你只需先建立直觉:好的量化方法,本质都是在和离群点搏斗。
下面用一张误差示意(数值为示意,非某具体模型实测)展示离群点的影响:
任何量化都会引入误差 e = w - w'。拆开看,主要有三类:
round() 把一个浮点值映射到最近整数时,最多损失半个格子宽度。这是量化固有的、不可避免的底噪,INT4 下约 ±scale/2。±7)时,会被「削顶」到边界值,造成永久失真。聪明的量化会主动选择 clip 范围(不一定用满 min-max),用少量截断换整体更细的格子——这是「截断—舍入」权衡的核心。高级量化方法(GPTQ、AWQ)做的,就是在给定比特预算下,用校准数据去联合优化 scale、clip 和权重补偿,把总误差 ||W - Ŵ|| 压到最小。理解了这点,你就不会再觉得那些方法「玄学」——它们是在解一个带约束的优化题。
光说不练假把式。我们手算一遍非对称 INT4 量化,把流程走熟。
已知一段权重范围 min = -0.9, max = 1.1,INT4 无符号范围 [0, 15]。
第一步,算 scale:scale = (1.1 - (-0.9)) / 15 = 2.0 / 15 ≈ 0.1333
第二步,算 zero_point:zero_point = round(-(-0.9) / 0.1333) = round(6.75) = 7
(验证:最小值 -0.9 → round(-0.9/0.1333)+7 = round(-6.75)+7 = -7+7 = 0,正好落在 0,符合预期。)
第三步,量化一个具体值,比如 w = 0.5:q = round(0.5 / 0.1333) + 7 = round(3.75) + 7 = 4 + 7 = 11
反量化:w' = (11 - 7) * 0.1333 = 4 * 0.1333 = 0.5332
误差 e = 0.5 - 0.5332 = -0.0332,约为半个格子宽,合理。
第四步,量化一个离群值 w = 1.05:q = round(1.05/0.1333)+7 = round(7.88)+7 = 8+7 = 15(削顶到上限 15)
反量化 w' = (15-7)*0.1333 = 1.0664,误差稍大但被「关」在边界,不影响其他值。
你看,整个过程就是「定 scale → 定 zero_point → 映射 → 反量化验证」。命令行里那些量化工具,本质都在自动、批量、带优化地做这同一件事。
下面用一段可直接运行的 Python,把上面公式落成代码。注意它刻意保持「教学最简版」,真实推理框架会在此基础上有大量工程优化(向量化、融合、GPU kernel),但数学内核完全一致。
import numpy as np def quantize_int4_symmetric(w: np.ndarray): """对称 INT4 量化(教学版,范围映射到 [-7, 7])""" scale = np.max(np.abs(w)) / 7.0 q = np.clip(np.round(w / scale), -7, 7).astype(np.int8) return q, scale def dequantize_int4_symmetric(q: np.ndarray, scale: float): """对称 INT4 反量化""" return q.astype(np.float32) * scale # 演示:一段含离群点的权重 w = np.array([-0.9, -0.2, 0.0, 0.3, 0.5, 1.1, -1.05, 0.8], dtype=np.float32) q, scale = quantize_int4_symmetric(w) w_hat = dequantize_int4_symmetric(q, scale) print("scale =", round(float(scale), 4)) print("q (int) =", q.tolist()) print("w' =", [round(float(x), 4) for x in w_hat]) print("误差 =", [round(float(a - b), 4) for a, b in zip(w, w_hat)])
运行后你会看到:scale 被离群点(1.1 和 -1.05)撑大,正常的小权重反量化后误差相对明显——这正是第三节讲的「离群点撑大 scale」在代码里的真实体现。把这个脚本的 scale 改成「按每 4 个元素一组计算」(即 per-group),再跑一次,你会直观看到正常权重的误差显著缩小。建议你亲手改一下 group size 参数,这是把本节数学「焊」进脑子的最快方式。
前五节都是定性直觉,这一节给你一个可量化的目标函数,让你真正站在「量化方法设计者」的视角看问题。后训练量化(PTQ)要解的,本质是一个最小化重构误差的问题:
min ||W - Ŵ||² (在给定比特预算下,让量化后权重 Ŵ 尽量逼近原始 W)
对单层权重矩阵 W,均匀量化引入的总失真(mean squared error,MSE)可以近似写成三项之和:
MSE ≈ (scale² / 12) · N_group + clip_loss + rounding_bias² └─ 舍入底噪 ─┘ └─ 截断损失 ─┘
第一项 scale²/12 是「均匀量化固有舍入方差」的经典结论(均匀分布四舍五入的方差恰为 Δ²/12,其中 Δ = scale 是格子宽);N_group 提醒你:分组越多、每组 scale 越小,这一项越小——这正是 per-group 有效的原因。第二项 clip_loss 来自超出范围的削顶,聪明的量化会主动选 clip 范围去权衡它和第一项。第三项 rounding_bias 来自 zero_point 取整偏移。
理解这个公式,你就能解释很多「反直觉」的现象:
2^(8-4)),第一项直接放大约 256 倍,所以 INT4 必须靠分组+补偿把 scale 压下来,否则底噪爆炸。注意:这张图是「分组越细、失真越低」的定性示意,不是某款具体模型的实测值;真实曲线会随模型结构、校准集质量波动,落地时请以你自己的校准+评估为准。
现在把数学和系统里常见的参数对上号,你以后看配置就不会发怵:
--group-size 128(llama.cpp / GGUF)←→ 第六节的 per-group,决定 scale 粒度。--sym / --asym ←→ 对称 / 非对称量化,即选不选 zero_point。你会发现,后面第 4 章实战里敲的每一条命令,都不过是这一节公式的某个旋钮。数学通了,工程就不慌。
写到这里,我先替你把几个高频误区拆掉,免得后面踩坑:
回到开头那句话——INT4 量化用数学语言说,就是用一对 scale/zero_point 把浮点权重线性映射到 16 个整数格子,而「怎么定这对因子、怎么分组、怎么处理离群点」决定了质量上限。 下一节(2.2)我们顺着这条线,专讲「校准集」:后训练量化不重训模型,它是靠什么数据来定这些因子的?又为什么校准集的质量能直接决定你量化后模型「聪不聪明」。