第 14 课:混合精度与综合面试

本课目标

完成后你需要能够:

  • 说出 FP32/FP16/BF16/FP8(e4m3, e5m2) 的位布局、范围与精度(epsilon),解释”范围”与”精度”的取舍。
  • 解释 FP16 训练失败的机制与三个修复:FP32 master weights、loss scaling、FP32 累积。
  • 说明 FP8 训练的做法(per-tile 量化)、难点(稳定性)与各方案的显存账本。
  • 独立完成一道”训练系统设计 + 精度选择”综合面试题。

核心概念

1. 现代浮点格式拓扑与动态范围 vs 精度权衡(如图 14-1 所示)

图 14-1:现代深度学习主流浮点数值格式位宽与动态范围对比

在现代大模型软硬件协同体系中,数值精度的选择直接决定了矩阵乘法单元(Tensor Core)的计算吞吐量与显存占用上限。 IEEE 754 标准浮点数由三个字段定义:符号位(Sign Bit ss)、指数位(Exponent ee)与尾数位(Mantissa / Fraction mm): v=(−1)s×2e−bias×(1+∑i=1mbi2−i)v = (-1)^s \times 2^{e - \text{bias}} \times \left(1 + \sum_{i=1}^m b_i 2^{-i}\right)

各主流精度的位布局与关键数值特性如下表所示:

格式名称 总位宽 符号位 指数位 (ee) 尾数位 (mm) 最大表示范围 (VmaxV_{\max}) 最小正规格数 (VminV_{\min}) 精度刻度(1.0 处的 ULP / ϵ\epsilon)
FP32 (单精度) 32 位 (4B) 1 8 位 23 位 ≈3.4×1038\approx 3.4 \times 10^{38} ≈1.18×10−38\approx 1.18 \times 10^{-38} 2−23≈1.19×10−72^{-23} \approx 1.19 \times 10^{-7}
FP16 (半精度) 16 位 (2B) 1 5 位 10 位 𝟔𝟓,𝟓𝟎𝟒\mathbf{65,504} ≈6.10×10−5\approx 6.10 \times 10^{-5} 2−10≈9.77×10−42^{-10} \approx 9.77 \times 10^{-4}
BF16 (Brain Float) 16 位 (2B) 1 8 位 7 位 ≈3.39×1038\approx 3.39 \times 10^{38} ≈1.18×10−38\approx 1.18 \times 10^{-38} 2−7≈7.81×10−32^{-7} \approx 7.81 \times 10^{-3}
FP8 E4M3 8 位 (1B) 1 4 位 3 位 448448 (E4M3FN) 2−6≈1.56×10−22^{-6} \approx 1.56 \times 10^{-2}(最小次正规数 2−9≈1.95×10−32^{-9} \approx 1.95 \times 10^{-3}) 2−3=0.1252^{-3} = 0.125
FP8 E5M2 8 位 (1B) 1 5 位 2 位 57,34457,344 ≈6.10×10−5\approx 6.10 \times 10^{-5} 2−2=0.252^{-2} = 0.25

深度工程洞见:为什么工业界全面拥抱 BF16 而淘汰原生 FP16?

  • FP16 的梯度下溢问题:FP16 仅有 5 位指数,其最小正规化数仅为 6.1×10−56.1 \times 10^{-5}。但在百亿/千亿大模型反向传播中,深层参数的梯度方差通常在 10−6∼10−810^{-6} \sim 10^{-8} 之间。这导致梯度在链式法则回传时极易直接截断下溢为零(Underflow),导致网络前浅层梯度更新完全消失。
  • BF16 的取舍哲学:Google 提出的 BF16 牺牲了 3 位尾数精度,直接借用了 FP32 完整的 8 位指数域。这意味着 BF16 的动态范围与 FP32 丝毫不差(覆盖超过 80 个数量级)。 在预训练中,梯度下溢和溢出的概率大幅降低,通常不再需要损失缩放(Loss Scaling),训练更稳定。

