2.1 量化的数学:从浮点到4个比特


文档摘要

2.1 量化的数学:从浮点到 4 个比特 上一章我们把「为什么量化」讲透了:显存墙把巨型模型挡在本地之外,而 INT4 提供的 4 倍压缩是把它们请回来的关键杠杆。这一节我们要做一件更硬核、也更重要的事——把「量化」翻译成数学。 为什么要先把数学讲清楚?因为后面你会在命令行里面对一堆参数(group size、校准样本数、clip 阈值……),如果你不知道这些数字背后到底在优化什么,就只能盲目抄别人配置、出了问题也不会调。而一旦你理解「scale 与 zeropoint 是怎么把浮点塞进 4 个比特的」,那些参数就不再是天书,而变成了你能主动驾驭的旋钮。

2.1 量化的数学:从浮点到 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 扮演的角色:

```mermaid flowchart LR subgraph S[对称量化] S1[浮点 w] --> S2["q = round(w/scale)"] S2 --> S3["w' = q·scale"] end subgraph A[非对称量化] A1[浮点 w] --> A2["q = round(w/scale)+zp"] A2 --> A3["w' = (q-zp)·scale"] end ```

二、INT4 的 16 个格子:为什么这么挤

INT4 只有 4 个比特,能表示的整数总共 2^4 = 16 个。无论你用有符号的 [-8, 7] 还是无符号的 [0, 15]可用的离散值就是 16 个

对比一下其他精度就明白 INT4 有多「挤」:

  • FP16:约 15 位尾数,等效上千个有效档位;
  • INT8:256 个整数,格子密度是 INT4 的 16 倍;
  • INT4:只有 16 个整数,是整条链路里最激进的一档。

于是「量化」的全部艺术,就收敛成一个问题:怎么选 scale 和 zero_point,让这 16 个格子尽量覆盖原始权重的真实分布,同时把被压进同一个格子的信息损失降到最低?

这里有一个关键洞察,也是后面所有高级量化方法的出发点:神经网络权重通常近似零均值、近似对称的高斯分布,但末尾拖着几条长长的「尾巴」——少数极端值(离群点,outlier)的绝对值远大于大多数权重。 如果傻乎乎地用全局 min-max 去定 scale,这几个离群点会把 scale 撑得巨大,导致占绝大多数的正常权重全被挤进 [-1, 0][0, 1] 这样一个极窄区间,大量信息被「揉」成一个整数。这正是 RTN(四舍五入量化)在 INT4 下经常翻车的根因。

三、量化粒度:per-tensor、per-channel、per-group

上面公式里的 min/max 是在「哪一段数据」上算的?这个「范围」的选择,就是量化粒度,直接决定 scale 的数量和精度。

  • per-tensor(整张量一个 scale):整个权重矩阵共用一对 scale/zero_point。最简单、最快,但最容易被离群点拖累——一个超大值就毁掉整片权重。
  • per-channel(每输出通道一个 scale):权重矩阵的每个输出通道单独算 scale。能把不同通道的分布差异照顾到,是很多 GPTQ/AWQ 实现的默认选择,质量明显好于 per-tensor。
  • per-group(分组量化,GGUF 的杀手锏):把权重进一步切成小块(如每 128 个连续元素一组),每组一个 scale。组越小,scale 越贴近局部分布,精度越高,但存储 scale 的开销越大。llama.cpp 的 GGUF 量化大量使用 per-group(典型 group size 128),这是它在 INT4 附近还能保持高质量的核心原因之一。

用一个结构图看三者的嵌套关系:

```mermaid flowchart TD T[权重矩阵] --> C1[通道1: scale1] T --> C2[通道2: scale2] T --> C3[通道3: scale3] C1 --> G1[组1: scale1a] C1 --> G2[组2: scale1b] C2 --> G3[组3: scale2a] C2 --> G4[组4: scale2b] ```

一句话经验:粒度越细,精度越高、开销越大。 实战里 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 + RTNper-group + 校准补偿 的效果可能天差地别。后面第 3 章会专门讲「离群点怎么处理」,这里你只需先建立直觉:好的量化方法,本质都是在和离群点搏斗。

下面用一张误差示意(数值为示意,非某具体模型实测)展示离群点的影响:

