
温馨提示:另有配套视频版,附带图解演示,观感不同,欢迎到各大视频平台搜索同名标题或【蛋先生说识】
什么是量化?
丹尼尔:蛋兄,最近在折腾大模型本地部署,经常遇到 "量化" 这个词,一直没搞懂到底是什么
蛋先生:就是用大模型量化交易啊,搞钱!
丹尼尔:真的?
蛋先生:(ಡωಡ) 假嘟,哈哈。此量化非彼量化啦
丹尼尔:额,那到底什么是量化?
蛋先生:打个比方吧——量化就像把 4K 高清视频压成 720P,文件小了,硬盘省了,内存也省了。虽然画质有点损失,但大部分时候你看剧根本感觉不到差别
量化的基本原理
丹尼尔:哦,原来是给大模型"瘦身"啊!那具体是怎么压缩的呢?
蛋先生:核心思路就是把位数多的浮点数(比如 FP16),映射成位数更少的整数(比如 INT8、INT4)来存储(不了解 FP16 的同学,可以先去各大平台搜索 《大模型参数存储格式揭秘:BF不是男朋友》补个基础)
丹尼尔:哦,那如何映射?
蛋先生:假设有一组浮点数:[-0.20, -0.14, -0.09, -0.05, -0.02, 0.00, 0.03, 0.07, 0.11, 0.15, 0.18, 0.20],值范围是 [-0.2, 0.2]。现在要把它们映射到 5 个整数编码 -2、-1、0、1、2,你会怎么分?
丹尼尔:( ̄. ̄) 容我思考片刻...!算了,你还是直接说答案吧
蛋先生:先把 [-0.2, 0.2] 这个区间五等分,每份交界处打个刻度,再给每个刻度贴上整数标签:
| 刻度位置 | −0.2 | −0.1 | 0 | 0.1 | 0.2 |
|---|---|---|---|---|---|
| 整数标签 | -2 | -1 | 0 | 1 | 2 |
量化的规则很简单:每个浮点数看自己离哪个刻度最近,就贴上那个刻度的整数标签
| 原始浮点 | 最近的刻度 | 贴上标签 |
|---|---|---|
| -0.20 | −0.2 | -2 |
| -0.14 | −0.1 | -1 |
| -0.09 | −0.1 | -1 |
| -0.05 | −0.1 | -1 |
| -0.02 | 0 | 0 |
| 0.00 | 0 | 0 |
| 0.03 | 0 | 0 |
| 0.07 | 0.1 | 1 |
| 0.11 | 0.1 | 1 |
| 0.15 | 0.2 | 2 |
| 0.18 | 0.2 | 2 |
| 0.20 | 0.2 | 2 |
丹尼尔:哇, amazing!这样存储确实省了不少空间。但怎么把整数还原成浮点数呢?
蛋先生:大模型推理时,大部分情况下的确是需要把整数权重"反量化"回浮点数来计算(少数情况下是可以直接用整数计算的),这个等讲到公式时再细聊
对称 INT4 / INT8 量化
丹尼尔:原理大概听懂了,可以再深入一点
蛋先生:量化方案有很多(GPTQ / AWQ / llama.cpp 等),但它们共享同一个底层基石:对称 INT4 / INT8 量化。下面我通过一个完整的演算示例带你走一遍
丹尼尔:真贴心,赶紧开讲
蛋先生:假设我们有一组 FP16 格式的权重参数,范围是 w ∈ [−0.2, 0.2]。准备用 INT4 量化。4 bit 有符号整数用二进制补码表示,编码范围是 q ∈ [−8, −7, ... 0 ... 6, 7]
丹尼尔:等等,为什么是 [−8, −7, ... 0 ... 6, 7],而不是 [−7, ... 0 ... 6, 7, 8]
蛋先生:这是二进制补码硬件标准,自行问 AI 去吧,这里不展开讲了
丹尼尔:好好,您继续
蛋先生:零偏移 zₚ = 0(这正是「对称」的含义:0 轴两侧的编码范围相对零点完全对称映射)。我们的目标只有一个:算出 scale,通过它才能让 INT4 跟 FP16 之间互转
丹尼尔:目前为止,云里雾里的
蛋先生:别急,一步步来
步骤 1:统计原始权重极值
蛋先生:第一步简单,先找出权重参数的最大值和最小值
丹尼尔:这个例子是 wₘᵢₙ = −0.2,wₘₐₓ = 0.2
步骤 2:计算最大绝对值 A
蛋先生:好助手。第二步,求两个原始权重极值的最大绝对值 A
丹尼尔:这我也会,A = max(|wₘₐₓ|, |wₘᵢₙ|) = max(0.2, 0.2) = 0.2
步骤 3:计算 scale
蛋先生:第三步,采用行业标准公式 s = A / 7
丹尼尔:等等,为啥是 7?INT4 编码不是 −8 到 7 吗?
蛋先生:关键就在这——7 是最大的正编码,我们想让最大的浮点数权重 A 完美对应到 q = 7,这样正方向不浪费任何编码空间
丹尼尔:没听懂
蛋先生:套公式看看 s = 0.2 / 7
验证一下:q = 7 ⇒ ŵ = 7 × s = 7 × 0.2 / 7 = 0.2,刚好对上。这就是反量化,后面再聊
丹尼尔:那为什么不能拿 ‑8 来算 scale?比如 s = A / 8?
蛋先生:问得好。我们来试一把 s = 0.2 / 8 = 0.025。正向最大编码还是 7,7 × 0.025 = 0.175。但我们真实权重最大值是 0.2,0.2 > 0.175,正向直接溢出,权重直接失真
丹尼尔:明白了
蛋先生:scale 拿到了,我们算下最关键的:最小值 q = −8 对应多少?−8 × 0.0285714 ≈ −0.22857
丹尼尔:下限 −0.22857 比 −0.2 还多出来一截?
蛋先生:没错,−8 这个编码档位其实是闲置的,因为没有权重参数会落到这个区间。这就是对称量化的一个小代价:正数侧最大可用编码只有 7,scale 分母只能用 7,导致必然有一个编码用不上
步骤 4:量化 — 浮点转整数
丹尼尔:好像有点理解 scale 了,它有点像一把尺子上的刻度之间的距离,相邻刻度之间的距离就是 scale
蛋先生:恩,理解得很到位。有了 scale,就可以来计算浮点数到整数编码的转换了。公式如下:
w_int = clip( round(w_float / scale), Qmin, Qmax )
丹尼尔:一看公式就头疼
蛋先生:其实很简单,就三步:除 scale → 四舍五入 → 截断。
除 scale:把浮点值按 scale 切分,算出它落在哪一档
四舍五入:可以算出它离这一段的两个头尾的整数编码哪个更近
截断:防御性编程,确定结果落到合法的范围(比如 INT4 的合法区间就是 [−8, 7]),小于最小值就取最小值,大于最大值就取最大值。对称量化的场景下,截断基本不会触发
丹尼尔:你还是举一些例子吧
蛋先生:正负两个浮点数,各一个示例吧
例 1:w = 0.1
① 除 scale:`w / s = 0.1 ÷ 0.0285714 ≈ 3.5`
② 四舍五入:`q = round(3.5) = 4`
③ 截断至 [−8, 7]:4 在区间内,无需截断
例 2:w = −0.18
① 除 scale:`w / s = −0.18 ÷ 0.0285714 ≈ −6.3`
② 四舍五入:`q = round(−6.3) = −6`
③ 截断至 [−8, 7]:−6 在区间内,无需截断
步骤 5:反量化 — 整数转回浮点
丹尼尔:看明白了。那量化完后,推理时怎么把整数变回浮点数呢?
蛋先生:直接用重建公式 ŵ = q × s,一步搞定
例 1 反量化:ŵ = 4 × 0.0285714 ≈ 0.11428
例 2 反量化:ŵ = −6 × 0.0285714 ≈ −0.17143
丹尼尔:咦?0.1 回去变成 0.11428,−0.18 变成 −0.17143,误差还不小?
蛋先生:这就是量化的代价嘛 (─.─||)。INT4 总共就 16 个取值档位,精度有限。比如 0.1 恰好落在 q=3 和 q=4 的正中间,往 4 靠就有了这个误差
存在什么问题?
丹尼尔:这种量化方式会有哪些问题呢?
蛋先生:有一些
比如权重分布如果不相对 0 轴对称——像 [0, 0.2] 这种全是正数的,对称量化会让 −8 到 −1 这 8 个编码直接荒废掉。这时就得用非对称量化了
再比如权重里混进了极端离群值,比如 [0.10, 0.12, 0.08, 0.11, 0.09, 0.80],全局 scale 会被 0.8 撑大,0.1 附近的参数全挤到同一档。解决方案是分组量化,各组独立算自己的 scale,把极端值的影响控制在一个小组内
丹尼尔:感觉脑袋快要装不下了……
蛋先生:我觉得不止你,正在看这篇文章的朋友们,应该也快打盹了 (─.─||)。今天就到这,有缘再聊!
丹尼尔:拜拜!
写在最后
亲们,都到这了,要不,点赞 / 收藏 / 关注支持下我呗 o( ̄▽ ̄)d