你有没有见过大型餐厅的后厨?主厨手上有一本精确到克的主菜谱(一份精心记录的“黄金配方”),但真正出菜时,切配、焯水、颠勺这些环节却由帮厨用最快的速度完成——速度优先,细节容后再调。大模型训练也是这么一回事:一群“数字”在后厨里忙得不可开交,有些步骤需要极高的精确度(FP32),有些步骤则完全可以“差不多就行”(FP16/BF16)。把这两类数字安排在同一套流水线里协同工作,就是混合精度训练。它让训练速度更快、显存占用更小,而最终做出来的“菜”(模型精度)几乎不变。本文将用“后厨分工”的比喻,带你看懂这套现代大模型训练标配技术的内幕,以及如何安全地把它用起来。

岗位关联

混合精度训练是大模型时代的“提速引擎”:掌握它,意味着你能用更少的显存跑更大的模型、用更短的时间完成训练迭代——这是每一位AI工程师走向大规模训练实战的必修课。

目录
  1. 可视化演示
  2. 浮点精度基础
  3. 混合精度原理
  4. 主权重副本
  5. 损失缩放机制
  6. 自动混合精度
  7. 实战收益与挑战
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

浮点精度基础

走进后厨,你会看到不同的计量工具:主厨手里的电子秤精确到克(FP32),帮厨用的量杯大概到毫升(FP16),还有一种烤箱温度计,范围极大但刻度稀疏(BF16)。浮点精度,本质上就是“用多少位二进制来记录一个数字”。FP32 占用 32 位(1 位符号 + 8 位指数 + 23 位尾数),能表示约 10^38 的数值范围,精度大约 7 位有效十进制数字。FP16 只有 16 位(1 位符号 + 5 位指数 + 10 位尾数),范围窄到约 10^5,精度约 3 位有效数字。BF16 同样 16 位,但它把指数位扩回 8 位、尾数压到 7 位——范围与 FP32 相当,但精度更低,像一个“刻度稀疏的大温度计”。

在深度学习中,FP32 是“主厨配方”:权重更新这样敏感的操作,需要它保证不跑偏。但它的 32 位存储意味着显存开销大、计算速度慢。FP16 和 BF16 的 16 位存储只有一半大小,GPU 上的吞吐量却近乎翻倍——因为同样的计算单元可以塞进更多数据。代价则是数值表示粗糙:FP16 稍不注意就“爆表”(上溢出到无穷大)或“失明”(下溢成 0),BF16 虽不怕溢出,却因为尾数太少,小数值间的差异会被抹平。

精度与范围的取舍,就像厨具的选择:没有一种工具全能。FP32 精确但笨重,FP16 轻快但娇气,BF16 皮实但粗糙——理解它们的性情,才能在后厨里排兵布阵。

1. 符号位:1 位二进制决定正负,0 为正、1 为负。

s∈{0,1}s \in \{0, 1\}

2. 指数位:决定数值的“尺度范围”。FP32 有 8 位指数,FP16 仅 5 位,BF16 也是 8 位。

e∈[1,2E−2]e \in [1, 2^E - 2]

3. 尾数位:决定数值的“精细程度”。FP32 有 23 位尾数,FP16 有 10 位,BF16 只有 7 位。

x=(−1)s×1.f×2e−biasx = (-1)^s \times 1.f \times 2^{e - \text{bias}}

用一句话记住:指数定范围、尾数定精度。FP16 在训练中最致命的问题,就是梯度(通常是 10^-4 级别的小数)在 5 位指数下直接下溢成 0——这为后续的损失缩放埋下了伏笔。

混合精度原理

后厨的分工逻辑是什么?主厨不亲自切配,但每道菜出锅前他都要亲自调味。混合精度训练正是这种“关键环节用 FP32 把关、计算密集环节用 FP16 跑量”的协作模式。具体来说:前向传播和反向传播中的矩阵乘法(torch.matmul、卷积等)交给 FP16 全速计算;权重更新(optimizer.step())则回到 FP32 精细操作。

为什么不全用 FP16?想象整道菜都用同一个粗糙量杯调味:盐多放一撮、糖少放半勺,累积起来味道就偏了。纯 FP16 训练时,每次权重更新都带来微小舍入误差,成百上千次迭代后,模型收敛质量下降甚至不收敛。反过来,全用 FP32 又太慢、太占显存。混合精度取了一个巧妙的中间态:让“跑量”的动作变快,“把关”的动作不失真。

