LLM 量化
用更少的位搬数据,把误差花在模型承受得住的地方
按 → 推进 · 底部时间轴可拖动 · F 全屏
同样一百亿个参数,位宽减半,权重字节就减半
一个 BF16 / FP16 权重占 2 byte;INT8 占 1 byte;4 bit 打包后平均占半个 byte。10B 权重的原始存储约为 20、10、5 GB。
十进制 GB;只算权重数据,不含 scale、zero-point、对齐、KV cache 与临时工作区。
量化前,先固定线性层的维度约定
Y = XWᵀ ∈ ℝᴹˣᴺ
| 维度 | 在推理中的含义 | 量化粒度示例 |
|---|---|---|
| M | 本次参与计算的 token 数 | activation 可按 token 取 scale |
| K | 输入通道 / reduction 维度 | weight 沿 K 每 g 个一组 |
| N | 输出通道 | 每行拥有自己的分组 scale |
W4A16 表示存储的权重 4 bit、激活 16 bit;它没有说明 KV 精度,也没有说明最终在哪种硬件指令上相乘。
4 bit 有 16 个编码,数值范围由格式决定
| 格式 | 可表示整数 | 需要约定的细节 |
|---|---|---|
| 无符号 INT4 | 0…15 | 常配合 zero-point 表示负数 |
| 有符号补码 INT4 | −8…7 | 最高位有负权重,解码时符号扩展 |
| 对称窄范围 INT4 | −7…7 | 本实验不用编码 −8 |
| FP4 / NF4 | 依格式定义的非均匀格点 | 不能套用同一整数反量化公式 |
先问清格式、scale、zero-point、分组轴和打包顺序,再谈“4 bit 模型能否被这个 kernel 读取”。
先缩放,再取整,最后截到能存的范围
把连续数轴变成有限格点:1.22 ÷ 0.25 = 4.88,四舍五入得到 q = 5。整数超出范围就 clamp 到边界;scale 决定一格有多宽。
此处采用对称的 −7…7 编码作教学示例;具体 INT4 kernel 的编码与 zero-point 约定可能不同。
zero-point 让整数零点对齐实数 0
x̂ = s(q−z)
教学例子:x 的范围 [−1,2],使用无符号 4 bit。s=(2−(−1))/15=0.2,z=5。
| x | q=round(x/0.2)+5 | 反量化 x̂ |
|---|---|---|
| −1 | 0 | 0.2×(0−5)=−1 |
| 0 | 5 | 0 |
| 0.6 | 8 | 0.6 |
| 2 | 15 | 2 |
z 通常取整数并限制在编码范围内;一般范围的端点不一定都精确命中。全零组要单独处理,避免 scale=0 后除零。
舍入误差有上界,裁剪误差可以大得多
范围内的舍入
若 x 没有被 clip,最近格点距离最多 s/2。取 s=0.125,x=0.1875 正好在 q=1 与 q=2 之间;ties-to-even 取 2,x̂=0.25。
范围外的裁剪
同样 s=0.125、q∈[−7,7],最大实数为 0.875。x=2 会被裁到 0.875,误差 1.125,远大于 s/2。
高分辨率、近似均匀误差时:E[e²] ≈ s²/12
s²/12 需要误差近似均匀等假设;比较实际 LLM 质量时仍要测真实数据、真实层输出与任务指标。
存的是 5,计算时按比例读成 1.25
反量化 x̂ = s × q,把整数重新解释为近似浮点数。取整丢掉的信息不会回来,这里留下 +0.03 的误差;截断到边界时误差可能更大。
一般仿射量化还带 zero-point z:q = clamp(round(x/s) + z),x̂ = s(q − z)。
两个 INT4,怎样真正装进一个字节
−7 的 4-bit 补码为 9;7 左移 4 位得 112。9|112=121=0x79。解码时 u≥8 就取 u−16,恢复符号。
真正的整数点积,要扣掉 zero-point
取 qₓ=(6,3)、zₓ=4、sₓ=0.5;q𝑤=(5,2)、z𝑤=3、s𝑤=0.25。
y = 0.5×0.25×5 = 0.625
反量化后 x=(1,−0.5),w=(0.5,−0.25),浮点点积同样为 0.625。
常见 INT8 路径在 INT32 中累加,再乘 scale。分组 scale 不同的部分要分别缩放;不能先把所有组加完再随便乘一个 scale。
一个 8,拉大了所有人的刻度
为了装下最大的 8,共用 scale 变成 8/7。0.2、0.4、−0.3 都舍入到 0。范围越大,同样位数的格点越粗,小值更容易被抹掉。
玩具权重;采用上一页的对称 −7…7 量化范围。
权重误差相同,对输出的影响可以差 100 倍
|Δy| ≤ ‖Δw‖₂ ‖x‖₂
教学例子:x=(10,0.1)。误差 Δw=(0.01,0) 导致 Δy=0.1;误差 Δw=(0,0.01) 只导致 Δy=0.001。
为什么要看激活
同样的权重绝对误差,乘上不同输入会被放大或缩小。仅最小化 ‖W−Ŵ‖,可能忽略重要通道。
还要看通道相关性
多个通道的误差会相加或抵消。校准数据的 XXᵀ 保存这些相关性,是二阶补偿的依据。
scale 的搜索,是舍入损失与裁剪损失的交换
把少量 outlier 裁掉,能缩小 s,让大量常见数分得更细;代价是 outlier 自身误差变大。
for clip_limit in candidate_limits:
scale = clip_limit / qmax
W_hat = dequant(quant(W, scale), scale)
loss = mean_square(X @ W.T - X @ W_hat.T)
choose_limit_with_smallest_loss_on_calibration_data()
这里优化的是教学线性层输出重构误差。部署前还要用独立评估集看困惑度、任务准确率和长上下文行为。
校准数据覆盖输入类型与长度;别用最终测试集挑参数,再把同一分数当成泛化效果。
分组以后,小值重新有了自己的刻度
per-channel 或 per-group 量化,让一小片权重共用一个 scale。误差通常更小,但要多存 scale,也要让 kernel 高效读取它们。
人为分组与最近偶数舍入仅展示原理;真实矩阵按通道或固定 group 分组。异常通道也可保留更高精度。
group 越小越灵活,也越要多存元数据
b 是权重位宽,m 是每组 metadata 字节,g 是组大小。取 INT4、每组一个 FP16 scale:m=2。
| g | bits/weight | 相对 FP16 压缩倍数 |
|---|---|---|
| 32 | 4+16/32=4.5 | 3.5556× |
| 128 | 4+16/128=4.125 | 3.8788× |
| 256 | 4+16/256=4.0625 | 3.9385× |
有 zero-point、padding 或分组尾部时继续加账。真实文件与显存还含未量化层、索引、对齐与 workspace。
SmoothQuant 把激活的难点搬到权重
激活第 j 通道特别大时,把 X 的该通道除以 sⱼ,权重对应列乘以 sⱼ;浮点输出保持不变。
α 控制迁移程度:更大 α 更压低激活范围,同时让权重更难量化。使用校准数据选择折中,面向 W8A8 等路径。
等价变换只是起点。加入量化后是否变好,取决于新的 activation / weight 误差之和;scale 往往可融合进相邻算子。
文件变小,只完成了一半
W4A16 里,权重以 4 bit 搬进芯片,kernel 在局部解包、按 scale 还原,再与 16 bit 激活计算。把整份权重先展开回 HBM,会吃掉搬运上的节省。
图中采用 W4 → W16 的示意路径;实际 kernel 取决于硬件。W8A8、FP8 等可使用对应低精度矩阵指令。
W4A16 kernel 的真实工作不止“少读 4 倍”
| 阶段 | 执行动作 | 观察什么 |
|---|---|---|
| HBM → 片上 | 搬 packed weights、scales、activation | 读取字节与 cache 命中 |
| 解包与反量化 | 移位、符号扩展、减 z、乘 scale | 指令数量、寄存器压力 |
| 矩阵乘法 | 按 kernel 支持的精度执行 MMA | 形状、对齐、Tensor Core 利用率 |
| 输出 | 缩放、bias / activation 融合、写回 | 中间张量与额外 launch |
一种常见路径是在片上把 W4 转成 FP16 再乘 A16。若先把整个矩阵反量化写回 HBM,再调用普通 GEMM,会吃掉很多带宽收益。
只压缩权重,KV cache 不会跟着变小
W4A16 表示权重 4 bit、激活 16 bit。长上下文或大量并发时,KV cache 仍可能占显存的大头;量化 KV 需要单独的格式、scale 与 attention kernel 支持。
条形长度为示意,并非某一模型的实际显存占比。
能否装下模型,要把权重和 KV 分开算
| 教学模型与请求 | 估算方式 | 大小 |
|---|---|---|
| 7B 参数,BF16 权重 | 7×10⁹×2 B | 约 13.04 GiB |
| W4,g=128,FP16 scale | 7×10⁹×4.125/8 B | 约 3.36 GiB |
| 单请求 BF16 KV | 2×32 层×8 KV heads×128×32768×2 B | 4 GiB |
| 临时空间预留 | 假设 2 GiB | W4 合计约 9.36 GiB |
这里忽略未量化层、额外对齐与分配碎片;真实分配会更大。把权重改成 W4 不会自动把 4 GiB 的 KV 也改成 INT4。
先算带宽下界,再解释为什么实测没到它
教学设定:7B dense 权重,每个 decode step 全部读取;有效带宽 900 GB/s,暂不计 KV。
| 路径 | 权重读取 | 带宽下界 |
|---|---|---|
| BF16 | 14 GB | 14/900≈15.56 ms |
| W4 g=128 | 3.609375 GB | 3.609375/900≈4.01 ms |
| W4 另有 2 ms 开销 | 解包 / 同步等教学假设 | 合计约 6.01 ms |
按这个账本,端到端加速约 15.56/6.01=2.59×。batch 增大后权重可被更多 token 复用,计算占比上升,收益还会变化。
实际应分别测 prefill、decode 与不同 batch;确认真的调用量化 kernel,再检查解包、访存和算力限制。
低位输入,不等于用低位把长串乘积加完
矩阵乘是很多乘积求和。INT8 路径常用 INT32 累加,避免低位累加很快溢出,再按 scale 转回输出。浮点低精度 FP8 则用指数与尾数表示数值,编码和整数不同。
三个乘法为示意。支持的指令、累加类型与缩放粒度要和 GPU、模型及 kernel 一起选择。
GPTQ 的目标是保住层输出,而非逐个权重
把校准激活转置为 A=Xᵀ∈ℝᴷˣᴹ。对一个输出通道,原权重为 w,量化误差为 δ=ŵ−w。
H = AAᵀ
H 的对角项描述通道能量,非对角项描述通道相关性。只对每个 w 四舍五入,相当于没有利用这些相关性。
完整 Hessian 是 2H;比例因子不影响下面的最优补偿。实际会加 λI 让病态或低秩矩阵更稳定。
量化一列后,把误差分摊给仍可修改的列
最小化 δHδᵀ,得到 δ = −e·H⁻¹[j,:]/H⁻¹[j,j]
推法:约束目标加上 λ(δⱼ+e),求导得 2Hδᵀ+λeⱼ=0;用 δⱼ=−e 解出 λ,再代回。
逐列执行
当前列写成量化值;其余列先补偿,再接受下一列的量化误差。已经量化的列保持固定。
GPTQ 的工程实现
采用固定列顺序、分块更新与逆 Hessian 的 Cholesky 形式,减少逐元素串行操作和重复矩阵处理。
手算两维补偿:让损失从 0.16 降到 0.12
∂L/∂δ₂=−0.4+2δ₂=0 → δ₂=0.2
这是量化当前列之后、下一列尚未量化时的条件最优值;下一列最终仍要落到自己的格点。
AWQ 用通道缩放保护更敏感的权重
量化后的输出:X′ Q(W′)ᵀ
激活更大的输入通道通常更敏感。把相应权重列放大,再把激活反向缩小,可以降低该通道反量化后的有效步长。
搜索什么
从激活统计构造候选通道 scale;对各候选量化权重,再比较校准数据上的层输出误差。
为什么不能无限放大
组内最大值也可能被推高,使整组 scale 变粗,伤害其他通道。必须同时看整层重构误差。
AWQ 不是把少数权重永远留在 FP16;核心是等价缩放,让统一的低位格式更好地保留重要通道。
让校准数据告诉你:误差会在哪些地方放大
PTQ:训练后量化
用代表性数据跑过模型,观察权重与激活范围。
选择位宽、group size、scale 与 clipping;GPTQ、AWQ、SmoothQuant 分别从重构误差、重要权重和激活范围着手。
量化后比较任务质量与长上下文表现。
最后测真正的推理路径
测权重、KV 和临时空间总量。
分别看 prefill、单请求 decode 与高并发吞吐。
同样是 4 bit,不同 GPU、矩阵形状和 kernel,可能得到完全不同的时间收益。
算出整数码、重构值与打包字节
展开参考解答
x/s=(−7,−2.5,1.5,7),舍入到偶数得 q=(−7,−2,2,7)。重构为 (−0.875,−0.25,0.25,0.875)。补码 nibble 为 (9,14,2,7),字节为 0xE9、0x72。最大误差 0.0625=s/2。
先自己算一遍,再展开解答。
一层 4096×4096,INT4 实际省多少?
展开参考解答
参数数 4096²=16,777,216。INT4 数据为 8,388,608 B=8 MiB;组数 131,072,scale 为 262,144 B=0.25 MiB。总计 8.25 MiB。FP16 为 32 MiB,压缩比 32/8.25≈3.8788×。
先自己算一遍,再展开解答。
相关性反号,补偿也会反号吗?
展开参考解答
代入得 L=0.16+0.4δ₂+δ₂²。导数 0.4+2δ₂=0,所以 δ₂=−0.2。L=0.16−0.08+0.04=0.12。相关性符号改变,最优补偿方向也改变。
先自己算一遍,再展开解答。
文件缩小了,为什么 prefill 反而慢了?
展开参考解答
这条路径至少要 27 ms,反而是 1.35× 的耗时。它重新写出了完整 FP16 权重,GEMM 也没变快。先检查是否有支持当前形状的融合 W4 kernel,再评估大 M 下的计算瓶颈;文件体积不能直接推出延迟收益。
先自己算一遍,再展开解答。
从字节到线性层误差,跑完一条小实验
python3 lab.py
python3 lab.py --group-size 32
默认输出:INT4 codes=[−7,−2,2,7];packed hex=e972;4.125 bits/weight;补偿目标 0.1600 → 0.1200;最后 PASS。
动手改数值
加入越界值观察 s/2 上界失效;改 group size 计算元数据开销;把 H 的相关性改为负数并预测补偿方向。
独立检查
手写字节 0x79、0xF8 验证补码解包。实验还网格枚举第二维,检查二次目标的最优补偿。
只需 Python 标准库。实验展示格式和重构目标,不实现完整 GPTQ/AWQ,也不代表 GPU kernel 性能。
把原理接回真实系统
-
GPTQ · Accurate Post-Training Quantization for Generative Pre-trained Transformers
利用近似二阶信息控制权重量化的输出误差。 -
AWQ · Activation-aware Weight Quantization
依据激活统计保护重要权重,面向低位部署。 -
SmoothQuant · Accurate and Efficient Post-Training Quantization for LLMs
将激活中的量化难点平滑到权重,服务 W8A8 推理。 -
KIVI · A Tuning-Free Asymmetric 2bit Quantization for KV Cache
KV 的分布与量化粒度需要单独设计。
继续:KV cache 压缩 · 混合精度 · Roofline