动画图解 LLM 系统 · 2026-10-20

LLM 量化

用更少的位搬数据,把误差花在模型承受得住的地方

按 → 推进 · 底部时间轴可拖动 · F 全屏

01 · 从字节开始

同样一百亿个参数,位宽减半,权重字节就减半

16 bit 8 bit 4 bit 2 byte 1 byte ½ byte

一个 BF16 / FP16 权重占 2 byte;INT8 占 1 byte;4 bit 打包后平均占半个 byte。10B 权重的原始存储约为 20、10、5 GB。

十进制 GB;只算权重数据,不含 scale、zero-point、对齐、KV cache 与临时工作区。

工程 · 算清楚,再实现

量化前,先固定线性层的维度约定

X ∈ ℝᴹˣᴷ,W ∈ ℝᴺˣᴷ
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 读取”。

02 · 量化三步

先缩放,再取整,最后截到能存的范围

对称 4 bit 示例:整数范围 −7 … 7,scale s = 0.25 -1.75 -1.5 -1.25 -1.0 -0.75 -0.5 -0.25 0.0 0.25 0.5 0.75 1.0 1.25 1.5 1.75 x = 1.22 q = clamp(round(x / s), −7, 7) q = 5

把连续数轴变成有限格点:1.22 ÷ 0.25 = 4.88,四舍五入得到 q = 5。整数超出范围就 clamp 到边界;scale 决定一格有多宽。

此处采用对称的 −7…7 编码作教学示例;具体 INT4 kernel 的编码与 zero-point 约定可能不同。

工程 · 算清楚,再实现

zero-point 让整数零点对齐实数 0

q = clip(round(x/s) + z, qmin, qmax)
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。

未裁剪:|x−x̂| ≤ s/2
高分辨率、近似均匀误差时:E[e²] ≈ s²/12

s²/12 需要误差近似均匀等假设;比较实际 LLM 质量时仍要测真实数据、真实层输出与任务指标。

03 · 反量化

存的是 5,计算时按比例读成 1.25

q = 5 × scale 0.25 x̂ = 1.25 原值 1.22 量化误差 = 1.25 − 1.22 = +0.03

反量化 x̂ = s × q,把整数重新解释为近似浮点数。取整丢掉的信息不会回来,这里留下 +0.03 的误差;截断到边界时误差可能更大。

一般仿射量化还带 zero-point z:q = clamp(round(x/s) + z),x̂ = s(q − z)。

工程 · 算清楚,再实现

两个 INT4,怎样真正装进一个字节

q₀=−7 → 1001 q₁=+7 → 0111 packed byte = 0x00 十进制 0 低半字节放 q₀,高半字节放 q₁
byte = (q₀ & 0xF) | ((q₁ & 0xF) << 4)

−7 的 4-bit 补码为 9;7 左移 4 位得 112。9|112=121=0x79。解码时 u≥8 就取 u−16,恢复符号。

工程 · 算清楚,再实现

真正的整数点积,要扣掉 zero-point

y ≈ sₓs𝑤 Σₖ (qₓ,ₖ−zₓ)(q𝑤,ₖ−z𝑤)

取 qₓ=(6,3)、zₓ=4、sₓ=0.5;q𝑤=(5,2)、z𝑤=3、s𝑤=0.25。

整数累加 = (6−4)(5−3)+(3−4)(2−3)=5
y = 0.5×0.25×5 = 0.625

反量化后 x=(1,−0.5),w=(0.5,−0.25),浮点点积同样为 0.625。

常见 INT8 路径在 INT32 中累加,再乘 scale。分组 scale 不同的部分要分别缩放;不能先把所有组加完再随便乘一个 scale。

04 · 离群值

一个 8,拉大了所有人的刻度

一组权重 0.2 0.4 -0.3 0.6 8 共用 scale s = 8 / 7 ≈ 1.14,小数几乎都落在 0 附近 0 0 0 1 7

为了装下最大的 8,共用 scale 变成 8/7。0.2、0.4、−0.3 都舍入到 0。范围越大,同样位数的格点越粗,小值更容易被抹掉。