一次标准训练循环就像一条流水线:FP32 主权重 → 转成 FP16 做前向 → FP16 算损失与反向梯度 → 梯度缩放回 FP32 → 更新 FP32 主权重。每个环节各自使用最合适的工具,最终在精度与速度之间找到平衡点。混合精度的核心收益并不来自某个孤立的运算,而是整条流水线叠加后的系统性提速。

1. 前向传播(FP16):用 FP16 权重和输入计算每层输出。

z(l)=Wfp16(l)⋅afp16(l−1)+bfp16(l)z^{(l)} = W^{(l)}_{fp16} \cdot a^{(l-1)}_{fp16} + b^{(l)}_{fp16}

2. 损失计算(FP32):为避免精度损失,损失值保存在 FP32。

L=loss(z(L),y)∈R32\mathcal{L} = \mathrm{loss}\big(z^{(L)}, y\big) \in \mathbb{R}^{32}

3. 反向传播(FP16):梯度在 FP16 下计算,但数值极小,需配合缩放。

∇W(l)=∂L∂W(l)×s−1\nabla W^{(l)} = \frac{\partial \mathcal{L}}{\partial W^{(l)}} \times s^{-1}

4. 权重更新(FP32):在主权重上完成精确保留的更新。

Wfp32(l)←Wfp32(l)−η⋅∇W(l)W^{(l)}_{fp32} \leftarrow W^{(l)}_{fp32} - \eta \cdot \nabla W^{(l)}

这里出现了一个关键人物——损失缩放(Loss Scaling)。它像后厨的“调味防呆”:在梯度进入 FP16 通道之前,先把损失人为放大若干倍,让梯度数值落到 FP16 能表示的范围内,反向传播结束后再缩小回来。这正是下一节的主角。

主权重副本

后厨里总有一本“祖传配方”被锁在保险柜里,帮厨手里的只是当天用的复印件。混合精度训练同样维护着两份权重:一份是 FP32 的主权重副本(Master Weight Copy),像保险柜里的配方原本;另一份是 FP16 的工作权重副本,像帮厨手边的便签。每次迭代开始时,FP32 主权重会被复制并转换成 FP16 供前向和反向使用;迭代结束时,FP16 算出的梯度经过缩放后,回到 FP32 上完成更新——而 FP32 主权重本身从未被降精度处理。

为什么要养两份副本,多占一份显存不心疼吗?实际上,FP16 权重副本只占 FP32 一半的空间,即使两份相加也才 1.5 倍 FP32 权重空间,远小于激活值、梯度、优化器状态这些“显存大户”。更大的收益在于:权重更新永远在 FP32 上进行,多次迭代的微小修正得以累积,不会因为 FP16 的舍入误差而逐渐漂移。

形象地说,主权重副本就是“主厨最后的味觉把关”。帮厨的手艺再糙,只要最后调味权在 FP32 主厨手里,菜品(模型权重)不会越走越偏。这也是混合精度训练能在大模型上稳定收敛的基石之一。

1. 复制转 FP16:每步开始,将 FP32 主权重转为 FP16 工作副本。

Wfp16=convert_to_fp16(Wfp32)W_{fp16} = \mathrm{convert\_to\_fp16}(W_{fp32})

2. 前向反向:FP16 副本参与计算。

out=ffp16(Wfp16,xfp16)\text{out} = f_{\text{fp16}}(W_{fp16}, x_{fp16})

3. 梯度缩放:反向得到的梯度先行放大,防下溢。

gfp16=scale(∂L∂a)g_{fp16} = \mathrm{scale}\big(\frac{\partial \mathcal{L}}{\partial a}\big)

4. 反缩放回 FP32:更新前将梯度缩小回真实尺度。

gfp32=gfp16×s−1g_{fp32} = g_{fp16} \times s^{-1}

5. 更新主权重:在 FP32 主副本上执行优化器步骤。

Wfp32←Wfp32−η⋅m^÷(v^+ϵ)W_{fp32} \leftarrow W_{fp32} - \eta \cdot \hat{m} \div (\sqrt{\hat{v}} + \epsilon)

整个过程环环相扣:没有主权重副本,FP16 的每次舍入误差都会累积;没有损失缩放,梯度在 FP16 里会直接消失。两者配合,混合精度才能“跑得快又稳”。