```mermaid xychart-beta title "不同 scale 策略下正常权重的可用格子数(示意)" x-axis ["per-tensor 全局scale", "per-channel", "per-group(gs128)"] y-axis "正常权重可用整数格数" 0 --> 16 line [3, 9, 15] ```

五、量化误差从哪来:截断、舍入、clipping

任何量化都会引入误差 e = w - w'。拆开看,主要有三类:

  1. 舍入误差(rounding)round() 把一个浮点值映射到最近整数时,最多损失半个格子宽度。这是量化固有的、不可避免的底噪,INT4 下约 ±scale/2。
  2. 截断误差(clipping):当原始值超出量化整数范围(比如超过 ±7)时,会被「削顶」到边界值,造成永久失真。聪明的量化会主动选择 clip 范围(不一定用满 min-max),用少量截断换整体更细的格子——这是「截断—舍入」权衡的核心。
  3. 零值偏移误差:非对称量化若 zero_point 取整不准,会导致 0 附近出现偏移,影响 ReLU 类激活的 0 保持性。

高级量化方法(GPTQ、AWQ)做的,就是在给定比特预算下,用校准数据去联合优化 scale、clip 和权重补偿,把总误差 ||W - Ŵ|| 压到最小。理解了这点,你就不会再觉得那些方法「玄学」——它们是在解一个带约束的优化题。

六、一个手算例子:把 [-0.9, 1.1] 量化到 INT4

光说不练假把式。我们手算一遍非对称 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 实现量化与反量化

下面用一段可直接运行的 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 取整偏移。

理解这个公式,你就能解释很多「反直觉」的现象:

  • 为什么 group size 128 是甜点而非 32? 组越小 scale 越精细(第一项降),但存储 scale 的元数据开销上升、且每组样本太少容易过拟合校准集(第二项变相上升)。128 是两者平衡的经验最优点。
  • 为什么 INT4 比 INT8 难这么多? INT4 的格子宽是 INT8 的 16 倍(2^(8-4)),第一项直接放大约 256 倍,所以 INT4 必须靠分组+补偿把 scale 压下来,否则底噪爆炸。
```mermaid xychart-beta title "不同 group size 下 INT4 量化失真趋势(示意)" x-axis ["32", "64", "128", "256"] y-axis "相对失真" 0 --> 100 line [88, 60, 35, 30] ```

注意:这张图是「分组越细、失真越低」的定性示意,不是某款具体模型的实测值;真实曲线会随模型结构、校准集质量波动,落地时请以你自己的校准+评估为准。

九、从公式回到工程:命令行参数对号入座

现在把数学和系统里常见的参数对上号,你以后看配置就不会发怵:

  • --group-size 128(llama.cpp / GGUF)←→ 第六节的 per-group,决定 scale 粒度。
  • --sym / --asym ←→ 对称 / 非对称量化,即选不选 zero_point。
  • 校准样本数(如 128 条)←→ 下一节(2.2)主题,决定能不能算准 scale 与补偿。
  • clip 比例(如 0.95)←→ 主动放弃 5% 极值范围,用少量截断换更细格子(第五节权衡)。

你会发现,后面第 4 章实战里敲的每一条命令,都不过是这一节公式的某个旋钮。数学通了,工程就不慌。

十、常见误区与本节小结

写到这里,我先替你把几个高频误区拆掉,免得后面踩坑:

  • 误区一:「量化就是四舍五入」。错。四舍五入(RTN)只是最朴素的一种,且 INT4 下几乎必翻车;真正好用的是带校准、带补偿、带分组的方法。
  • 误区二:「INT4 格子这么少,肯定不能用」。错。格子少是事实,但配合 per-group 和校准补偿,INT4 在绝大多数任务上能压到「肉眼难辨」的质量损失,这也是它成为本地推理主流的原因。
  • 误区三:「scale 越小越好」。错。scale 太小会让大值被截断(clipping),要在「舍入」和「截断」之间取平衡,而不是一味压小。

回到开头那句话——INT4 量化用数学语言说,就是用一对 scale/zero_point 把浮点权重线性映射到 16 个整数格子,而「怎么定这对因子、怎么分组、怎么处理离群点」决定了质量上限。 下一节(2.2)我们顺着这条线,专讲「校准集」:后训练量化不重训模型,它是靠什么数据来定这些因子的?又为什么校准集的质量能直接决定你量化后模型「聪不聪明」。


作者与出处
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 前端切图仔转型中的小龙虾 转发
评论区 (0)
U