玩具权重;采用上一页的对称 −7…7 量化范围。

工程 · 算清楚,再实现

权重误差相同,对输出的影响可以差 100 倍

Δy = Δw · x
|Δ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()

这里优化的是教学线性层输出重构误差。部署前还要用独立评估集看困惑度、任务准确率和长上下文行为。

校准数据覆盖输入类型与长度;别用最终测试集挑参数,再把同一分数当成泛化效果。

05 · 更细的 scale

分组以后,小值重新有了自己的刻度

组 A:小值 组 B:大值 0.2 0.4 -0.3 0.6 s = 0.6 / 7 8 s = 8 / 7 0.171 0.429 −0.343 0.600 8.000

per-channel 或 per-group 量化,让一小片权重共用一个 scale。误差通常更小,但要多存 scale,也要让 kernel 高效读取它们。

人为分组与最近偶数舍入仅展示原理;真实矩阵按通道或固定 group 分组。异常通道也可保留更高精度。

工程 · 算清楚,再实现

group 越小越灵活,也越要多存元数据

每权重有效位数 = b + 8m/g

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 把激活的难点搬到权重

XWᵀ = (XS⁻¹)(WS)ᵀ,S=diag(s₁…sₖ)

激活第 j 通道特别大时,把 X 的该通道除以 sⱼ,权重对应列乘以 sⱼ;浮点输出保持不变。

sⱼ = max|Xⱼ|ᵅ / max|W[:,j]|¹⁻ᵅ

α 控制迁移程度:更大 α 更压低激活范围,同时让权重更难量化。使用校准数据选择折中,面向 W8A8 等路径。

等价变换只是起点。加入量化后是否变好,取决于新的 activation / weight 误差之和;scale 往往可融合进相邻算子。

06 · 数据走到计算单元

文件变小,只完成了一半

HBM:W4 打包 寄存器 / shared 矩阵乘 + 累加 输出激活 scale W4 W16 A16 Y 解包、反量化 常用更高精度累加

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,会吃掉很多带宽收益。

07 · 别把三份精度混在一起

只压缩权重,KV cache 不会跟着变小

一张卡上的显存账本 权重 W KV cache 激活 / 临时 W4A16 KV 仍可能是 BF16 W 的位宽、A 的位宽、KV 的位宽,是三个独立选择。

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,再检查解包、访存和算力限制。

08 · 计算精度

低位输入,不等于用低位把长串乘积加完

INT8 × INT8 → INT32 累加 → 乘 scale 2 × 3 6 -1 × 4 -4 3 × -2 -6 整数和 −4 × sₓ × s𝑤 → 近似浮点输出

矩阵乘是很多乘积求和。INT8 路径常用 INT32 累加,避免低位累加很快溢出,再按 scale 转回输出。浮点低精度 FP8 则用指数与尾数表示数值,编码和整数不同。

三个乘法为示意。支持的指令、累加类型与缩放粒度要和 GPU、模型及 kernel 一起选择。

工程 · 算清楚,再实现

GPTQ 的目标是保住层输出,而非逐个权重

把校准激活转置为 A=Xᵀ∈ℝᴷˣᴹ。对一个输出通道,原权重为 w,量化误差为 δ=ŵ−w。

损失 = ‖δA‖₂² = δ(AAᵀ)δᵀ
H = AAᵀ

H 的对角项描述通道能量,非对角项描述通道相关性。只对每个 w 四舍五入,相当于没有利用这些相关性。

完整 Hessian 是 2H;比例因子不影响下面的最优补偿。实际会加 λI 让病态或低秩矩阵更稳定。

工程 · 算清楚,再实现

量化一列后,把误差分摊给仍可修改的列

e = wⱼ−Q(wⱼ),要求 δⱼ=−e
最小化 δHδᵀ,得到 δ = −e·H⁻¹[j,:]/H⁻¹[j,j]

推法:约束目标加上 λ(δⱼ+e),求导得 2Hδᵀ+λeⱼ=0;用 δⱼ=−e 解出 λ,再代回。

逐列执行