2. 混合精度训练的三大支柱与动态损失缩放

虽然低精度能带来巨大的速度加成,但直接在全网采用 16 位浮点更新必然导致训练发散崩溃。经典的 Mixed Precision 训练架构依靠三大支柱确保数值收敛与精度无损:

支柱一:FP32 主权重镜像(Master Weights)消灭更新吞噬(ULP Swallowing)

这是混合精度训练中最经典的物理现象:

  • 设当前参数权重量级为 w=1.0w = 1.0。在 BF16 格式下,1.0 处末位有效数字的单位精度(Unit in the Last Place, ULP)为 ΔULP=2−7≈7.81×10−3\Delta_{\text{ULP}} = 2^{-7} \approx 7.81 \times 10^{-3}。

  • 设当前学习率 η=10−4\eta = 10^{-4},优化器计算出的反向梯度为 g=0.5g = 0.5。单步实际参数更新量应为: Δw=−η⋅g=−5×10−5\Delta w = -\eta \cdot g = -5 \times 10^{-5}

  • 若直接在 BF16 上就地累加: wnew=1.0−0.00005→BF16 舍入1.0w_{\text{new}} = 1.0 - 0.00005 \xrightarrow{\text{BF16 舍入}} 1.0 因为更新量 Δw\Delta w 远小于 BF16 当前精度的半个 ULP,更新值在浮点舍入时被吸收。若每步都如此,参数不会变化,损失无法下降。

  • 解决方案:必须在优化器显存中常驻一份 FP32 Master 权重。前向与反向计算使用 BF16 算子跑高速 GEMM,但在 optimizer.step() 时,将梯度累加到 FP32 Master 权重上。FP32 的 ULP 高达 1.19×10−71.19 \times 10^{-7},足以精准记录微小的参数累积演化,步结时再截断投影回 BF16 供下一步使用。

支柱二:动态损失缩放(Dynamic Loss Scaling,如图 14-2 所示)

图 14-2:动态损失缩放(Dynamic Loss Scaling)溢出检测与自适应更新时序流

在不得不使用 FP16 的老旧硬件(如 V100)上,必须引入自适应损失缩放器(Loss Scaler):

  1. 反向前放大:在反向传播启动前,将标量损失乘以一个巨大的缩放系数 SS(如 S=215=32768S = 2^{15} = 32768):ℒscaled=ℒ×S\mathcal{L}_{\text{scaled}} = \mathcal{L} \times S;

  2. 链式法则伴随放大:反向传播计算出的所有梯度张量被等比放大 SS 倍,强行推入 FP16 的正规表示区间,规避下溢;

  3. 溢出检测与缩放退避:在优化器执行更新前,检查全网梯度是否包含 Inf 或 NaN。若发生溢出,说明 SS 过大导致高位被击穿,立即放弃本步更新,并将 SS 减半(S←S/2S \leftarrow S / 2);若连续 NN 步无溢出,则尝试增大缩放因子(S←S×2S \leftarrow S \times 2)。

支柱三:敏感算子的 FP32 强制累加与约简

Softmax 的分母指数累加 ∑exp⁡(zi)\sum \exp(z_i)、LayerNorm 的均值与方差统计量、以及梯度累积(Gradient Accumulation)跨步累加,必须强制升精至 FP32 计算,最后再 Cast 回低精度。

3. 前沿 FP8 训练范式与多精度显存账本(DeepSeek-V3 核心工程)

随着 NVIDIA Hopper(H100/H800)架构的普及,Tensor Core 提供了高达 BF16 两倍吞吐的 FP8 GEMM 计算能力。

A. FP8 混合搭配策略(E4M3 vs E5M2)

  • Transformer Engine 的 hybrid 方案:前向 GEMM 的激活与权重用 E4M3(3 位尾数,精度更高);反向的梯度用 E5M2(5 位指数,范围更大,适合动态范围大的梯度)。
  • DeepSeek-V3 的方案:所有 GEMM 输入(包括反向的梯度)统一使用 E4M3,用下面的细粒度分块缩放弥补动态范围不足,从而在所有张量上获得更高精度。

