为什么 FP8 微调不能只看”训练跑起来了”
FP8 常被描述成一次简单的精度开关——把 BF16 换成 FP8,就能获得更高吞吐和更低显存占用。这个理解忽略了训练与推理量化之间最关键的差别。
推理权重量化主要关心静态权重压缩后是否还能保持输出质量;而训练中的激活、权重和梯度分布会持续变化,缩放因子必须跟随数据和优化过程动态更新。一次异常 Batch、少数离群值、不同 Rank 的尺度不一致,或者某个小矩阵的转换开销,都可能让 FP8 的收益消失,甚至产生不容易被总体 Loss 发现的静默退化。
因此,FP8 微调上线的真正目标不是”所有层都启用 FP8”,而是建立一套可审计的低精度训练策略:
- 哪些算子进入 FP8,哪些保持 BF16
- E4M3、E5M2 和不同 Scaling Recipe 如何选择
- Amax 和 Scale 是否在合理范围内
- 分布式 Rank 是否使用一致尺度
- 发生饱和、下溢或能力退化时如何局部回退
- FP8 制品是否通过与 BF16 基线一致的发布门禁
核心原理:FP8 的关键不是 8 位,而是动态范围管理
E4M3 与 E5M2 承担不同职责
FP8 常见的两种格式是 E4M3 和 E5M2:
| 格式 | 指数位 | 尾数位 | 特点 | 典型用途 |
|---|---|---|---|---|
| E4M3 | 4 | 3 | 精度更高,动态范围较小 | 前向权重与激活 |
| E5M2 | 5 | 2 | 动态范围更大,精度较低 | 反向梯度 |
在典型混合配方中,NVIDIA Transformer Engine 将 E4M3 + E5M2 组合定义为 Format.HYBRID。但这并不意味着所有张量都应强制使用同一格式——Embedding、Normalization、Loss、LM Head、极小 Linear 和数值敏感模块通常需要单独验证,必要时保持 BF16。
Scale 决定数值是否进入可表示区间
高精度张量转换成 FP8 前,需要根据张量幅值计算缩放因子。Transformer Engine 的 Delayed Scaling 默认形式可概括为:
scale = (FP8_MAX / amax) / (2 ^ margin)
其中:
- amax:张量绝对值最大值
- FP8_MAX:目标格式可表示的最大值
- margin:为异常波动预留安全空间
Scale 太大,离群值容易饱和;Scale 太小,大量小值会被压到零附近。生产治理需要关注的是缩放质量,而不只是 Loss 曲线。
Delayed Scaling 用历史换取稳定性
Delayed Scaling 不直接使用当前迭代计算出的 Scale,而是记录一段 Amax History,根据历史窗口选出代表值,再供下一次迭代使用。
常见策略对比:
| 策略 | 行为 | 优势 | 风险 |
|---|---|---|---|
max | 使用窗口内最大 Amax | 降低突发溢出风险 | 普通 Batch 有效精度下降 |
most_recent | 使用最近一次 Amax | 响应更快 | 对离群 Batch 更敏感 |
| 自定义(分位数/截尾) | 按需设计 | 灵活适配 | 必须有充足回放验证 |
Amax History 长度不是越长越好。窗口过长会让一次历史峰值长期压低后续张量的有效精度;窗口过短则容易随 Batch 抖动。应依据数据分布、梯度累积、序列长度和训练阶段做实验。
分布式训练必须治理 Scale 一致性
FSDP、Tensor Parallel 或其他分布式训练会把权重和激活切分到多个 Rank。若各 Rank 独立维护 Amax 和 Scale,相同逻辑张量可能使用不同量化尺度,影响单卡与多卡语义一致性。
Transformer Engine 默认可对 Amax 做分布式归约,TorchTitan 的 Float8 文档也强调在 FSDP 和 TP 场景中需要通信全局 max(abs)。若为了性能关闭 Amax 归约,就必须把本地 Scale 视为训练状态的一部分:所有 Rank 都要进入检查点,恢复时也要保持正确映射。
工程落地:从全局开关改成逐层准入
第一步:建立 BF16 可复现基线
在启用 FP8 前,先冻结一条 BF16 基线:
- 模型和 Tokenizer 指纹
- 数据集版本、采样顺序和 Chat Template
- Optimizer、Scheduler、Gradient Clipping 和随机种子
- 每步 Token 数、梯度累积和有效 Batch Size
- Loss、梯度范数、吞吐、显存和关键能力集结果
- 固定步骤的 Checkpoint 与生成样本
没有可复现的 BF16 基线,FP8 的性能收益和质量变化就无法归因。
第二步:只转换收益明确的矩阵层
推荐从 Transformer 中尺寸较大的 Linear 层开始,逐步扩展:
- Attention 的 Q/K/V 和输出投影
- MLP 的上投影、门控投影和下投影
- 经过实测后再扩展到其他模块
初始阶段的 BF16 保留清单应包括:
- Embedding 与 LM Head
- RMSNorm、LayerNorm 和最终 Loss
- 尺寸过小、转换开销高于 Tensor Core 收益的 Linear
- 历史上 Amax 波动显著或易出现梯度异常的层
- 自定义算子及尚未验证的融合 Kernel
层级白名单应记录模块 FQN、Shape、Recipe 和回退原因,作为训练制品的一部分。
第三步:固定 Recipe,禁止运行中静默漂移
生产配置中应把以下字段写入不可变 Manifest:
precision_policy:
base_dtype: bf16
fp8_format: hybrid
scaling: delayed
amax_history_len: 16
amax_compute_algo: max
margin: 0
reduce_amax: true
include_modules:
- "*.self_attn.q_proj"
- "*.self_attn.k_proj"
- "*.self_attn.v_proj"
- "*.mlp.*_proj"
exclude_modules:
- "embed_tokens"
- "lm_head"
- "*.norm"
升级 Transformer Engine、PyTorch、CUDA、cuDNN、GPU 型号或并行拓扑时,必须重新跑数值与吞吐回放。不要把 FP8 Recipe 当成与运行环境无关的普通超参数。
第四步:建立 Amax 与低精度异常画像
每个被转换模块至少应输出以下分层指标:
| 指标类别 | 具体指标 |
|---|---|
| 缩放状态 | Amax 当前值及历史分位数、Scale 与步间变化率 |
| 量化质量 | 接近 FP8 上限的值占比、量化后零值比例 |
| 梯度健康 | 输入/权重/梯度范数、NaN/Inf/Overflow/跳步次数 |
| 精度对比 | FP8 与 BF16 抽样输出的相对误差 |
| Kernel 效率 | 每层 FP8 Kernel 命中率与回退率 |
建议使用三个等级的告警:
| 等级 | 触发条件 | 动作 |
|---|---|---|
| 观察 | Scale 抖动、零值比例上升,但 Loss 和能力集正常 | 记录并持续监控 |
| 降级 | 连续多窗口饱和、异常梯度或 FP8/BF16 差分扩大 | 局部切换 BF16 |
| 停止 | NaN、不可恢复发散、多 Rank 不一致或关键能力跌破硬门槛 | 暂停训练并排查 |
第五步:实现按层 BF16 回退,而不是全局二选一
精度策略应允许不同层使用不同 Recipe。Transformer Engine 支持在同一训练中嵌套多个 autocast 区域。工程上可以维护一份动态但可审计的回退表:
precision_overrides:
"model.layers.17.mlp.down_proj":
dtype: bf16
reason: "persistent saturation"
activated_at_step: 18240
"model.layers.29.self_attn.o_proj":
dtype: bf16
reason: "capability regression isolation"
activated_at_step: 20100
回退动作必须同时记录:触发指标、阈值、步骤、数据版本、前后 Checkpoint 和恢复结论。局部 BF16 回退后,应观察吞吐损失是否可接受,并重新计算整体训练 ETA。
第六步:用双轨评测判断”收敛是否等价”
FP8 不能只与自己的上一版本比较,至少需要三条轨道:
- BF16 控制组:同数据、同 Token 数和相同优化参数
- FP8 候选组:目标 Recipe 和层级白名单
- FP8 回退组:对高风险层保持 BF16,用于定位退化来源
对比维度:
- 相同步数与相同训练 Token 下的 Loss
- 梯度范数和更新范数分布
- 关键能力集、格式遵循、拒答与安全指标
- 长短序列、不同语言和难例分桶
- 训练吞吐、显存峰值、通信开销和有效 Token/秒
- Checkpoint 恢复后的指标连续性
不要把单个最终分数当成等价证明。FP8 可能总体平均分接近 BF16,却在代码、多语言、长上下文或低频格式任务上发生局部退化。
适用场景
适合 FP8 微调的场景:
- H100、H200、Blackwell 等具备相应低精度硬件能力的平台
- 以大尺寸 Linear GEMM 为主要开销的 SFT、持续预训练或大规模对齐训练
- 有稳定 BF16 基线、能力评测集和分层可观测性的团队
- 训练 Token 量足够大,能摊薄转换、Scale 计算和编译成本
- 需要与 FSDP、TP 或其他分布式策略组合,且能治理 Scale 通信
应谨慎的场景:
- 数据量很小、训练时间很短的 LoRA 微调
- 大量小矩阵、自定义算子或频繁动态 Shape
- 没有 BF16 对照、无法复现数据顺序或缺少能力回归集
- 运行环境频繁更换 GPU、CUDA、框架与 Kernel 版本
- 只允许观察最终 Loss,无法采集分层数值指标
常见误区
误区一:硬件支持 FP8,就能自动获得加速
硬件支持只是必要条件。小矩阵、频繁 Cast、Scale Kernel、通信和未融合算子可能抵消收益。需要按模块测量端到端 Token/秒,而不是只看某个 GEMM 微基准。
误区二:Loss 没发散,就说明质量没有问题
Loss 对局部能力退化并不敏感。格式遵循、多语言、长序列和代码能力需要独立回归,分层 Amax 和梯度指标也不能省略。
误区三:Amax History 越长越稳定
长窗口可以减少抖动,但历史离群值会持续影响 Scale,使多数普通值失去有效精度。窗口长度必须基于数据和训练阶段验证。
误区四:所有 Rank 独立计算 Scale 更快,也不会影响结果
局部 Scale 可能破坏分布式单设备语义。若关闭 Amax 归约,必须明确接受数值差异,并完整保存每个 Rank 的本地量化状态。
误区五:出现异常只能整模型回退 BF16
逐层白名单和局部回退通常能保留大部分性能收益。真正需要的是可定位、可审计的精度策略,而不是全局开关。
上线检查清单
环境与制品
- GPU、驱动、CUDA、cuDNN、PyTorch 和 Transformer Engine 版本已固定
- FP8 Recipe、层级白名单和排除列表进入版本控制
- 模型、Tokenizer、数据集和 Chat Template 指纹已记录
- FP8 相关 Scale/Amax 状态能随 Checkpoint 正确恢复
- 不同 World Size 和并行拓扑的恢复语义已验证
数值与质量
- BF16 控制组可复现
- Amax、Scale、零值率、饱和率、梯度范数按层可观测
- NaN、Inf、Overflow 和异常跳步有硬门禁
- 核心能力集完成 FP8/BF16 双轨差分
- 长度、语言、领域和难度分桶没有显著回退
- 局部 BF16 回退流程已演练
性能与运维
- 统计的是端到端有效 Token/秒,而非单算子理论峰值
- Scale 计算、Cast、编译和分布式 Amax 通信成本已计入
- 小矩阵和收益不足模块已自动或人工过滤
- 故障后恢复不会重置或错配 Scale History
- 发布 Manifest 可还原完整精度策略
- 训练停止、降级和恢复阈值已经审批
参考资料
- NVIDIA Transformer Engine — Using FP8 and FP4 with Transformer Engine
- NVIDIA Transformer Engine — Common API: DelayedScaling / Current Scaling / MXFP8
- PyTorch AO — torchao.float8 API
- PyTorch TorchTitan — Float8 Training
- Micikevicius et al. — FP8 Formats for Deep Learning
- Liang et al. — TorchTitan: One-stop PyTorch Native Solution for Production Ready LLM Pre-training