损失缩放机制

想象在黑暗的后厨里调酱汁——盐罐刻度太粗,一小撮和一大勺看起来差不多。FP16 的“黑暗”来自它的窄范围:当梯度小于约 6×10^-5 时,直接下溢成 0,就像盐全撒在了黑暗中看不见。损失缩放(Loss Scaling)点亮了一盏灯:在反向传播之前,先把损失值乘以一个较大的缩放因子 s(如 1024),再执行反向传播。梯度随之被整体放大,落入 FP16 的安全表示区间;计算完成后,再除以 s 恢复原值。

缩放因子应该设多大?大了会溢出(梯度超过 65504 变成无穷大),小了又防不住下溢。于是有了动态损失缩放(Dynamic Loss Scaling):训练时持续监控梯度是否溢出——若连续 N 步无溢出,则把 s 调大一倍;一旦发生溢出,则跳过该步更新,把 s 减半。这个“自适应”过程,就像自动调节的烤箱温度:太热了降温,冷却了就升温,始终让食材在最佳温度下烹调。

静态缩放(固定 s)在部分场景下也能工作,但需要人工调参;动态缩放则是主流框架的默认选择。它解决的痛点是:模型不同、数据不同,梯度尺度千差万别,手动缩放要么频繁溢出、要么防不住下溢。动态机制让训练过程自我调节,免去手工干预。

1. 前向与损失:常规计算损失值。

L=loss(ypred,y)\mathcal{L} = \mathrm{loss}(y_{\text{pred}}, y)

2. 缩放损失:损失乘以缩放因子 s。

Lscaled=L×s\mathcal{L}_{\text{scaled}} = \mathcal{L} \times s

3. 反向传播:在放大后的损失上计算梯度。

gscaled=∇θLscaledg_{\text{scaled}} = \nabla_{\theta} \mathcal{L}_{\text{scaled}}

4. 反缩放梯度:更新前把梯度缩小回来。

gfp32=gscaled÷sg_{\text{fp32}} = g_{\text{scaled}} \div s

5. 溢出检测与因子调整:

