第 20 章 量化与压缩
第 20 章 量化与压缩
上一章说清了一件事:现代推理的瓶颈多半不在算力,而在把数据搬来搬去。量化就是直接冲着"把数据变小"去的手段——同时也顺带把算力换成了更便宜的那种。
学习目标
读完本章,你应当能够:
- 说出 FP32、TF32、FP16、BF16、FP8、INT8、INT4 的位布局、动态范围与典型用途,并解释 BF16 与 FP16 的本质区别。
- 从"让最大幅值恰好落满 int8 范围"这一目标出发,推导线性量化的
s = max(|r_min|, |r_max|)/(2^(n−1) − 1)。 - 辨析对称量化与非对称量化,解释 zero-point 的作用,以及权重与激活为何偏好不同的方案。
- 推导缩放因子拆成
s = a/2^b之后的乘法与加法公式,说明为什么可以退化成移位与加法,并论证"最大可行b“的单调性。 - 分析量化误差的三个来源(舍入、截断、离群值),说出 PTQ 与 QAT 的流程差异、校准集的作用以及直通估计器(STE)的必要性。
- 说明量化推理中反量化的时机,以及为什么 int8×int8 必须用 int32 累加器。
- 复现全整型量化矩阵乘的完整规则,包括 half-up 舍入与
b的搜索。
前置知识检查
| 你需要知道 | 在哪一节补齐 |
|---|---|
| IEEE 754 浮点数的符号/指数/尾数结构 | 第 5 章(数值计算基础);本章 20.1 会重述 |
| 二进制补码与整数取值范围 | 本章 20.2、20.6 |
| 移位运算与定点表示 | 本章 20.3(从零推导) |
| 矩阵乘法的分块与求和公因子提取 | 第 1 章、第 19 章 19.2.2 |
| 舍入模式(half-up 与银行家舍入) | 第 26 章;本章 20.7 给出判别方法 |
| 逐元素算子的访存瓶颈 | 第 19 章 19.5 |
20.1 数值格式谱系
20.1.1 从位布局理解一个格式
一个浮点数由三段构成:符号位 S、指数位 E、尾数位 M。总位宽 = 1 + E + M。它表达的值是:
x = (−1)^S × 1.M₂ × 2^(e − bias),其中 e 是 E 位的无符号读数,bias 是固定的偏移量。
三段的分工决定了格式的性格:
- 指数位宽决定动态范围:能表示的最大量级约为
2^(2^E − 1 − bias),相邻两个可表示数的间距也随指数增长。 - 尾数位宽决定精度:在同一个二进制数量级内,尾数提供了
2^M个等分点,因此相对精度约为2^(−M)。
这两者不可兼得——总位宽固定时,给指数多一位,尾数就少一位。
| 格式 | 总位宽 | S/E/M | 动态范围(数量级) | 相对精度 | 典型用途 |
|---|---|---|---|---|---|
| FP32 | 32 | 1/8/23 | 约 10^(±38) | 约 2^(−23) ≈ 1.2×10⁻⁷ | 训练主权重、优化器状态 |
| TF32 | 19(存储 32) | 1/8/10 | 与 FP32 同 | 约 2^(−10) ≈ 10⁻³ | Tensor Core 的 FP32 加速通道 |
| FP16 | 16 | 1/5/10 | 约 ±65504 | 约 2^(−10) ≈ 10⁻³ | 推理权重与激活、混合精度训练 |
| BF16 | 16 | 1/8/7 | 与 FP32 同(10^(±38)) | 约 2^(−7) ≈ 8×10⁻³ | 大模型训练与推理(首选) |
| FP8 E4M3 | 8 | 1/4/3 | 约 ±448 | 约 2^(−3) = 0.125 | 前向激活、KV Cache |
| FP8 E5M2 | 8 | 1/5/2 | 约 ±57344 | 约 2^(−2) = 0.25 | 反向梯度(需要范围) |
| INT8 | 8 | 定点 | [−128, 127] | 由缩放因子决定 | 权重+激活量化推理 |
| INT4 | 4 | 定点 | [−8, 7] | 由缩放因子决定 | 权重压缩(weight-only) |
重要
BF16 与 FP16 的本质区别是位宽分配策略,不是总位宽。 两者都是 16 位:
- FP16 精度优先:5 位指数(够用)+ 10 位尾数(精度高),代价是范围只有
±65504。 - BF16 范围优先:8 位指数(与 FP32 完全一致)+ 7 位尾数(精度低),范围与 FP32 相同。
大模型训练偏好 BF16 有两个原因,都与"大"有关:
- 大模型的激活、梯度、损失尺度动辄上万,FP16 的
65504上限容易被突破而溢出为 inf;一旦出现 inf,梯度立刻变成 NaN,训练崩掉。 - BF16 与 FP32 指数位宽相同,
FP32 → BF16的转换几乎不会溢出,只需截断尾数,不再需要精细的 loss scaling(FP16 必须配 loss scaling 防溢出)。
代价是 BF16 尾数精度只有 FP16 的约 1/8。但深度学习对随机噪声的容忍度远高于对溢出的容忍度:误差大一点只是收敛略慢,溢出一次可能白跑几天。
提示
记忆锚点:FP16 偏"细”,BF16 偏"广"。需要广动态范围(训练、大激活)选 BF16;需要高精度且范围可控(部分推理)选 FP16。
20.1.2 定点的特殊性
INT8、INT4 不是浮点:它们没有指数,只有一组等间距的整数。因此一个整数本身没有物理含义,必须配一个缩放因子(scale)才能还原成真实值。这就是 20.2 要建立的框架。
20.2 线性量化数学
20.2.1 三个基本公式
线性量化(linear quantization)把连续的实数 r 映射成 n 位整数 q:
| |
其中 s 叫缩放因子(scale)。round 通常取四舍五入。
20.2.2 为什么分母是 2^(n−1) − 1
先看 n 位有符号整数能表示什么:补码下范围是 [−2^(n−1), 2^(n−1) − 1]。例如 n = 8 时是 [−128, 127]。
负半轴有 128 个值,正半轴只有 127 个——这个不对称不是笔误,而是补码的必然结果:0 占了正半轴的一个位置,所以"最负"比"最正"多一个。
现在设 m = max(|r_min|, |r_max|) 是待量化张量中绝对值最大的元素。我们要让这个最大幅值恰好落在整数范围的边界上(既不留空、也不越界)。设它落在正边界:
m / s = 2^(n−1) − 1 = 127
解出
s = m / (2^(n−1) − 1)
这一步是整个公式的关键。逐一确认它同时满足三个要求:
- 不越界:任何
|r| ≤ m,于是|r/s| ≤ m/s = 127,取整后不超过127 ≤ 2^(n−1) − 1✓ - 不浪费:存在一个元素(那个绝对值最大的)取整后正好是
±127,幅值被填满 ✓ - 均匀:所有元素共用同一个
s,相邻可表示值间距恒为s✓
那为什么不用 2^(n−1) = 128 做分母?因为 128 在 int8 里表示不出来(int8 的正边界是 127)。若强行用 128,那个最大元素会取整到 128 而溢出,于是必须 clip——但 clip 意味着信息永久丢失,而且这个元素恰好是最重要的那个。用 127 则既填满又安全。
警告
分母写 2^n − 1(即 255)是常见错误。它把 int8 当成了无符号数,会让所有量化值挤在范围的一半以内,白白损失一位精度。看到 n−1 就想到"有一位要留给符号"。
20.2.3 量化误差的界
由取整的定义,误差满足
|r − s·q| = |r − s·round(r/s)| ≤ s/2
即最大绝对误差是半个量化步长。相对误差则是 ≤ s/(2|r|),元素越小相对误差越大——这是低精度量化中"小值被噪声淹没"的根源,也是后面要引入逐通道与分组粒度的直接原因。
20.3 对称量化、非对称量化与粒度
20.3.1 对称 vs 非对称
| 方案 | 映射式 | 零点 | 映射区间 | 常用对象 |
|---|---|---|---|---|
| 对称(symmetric) | q = round(r/s) | 固定为 0 | [−m, m] 关于 0 对称 | 权重 |
| 非对称(asymmetric) | q = round(r/s) + z | z ≠ 0 | 任意 [r_min, r_max] | 激活 |
非对称量化多了一个 zero-point(零点)z,它的作用是:让真实值 0 精确地映射到某个整数(具体地,映射到 z)。反量化式变成 r ≈ s·(q − z)。
为什么零点的存在很重要:在神经网络里 0 是特殊值——padding、ReLU 的输出、掩码位置全都需要精确的 0。若用对称量化去表示一个全部为正的区间(例如 ReLU 之后激活值恒 ≥ 0),那么有一半的整数码点(负半轴)被完全浪费,有效精度直接减半。非对称量化把 [r_min, r_max] 拉伸到整个整数范围,避开这个浪费。
为什么权重常用对称、激活常用非对称:
- 权重分布近似以 0 为中心,
max(|r_min|,|r_max|)的两侧大致相等,对称量化几乎不浪费——而且对称量化下z = 0,s·(q−z)简化为s·q,整数点积后只需乘一个缩放因子,硬件实现简单、无额外偏移项。 - 激活分布明显偏移(ReLU 后全正,或存在单侧离群值),若强行对称,一半码点闲置;更糟的是,
max(|r_min|,|r_max|)会被一侧的极端值支配,把s撑得很大,导致另一侧精度崩掉。
20.3.2 量化粒度
| 粒度 | scale 数量 | 精度 | 开销 |
|---|---|---|---|
| 逐张量(per-tensor) | 1 | 最差(受全局最大/最小值支配) | 最低 |
| 逐通道(per-channel) | 每个输出通道 1 个 | 好 | 需存 C_out 个 scale,硬件需按通道选择 |
| 逐分组(per-group) | 每组(如 128 个元素)1 个 | 最好 | 每 128 个元素一个 scale,存储与索引开销上升 |
以权重矩阵 W ∈ R^{C_out × C_in} 为例:逐张量只有一个 s,只要C_out 中某一行的幅值特别大,其他所有行都跟着用这个被撑大的 s;逐通道给每一行一个 s,各行独立;逐分组则在行内再切段,进一步隔离离群值。
这是**“隔离离群值的影响范围”**这一思路的连续谱:粒度越细,被一个极端值污染的邻居越少,精度越高,代价是元数据(scale 表)变大、内核实现更复杂。INT4 权重几乎总是配逐分组,因为 4 位的码点太少,容不下任何浪费。
20.4 把缩放因子拆成 a / 2^b
20.4.1 动机
s 是个实数。若把它直接拿去乘整数,还是得做浮点乘法,在只有整数运算单元的硬件(DSP、NPU、端侧 MCU)上就跑不起来。解决办法是把这个实数也表示成"整数 ÷ 2 的幂":
s = a / 2^b,其中 a 是整数、b 是非负整数。
2^b 是 2 的幂,因此"除以 2^b“或"乘以 2^b“在二进制里就是右移 / 左移——零成本、无精度损失、无浮点单元参与。这就是全部动机。
20.4.2 乘法为什么能变成"a 相乘、b 相加”
设两个实数分别是 f₁ = a₁ / 2^(b₁)、f₂ = a₂ / 2^(b₂)。由指数的乘法法则 2^(b₁)·2^(b₂) = 2^(b₁+b₂):
| |
结论:
- 整数部分:
a₁ · a₂——一次纯整数乘法; - 指数部分:
b₁ + b₂——一次整数加法; - 整体:一次整数乘法 + 一次移位,没有任何浮点运算。
20.4.3 加法为什么能变成"对齐后相加”
加法不能直接移项,因为两个分数的分母不同,必须先通分。统一到较大的那个分母 2^max(b₁,b₂)。设 b₂ ≥ b₁:
| |
写成一般形式:
f₁ + f₂ = ( a₁ << (max(b₁,b₂) − b₁) + a₂ << (max(b₁,b₂) − b₂) ) / 2^max(b₁,b₂)
结论:两次左移 + 一次整数加法 + 一次右移。b 较大的一方不移(移位量为 0),b 较小的一方左移 Δb 位——这正是"把小数点对齐"在二进制里的说法。
20.4.4 为什么 b 越大越好,以及怎么找最大的 b
b 越大,精度越高。 理由:s = a/2^b 中真正的有效数字全在 a 里,而 a 是个位数有限的整数。条件数上看:
s ≈ a / 2^b,当 a 只有 0~127 这 128 个可能取值时,能表示的 s 是离散的。把 s 的绝对值放大 2^b 倍再取整,能保留的有效位就更多。换句话说,b 越大,a 越能"细粒度"地逼近 s·2^b,表示的相对误差越小。
约束是 a ≤ 127(8 位量化下 a 也要能塞进 int8)。于是问题变成:在 a ≤ 127 的前提下,b 能取多大?
给定 s > 0,要求 half_up(s · 2^b) ≤ 127。定义
F(b) = s · 2^b
单调性论证:F(b) 关于 b 严格单调递增(因为 2^b 随 b 递增,而 s > 0)。因此:
- 若
F(b) ≤ 127,则对所有b' < b都有F(b') ≤ 127(可行集合是向下封闭的,形如{0, 1, …, b*}); - 一旦某个
b使F(b) > 127,之后不会再退回来。
所以最大可行 b 就是"可行集合的上端点",从上限(如 30)往下扫,遇到的第一个满足者即是答案。不需要二分,更不需要检查所有 b 的可行性——单调性保证了"第一个可行"就是"最大可行"。
| |
注意 half_up 与 round 的区别在这里会直接改变答案(见 20.7 的算例)。
20.5 量化误差分析与离群值治理
20.5.1 误差的三个来源
| 来源 | 机制 | 后果 | 对策 |
|---|---|---|---|
| 舍入误差 | round 造成的 ≤ s/2 偏差 | 随机、无偏,逐层累积 | 提高粒度、更高位宽 |
| 截断误差 | 为防越界而 clip 超出范围的值 | 系统性、有偏,不可恢复 | 增大 scale(牺牲精度)或先缩范围 |
| 离群值(outlier) | 个别极大值把 s 撑大 | 其余元素全部挤在少数码点上,有效位宽骤降 | per-channel、clipping、AWQ、SmoothQuant |
离群值才是 LLM 量化的主要障碍,也是三条治理思路的靶子:
- per-channel / per-group:缩短短板的影响范围(20.3.2)。
- clipping(截断):故意把
s定得比m/(2^(n−1)−1)小一点,让最极端的几个值被 clip 掉,换取其余 99.9% 元素的精度。是否划算取决于"被牺牲的元素对最终结果的重要性"——这是 AWQ 的核心洞察:并非所有权重同等重要,激活值大的通道对应的权重更关键,应当受保护而不是被牺牲。 - SmoothQuant:观察到激活难量化、权重好量化,于是在两者之间做一次等价变换——对激活除以逐通道的缩放
diag(s),对权重乘上它:(X diag(s)^(−1))·(diag(s) W) = XW。数学上完全等价,但把量化难度从激活"迁移"到了权重。这是"不改结果只改数值分布"的典范技巧。
20.5.2 PTQ 与 QAT
| 维度 | PTQ(post-training quantization) | QAT(quantization-aware training) |
|---|---|---|
| 时机 | 训练完成后 | 训练/微调过程中 |
| 需要什么 | 一小批校准集(几百条样本) | 完整训练流程 + 若干 epoch |
| 流程 | 跑校准集统计 min/max 或直方图 → 定 scale → 转格式 | 前向插入伪量化节点 → 反向用 STE → 权重在量化约束下收敛 |
| 成本 | 分钟级 | 小时到天级 |
| 精度 | 8 位通常可接受,4 位常明显掉点 | 4 位及以下仍可保持较好精度 |
| 典型场景 | 服务端 INT8/FP8 部署 | 端侧 INT4 部署 |
校准集的作用:PTQ 需要知道激活的实际分布范围才能定 scale,而这个范围只能通过让真实数据过一遍模型来观测。校准集不必与训练集同分布,但必须覆盖部署时的输入分布——否则 scale 定偏,精度无从谈起。这也解释了为什么"用一批随机噪声校准"是典型的失败做法。
伪量化(fake quant):QAT 的前向里插入 Q(x) = s·round(x/s) 这种"量化再反量化"的算子。它让前向的数值行为与部署时一致(含量化误差),从而让训练能感知到误差并适应它;输出仍是浮点,可以继续正常前向。
直通估计器(straight-through estimator, STE):round 的导数几乎处处为 0(阶跃函数),链式法则下梯度会变成全 0,网络无法训练。STE 的做法是反向时直接把 round 当作恒等映射:
| |
这不是数学上严格的梯度,但工程上极其有效:它给出了一个方向正确的近似梯度信号。可以理解为"告诉优化器:这里量化有点噪声,你照常更新,只是别忘了把权重推向量化格点附近"。
20.6 量化推理:反量化时机与累加位宽
20.6.1 反量化放在哪一步
反量化的位置决定了整个推理的形态:
| 方案 | 做法 | 精度 | 加速来源 |
|---|---|---|---|
| 权重-only 量化 | 权重存低精度,读入后立刻反量化回 FP16,算浮点乘 | 高 | 只省显存与带宽,不省算力 |
| 权重+激活全整型 | 两边都是整数,做整数点积,最后统一反量化 | 中 | 省带宽 也省算力(INT8 张量核吞吐约为 FP16 的 2 倍) |
| 混合(W4A16 / W8A8) | 权重低位宽、激活力求高位宽 | 高 | 主要用于省显存 |
核心原则:能拖到最后的反量化就不要提前做。 因为一旦反量化,数据就变回 FP16、体积翻倍,前面省下的带宽又还回去了。全整型量化的最优形态是:整数点积在整个求和过程中保持整数,只在最后乘一次 s_A·s_B。
20.6.2 为什么 int8×int8 要用 int32 累加
先算单个乘积的量级:两个 int8 的乘积,最坏情况是 127 × 127 = 16129。
再看累加:K 项求和的最坏情况是 K × 16129。K = 1024 时约 1.65 × 10⁷;K = 4096 时约 6.6 × 10⁷。
对照整数类型的表示范围:
| 类型 | 范围 | 能否容纳 K = 4096 的最坏累加 |
|---|---|---|
| int8 | [−128, 127] | 否,单个乘积就溢出 |
| int16 | [−32768, 32767] | 否,只能容纳 32767/16129 ≈ 2 项 |
| int32 | 约 [−2.1×10⁹, 2.1×10⁹] | 是,K 到约 13 万都安全 |
结论:int8×int8 必须用 int32 累加器。 用 int16 会溢出,用 int32 有充足余量(2.1×10⁹ / 16129 ≈ 130000 项)。
真实的量化误差是随机的、可正可负,实际和远小于最坏情况;但硬件设计必须按最坏情况定累加器位宽,否则会是偶发的、极难复现的数值错误。这也解释了 NVIDIA 的 INT8 Tensor Core 为什么规定累加器是 int32——这是被写入指令集语义的约束。
重要
记住这条链:int8 → int32 累加 → 最后乘 s_A·s_B 反量化。中间任何一步提前回到浮点,都会把带宽收益吐回去。
20.7 全整型量化矩阵乘的完整规则
【2026-09-02 编程题 2】 要求输入已量化的 A(m×k)与未量化的 B(k×n,按列给出),输出全整型量化的 C = A×B,每个元素输出三个整数 量化值 a b。
20.7.1 为什么可以全程走整数
把 A 的第 i 行与 B 的第 j 列的每个因子都写成"整数 × 缩放因子":
A[i][t] = Ā[i][t] · sᴀ_i,B[t][j] = B̄[t][j] · sʙ_j
代入矩阵乘的定义并提取公因子(同一行同一列的缩放因子在整个求和里是常量,可以提到求和号外):
| |
于是输出的三段信息各有明确出处:
| 输出位 | 来源 | 运算 |
|---|---|---|
| 量化值 | 纯整数点积 Σ Ā[i][t]·B̄[t][j] | 整数乘加(累加器 int32) |
| a | aᴀ_i × aʙ_j | 两个 a 相乘 |
| b | bᴀ_i + bʙ_j | 两个 b 相加 |
第二、三行正是 20.4.2 的乘法公式:s = a/2^b 相乘即"a 相乘、b 相加"。题面说明结果无需约简,所以量化值允许超过 127、b 允许超过 30。
20.7.2 对 B 的每一列做量化并拆分
| |
B 是按列给出的,所以"读一列立刻量化"最自然——这一点在 IO 层面也是必要的,因为要算 mx 必须先拿到整列。
20.7.3 half-up 舍入:一个会改变全部答案的细节
half-up(逢五进一) 指恰为 .5 时向上取整:round(63.5) = 64、round(64.5) = 65。
而多数语言内置的 round() 用的是银行家舍入(round-half-even):恰为 .5 时舍入到最近的偶数。于是 round(63.5) = 64(得偶数),但 round(64.5) = 64(而不是 65)、round(2.5) = 2(而不是 3)、round(0.5) = 0。也就是说:凡是恰好落在 .5 上的取值,两种舍入给出的结果可能相差 1。
为什么这个差别会毁掉整道题:a 与 b 都是由 s 经这个函数算出来的,a 差 1、b 差 1,输出的后两个整数全部对不上,而且 b 一变还会牵动 find_b 的返回值。判别与实现:
| |
20.7.4 一个可手算的算例
样例中 B 的两列分别是 (1.0, 2.0)(第 0 列)与 (3.0, 4.0)(第 1 列)。逐列走一遍:
| |
A 两行(题面给出)为:第 0 行量化值 [64, 127]、(a, b) = (65, 12);第 1 行量化值 [95, 127]、(a, b) = (65, 1)。
现在算 C[0][0]:
| |
算 C[1][1]:
| |
警告
手算时必须自己验一遍小数。s = 2/127 的真实值约为 0.0157480...,乘 2^12 得 64.50078...——严格大于 64.5,因此 half-up 得 65。这一步正是 【2026-09-02 编程题 2】 最容易翻车的地方:.5 附近的浮点误差、以及误用银行家舍入,都会让后两个输出整数整体偏移。在代码里不要试图"肉眼判断是不是恰好 .5",直接用 floor(x + 0.5) 并按题面口径实现。
20.7.5 量化对精度与吞吐的实际影响
| 方案 | 显存(相对 FP16) | 带宽需求 | 算力上限 | 典型精度影响 |
|---|---|---|---|---|
| FP16 / BF16 | 1× | 1× | 1× | 基准 |
| W8A8(INT8) | 约 1/2 | 约 1/2 | 约 2×(INT8 张量核) | 通常 < 0.5% 掉点 |
| W4A16 | 约 1/4 | 约 1/4(权重主导时) | 1×(仍走 FP16 算) | 需分组量化 + AWQ/GPTQ,可 < 1% |
| W4A8 / W4A4 | 约 1/4 | 约 1/4 | 高 | 需 QAT 或精细校准,风险较大 |
混合精度量化策略:并非所有层都同样耐受低精度。实践中通常保持高精度的部分包括:
- 第一层与最后一层(直接接触输入输出,误差影响大);
- LayerNorm / RMSNorm 与 Softmax(数值敏感,且本身访存占比低,量化收益小);
- 注意力分数计算(softmax 的输入精度直接影响分布形状);
- KV Cache 可按 FP8/INT8 存储,收益大且精度损失可控。
这与 20.5 的结论一致:量化收益来自矩阵乘,风险来自敏感算子——把收益大的地方压低精度、把风险大的地方保持高精度,就是混合精度的全部内容。
本章小结
| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| 数值格式 | 指数位宽定范围、尾数位宽定精度,两者不可兼得 | 认为 BF16 与 FP16 只是名字不同 |
| BF16 vs FP16 | BF16 指数 8 位(同 FP32,范围大),FP16 尾数 10 位(精度高、范围窄) | 认为 BF16 精度更高 |
| 线性量化 | q = round(r/s)、s = m/(2^(n−1) − 1) | 分母误用 2^n − 1 或 2^(n−1) |
| 对称 vs 非对称 | 权重对称(分布居中、实现简单),激活非对称(分布偏移,需 zero-point) | 对所有张量都用同一种方案 |
| 粒度 | 逐张量 < 逐通道 < 逐分组,越细越准、元数据越大 | 用逐张量量化 4 位权重 |
s = a/2^b | 乘法 → a 相乘、b 相加;加法 → 移位对齐后相加 | 忘记通分(对齐 b)就直接加 |
最大 b | s·2^b 单调递增 ⟹ 可行集合向下封闭 ⟹ 从上限下扫第一个可行者 | 试图二分,或误以为需要枚举 |
| 累加位宽 | int8×int8 最坏 127×127×K,必须 int32 累加 | 用 int16 累加 |
| PTQ vs QAT | PTQ 用校准集定 scale;QAT 插入伪量化 + STE | 认为 PTQ 不需要真实数据 |
| 反量化时机 | 尽量拖到最后,只乘一次 sᴀ·sʙ | 逐算子提前反量化,丢掉带宽收益 |
| half-up | .5 一律向上;round() 是银行家舍入,会给出不同答案 | 直接用内置 round |
自测题
Q1(推导)设某层权重的最大幅值为 m = 3.2,采用 8 位对称量化。求缩放因子 s,并说明当 r = 1.0 时反量化后的误差上界。
答案与解析
s = m / (2^(8−1) − 1) = 3.2 / 127 ≈ 0.025197。
q = round(1.0 / 0.025197) = round(39.687) = 40。
反量化:r̂ = s × q = 0.025197 × 40 = 1.00787。
误差 |r − r̂| = 0.00787,确实不超过 s/2 ≈ 0.0126 ✓
常见错误:把分母写成 128 得 s = 0.025,结果 q = 40、r̂ = 1.0——这个例子恰好看不出差别,但换一个 m 就会让最大元素算出 128 而溢出。不要用"对得上样例"来验证公式,要用边界条件验证。
Q2(推导)已知 f₁ = 3/2²、f₂ = 5/2³。分别用 20.4 的公式计算 f₁ × f₂ 与 f₁ + f₂,写出每一步的整数操作。
答案与解析
乘法(a 相乘、b 相加):
| |
直接验算:f₁ = 0.75、f₂ = 0.625,乘积 = 0.46875 ✓。整数操作:一次乘法、一次加法。
加法(对齐后相加,max(b₁,b₂) = 3):
| |
直接验算:0.75 + 0.625 = 1.375 ✓。整数操作:两次左移(其中一次移位量为 0)、一次加法、一次右移。
关键点:加法必须先通分。若直接写 (3+5)/2^(2+3) = 8/32 = 0.25,就完全错了——那是把加法当乘法处理,是本节最典型的错误。
Q3(判断)某工程师说:“我们用 round() 做量化,a 和 b 都从 s 算出来,两种舍入最多让结果差 1,不影响正确性。“请评价这个说法。
答案与解析
该说法错误,原因有三层:
- 误差会被点积放大:
a差 1 是缩放因子的相对误差约1/64 ≈ 1.6%;C的量化值是整数点积,本身量级可达2×10⁴,而上层再乘a时,1.6% 的误差会体现在最终数值上。 b会级联影响a:find_b的判据是half_up(s·2^b) ≤ 127。若银行家舍入把某个b的判定值从127.5舍成128(溢出)或反之,返回的b会差 1,进而a = half_up(s·2^b)也跟着变。这不是"差 1”,而是(a, b)整体错位。- 题目是规则实现题:
【2026-09-02 编程题 2】的判分是逐字符比对输出。规则写的是 half-up,就必须是 half-up。“偏差很小所以应该算对"在规则题里不成立。
正确做法:明确实现 half_up(x) = floor(x + 0.5),并不要依赖语言内置 round。若语言默认就是 half-up(如部分 C 运行时),也应在注释里写明依据。
Q4(计算)一个线性层的权重形状为 4096 × 4096,用 INT8 存储。问:(a) 单次前向读取权重需要多少字节?(b) 若 K = 4096,用 int8 累加会发生什么?(c) 用 int32 的安全余量是多少?
答案与解析
(a) 4096 × 4096 × 1 B = 16.8 MB。(作为对比,FP16 需要 33.6 MB,FP32 需要 67 MB。)
(b) 单个 int8 乘积最大 127 × 127 = 16129。int16 上限 32767,只能容纳 32767 / 16129 ≈ 2 项——第 3 项就可能溢出。int8 累加更是不可能(单个乘积就远超 127)。所以用 int8/int16 累加会得到静默的错误结果(溢出回绕),不报错、不崩溃,是最危险的一类 bug。
(c) int32 上限约 2.147×10⁹,除以最坏单项 16129 得约 133000 项——K = 4096 只用了约 3% 的余量。这个余量并非浪费:它要同时覆盖"多个通道累加"“bias 叠加"“量化误差带来的额外幅度”,且硬件必须按最坏情况设计,因为量化误差不可能提前预知。
补充理解:真实误差是随机符号的,K 项求和的绝对值通常按 √K 增长而非 K,实际占用远小于最坏情况。但不能用平均值来定硬件位宽。
Q5(辨析)某团队用 PTQ 把模型量化到 INT4,用一批 N(0,1) 随机噪声做校准,结果精度大幅下降。请指出问题所在,并给出两条改进方案。
答案与解析
问题:校准集的分布与部署时的输入分布完全不一致。PTQ 的全部信息都来自校准集:它决定了激活的观测范围 [r_min, r_max],进而决定 s。随机噪声给出的范围与真实激活差别很大——真实激活往往带有明显的离群通道,而这些通道正是决定 s 的关键。用噪声校准等于把 scale 定在错误的量级上,后面的量化误差自然无法控制。
改进方案:
- 换真实校准集:从训练集或部署日志里采几百条覆盖真实分布的样本。校准集必须覆盖部署分布,不必与训练集同分布。
- 提高粒度 + 保护离群值:从 per-tensor 改为 per-group(如 128 一组),并配 AWQ/GPTQ 这类方法——AWQ 的思路是识别出激活大的重要通道并加以保护,GPTQ 则利用二阶信息逐列地最小化量化误差。
- (可选)混合精度:对第一层/最后一层、Norm、Softmax 保持 FP16,把 4 位集中用在中间的线性层上。
一句话总结:PTQ 的精度上限由校准集质量与量化粒度共同决定;4 位量化几乎必然需要"细粒度 + 离群值治理"两者同时到位。
Q6(计算)对一列 [0.5, 2.0] 做 8 位量化并拆成 a/2^b,写出 (q, a, b)。b 的可行上界在哪一步被卡住?
答案与解析
| |
找 b:要求 half_up(s·2^b) ≤ 127。
| |
答案:q = [32, 127]、a = 65、b = 12。
b 在哪里被卡住:b = 13 时 a 的候选值约 129,超过了 int8 的上界 127,因此不可行。由于 s·2^b 单调递增,b ≥ 13 全部不可行,可行集合是 {0, …, 12},最大可行值就是 12。
注意 s·2^12 = 64.5008 严格大于 64.5,所以 half-up 得 65 而不是 64——这正是浮点误差最容易误导判断的地方。
与真题的关联
| 真题 | 考查角度 | 对应本章小节 |
|---|---|---|
【2026-09-02 编程题 2】 全整型量化矩阵乘 | 线性量化公式、s = a/2^b 的拆分与移位实现、half-up 舍入、整数点积与 a 相乘 b 相加 | 20.2、20.4、20.6、20.7 |
【2026-08-05 选择 Q13】 推理峰值显存构成 | 权重之外还有激活、临时工作区与多分支并存结果;量化正是压缩这些常数的直接手段 | 20.6.1、20.7.5 |
【2026-09-02 选择 Q4】 算子融合 | 逐元素算子访存受限,与量化"减少搬运字节"的目标一致 | 20.6.1 |
【2026-08-05 选择 Q20】 混合精度与训练稳定性 | BF16 的指数位宽与 FP32 相同,故动态范围大、不易溢出 | 20.1.1 |
【2026-09-09 选择 Q3】 KV Cache 显存公式 | KV Cache 也可量化(FP8/INT8),直接按公式比例压缩 | 20.7.5 |
【2026-09-04 编程题 1】 数据清洗与异常值处理 | 异常值检测(` | v−μ |
延伸阅读
- Jacob et al., Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference, CVPR 2018 —— 线性量化与 int32 累加的标准参考文献。
- Dettmers et al., LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale, NeurIPS 2022 —— 首次系统刻画 LLM 激活中的离群特征。
- Frantar et al., GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers, ICLR 2023;Lin et al., AWQ: Activation-aware Weight Quantization, MLSys 2024 —— PTQ 的两条主线。
- Xiao et al., SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models, ICML 2023 —— 激活到权重的难度迁移技巧。
- Micikevicius et al., FP8 Formats for Deep Learning, 2022 —— E4M3 与 E5M2 的设计取舍。
- 第 5 章(IEEE 754 与舍入模式)补充浮点位布局的细节;第 26 章(工程实现纪律)讨论 half-up 与银行家舍入的通用判别方法。