B. 细粒度分块量化(Tile-wise Fine-Grained Scaling)

FP8 的最大动态范围极窄(E4M3 仅 448)。若采用全张量(Tensor-wise)量化,异常离群点(Outliers)会使整张矩阵精度崩溃。 DeepSeek-V3 提出了细粒度分块量化:

  • 对激活张量按 1×1281 \times 128 瓦片(Token 内部特征向量)独立求缩放系数;
  • 对权重矩阵按 128×128128 \times 128 空间瓦片独立求缩放系数;
  • 配合专用的 FP8 算子,在 Hopper 架构上实现了几乎无损的 671B 庞大参数预训练。

C. 主流混合精度方案静态显存账本对比(每参数字节数 Bytes/param)

训练方案与精度配置 GEMM 精度 Master 权重 累积梯度 存储权重 存储梯度 优化器状态 (AdamW) 单参数静态总显存
传统 BF16(含 FP32 梯度累积) BF16 (2B) FP32 (4B) FP32 (4B) BF16 (2B) BF16 (2B) FP32 mm + FP32 vv (8B) 20 Bytes / param
标准 BF16(无 FP32 梯度累积) BF16 (2B) FP32 (4B) — (0B) BF16 (2B) BF16 (2B) FP32 mm + FP32 vv (8B) 16 Bytes / param
Transformer Engine FP8 FP8 (1B) — (0B) — (0B) FP32 (4B) FP32 (4B) FP32 mm + FP32 vv (8B) 16 Bytes / param
DeepSeek-V3 工业级 FP8 方案 FP8 (1B) FP32 (4B) FP32 (4B) FP8 (1B) BF16 (2B) BF16 mm + BF16 vv (4B) 15 Bytes / param
极致极简 FP8 (FP8-LM O3) FP8 (1B) FP16 (2B) FP16 (2B) FP8 (1B) FP8 (1B) FP8 mm + FP16 vv (3B) 9 Bytes / param

核心启示:计算精度(GEMM Precision)与存储精度(Storage Precision)是两个完全解耦的维度。FP8 训练的核心红利是直接将 Tensor Core 翻倍加速,而静态显存能否降低,完全取决于 Master 权重与优化器状态的格式抉择。

具体演示与数值推演

以一个在 BF16 精度下的模型参数更新步骤为例,设当前权重 w=1.0w = 1.0,反向求导计算所得的更新量为 Δ=1×10−3\Delta = 1 \times 10^{-3}(即 0.0010.001):

  • 在 BF16 下的舍入吸收: BF16 在基准点 1.01.0 处的尾数有效位为 7 位,其最小分辨精度为: ΔULP=2−7=1128≈0.0078125\Delta_{\text{ULP}} = 2^{-7} = \frac{1}{128} \approx 0.0078125 由于更新量 Δ=0.001<12ΔULP≈0.0039\Delta = 0.001 < \frac{1}{2} \Delta_{\text{ULP}} \approx 0.0039,在浮点数四舍六入五成双(Round to Nearest Even)规则下: bf16(1.0+0.001)≡1.0\text{bf16}(1.0 + 0.001) \equiv 1.0 本次更新没有生效。

  • 引入 FP32 Master 权重镜像: FP32 在 1.01.0 处的末位精度为: ΔULP_FP32=2−23≈1.192×10−7\Delta_{\text{ULP\_FP32}} = 2^{-23} \approx 1.192 \times 10^{-7} 更新量 Δ=0.001\Delta = 0.001 是其最小刻度的 83888388 倍。在 FP32 Master 权重上: wmaster=1.0+0.001=1.001000047…w_{\text{master}} = 1.0 + 0.001 = 1.001000047 \dots 单步更新无法在 BF16 中体现,但累积 4 步后 Master 权重为 1.0041.004,超过 1.0+12ΔULP=1.003906251.0 + \frac{1}{2}\Delta_{\text{ULP}} = 1.00390625,Cast 回 BF16 时按就近舍入得到 1.00781251.0078125。这就是混合精度中“低精度负责前向反向计算,高精度负责记账”的分工。