st+1={2×st无溢出0.5×st有溢出s_{t+1} = \begin{cases} 2 \times s_t & \text{无溢出} \\[4pt] 0.5 \times s_t & \text{有溢出} \end{cases}

自动混合精度

读到这里,你可能已经在想:“这些细节难道都要手写吗?”大可放心。现代深度学习框架把混合精度封装成了开箱即用的工具。在 PyTorch 中,torch.cuda.amp 就是这样一个“后厨总管”:它自动判断哪些操作适合 FP16、哪些必须留在 FP32,并管理损失缩放的一切动态调整。

使用者只需把握三个要点:第一,autocast 上下文包裹前向传播和损失计算,让框架在算子级别自动选择精度;第二,GradScaler 对象负责“放大损失 → 反向 → 反缩放 → 更新权重”的完整流程;第三,unscale_ 和 step 的顺序必须遵循:先反缩放梯度,再调用优化器更新。整个接入过程通常只需几行代码,大模型训练从 FP32 切到混合精度的迁移成本极低。

以 PyTorch 为例,经典 AMP 训练循环如下:

这四行模式(scale → backward → step → update)覆盖了损失缩放、梯度反缩放、权重更新和缩放因子自适应。对工程师而言,AMP 最大的价值是“把正确的事变简单”:你不需要手算梯度范围,不需要逐层检查数值溢出,框架在底层替你维护了那条“安全通道”。

1. autocast 自动选精度:上下文内,框架按算子白名单选择 FP16/FP32。

op_dtype=autocast_policy(op,inputs)\text{op\_dtype} = \text{autocast\_policy}(\text{op}, \text{inputs})

2. 缩放损失并反向:

Lscaled=L×s,backward(Lscaled)\mathcal{L}_{\text{scaled}} = \mathcal{L} \times s,\quad \text{backward}(\mathcal{L}_{\text{scaled}})

3. 反缩放梯度:

scaler.unscale_(optimizer)  ⟺  gi←gi÷s\text{scaler.unscale\_(optimizer)} \iff g_i \leftarrow g_i \div s

4. 更新权重:

scaler.step(optimizer)=optimizer.step() if no inf\text{scaler.step(optimizer)} = \text{optimizer.step()} \ \text{if no inf}

5. 动态调整缩放因子:

s←update(s, inf_found)s \leftarrow \text{update}(s,\ \text{inf\_found})

python9 行
scaler = torch.cuda.amp.GradScaler()for data, target in loader:    optimizer.zero_grad()    with torch.autocast(device_type='cuda', dtype=torch.float16):        output = model(data)        loss = criterion(output, target)    scaler.scale(loss).backward()    scaler.step(optimizer)    scaler.update()

实战收益与挑战

混合精度的“收益账本”要从两个维度看。一是显存:FP16 权重和激活值占用的显存约为 FP32 的一半,意味着同样的 GPU 可以塞进更大的 batch size 或更大的模型;二是吞吐:GPU 的 Tensor Core 对 FP16 矩阵乘法的计算速度可达 FP32 的 2~8 倍,训练迭代更快,单位时间处理的样本更多。两者叠加,大模型训练从“跑不动”变成“跑得起”。

然而,收益并非白给。挑战同样真实:一是小模型收益甚微——当模型足够小、计算时间远低于通信开销时,混合精度省下的显存与时间几乎可忽略,反而可能因精度损失导致收敛变慢;二是精度下降风险——某些对数值敏感的任务(如超长序列 Transformer)需要验证混合精度下的收敛曲线是否与 FP32 一致;三是调试复杂度上升——当出现 NaN/Inf 时,你需要判断是“溢出”还是“下溢”,是“缩放因子过大”还是“学习率过高”。这些都要求工程师对底层机制有足够的直觉。

我们在前面章节埋下的所有概念——FP16 范围、主权重副本、损失缩放、AMP 封装——在实战中就汇聚成一份行动清单:默认开启动态损失缩放、监控梯度范数、对比 FP32 基线验证收敛、小模型谨慎启用。记住“主厨掌勺、帮厨跑量”的分工智慧,你就能在精度与速度之间,找到属于自己的最优平衡。

1. 显存收益:

memamp≈12(weightsfp16+activationsfp16)+weightsfp32\text{mem}_{\text{amp}} \approx \frac{1}{2}\big(\text{weights}_{fp16} + \text{activations}_{fp16}\big) + \text{weights}_{fp32}

2. 吞吐收益:

speedup=Tfp32Tfp16≈2∼8\text{speedup} = \frac{T_{\text{fp32}}}{T_{\text{fp16}}} \approx 2 \sim 8

3. 模型收益临界条件(规模足够大时收益显著):

benefit∝计算时间总时间×speedup−精度损失成本\text{benefit} \propto \frac{\text{计算时间}}{\text{总时间}} \times \text{speedup} - \text{精度损失成本}

AI 应用场景

大语言模型预训练加速

阶段:训练与优化

解决的问题:训练千亿参数大模型时,若全用 FP32,显存需求动辄数 TB,远超单卡 A100/H100 的 80GB 容量;且训练周期以月计,算力成本极高。混合精度训练通过“FP16 跑前反向”将单卡显存峰值降低约 40%,并借助 Tensor Core 将矩阵乘法吞吐提升 2~3 倍,让原本“训不起”的模型“训得起”。

具体案例:某团队在 512 张 A100 GPU 上预训练 130 亿参数语言模型。启用 AMP + 动态损失缩放后,单卡 batch size 从 12 提升至 20,总吞吐量提升约 1.8 倍;通过梯度范数监控与 FP32 基线对比验证,最终模型在验证集上的困惑度下降曲线与 FP32 基本重合,收敛质量无损。

视觉 Transformer 微调

阶段:微调与部署

解决的问题:在资源受限的推理服务器上对 ViT-Base 模型进行领域微调,显存瓶颈限制了 batch size,导致收敛速度慢。混合精度让激活值显存减半,batch size 翻倍,微调周期缩短约 1/3;BF16 模式则避免了 FP16 在敏感层上的溢出风险。

具体案例:医疗影像团队使用 PyTorch AMP + GradScaler 微调 Swin-Transformer。固定随机种子后,混合精度模型在病灶分割任务上的 Dice 指标(0.861)与 FP32 基线(0.863)仅差 0.2%,而训练时间从 14 小时缩短至 9.5 小时。

Python 演示

python113 行
import mathimport random  class SimpleMLP:    """A tiny 2-layer MLP for mixed-precision demo."""    def __init__(self, in_dim=4, hid_dim=8, out_dim=1):        self.w1 = [[random.uniform(-0.5, 0.5) for _ in range(in_dim)] for _ in range(hid_dim)]        self.b1 = [0.0] * hid_dim        self.w2 = [random.uniform(-0.5, 0.5) for _ in range(hid_dim)]        self.b2 = 0.0     def forward(self, x):        # Linear + ReLU (hidden)        h = [0.0] * len(self.w1)        for i in range(len(self.w1)):            z = self.b1[i] + sum(self.w1[i][j] * x[j] for j in range(len(x)))            h[i] = z if z > 0 else 0.0        # Linear output (scalar)        out = self.b2 + sum(self.w2[i] * h[i] for i in range(len(h)))        return out, h     def train_step(self, x, y_true, lr=0.01):        """One SGD step returning loss and gradients norm."""        pred, h = self.forward(x)        diff = pred - y_true        loss = 0.5 * diff * diff        # Gradients (FP32 scale)        g_out = diff                      # dL/dout        g_w2 = [g_out * h[i] for i in range(len(h))]        g_b2 = g_out        g_h = [g_out * self.w2[i] for i in range(len(h))]        g_w1 = []        g_b1 = []        for i in range(len(self.w1)):            dh = g_h[i] if h[i] > 0 else 0.0            g_w1.append([dh * x[j] for j in range(len(x))])            g_b1.append(dh)        # Update FP32 weights        for i in range(len(self.w1)):            for j in range(len(self.w1[i])):                self.w1[i][j] -= lr * g_w1[i][j]            self.b1[i] -= lr * g_b1[i]        for i in range(len(self.w2)):            self.w2[i] -= lr * g_w2[i]        self.b2 -= lr * g_b2        grad_norm = math.sqrt(sum(v * v for row in g_w1 for v in row) +                              sum(v * v for v in g_w2))        return loss, grad_norm  def to_fp16_sim(x):    """Simulate FP16 rounding by keeping 3 significant decimals."""    if x == 0.0:        return 0.0    s = 1 if x < 0 else -1    ax = abs(x)    exp = math.floor(math.log10(ax))    mant = round(ax / (10 ** exp), 3)    return s * mant * (10 ** exp)  def train_mixed_precision(epochs=30):    """Train with simulated FP16 forward/backward and FP32 master weights."""    model = SimpleMLP()    lr = 0.05    scaler = 64.0          # loss scaling factor, simulating AMP's GradScaler    inf_count = 0    safe_steps = 0     for ep in range(epochs):        loss = 0.0        gnorm = 0.0        for step in range(20):            x = [random.uniform(-1.0, 1.0) for _ in range(4)]            y_true = sum(x) * 0.5 + 0.1            # FP16 forward: cast inputs and weights to fp16-sim            x16 = [to_fp16_sim(v) for v in x]            w1_16 = [[to_fp16_sim(v) for v in row] for row in model.w1]            w2_16 = [to_fp16_sim(v) for v in model.w2]            # Scaled loss backward (fp16-sim)            pred, h = model.forward(x16)            diff = pred - y_true            loss = 0.5 * diff * diff            g_out_scaled = diff * scaler            if math.isinf(g_out_scaled) or math.isnan(g_out_scaled):                inf_count += 1                scaler *= 0.5      # overflow: reduce scale                safe_steps = 0                continue            safe_steps += 1            if safe_steps > 10:                scaler *= 2.0      # no overflow recently: try larger scale                safe_steps = 0            # Unscale gradient, simulate fp32 update            local_lr = lr / scaler            g_w2_16 = [to_fp16_sim(g_out_scaled * h[i]) for i in range(len(h))]            for i in range(len(model.w2)):                model.w2[i] -= local_lr * g_w2_16[i]            # simplified weight updates for w1 as well            for i in range(len(model.w1)):                dh = g_out_scaled if h[i] > 0 else 0.0                for j in range(len(model.w1[i])):                    model.w1[i][j] -= local_lr * to_fp16_sim(dh * x16[j])            gnorm = abs(g_out_scaled / scaler)        if ep % 5 == 0:            print("epoch %2d | loss %.4f | grad_norm %.4f | scale %.1f" %                  (ep, loss, gnorm, scaler))    print("done. infs=%d final_scale=%.1f" % (inf_count, scaler))  if __name__ == "__main__":    train_mixed_precision()
目的:该代码用一个小型两层 MLP 和自定义的 FP16 模拟器,演示了混合精度训练的核心流程——FP32 主权重、FP16 近似计算、损失缩放、溢出回退与自适应缩放因子调整。
  1. to_fp16_sim(x)用保留 3 位有效数字模拟 FP16 精度损失。
  2. w1_16 = [[to_fp16_sim(v) for v in row] for row in model.w1]前向传播前,将 FP32 主权重转为 FP16 副本。
  3. loss = 0.5 * diff * diff计算 MSE 损失(保留在 FP32 精度下)。
  4. g_out_scaled = diff * scaler损失缩放:把梯度放大到 FP16 的安全表示区间。
  5. math.isinf(g_out_scaled)检测溢出,若发生则跳过本步更新并缩小缩放因子。
  6. if safe_steps > 10: scaler *= 2.0若连续 10 步无溢出,则尝试更大缩放因子以提升精度。
  7. model.w2[i] -= local_lr * g_w2_16[i]反缩放后更新 FP32 主权重。

常见误区

误区“反正要提速,把所有张量都转成 FP16 不就行了?反正一半精度一半显存。”初学者容易把混合精度理解成“全换 FP16”,结果模型训练发散。

正解混合精度的核心是“混合”——敏感环节用 FP32(主权重更新、损失计算),计算密集环节用 FP16/BF16(矩阵乘法)。全 FP16 训练会累积舍入误差并急剧缩小数值范围,导致收敛失败。正确做法是让框架自动分配精度。

为何常见:因为“FP16 省一半显存、快一倍”这句话太动听,人们容易忽略“快”只来自 Tensor Core 计算加速,而“稳”来自 FP32 的主权重兜底。

误区“既然小梯度会下溢,那就把缩放因子调得非常大,所有梯度都能表示。”于是把 s 设成 10^6,结果频繁溢出、模型完全训不动。

正解缩放因子过大,梯度会突破 FP16 上限 65504 变成无穷大,直接触发更新跳过。动态损失缩放的意义在于自动寻找“既不溢出、也不下溢”的中间点:溢出就减半、安全就翻倍。手动设超大因子是典型的过犹不及。

为何常见:人们天然倾向于“越大越保险”,但忽略了 FP16 是两端受限的数值系统——缩小梯度的同时也在放大溢出的可能。

误区“框架有 autocast 和 GradScaler,我什么都不用管,自动就加速、自动就稳定。”

正解autocast 只负责算子的精度选择,GradScaler 只负责损失缩放,它们不会修正学习率过高导致的发散,也不会覆盖分布式训练的梯度同步顺序错误。使用 AMP 仍需监控梯度范数、对比 FP32 基线、检查 NaN/Inf 出现位置。

为何常见:框架 API 太“傻瓜化”了,让人误以为底层数值稳定也是全自动的——实际上 AMP 是一个需要配合纪律使用的工具。

误区“既然大模型用混合精度提速 2 倍,我的小模型应该也能提速 2 倍。”

正解混合精度的收益与计算密集度强相关,小模型上矩阵乘法耗时短、host 开销占比高,FP16 带来的加速往往被通信/启动开销淹没。某些 MLP 或小 CNN 上混合精度甚至更慢。建议先做 benchmark 验证收益,再决定是否启用。

为何常见:看到成功案例就照搬,忽略了收益与大模型规模的耦合关系。

面试题

Q1. FP16 相比 FP32,最主要的数值缺陷是什么?

单选题 · easy

  • A.计算速度太慢
  • B.表示范围窄,易上溢或下溢← 正确答案
  • C.无法在 GPU 上运行
  • D.无法表示负数

答案:B。FP16 的指数位只有 5 位,可表示范围远小于 FP32,训练中梯度容易下溢成 0 或上溢成无穷大。类比“量杯刻度太粗,小撮盐看不见,一大勺又洒满桌”。A/ C/ D 都与 FP16 特性不符。

Q2. 混合精度训练中,主权重副本通常以什么精度保存?

单选题 · easy

  • A.FP16
  • B.BF16
  • C.FP32← 正确答案
  • D.INT8

答案:C。主权重副本保存为 FP32,保障权重更新的精度不因舍入误差累积而漂移。类似“主厨的配方原本锁在保险柜,绝不给帮厨随意涂改”。

Q3. 损失缩放的直接目的是什么?

单选题 · mid

  • A.提高模型准确率
  • B.防止梯度在 FP16 下下溢成 0← 正确答案
  • C.加快卷积计算速度
  • D.减少显存占用

答案:B。FP16 表示范围窄,训练中梯度常小于 6×10^-5,直接下溢成 0。损失缩放先把损失放大 s 倍,梯度随之落入 FP16 安全区间,反向后再缩小回来。这不改变准确率上限,也不直接加速卷积或减少显存。

Q4. 动态损失缩放中,“连续 N 步无溢出”后缩放因子如何变化?

单选题 · mid

  • A.保持不变
  • B.减半
  • C.变为 0
  • D.翻倍← 正确答案

答案:D。算法逻辑是“保守时缩小、安全时激进”。连续 N 步无溢出说明当前缩放因子偏低,于是翻倍以提升梯度表示能力;若检测到溢出则减半并跳过该步更新。

Q5. 关于 BF16 与 FP16,下列说法正确的是?

多选题 · mid

  • A.BF16 的指数位与 FP32 相同,均为 8 位← 正确答案
  • B.BF16 的精度比 FP16 更高
  • C.BF16 不容易上溢/下溢,但尾数精度较低← 正确答案
  • D.BF16 常用于省去损失缩放的场景← 正确答案

答案:ACD。BF16 的指数位与 FP32 同为 8 位,表示范围大,所以不易溢出,可减少损失缩放需求(选项 A、C、D 对);但 BF16 尾数只有 7 位,精度比 FP16 的 10 位更低(选项 B 错)。

Q6. PyTorch AMP 的正确训练流程是?

单选题 · hard

  • A.loss.backward() → scaler.step() → scaler.update()
  • B.scaler.scale(loss).backward() → scaler.step() → scaler.update()← 正确答案
  • C.scaler.scale(loss) → optimizer.zero_grad() → loss.backward()
  • D.optimizer.step() → scaler.scale(loss).backward()

答案:B。标准流程是:scaler.scale(loss).backward() 放大损失并反向,scaler.step(optimizer) 内部反缩放并执行权重更新,最后 scaler.update() 动态调整缩放因子。A 缺缩放过;C 先放大却没反向;D 顺序颠倒。

Q7. 混合精度训练中,为什么权重更新必须在 FP32 上执行?

单选题 · hard

  • A.FP32 计算速度更快
  • B.防止舍入误差累积导致收敛漂移← 正确答案
  • C.FP16 不支持减法
  • D.为了减少显存

答案:B。每次 FP16 更新都会引入舍入误差,成百上千次迭代后误差累积,模型精度下降。FP32 主权重副本保留细粒度更新。类比“主厨亲自调味,帮厨只负责切配”。

Q8. 以下哪些情况说明混合精度训练出现了数值问题?

多选题 · hard

  • A.损失变为 NaN← 正确答案
  • B.权重中出现无穷大值← 正确答案
  • C.梯度范数长期为 0← 正确答案
  • D.训练 loss 平稳下降

答案:ABC。NaN 损失、权重无穷大、梯度范数为 0 都是典型的 FP16 数值异常信号(分别对应上溢、溢出未回退、下溢)。loss 平稳下降是正常表现,不属于问题。

Q9. AMP 中的 autocast 上下文主要作用是什么?

单选题 · easy

  • A.自动调整学习率
  • B.自动为算子选择合适的浮点精度← 正确答案
  • C.自动保存模型检查点
  • D.自动做数据增强

答案:B。autocast 按预定义算子策略自动选择 FP16/FP32:矩阵乘法、卷积等计算密集算子用 FP16,敏感归一化、损失计算用 FP32。它不负责学习率、检查点或数据增强。

Q10. 关于混合精度训练的收益与挑战,正确的有?

多选题 · mid

  • A.显存占用可显著降低← 正确答案
  • B.吞吐量可能提升 2~8 倍← 正确答案
  • C.小模型收益不如大模型显著← 正确答案
  • D.启用后无需进行精度验证

答案:ABC。混合精度的收益包括显存减半和 Tensor Core 吞吐翻倍,且收益随模型规模增大而更明显(A/B/C 对)。D 错——任何启用混合精度的项目都应对比 FP32 基线验证收敛质量。