当前列写成量化值;其余列先补偿,再接受下一列的量化误差。已经量化的列保持固定。

GPTQ 的工程实现

采用固定列顺序、分块更新与逆 Hessian 的 Cholesky 形式,减少逐元素串行操作和重复矩阵处理。

工程 · 算清楚,再实现

手算两维补偿:让损失从 0.16 降到 0.12

固定第一维 δ₁=−0.4;第二维仍可调 不补偿:δ=(−0.4,0) 损失 0.1600 补偿后:δ=(−0.4,δ₂) 0.1600 δ₂ = 0.0000 H = [[1, 0.5], [0.5, 1]]
L(δ₂)=0.16−0.4δ₂+δ₂²
∂L/∂δ₂=−0.4+2δ₂=0 → δ₂=0.2

这是量化当前列之后、下一列尚未量化时的条件最优值;下一列最终仍要落到自己的格点。

工程 · 算清楚,再实现

AWQ 用通道缩放保护更敏感的权重

W′ = WS,X′ = XS⁻¹
量化后的输出:X′ Q(W′)ᵀ

激活更大的输入通道通常更敏感。把相应权重列放大,再把激活反向缩小,可以降低该通道反量化后的有效步长。

搜索什么

从激活统计构造候选通道 scale;对各候选量化权重,再比较校准数据上的层输出误差。

为什么不能无限放大

组内最大值也可能被推高,使整组 scale 变粗,伤害其他通道。必须同时看整层重构误差。

AWQ 不是把少数权重永远留在 FP16;核心是等价缩放,让统一的低位格式更好地保留重要通道。

09 · 怎样量化一个真实模型

让校准数据告诉你:误差会在哪些地方放大

PTQ:训练后量化

用代表性数据跑过模型,观察权重与激活范围。

选择位宽、group size、scale 与 clipping;GPTQ、AWQ、SmoothQuant 分别从重构误差、重要权重和激活范围着手。

量化后比较任务质量与长上下文表现。

最后测真正的推理路径

测权重、KV 和临时空间总量。

分别看 prefill、单请求 decode 与高并发吞吐。

同样是 4 bit,不同 GPU、矩阵形状和 kernel,可能得到完全不同的时间收益。

练习 1 · 计算与分析

算出整数码、重构值与打包字节

使用对称窄范围 INT4,s=0.125,ties-to-even。量化 x=(−0.875,−0.3125,0.1875,0.875),两两打包,偶数索引放低半字节。
展开参考解答

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。

先自己算一遍,再展开解答。

练习 2 · 计算与分析

一层 4096×4096,INT4 实际省多少?

沿 K 每 128 个权重一组;每组只存一个 FP16 scale,没有 zero-point。忽略 padding,求权重、scale 总大小与相对 FP16 压缩倍数。
展开参考解答

参数数 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×。

先自己算一遍,再展开解答。

练习 3 · 计算与分析

相关性反号,补偿也会反号吗?

H=[[1,−0.5],[−0.5,1]]。第一维量化误差固定 δ₁=−0.4,第二维尚未量化。求最小化 δHδᵀ 的 δ₂ 和最小损失。
展开参考解答

代入得 L=0.16+0.4δ₂+δ₂²。导数 0.4+2δ₂=0,所以 δ₂=−0.2。L=0.16−0.08+0.04=0.12。相关性符号改变,最优补偿方向也改变。

先自己算一遍,再展开解答。

练习 4 · 计算与分析

文件缩小了,为什么 prefill 反而慢了?

教学 trace:BF16 prefill 为 20 ms;W4 路径先反量化到 HBM 用 7 ms,再调用同一个 FP16 GEMM 用 20 ms。模型文件小了约 4 倍,是否应该预期 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 验证补码解包。实验还网格枚举第二维,检查二次目标的最优补偿。

下载 lab.py

只需 Python 标准库。实验展示格式和重构目标,不实现完整 GPTQ/AWQ,也不代表 GPU kernel 性能。

参考与延伸阅读

把原理接回真实系统

继续:KV cache 压缩 · 混合精度 · Roofline