代码填空题

实现浮点格式属性计算 + 低精度更新吸收模拟 + 各方案显存账本。

Code
import torch

def float_props(exp_bits: int, mantissa_bits: int) -> tuple[float, float, float]:
    """
    返回 (max_value, min_normal, epsilon)。
    教学用 IEEE-like 常规格式(忽略 subnormal、NaN/Inf 编码与 E4M3FN 等具体 FP8 变体):
      bias      = 2^(exp_bits-1) - 1
      max_value = (2 - 2^-mantissa_bits) * 2^bias
      min_normal= 2^(1 - bias)
      epsilon   = 2^-mantissa_bits      (1.0 之后第一个可表示数)
    """
    bias = 2 ** (exp_bits - 1) - 1
    max_value = ______                  # 填空
    min_normal = ______                 # 填空
    epsilon = ______                    # 填空
    return max_value, min_normal, epsilon

FORMATS = {
    "fp32": (8, 23),
    "fp16": (5, 10),
    "bf16": (8, 7),
    "fp8_e4m3": (4, 3),
    "fp8_e5m2": (5, 2),
}

def show_formats():
    print(f"{'format':10s} {'max':>12s} {'min_normal':>12s} {'epsilon':>12s}")
    for name, (e, m) in FORMATS.items():
        mx, mn, eps = float_props(e, m)
        print(f"{name:10s} {mx:12.3e} {mn:12.3e} {eps:12.3e}")

def simulate_absorption(w: float, update: float, dtype: torch.dtype) -> tuple[float, float, bool]:
    """
    模拟低精度存储下的权重更新:w += update。
    返回 (更新前的低精度值, 更新后的低精度值, 是否被吸收)。
    用 torch 的低精度 dtype 直接做舍入(CPU 也支持 fp16/bf16)。
    """
    w_low = torch.tensor(w, dtype=dtype).float().item()
    w_new = (torch.tensor(w, dtype=dtype) + torch.tensor(update, dtype=dtype)).float().item()
    absorbed = ______                   # 填空:更新后与更新前是否相同
    return w_low, w_new, absorbed

def fp8_memory_ledger() -> dict[str, int]:
    """
    教材表格:各方案每参数 bytes 的分量(顺序:master / 累积梯度 / 权重 / 梯度 / 优化器状态)。
    BF16 基线(含 FP32 梯度累积)另有 GEMM 精度说明,这里只计存储分量。
    返回 {方案名: 每参数总 bytes}。
    """
    schemes = {
        "bf16_mp_no_fp32acc":   dict(master=4, acc_grad=0, w=2, g=2, opt=8),   # = 16
        "bf16_mp_fp32acc":      dict(master=4, acc_grad=4, w=2, g=2, opt=8),   # = 20
        "transformer_engine":   dict(master=0, acc_grad=0, w=4, g=4, opt=8),   # = 16
        "fp8lm_o3":             dict(master=2, acc_grad=2, w=1, g=1, opt=3),   # = 9
        "deepseek_v3":          dict(master=4, acc_grad=4, w=1, g=2, opt=4),   # = 15
        "nanotron_fp8":         dict(master=2, acc_grad=4, w=1, g=1, opt=2),   # = 10
    }
    totals = {}
    for name, comp in schemes.items():
        totals[name] = ______           # 填空:各分量求和
    return totals

