第 8 章 sub-8-bit 格式:FP8 / FP4 / NF4 / 三元
第 8 章 sub-8-bit 格式:FP8 / FP4 / NF4 / 三元
第 7 章的 LLM 量化把权重压到了 4bit,这已经是整数量化的主流极限。但再往下,还有一整个世界:FP8、FP4、NF4,甚至只有 1.58 bit 的三元网络。这些"sub-8-bit 格式"各有各的编码逻辑,也各有各的代价。这一章用原书仓库 ch8/ 目录的五个脚本(ch8_fp8_formats.py、ch8_fp4_blockscale.py、ch8_qlora_nf4.py、ch8_ternary.py、ch8_pareto_frontier.py),把它们逐个拆开。
8.1 FP8:浮点,但是 8 位
ch8_fp8_formats.py 研究的是 FP8 的两种格式:E4M3(4 位指数 + 3 位尾数)和 E5M2(5 位指数 + 2 位尾数)。它们不是整数,而是"更小号的浮点数"——保留了浮点数的动态范围优势。
# 来自 ch8/ch8_fp8_formats.py(核心)
class FP8Spec:
def __init__(self, exp_bits, mantissa_bits, bias, ...):
self.exp_bits = exp_bits
self.mantissa_bits = mantissa_bits
...
def enumerate_fp8_values(spec):
# 枚举 FP8 能表示的所有值,画数轴
...
def fp8_encode_decode_demo(spec):
# 编码 → 解码 往返,验证精度
...E4M3 vs E5M2 的取舍(对应 run_compare 实验):
- E4M3:4 位指数,动态范围中等,但 3 位尾数精度更高。适合权重和激活的前向传播(精度要求高)。
- E5M2:5 位指数,动态范围更大,但只有 2 位尾数。适合反向传播的梯度(梯度的动态范围变化大,但对精度的要求相对宽松)——这也是 NVIDIA 在 Hopper/Ada GPU 上 FP8 训练的默认搭配:前向 E4M3、反向 E5M2。
FP8 是"硬件原生的 sub-8-bit 格式"——NVIDIA H100 等 GPU 有 FP8 张量核,所以 FP8 的加速是真实的、不用模拟的。但 fp8_encode_decode_demo 也提醒你:FP8 毕竟只有 8 位,run_perplexity 会显示 FP8 量化大模型时的困惑度变化——它比 INT8 更接近 FP16 的表现(因为动态范围好),但依然有损失。
8.2 FP4:blockwise 缩放是救命稻草
ch8_fp4_blockscale.py 研究 FP4(E2M1,2 位指数 + 1 位尾数)。4 位浮点,每格只有 16 个可表示的数,直接 per-tensor 量化会惨不忍睹。FP4 能用的唯一原因是 blockwise scaling(分块缩放):
# 来自 ch8/ch8_fp4_blockscale.py(核心)
def quantize_block_fp4(tensor, block_size=16, ...):
# 1. 把张量切成 block(例如每 16 个元素一块)
# 2. 每块算一个 absmax 作为缩放因子
# 3. 块内元素除以缩放因子 → 量化到 FP4 → 乘回缩放因子
...为什么 per-tensor FP4 不行、blockwise FP4 就行:第 3 章的粒度教训在 4bit 浮点上被放大到极致。per-tensor 的单个 scale 无法同时照顾不同 block 的量级,blockwise 让每个局部区域有自己的 scale,才把 FP4 的精度拉到可用水平。这正是 NVIDIA 的 FP4 推理(Blackwell 架构)所用的方法——quantize_int4_per_tensor 和 quantize_block_fp4 的困惑度对比会直观展示这一点。
block size 的权衡:block 越小,缩放越精细、精度越高,但 scale 的数量越多、压缩率越低。
run_blockscale会扫不同 block size,画出精度 vs 压缩率曲线。和 INT4 group 量化(第 3.5 节)是同一套权衡逻辑,只是编码从整数换成了浮点。
8.3 NF4:QLoRA 的 4bit 秘密武器
ch8_qlora_nf4.py 研究 NF4(NormalFloat4)——QLoRA(Dettmers et al., 2023)用来把大模型微调成本打下来的 4bit 格式。NF4 不是均匀网格,也不是浮点,而是从正态分布分位数构造的非均匀码本。
# 来自 ch8/ch8_qlora_nf4.py(核心)
def build_nf4_codebook(offset=0.9677083):
"""从正态分布分位数构造 16 级 NF4 码本。"""
# 1. 正侧 8 级:分位数均匀分布在 (0.5, offset]
pos_quantiles = np.linspace(0.5, offset, 9)[1:] # 8 个值
pos_levels = norm.ppf(pos_quantiles) # 分位点 → 值
# 2. 负侧 7 级:分位数均匀分布在 [1-offset, 0.5)
neg_quantiles = np.linspace(1 - offset, 0.5, 8)[:-1]
neg_levels = norm.ppf(neg_quantiles)
# 3. 合并 + 归一化到 [-1, 1]
all_levels = np.sort(np.concatenate([neg_levels, [0.0], pos_levels]))
return all_levels / np.abs(all_levels).max()NF4 设计的精妙之处:
- 为什么用分位数:权重分布近似正态,把 16 级放在等概率的分位点上(每级覆盖相同的概率质量),量化误差期望最小。这是信息论意义上的最优。
- 非对称分格:7 负 + 0 + 8 正——正侧多一格。因为 absmax 归一化后权重分布略偏正,多一格给概率质量集中的地方(源码注释 #A 专门解释了这个故意的非对称)。
- 0 必须精确可表示:为了权重稀疏性,码本里必须有 0。
验证码本:源码里还有 bitsandbytes 的参考码本 BNB_NF4_CODEBOOK,自己构建的码本要和它对得上(run_codebook 实验)。
QLoRA 的完整流程(run_qlora 实验):把基础模型冻结成 NF4,插上少量可训练的 LoRA 低秩适配器,在低显存下微调大模型。run_memory 会算出:同样的 7B 模型,FP16 微调要 ~28GB 显存,NF4 + LoRA 只要 ~7GB——这就是 QLoRA 让普通人在单卡上微调大模型成为可能的秘密。
8.4 三元 1.58-bit:BitNet 的极端压缩
ch8_ternary.py 研究的是最极端的格式:三元(ternary)——每个权重只有三个值 {-1, 0, +1},理论上 1.58 bit/权重(log2(3))。
# 来自 ch8/ch8_ternary.py(核心)
def quantize_ternary_naive(w):
"""朴素三元:w_q = sign(w) * mean(|w|)。"""
scale = w.abs().mean()
return torch.sign(w) * scale
# 问题:无法产生 0 值 → 退化成二值网络!
def quantize_ternary_twn_per_tensor(w, threshold_factor=0.7):
"""TWN(Ternary Weight Networks):加阈值,让小权重归零。"""
abs_w = w.abs()
threshold = threshold_factor * abs_w.mean() # τ = 0.7 * E[|w|]
mask = abs_w > threshold # 超过阈值的才激活
alpha = abs_w[mask].mean() # 激活权重的均值作 scale
return torch.sign(w) * alpha * mask两个细节非常有意思:
- 朴素三元是陷阱:
sign(w) * mean(|w|)根本产生不了 0(除非权重精确为 0),实际退化成了二值网络,丢失了三元"可以稀疏化不重要权重"的核心优势(源码注释 #A 专门强调了这一点)。 - TWN 的阈值:
τ = 0.7 * E[|w|]是 Li et al. (2016) 证明的、对高斯权重在 MSE 目标下的最优阈值。低于阈值的权重被置 0——这是三元网络能够"剪枝"的机制。
脚本还实现了 BitNet b1.58 的 BitLinear 模块:
# 来自 ch8/ch8_ternary.py 的 BitNet 量化
def weight_quant_bitlinear(w, eps=1e-5):
"""BitNet b1.58 权重量化:absmean → round → clamp(-1, 1)。"""
scale = 1.0 / w.abs().mean().clamp(min=eps)
return (w * scale).round().clamp(-1, 1) / scale
def activation_quant_bitlinear(x, eps=1e-5):
"""BitNet b1.58 激活量化:per-token absmax INT8。"""
scale = 127.0 / x.abs().amax(dim=-1, keepdim=True).clamp(min=eps)
return (x * scale).round().clamp(-128, 127) / scaleBitNet b1.58 用 microsoft/bitnet-b1.58-2B-4T 做端到端验证——权重只有 {-1,0,+1},激活用 per-token INT8,run_perplexity 会告诉你这种极端压缩下困惑度会涨多少。结论通常很诚实:1.58-bit 大幅省显存省带宽,但质量损失显著,它更适合"专门为它训练的模型"(BitNet 系列是从头训练的,不是事后量化现有模型)。
8.5 Pareto 前沿:把所有格式放一张图上
ch8_pareto_frontier.py 做了一件非常有工程价值的事:在 OPT-6.7B 上,把这一章的所有格式(FP8-E4M3、FP8-E5M2、FP4-block、NF4、三元、INT8、INT4)放到同一张精度 vs 压缩率图上,画出 Pareto 前沿。
graph LR
A[所有格式] --> B[每个格式计算 困惑度 & 模型大小]
B --> C[画 Pareto 图]
C --> D[选出'不吃亏'的格式组合]怎么读 Pareto 前沿:图上的点,如果一个格式在"更小的模型 + 更低的困惑度"上都优于另一个,那后者就被前者支配(不在前沿上)。Pareto 前沿上的点,是"想再小一点就得牺牲精度、想再准一点就得牺牲压缩"的那些取舍。选型建议就藏在图里——你的硬件能承受多少显存、你的任务能接受多少困惑度损失,交点就是答案。
8.6 本章小结
| 格式 | 位宽 | 编码 | 典型用途 | 关键要点 |
|---|---|---|---|---|
| FP8 E4M3 | 8 | 浮点 | 前向传播 | 精度好,硬件原生 |
| FP8 E5M2 | 8 | 浮点 | 反向梯度 | 动态范围大 |
| FP4 blockwise | 4 | 浮点+分块缩放 | 推理 | 必须 blockwise 才可用 |
| NF4 | 4 | 正态分位码本 | QLoRA 微调 | 分布匹配,0 可表示 |
| Ternary | 1.58 | {-1,0,+1} | BitNet | 需阈值产生 0,专为它训练的模型 |
| INT8/INT4 | 8/4 | 整数 | 通用 | 第 3/7 章已详述 |
选型三句话:
- 有硬件支持就用硬件原生格式:FP8(H100+)、FP4(Blackwell+)的加速是真实的。
- 微调大模型用 NF4:QLoRA 是低显存微调的事实标准。
- 追求极限压缩才碰三元:1.58-bit 的代价是质量,且需要模型本身是为它设计的。
到这里,量化算法层面的内容就完整了。从第 9 章开始,我们把量化模型真正送进生产——部署管线、GGUF/llama.cpp、边缘移动端,最后是一个完整的 4bit LLM 服务器 + 端侧视觉管线案例。
作者: itech001 来源: 公众号:AI人工智能时代(the-ai-era) 网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top
本文首发于 AI人工智能时代,转载请注明出处。