if __name__ == "__main__":
    show_formats()

    print("\n权重更新吸收演示(w=1.0):")
    for dt, name in ((torch.float32, "fp32"), (torch.float16, "fp16"), (torch.bfloat16, "bf16")):
        for upd in (1e-3, 1e-5):
            before, after, absorbed = simulate_absorption(1.0, upd, dt)
            print(f"  {name:5s} update={upd:8.1e} -> w={after:.6f} 吸收={absorbed}")

    print("\n各方案每参数 bytes:")
    for name, total in fp8_memory_ledger().items():
        print(f"  {name:22s} {total} bytes")

三个问答题

Q1

BF16 与 FP16 同为 16 位,为什么 BF16 成为预训练默认?请用”范围”与”精度”两个维度回答,并解释为什么”BF16 范围与 FP32 相同”不能推出”BF16 可以替代 FP32”(给出 epsilon 对比)。什么场景下 FP16 反而比 BF16 合适(提示:数值都小且需要精度时)?

Q2

详细解释低精度权重更新被 ULP 吸收的机制,并说明为什么“变成 0 就永远回不来”并不准确。然后说明三个修复各自作用于链条的哪一环:FP32 master weights、loss scaling、FP32 累积。

Q3

综合面试题(口述/写作):某公司要在 8×H100 节点上训练 30B 稠密模型,seq=8192,目标 MFU ≥ 40%,预算只允许 1 个节点。请给出:(1) 并行配置与理由;(2) 精度方案(BF16 还是 FP8)与显存账本估算;(3) 若改用 FP8,哪些组件必须保持高精度,为什么;(4) 一个你会在训练前做的 benchmark/验证实验。请用本课程所有相关结论作答。

检查与通过标准

总分 10 分:代码正确 4 分(格式属性、吸收判断、账本求和)、三题各 2 分、通过线 8 分。

一票否决项:

  • 认为 BF16 精度与 FP32 相当。
  • 说不清 master weights 解决的具体失败机制(更新被 ulp 吸收)。
  • 认为 FP8 只是”更小的 BF16”,不知道需要量化与稳定性问题。
  • 综合题中给出自相矛盾的配置(如 TP 跨节点、gbs 公式错误、静态账本漏项)。

参考答案(仅 answer 分支)

先完成题目再核对;核对后必须解释关键公式,并改一个规模重新计算。

Code
import torch

def float_props(exp_bits: int, mantissa_bits: int) -> tuple[float, float, float]:
    """
    返回 (max_value, min_normal, epsilon)。
    教学用 IEEE-like 常规格式(忽略 subnormal、NaN/Inf 编码与 E4M3FN 等具体 FP8 变体):
      bias      = 2^(exp_bits-1) - 1
      max_value = (2 - 2^-mantissa_bits) * 2^bias
      min_normal= 2^(1 - bias)
      epsilon   = 2^-mantissa_bits      (1.0 之后第一个可表示数)
    """
    bias = 2 ** (exp_bits - 1) - 1
    max_value = (2 - 2 ** (-mantissa_bits)) * 2 ** bias                  # 填空
    min_normal = 2 ** (1 - bias)                 # 填空
    epsilon = 2 ** (-mantissa_bits)                    # 填空
    return max_value, min_normal, epsilon

FORMATS = {
    "fp32": (8, 23),
    "fp16": (5, 10),
    "bf16": (8, 7),
    "fp8_e4m3": (4, 3),
    "fp8_e5m2": (5, 2),
}

def show_formats():
    print(f"{'format':10s} {'max':>12s} {'min_normal':>12s} {'epsilon':>12s}")
    for name, (e, m) in FORMATS.items():
        mx, mn, eps = float_props(e, m)
        print(f"{name:10s} {mx:12.3e} {mn:12.3e} {eps:12.3e}")

def simulate_absorption(w: float, update: float, dtype: torch.dtype) -> tuple[float, float, bool]:
    """
    模拟低精度存储下的权重更新:w += update。
    返回 (更新前的低精度值, 更新后的低精度值, 是否被吸收)。
    用 torch 的低精度 dtype 直接做舍入(CPU 也支持 fp16/bf16)。
    """
    w_low = torch.tensor(w, dtype=dtype).float().item()
    w_new = (torch.tensor(w, dtype=dtype) + torch.tensor(update, dtype=dtype)).float().item()
    absorbed = w_new == w_low                   # 填空:更新后与更新前是否相同
    return w_low, w_new, absorbed

def fp8_memory_ledger() -> dict[str, int]:
    """
    教材表格:各方案每参数 bytes 的分量(顺序:master / 累积梯度 / 权重 / 梯度 / 优化器状态)。
    BF16 基线(含 FP32 梯度累积)另有 GEMM 精度说明,这里只计存储分量。
    返回 {方案名: 每参数总 bytes}。
    """
    schemes = {
        "bf16_mp_no_fp32acc":   dict(master=4, acc_grad=0, w=2, g=2, opt=8),   # = 16
        "bf16_mp_fp32acc":      dict(master=4, acc_grad=4, w=2, g=2, opt=8),   # = 20
        "transformer_engine":   dict(master=0, acc_grad=0, w=4, g=4, opt=8),   # = 16
        "fp8lm_o3":             dict(master=2, acc_grad=2, w=1, g=1, opt=3),   # = 9
        "deepseek_v3":          dict(master=4, acc_grad=4, w=1, g=2, opt=4),   # = 15
        "nanotron_fp8":         dict(master=2, acc_grad=4, w=1, g=1, opt=2),   # = 10
    }
    totals = {}
    for name, comp in schemes.items():
        totals[name] = sum(comp.values())           # 填空:各分量求和
    return totals

if __name__ == "__main__":
    show_formats()

    print("\n权重更新吸收演示(w=1.0):")
    for dt, name in ((torch.float32, "fp32"), (torch.float16, "fp16"), (torch.bfloat16, "bf16")):
        for upd in (1e-3, 1e-5):
            before, after, absorbed = simulate_absorption(1.0, upd, dt)
            print(f"  {name:5s} update={upd:8.1e} -> w={after:.6f} 吸收={absorbed}")

    print("\n各方案每参数 bytes:")
    for name, total in fp8_memory_ledger().items():
        print(f"  {name:22s} {total} bytes")

补全后的代码(关键填空处)

def float_props(exp_bits, mantissa_bits):
    bias = 2 ** (exp_bits - 1) - 1
    max_value = (2 - 2 ** (-mantissa_bits)) * 2 ** bias
    min_normal = 2 ** (1 - bias)
    epsilon = 2 ** (-mantissa_bits)
    return max_value, min_normal, epsilon

def simulate_absorption(w, update, dtype):
    ...
    absorbed = (w_new == w_low)     # 更新后被舍入回原值

def fp8_memory_ledger():
    totals[name] = sum(comp.values())   # 每方案各分量求和

输出:

format      max      min_normal   epsilon
fp32     3.403e+38    1.175e-38   1.192e-07
fp16     6.550e+04    6.104e-05   9.766e-04
bf16     3.390e+38    1.175e-38   7.812e-03
fp8_e4m3 2.400e+02    1.562e-02   1.250e-01
fp8_e5m2 5.734e+04    6.104e-05   2.500e-01

权重更新吸收演示(w=1.0):
fp32 update=1e-3 → 1.001000   吸收=False
fp32 update=1e-5 → 1.000010   吸收=False
fp16 update=1e-3 → 1.000977   吸收=False(ulp≈9.8e-4,勉强留下)
fp16 update=1e-5 → 1.000000   吸收=True
bf16 update=1e-3 → 1.000000   吸收=True(ulp≈7.8e-3 > 1e-3)
bf16 update=1e-5 → 1.000000   吸收=True

各方案每参数 bytes:
bf16_mp_no_fp32acc   16
bf16_mp_fp32acc      20
transformer_engine   16
fp8lm_o3              9
deepseek_v3          15
nanotron_fp8         10

三个问答题要点

Q1

  • BF16 指数位与 FP32 相同(8 位)→ 范围相同;但尾数只有 7 位 → 精度 ~1e-2(epsilon≈7.8e-3),比 FP16 的 ~1e-3 差约 10 倍;
  • 预训练默认 BF16 的原因:训练中梯度/权重动态范围大,FP16 范围窄(max 6.5e4、min_normal 6e-5)容易上下溢,需要 loss scaling 等额外机制;BF16 省掉范围问题,代价是精度——用 FP32 master weights + FP32 累积兜底;
  • “范围相同”≠“精度足够”:epsilon 决定小数分辨率,BF16 在 1 附近只能分辨 ~7.8e-3 的差异;
  • FP16 更合适的场景:数值范围已知且较小、需要更高精度时(如某些激活/量化中间表示),FP16 尾数更多。

Q2

  • 机制:低精度格式下 w 的 ulp 随 |w| 增大(如 w=1.0 时 BF16 ulp≈7.8e-3);更新量 Δ < ulp/2 时 w+Δ 舍入后不变 → 更新被吸收;若更新小于当前量级的半个 ULP,w+Δ 会舍入回原值;0 本身并非不可恢复,只要后续更新在目标格式中可表示,就能变为非零;
  • 修复:a) FP32 master weights:更新发生在 FP32(ulp~1e-7),再转回低精度用于前向——低精度只负责计算、FP32 负责记账;b) loss scaling:梯度远小于 1,先乘大系数再过 FP16 backward,之后除回——防梯度下溢;c) FP32 累积:求和/平均(loss、梯度累积、softmax 分母)在 FP32 做,防止中间溢出/精度损失。

Q3(综合题要点)

    1. 配置:单节点 8 卡有两种基本选择。a) TP=8(节点内 NVLink):此时 DP=1,ZeRO 没有 DP 维可分片,静态账本只靠 TP 切分,gbs 只能由 grad_acc 凑齐;b) DP=8 + ZeRO-3(FSDP),不用 TP:每层 All-Gather 参数,在 NVLink 上可用 prefetch 隐藏。也可取 TP=2/4 × DP=4/2 + ZeRO-3 折中。seq=8192 时 CP 会挤占 TP/DP 的卡数,单节点一般不开,激活靠 SP、重计算与 mbs=1 控制;
    1. 精度与账本:BF16 + Adam 静态账本 30e9 × 16 B = 480 GB(参数 60 GB、梯度 60 GB、master + m + v 共 360 GB)。上述 a) 或 b) 都把它均分到 8 卡,约 60 GB/卡,剩约 20 GB 给激活、通信缓冲与运行时,必须开重计算且 mbs=1,余量很小;FP8 方案(DeepSeek-V3 式 15 B/参数)静态约 450 GB / 8 ≈ 56 GB/卡,只省约 4 GB,主要收益在 GEMM 吞吐。若仍放不下,可把优化器状态卸载到 CPU 或改用 8-bit 优化器(会影响速度或数值,需要验证);
    1. FP8 必须保持高精度:master weights(FP32/BF16)、梯度累积(FP32)、部分归一化统计量;GEMM 输入/权重才用 FP8 并做 per-tile 量化;
    1. 训练前实验:单卡 memory profile(max_memory_allocated 曲线)+ micro-benchmark 各配置的 step time 与 MFU,验证 t_comm/t_compute(本配置以 TP 为主,用 TP 公式预判)。

通过要点

  • 混合精度的核心思想:低精度负责”计算”(快、省激活),高精度负责”记账”(master、累积、优化器状态);
  • FP8 的难点不是精度本身,而是动态范围小 → 必须量化(per-tile scaling)+ 稳定性管理;
  • 面试作答结构:范围/精度 → 失败机制 → 三个修复 → 账本 → 配置决策。

官方主参考