跳转至

分布式训练面

训练一个 10B 参数模型,FP16 混合精度,AdamW,仅权重、梯度、优化器状态三部分共需多少显存?

训练一个 10B(100亿)参数的模型,若使用 FP16 混合精度和 AdamW 优化器,我们可以将显存消耗分为三大部分:模型权重、梯度、优化器状态。在混合精度训练中,通常采用“权重半精度存储、前向计算用半精度、主权重和优化器状态用全精度”的策略。具体计算如下:

3dd53383-db8b-485d-993d-38e6909e45d9.png

3dd53383-db8b-485d-993d-38e6909e45d9.png

  • FP16 权重:参数总量为 10×10⁹,每个参数占 2 字节(FP16),因此权重部分占用 20 GB。

  • FP16 梯度:反向传播时,需要为每个可训练参数存储对应的梯度,也使用 FP16 精度,所以同样占用 20 GB。注意,在一些框架实现中,梯度可能以 FP32 存储,但当前主流方案是梯度和权重在前向/反向中使用 FP16,仅在更新时转为 FP32,所以这里按 FP16 梯度计算。

  • AdamW 优化器状态:Adam 需要保存每个参数的一阶动量(m)和二阶动量(v),通常这两者以及主权重(master weights)都以 FP32 存储。有些实现将主权重与优化器状态合并,实际上我们可把优化器状态视为 FP32 的 m 和 v 以及 FP32 的主权重。但通常所说的“优化器状态”主要指 m 和 v,而主权重是额外的一份 FP32 参数副本。综合计算,每个参数需要存储的 FP32 数据量为 4 字节 × 3 = 12 字节(m、v、master weight)。因此优化器相关的状态总显存为 10B × 12 bytes = 120 GB。若只算 m 和 v 则为 80 GB,加上主权重 40 GB,共 120 GB。

合计:权重 20GB + 梯度 20GB + 优化器状态 120GB = 160 GB。但这里需澄清:在混合精度训练中,梯度通常在 FP16 下计算,但也可能以 FP32 累积,实际显存消耗可能略高。主流框架(如 DeepSpeed、PyTorch AMP)在 FP16 模式下,梯度和权重以 FP16 存储,而优化器状态(m, v)和主权重(FP32)是必须的。所以理论上至少需要 160 GB 显存,这远超单张 GPU(如 80GB A100)的容量,因此必须借助模型并行或 ZeRO 等显存优化技术。

推导上述数值:10B 参数,FP16 权重=20GB,梯度=20GB,Adam 状态(FP32)=80GB,合计 120GB。

这个推导基于常见的理解:梯度也以 FP16 存储,优化器状态仅包含 m 和 v(FP32),合计 80GB,加上权重和梯度各 20GB,总共 120GB。这与上一问的 160GB 差异在于是否将主权重(master weights)单独计算。实际上,在混合精度训练的标准实践中,需要一份 FP32 的主权重用于参数更新,所以显存消耗应该是 160GB。然而,很多资料将主权重算作优化器状态的一部分,或者认为它与 m/v 一起组成 FP32 的参数副本,因此三部分共 120GB 可能是指“权重(FP16)、梯度(FP16)、优化器状态(FP32 m+v)”而没有计入 master weight。这里为了清晰,可以这样解释:

  • FP16 权重:10B × 2 bytes = 20 GB

  • FP16 梯度:10B × 2 bytes = 20 GB

  • Adam 状态(m 和 v,均为 FP32):2 × 10B × 4 bytes = 80 GB

  • 总计:20 + 20 + 80 = 120 GB

但这是在忽略 FP32 主权重的前提下。若加上主权重(也是 10B × 4 bytes = 40 GB),则总显存为 160 GB。两种说法都常见,关键是理解各部分的精度和数量。

激活值显存如何估算?与 batch size、序列长度、隐藏维度、层数的关系。

激活值(activations)是前向传播过程中各层产生的中间张量,它们必须在反向传播时被保留用于计算梯度。激活值显存的大小由模型架构、输入规模决定。

对于 Transformer 模型,激活值主要来自:

  • 每个注意力头的 Q、K、V 矩阵(形状通常为 [batch, seq_len, num_heads, head_dim])

  • 注意力得分矩阵(softmax 之前或之后),形状 [batch, num_heads, seq_len, seq_len](显存复杂度 O(L²))

  • 前馈网络(FFN)中间层的输出,形状 [batch, seq_len, intermediate_dim]

  • 层归一化的输入/输出等较小张量

  • Dropout 掩码(如果使用)

激活值显存与参数的关系可近似为:

Activation Memory≈batch_size×seq_len×hidden_dim×num_layers×constant

其中 constant 依赖于具体的模型细节和是否存储注意力矩阵。对于长序列,注意力矩阵的 O(L²) 存储需求会占主导地位,使得激活显存与序列长度成二次关系;对于较短序列或使用了优化(如 FlashAttention),则近似线性。

对于 Transformer 模型,为什么激活显存与序列长度 L 呈线性或二次关系?

在标准 Transformer 自注意力中,需要计算 queries 和 keys 的点积,得到一个形状为 [batch, heads, L, L] 的注意力分数矩阵。在 softmax 之后,为了反向传播,通常需要保存该矩阵(或保存 softmax 之前的 logits 以节省计算)。这个矩阵的显存占用与 L² 成正比,因此对于长序列,注意力矩阵成为激活显存的绝对瓶颈,此时激活显存与序列长度呈二次关系。

但是,如果序列长度较短,或者使用了不保存注意力矩阵的注意力实现(如 FlashAttention 通过分块计算和在线 softmax 避免存储完整矩阵),则激活显存主要由 Q、K、V 和 FFN 中间输出决定,这些张量与 L 成线性关系。因此,激活显存与 L 的关系可以是线性或二次,取决于具体实现和序列长度。

标准注意力矩阵的显存复杂度是 O(L²),在训练时是否一定要存储?如何避免?

在标准训练中,为了反向传播计算 softmax 的梯度,确实需要保存注意力分数矩阵(或 softmax 输出)。但这不是必须的,可以采用几种方法避免存储完整的 O(L²) 矩阵:

  • FlashAttention:通过分块(tiling)和重计算技术,在前向时只保留 softmax 的归一化因子(标量),反向时重新计算注意力矩阵,从而将显存复杂度从 O(L²) 降到 O(L)。这是当前最主流的方法,已被 PyTorch 2.0 和 xFormers 等库支持。

  • Gradient Checkpointing:在注意力层使用检查点,前向时不存储中间结果,反向时重新计算该层的前向。虽然可以节省显存,但注意力矩阵仍会在重计算时临时占用显存,效率不如 FlashAttention。

  • 稀疏/线性注意力:通过限制注意力模式(如局部窗口、低秩近似)来减少需要计算的 token 对数量,从根本上避免存储完整矩阵。

  • 混合精度训练:使用 FP16 存储注意力矩阵可以减半显存,但不改变二次复杂度。

因此,虽然理论上必须存储,但通过算法创新可以避免实际存储完整矩阵,从而支持更长序列的训练。

什么是 Gradient Checkpointing?它如何减少显存?写出其交换率(一般省 50% 激活,增加 20% 计算)。

Gradient Checkpointing(梯度检查点)是一种用计算换显存的技术。在训练网络时,通常需要保存每一层的中间激活值,以便在反向传播时计算梯度。梯度检查点则在前向传播时,只选择性地保存部分层的输入(称为检查点),而丢弃其他中间激活。当反向传播需要这些丢弃的激活值时,就从最近的检查点开始重新执行前向计算来恢复它们。

对于 Transformer,通常以每个层(一个注意力块+一个FFN块)为单元设置检查点。前向时,只保存该层的输入张量(即上一层的输出),该层内部的 Q、K、V、注意力矩阵、FFN 中间输出等全部丢弃。反向传播到该层时,用保存的输入重新运行该层的前向,得到所有需要的中间激活,再计算梯度。

节省显存:这种方法使得激活值的总显存从“所有层的全部中间结果”降低到“只存检查点张量”,典型情况下能将激活显存减少约 50%~70%。对于深层网络,节省效果显著。

计算开销:每个检查点层需要额外执行一次前向传播,因此总计算量增加。对于 Transformer,通常增加约 15%~25% 的计算量(取决于检查点划分的粒度)。一般说法是“省 50% 激活,增加 20% 计算”。

交换率公式可近似为:

image.png

其中 segments 是将网络划分的段数。若每个层为一个段,则能极大节省激活显存。

如果对一个 7B 模型使用 ZeRO-1,能节省哪部分显存?节省多少?

ZeRO-1 (Optimizer State Partitioning) 将原本在数据并行中每张 GPU 都完整保存的优化器状态(如 Adam 的 m 和 v)分片(partition)到所有 GPU 上,每张 GPU 只持有 1/N 的优化器状态,并只负责更新对应分片的参数。

对于 7B 模型,使用 AdamW 优化器,优化器状态(FP32 的 m 和 v)原本占用:

image.png

如果使用 8 张 GPU,则每张卡只需要 56 GB / 8 = 7 GB 的优化器状态显存。节省了 49 GB。加上主权重(master weights)也需要分片,实际上 ZeRO-1 通常连同主权重一起分片,或者主权重也作为优化器状态的一部分。总体来看,优化器相关的显存从 84 GB(56 GB m/v + 28 GB master weights)降至约 10.5 GB per GPU。总节省约 73.5 GB per GPU。这使得原本显存不足的大模型得以训练。

需要注意的是,ZeRO-1 不会减少模型参数和梯度的显存占用,它们仍然是完整副本。但优化器状态通常是最大的显存消耗源之一(2倍于参数量),因此节省效果显著。

ZeRO-2 在 ZeRO-1 基础上进一步节省了什么?举例说明。

ZeRO-2 (Gradient Partitioning) 在分片优化器状态的基础上,进一步对梯度进行分片。在反向传播时,每张 GPU 只计算和保留自己所负责那部分参数的梯度,其他部分的梯度在计算后通过 ReduceScatter 通信操作直接分片到对应的 GPU,不占用完整梯度存储。

仍以 7B 模型、8 GPU 为例:

  • ZeRO-1 节省了优化器状态显存(从 84 GB → ~10.5 GB per GPU)

  • ZeRO-2 额外节省了梯度显存。完整梯度占用 7B × 2 bytes (FP16) = 14 GB。分片后,每张 GPU 只需存储约 14 GB / 8 = 1.75 GB,节省了约 12.25 GB。

因此,ZeRO-2 相比 ZeRO-1 每卡又减少了约 12 GB 的显存占用,使得总显存需求进一步降低。

举例:训练一个 7B 模型,单卡原本需要 ~120 GB(权重 14 GB + 梯度 14 GB + 优化器状态 84 GB + 激活值等),用 ZeRO-2 后,每卡显存需求降至约 14 (权重) + 1.75 (梯度) + 10.5 (优化器状态) = 26.25 GB,大幅低于单卡 80GB 限制。

ZeRO-3 分片参数后,前向和反向传播时参数如何获取?对显存有何影响?

ZeRO-3 (Parameter Partitioning) 在 ZeRO-2 的基础上进一步将模型参数本身分片到所有 GPU 上。每张 GPU 仅持久保存一部分参数(例如一层中的一部分权重)。在前向传播和反向传播需要完整参数时,通过 AllGather 通信操作从其他 GPU 收集所需的参数片段,组装成完整参数,计算完毕后立即丢弃(释放显存)。这种“随用随取,用完即弃”的策略极大降低了单卡持久存储的模型参数显存。

具体流程:

  • 前向传播:当计算到第 i 层时,该层的所有参数通过 AllGather 从各 GPU 的分片中收集完整权重;进行该层计算;计算完成后,释放掉该层参数的显存,仅保留自己分片的那部分。

  • 反向传播:同理,需要重新 AllGather 该层参数来计算梯度。计算出的梯度同样通过 ReduceScatter 分片到对应 GPU 进行更新。

对显存的影响:模型参数不再是每张卡的常驻内存,而是被均摊到所有 GPU。对于 7B 模型,FP16 权重占用 14 GB,用 8 GPU 分片后,每卡仅需 14/8 = 1.75 GB 持久存储参数。这使得单卡可以训练远超自己显存的模型。然而,通信量显著增加,因为每层前向和反向都需要全量参数的 AllGather 和梯度的 ReduceScatter。即便如此,ZeRO-3 仍是训练超大模型的核心技术。

假设单卡 80GB,训练 13B 模型,FP16,AdamW,batch size=1,序列长度 2048,是否可行?用计算说明。

我们逐步估算所需的显存,假设使用最基础的配置(无任何显存优化,如 ZeRO、Gradient Checkpointing 等)。

模型参数:

  • 13B 参数,FP16 存储:13 × 10^9 × 2 bytes = 26 GB

梯度:

  • FP16 梯度:26 GB(与参数相同)

优化器状态:

  • Adam 的 m 和 v (FP32):2 × 13 × 10^9 × 4 bytes = 104 GB

  • 主权重 (master weights) (FP32):13 × 10^9 × 4 bytes = 52 GB

  • 优化器状态合计:104 + 52 = 156 GB

以上三项合计:26 + 26 + 156 = 208 GB。显然已远超 80GB 单卡容量。

激活值估算:

对于 Transformer 模型,激活值主要受 batch size、序列长度、隐藏维度、层数影响。13B 模型的典型配置可能是:hidden_dim≈5120, num_layers≈40, intermediate_dim≈13824, num_heads≈40。

  • 序列长度 L=2048,batch=1。

  • 注意力矩阵 (per layer):每个注意力头的注意力得分矩阵大小为 L×L=4M 元素,共 40 头,FP16 下约 40 × 4M × 2 bytes ≈ 320 MB。40 层累计约 12.8 GB。

  • Q/K/V 矩阵:每层大小约为 batch×L×hidden_dim×3 × 2 bytes ≈ 1×2048×5120×3×2 ≈ 62 MB,40 层约 2.5 GB。

  • FFN 中间激活:每层 batch×L×intermediate_dim × 2 bytes ≈ 1×2048×13824×2 ≈ 56 MB,40 层约 2.2 GB。

  • 其他(LayerNorm、残差等)大约 1~2 GB。

  • 总激活值估算:约 12.8 + 2.5 + 2.2 + 2 ≈ 19.5 GB。

即使只算模型参数、梯度、优化器状态就已经 208 GB,远远超过 80 GB。因此,不采用任何显存优化时,完全不可行。

如果使用 ZeRO 和 Gradient Checkpointing 呢?假设使用 ZeRO-2 (分片优化器状态和梯度) 并使用 Gradient Checkpointing 来减少激活显存:

  • 参数:26 GB(若用 ZeRO-3 可分片,ZeRO-2 下参数仍需全量 26 GB)

  • 梯度:分片后假设 8 GPUs,每卡 26/8 ≈ 3.25 GB(但单卡仍需存储,因为是单卡训练,分片无效。单卡无法使用 ZeRO 分片,因为分片需要在多卡间分担。所以单卡训练无法利用 ZeRO 节省显存。)

因此,对于单卡训练,必须依赖模型并行(如张量并行)或使用 ZeRO-Offload 将优化器状态卸载到 CPU 内存。单卡 80GB 训练 13B 模型非常困难,通常需要至少 2~4 张 GPU 并配合 ZeRO-2/3 和 Gradient Checkpointing 等技术。结论:单卡 80GB 不可行。

如果上述条件不可行,你首先会采用哪种显存优化措施?(梯度检查点?ZeRO?)

当单卡80GB显存不足以完整存放模型状态(参数+梯度+优化器)时,我们需要根据现有硬件条件选择最直接有效的优化路径。假设前提是单卡训练13B模型,上面已经计算纯模型状态就需要208GB,远超标。

单卡场景下的最优选择:梯度检查点 + ZeRO-Offload

  • 梯度检查点 (Gradient Checkpointing):激活值是除模型状态外的另一大显存消耗源。对于长序列,注意力矩阵的O(L²)显存会占据主导。梯度检查点通过丢弃大部分中间激活,在反向传播时重计算来换取显存。对于Transformer,通常以每个层为检查点,激活显存可降低至原来的约1/3~1/2,增加的计算量约15%~25%。这是成本最低、实现最简单的显存优化手段,几乎不改变训练代码,只需开启框架选项。

  • ZeRO-Offload (CPU卸载):单卡无法利用ZeRO的多卡分片优势,但可以将优化器状态(Adam的m、v以及FP32主权重)卸载到CPU内存中。模型参数和梯度仍驻留在GPU,前向和反向在GPU上执行;当需要更新参数时,梯度从GPU拷贝到CPU,CPU利用其大内存完成Adam计算,再将更新后的FP16权重传回GPU。这样,GPU仅需承担参数和梯度的显存,优化器状态完全由CPU内存负担。代价是PCIe传输成为瓶颈,训练速度通常下降30%~50%。

多卡场景下的首选:ZeRO-2 或 ZeRO-3

如果拥有至少两张GPU,则ZeRO系列的分布式显存优化成为首选:

  • ZeRO-2:分片优化器状态+梯度。每张卡不再存储全部优化器状态和全部梯度,而是只持有一部分。对于8卡训练13B,单卡优化器状态从156GB降至19.5GB,梯度从26GB降至3.25GB,加上全量参数26GB,单卡模型状态总显存约49GB,变得可行。

  • ZeRO-3:进一步分片模型参数本身,将单卡参数存储降至26/8=3.25GB,使训练更大模型成为可能,但通信量相应增大。

选择逻辑:通常优先启用梯度检查点(几乎零成本),若显存仍不足,则在多卡上使用ZeRO-2,再不够则启用ZeRO-3。单卡且无法增加GPU时,只能借助CPU卸载与梯度检查点组合。


训练 30B 模型,使用 8 卡 A100 80GB,如何通过并行和 ZeRO 使其可训练?给出配置思路。

显存需求估算

30B参数模型,使用混合精度训练:

  • FP16权重:30B × 2 bytes = 60 GB

  • FP16梯度:60 GB

  • Adam优化器状态(m+v FP32 + FP32主权重):30B × 12 bytes = 360 GB

  • 模型状态总计:60 + 60 + 360 = 480 GB

  • 8张A100总显存为 8 × 80 = 640 GB,看似足够,但数据并行下每张卡需保存全部模型状态,单卡480GB远超80GB,因此必须采用显存优化技术。

配置思路

  1. ZeRO-2 分析:
  2. 分片优化器状态:360 GB / 8 = 45 GB per GPU
  3. 分片梯度:60 GB / 8 = 7.5 GB per GPU
  4. 全量参数:60 GB per GPU(未分片)
  5. 单卡模型状态总计:60 + 7.5 + 45 = 112.5 GB,仍超出80GB。因此纯ZeRO-2不行。

  6. ZeRO-3 分析:

  7. 进一步分片模型参数:60 GB / 8 = 7.5 GB per GPU
  8. 梯度分片:7.5 GB
  9. 优化器状态分片:45 GB
  10. 单卡模型状态总计:7.5 + 7.5 + 45 = 60 GB,在80GB以内。激活值额外占用约10~20GB(取决于序列长度和batch size),使用梯度检查点后压缩到10GB以内,总显存约70GB,可行。

  11. 混合并行方案(更优):

  12. 引入张量并行 (Tensor Parallelism, TP) 与 ZeRO-3 结合。例如设置TP=2,则模型在2卡间切分单层权重,数据并行度降为4。ZeRO-3可在此基础上进一步分片。
  13. 流水线并行 (Pipeline Parallelism, PP) 也可将层分配到不同设备,降低单卡存储层数。
  14. 典型配置:8卡分为2个TP组(每组4卡),组内TP切分注意力头和FFN维度;组间使用ZeRO-2/3数据并行。这样单卡参数和优化器状态都大幅减小,激活值也因TP切分而降低。

最终可行配置:ZeRO-3 + Gradient Checkpointing,或 Megatron-LM 的 TP=2 + PP=2 + DP=2 + ZeRO-2。具体选择需权衡通信开销与实现复杂度。


什么是“Model Parallel”中的“参数分片”?它与 ZeRO-3 的参数分片有何不同?

模型并行中的参数分片:主要指张量并行 (Tensor Parallelism),将单个Transformer层内的权重矩阵(如QKV投影、FFN的线性层)沿列或行切分到多个GPU上。每张卡持有该层权重的一部分,在前向和反向传播时需要通信(如AllReduce)来合并激活或梯度。这种分片粒度细(层内分片),通信频率高(每层多次),但能有效降低单卡显存和计算量。通常与流水线并行结合,用于单个模型副本跨多卡。

ZeRO-3 的参数分片:属于数据并行的扩展,将整个模型的参数(包括所有层)平均分布到数据并行组的所有GPU上。每张卡只持久保存一部分参数(例如某几层的完整权重,或每层权重的部分切片)。当某层参与计算时,通过AllGather从其他卡收集该层完整参数,计算后立即释放非本地分片。这种分片粒度粗(层间或参数张量级),但保留了数据并行的独立计算特性(每张卡处理不同的micro-batch数据),通信主要发生在层边界。

关键区别:

  • 分片维度:TP分片在层内(如按列切分矩阵),ZeRO-3分片在层间或参数张量维度。

  • 数据依赖:TP的张量并行组内的GPU共同处理同一份输入;ZeRO-3的GPU各自处理不同的micro-batch,与其他数据并行一致。

  • 通信模式:TP需要每层进行AllReduce或AllGather/ReduceScatter,通信频繁;ZeRO-3的AllGather只在每层前向和反向各发生一次,通信量较大但频率较低。

  • 扩展性:TP受限于单层参数量和通信带宽,通常不能跨节点高效扩展;ZeRO-3可跨节点扩展,但通信量随节点数增加。

实践中,两者常结合使用:节点内使用TP利用NVLink高带宽,节点间使用ZeRO-3降低显存和通信压力。


数据并行时,每张卡上的优化器状态是完全相同还是各自存储?为什么 ZeRO-1 要分片优化器状态?

在传统的数据并行中,每张GPU拥有完整的模型副本,它们独立处理不同的输入数据,各自计算梯度。随后通过AllReduce通信将梯度全局平均或求和,使每张卡获得相同的全局梯度。每张卡使用相同的全局梯度和相同的优化器(如Adam)更新自己的本地参数,因此每张卡上的优化器状态(m和v)完全相同,也是完整的副本。

ZeRO-1 分片优化器状态的动机:这种完全冗余的优化器状态造成了巨大的显存浪费。对于N张卡的数据并行,优化器状态占用的总显存是单卡的N倍,但存储的信息完全一样。ZeRO-1将优化器状态均匀分片到N张卡,每张卡只存储1/N的优化器状态,并负责更新对应分片的参数。更新完成后,通过AllGather将更新后的参数广播给所有卡,使它们重新持有完整参数。这样,优化器状态的显存冗余被消除,单卡优化器显存降至原来的1/N,而额外增加的通信量(AllGather)相对较小,因为优化器状态不需要在每次迭代中全部传输。

本质:ZeRO-1用一次小规模的AllGather参数通信,换取了优化器状态显存的指数级压缩,使数据并行可以容纳更大的模型。


分析使用 ZeRO-3 时,前向传播的一次 AllGather 通信量是多少。

在ZeRO-3中,模型参数被分片到所有DP组内的GPU上。前向传播计算某一层时,该层的全部参数必须先在当前设备上可用。因此,需要执行一次AllGather操作,从组内所有GPU收集该层参数的各个分片,拼接成完整参数。

设模型总参数量为P(单位:参数个数),数据并行度为N,层数为L,且参数在各层间均匀分布。则每层参数量约为 P/L。在一次前向传播中,对于每一个Transformer层,都需要一次AllGather该层的完整参数。

单层 AllGather 通信量:每个GPU贡献该层参数的1/N(即 P/(LN) 个参数),并从其他N-1个GPU各接收同样大小的数据。AllGather通信的总数据量(所有GPU发送的数据总量)为 (N-1)/N * (P/L) * 参数字节数。对于单个GPU,它发送约 P/(LN) 的数据,接收约 (N-1)P/(LN) 的数据,单GPU收发总量约 P/L * (1 - 1/N) ≈ P/L(当N较大时)。

若参数以FP16存储,则每层AllGather的通信量约为 2 * P / L 字节。整个模型所有L层在前向传播中,总AllGather通信量约为 2 * P 字节。反向传播同样需要AllGather参数一次,因此前向+反向总AllGather通信量约为 4 * P 字节。

实例:训练30B模型,8卡ZeRO-3,P=30×10^9,则单层参数约P/L(假设L=48,约625M)。每层AllGather通信量约为 2 * 625e6 ≈ 1.25 GB。全模型前向AllGather总通信量约 2 * 30e9 * 2 bytes = 120 GB(注意单位:参数量×2字节)。反向同样120 GB,故AllGather合计约240 GB。此外还有梯度ReduceScatter通信。


为什么说 ZeRO-3 的通信量比数据并行大约多 50%?多在哪里?

标准数据并行(DP)的通信主要发生在梯度同步阶段:每个GPU计算得到本地梯度后,通过AllReduce将梯度全局求和(或平均),使得所有卡获得相同的全局梯度。AllReduce的通信量为 2 * P 字节(每个参数对应一个梯度,FP16下为2字节;AllReduce的经典实现有2倍数据量的通信)。

ZeRO-3的通信由两部分构成:

  1. 前向传播参数收集:每个层都需要AllGather完整参数,总通信量 2 * P 字节。

  2. 反向传播参数收集:同样需要AllGather参数,总通信量 2 * P 字节。

  3. 梯度同步:在ZeRO-3中,梯度不再通过AllReduce,而是每个GPU只计算自己分片那部分参数的梯度,然后通过ReduceScatter将梯度分片到对应的GPU。ReduceScatter的通信量也是 2 * P 字节(与AllReduce相等)。

因此,ZeRO-3的总通信量 = 前向AllGather (2P) + 反向AllGather (2P) + 梯度ReduceScatter (2P) = 6P。而标准数据并行只有梯度AllReduce的通信量 2P。ZeRO-3的通信量是标准DP的3倍。但通常所说的“比数据并行大约多 50%” 可能是指在某些优化实现中,ZeRO-3的通信量相比于ZeRO-2(其通信量约为 2P)多了AllGather参数的开销。更准确的表述是:ZeRO-3相比ZeRO-2,额外增加了参数收集的通信量,使总通信量从 ~2P 增加到 ~6P,多了约 200%。具体数字依赖于并行度和层数等因素。

多在哪里:多出的通信量全部来自参数的重复收集(前向+反向各一次全模型参数的AllGather)。这是ZeRO-3为了极致节省显存(分片参数)所付出的通信代价。


解释“参数服务器” (Parameter Server) 与 ZeRO 在显存使用上的差异。

参数服务器架构:经典的分布式训练架构,由多个工作节点(worker)和服务器节点(server)组成。服务器节点存储完整的模型参数和优化器状态,工作节点只负责计算梯度,并将梯度推送给服务器;服务器使用梯度更新参数。这种架构下,工作节点的显存占用很小(只需存储当前批次的激活值和梯度),但服务器节点需要巨大的显存来容纳完整模型状态(参数+优化器),因此服务器往往成为瓶颈,且扩展性受限。

ZeRO架构:将模型状态(参数、梯度、优化器状态)完全分布到所有参与训练的GPU上,没有中心服务器。每张GPU既是工作节点,也承担了一部分存储任务。通过分片和按需收集的方式,任何一张GPU都不需要持有完整的模型状态,从而实现显存的均衡利用。

显存使用差异:

  • 参数服务器:显存使用极不均衡。服务器端显存需求与模型大小成正比(所有参数+优化器),而工作节点显存需求低。当模型巨大时,单台服务器无法承载,必须采用多服务器,增加了系统复杂度。

  • ZeRO:显存使用均衡。每张GPU的显存消耗大约为(模型状态总量 / 数据并行度)。通过分片,整个集群的总显存被高效聚合,理论上可训练的模型大小上限为单卡显存乘以卡数(再考虑通信和激活值开销)。

因此,ZeRO彻底消除了传统参数服务器的中心存储瓶颈,是当前训练千亿乃至万亿参数模型的基础。


在使用 ZeRO-Offload 时,哪些数据被移到 CPU?对训练速度的影响如何?

被卸载到CPU的数据:

  • 优化器状态 (Optimizer States):Adam的动量(m)和方差(v),以FP32格式存储。这是显存占比最大的部分(2倍参数量)。

  • FP32 主权重 (Master Weights):用于参数更新的全精度权重副本,通常也移到CPU。

  • 梯度 (Gradients):可选,也可将梯度暂时保留在GPU,待需要更新时再传到CPU;或者直接将FP16梯度传至CPU,在CPU上累积为FP32并更新。

留在GPU上的数据:

  • FP16 模型参数:用于前向和反向计算,始终驻留GPU。

  • 激活值 (Activations):前向传播产生的中间张量,仍占用GPU显存。

  • 临时缓冲区。

训练流程:

  1. 前向和反向传播在GPU上完成,得到FP16梯度。

  2. 梯度被拷贝到CPU内存。

  3. CPU使用FP32主权重和优化器状态,执行Adam更新步骤,产生更新后的FP32权重。

  4. 更新后的权重从FP32转为FP16,再拷贝回GPU,更新模型参数。

  5. 此过程每个训练步重复。

对训练速度的影响:

  • PCIe带宽瓶颈:GPU与CPU之间的数据传输受限于PCIe带宽(如PCIe Gen4 x16单向约32 GB/s)。30B模型FP16梯度约60GB,即使仅传输梯度和更新后的权重,每步传输量也达上百GB,导致通信时间显著增加。

  • 速度下降幅度:通常ZeRO-Offload会使单步训练时间增加30%~70%,具体取决于模型大小、PCIe带宽和计算/通信重叠程度。为了隐藏延迟,可采用双缓冲、异步拷贝和计算流重叠等技术。

  • 适用场景:当GPU显存严重不足且无法增加GPU数量时,ZeRO-Offload是一种“用速度换规模”的策略,使得在有限的GPU上训练原本无法容纳的大模型成为可能。


如果硬件为 4 卡 A100 80GB,如何估算最大可训练模型规模?给出公式和推导。

假设使用混合精度训练、AdamW优化器,采用ZeRO-3(最优显存节省)和梯度检查点。

显存预算:每卡80GB,4卡总显存320GB,但ZeRO-3下每卡只承担1/4的模型状态,且不能简单用总显存除以单卡开销,因为激活值等每卡独立。

单卡显存构成:

  • 模型状态 (参数+梯度+优化器状态):ZeRO-3将它们分片,单卡存储量 = (P * (2 bytes FP16) + P * (2 bytes FP16) + P * (12 bytes FP32)) / 4 = (2P + 2P + 12P) / 4 = 16P / 4 = 4P 字节。其中P为参数量。

  • 激活值 + 临时缓冲 + 框架开销:设为 A GB,与模型大小、序列长度、batch size等有关,通常在几GB到十几GB。这里暂且记为 act_mem

单卡显存约束:

4P + act_mem ≤ 80 GB (1)

最大参数量 P 的估算:

act_mem 的保守估计可取 10~15 GB(使用梯度检查点后)。代入(1)式:

若 act_mem = 15 GB,则 4P ≤ 65 GB → P ≤ 65 / 4 = 16.25 B。

若 act_mem = 10 GB,则 4P ≤ 70 GB → P ≤ 17.5 B。

更精确的估计:还需考虑通信缓冲区(通常与参数分片大小相当,可忽略或包含在ZeRO开销中)和CUDA context等杂项(约1~2GB)。所以,在4卡80GB A100下,采用ZeRO-3 + 梯度检查点,最大可训练模型规模约 15~20B 参数。若要训练20B以上,可能需要开启ZeRO-Offload或增加GPU数量。


显存估算中,除了参数、梯度、优化器,还有哪些容易被忽略的显存占用?(如临时缓冲、框架开销)

容易被忽略的显存消耗:

  • 激活值 (Activations):前向传播产生的中间张量,用于反向传播。若未使用梯度检查点,这部分可能超过模型状态本身。对于长序列,注意力矩阵的 O(L²) 消耗尤为恐怖。

  • 临时缓冲区 (Workspace):框架在执行某些算子时分配的临时空间,例如 cuDNN 卷积算法需要的工作区、AllReduce通信的缓冲区、算子融合的中间结果等。通常几百MB到几GB不等。

  • CUDA 上下文和内核开销:GPU驱动和CUDA运行时需要一定的显存来维护设备状态、加载内核等,约占用 0.5~2 GB。

  • 碎片化内存:PyTorch的缓存分配器(caching allocator)在频繁分配和释放不同大小的张量时,会产生显存碎片,导致实际可用显存低于理论值。预留内存(reserved memory)往往大于实际分配内存(allocated memory),即 torch.cuda.memory_reserved() 大于 torch.cuda.memory_allocated()

  • 通信缓冲区:在分布式训练中,NCCL等通信库会分配环形缓冲区用于聚合数据,这些缓冲区占用显存且通常不会被及时释放。

  • Dataloader 的 pinned memory:虽然是在CPU端,但如果开启了 pin_memory=True,会占用CPU物理内存,间接影响系统整体内存压力,极端时可能导致OOM。GPU端也可能有对应的暂存区。

  • 框架自身开销:如 PyTorch 自动求导所需的图结构、保存的 backward hooks 等,占用少量显存。

进行精确估算时,通常需要在目标配置下实际运行一个小型模型,测量这些“隐性”开销,然后外推到大规模。经验法则:为安全计,实际显存需求 = 理论计算 × (1.1~1.3) 来留出余量。

PyTorch 中,torch.cuda.memory_allocated() 与 memory_reserved() 的区别

在训练大模型时,我们经常调用这两个函数来监控GPU显存使用情况。它们的含义和指向的对象有本质区别,理解清楚对排查OOM和优化显存至关重要。

torch.cuda.memory_allocated() 返回的是当前实际被张量占用的显存总量。换句话说,你创建的每一个张量(包括模型参数、梯度、激活值、中间计算结果等)所占用的显存,都会被计入这个值。当一个张量被Python的垃圾回收机制回收后,这部分显存会被PyTorch的缓存分配器标记为空闲,此时memory_allocated()会下降。

torch.cuda.memory_reserved() 返回的是PyTorch的缓存分配器从CUDA驱动那里预先申请并保留的显存总量。PyTorch为了提高显存分配效率,不会在每次张量释放时立即将显存归还给CUDA驱动,而是将这些空闲显存保留在自己的缓存池中,以便下次创建新张量时快速复用。这部分保留但可能未被实际使用的显存,就被计入了memory_reserved()

二者关系可以类比于餐馆经营:memory_reserved() 是餐厅从物业那里租下的整个店面面积,memory_allocated() 是当前正在用餐的顾客实际占据的座位面积。空闲座位虽然没被占用,但物业仍然会收取租金。

排查显存泄漏时,如果发现memory_reserved()持续增长且不回落,通常意味着PyTorch的缓存分配器在不断向CUDA驱动申请更多显存,这可能是因为训练循环中持续产生新的张量且未被及时释放,导致缓存池被耗尽后不得不扩张。而如果仅memory_allocated()增长,可能是因为某些张量被无意中保留(例如存储了loss history但未detach),导致分配器无法回收它们。

训练时突然 OOM,如何通过加大 gradient_accumulation_steps 来解决?

OOM的根本原因是当前配置下,单张GPU在一次前向+反向传播过程中,所需的总显存超过了可用容量。总显存需求主要由两部分组成:模型状态(参数、梯度、优化器状态)和激活值。激活值的大小与单次送入模型的样本数(即micro batch size)和序列长度直接相关。

加大gradient_accumulation_steps(梯度累积步数)解决OOM的逻辑链条如下:

  • 首先,我们减小每张GPU一次前向传播所处理的样本数,即micro batch size。例如从4降到1。micro batch size减小,激活值显存基本等比例下降,这是最直接的显存释放。

  • 但减小micro batch size会导致全局批次大小(global batch size = micro_batch_size × num_gpus × accumulation_steps)下降,过小的全局批次会导致训练不稳定、收敛变慢。

  • 为了保持全局批次大小不变,我们同步增大gradient_accumulation_steps。例如原本micro batch size=4, accumulation_steps=1,现在改为micro batch size=1, accumulation_steps=4。模型每处理完一个micro batch就计算梯度并累加到梯度缓冲区中,直到累积了4步的梯度后,才执行一次优化器更新。

  • 这种操作的好处是:激活值只在当前micro batch存在时占用显存,处理完立即释放,下一个micro batch复用显存空间。因此,激活值峰值由micro batch size决定,而不是全局批次大小。通过减小micro batch size,我们压低了激活峰值,从而避免OOM。模型状态(参数、梯度、优化器状态)的显存占用不受梯度累积的影响,因为它们需要在整个累积周期内持久存在。

需要注意的是,加大梯度累积步数会增加训练时间,因为前向和反向的计算总量不变,但参数更新的频率降低了,通信次数也可能增加(取决于框架优化)。此外,对于使用了BatchNorm的模型,微批次过小会导致BN统计不准,但Transformer中普遍使用LayerNorm,避开了这一问题。

分析“微批次大小 (micro batch size)”与“全局批次大小 (global batch size)”对激活显存的影响。

微批次大小(mbs)和全局批次大小(gbs)是两个不同层面的概念,对激活显存的影响也完全不同。

微批次大小直接决定激活显存峰值。在一次前向传播中,模型需要为mbs个样本生成所有的中间激活张量(Q、K、V、注意力矩阵、FFN隐层输出等),这些张量在反向传播时被消费。激活显存与mbs基本成正比:mbs翻倍,激活显存也大致翻倍。因此,当显存紧张时,最直接的办法就是减小mbs,哪怕把mbs降到1也在所不惜。这也是梯度累积技术存在的价值——它允许我们用极小的mbs来训练,同时通过累积多步来维持大的gbs。

全局批次大小不直接影响激活显存。gbs决定了模型在多步累积后,进行一次参数更新的总样本数。由于激活值在每个micro batch处理完后被释放,gbs的增大并不会让激活峰值增加。但gbs会影响梯度的统计特性:gbs越大,累积的梯度噪声越小,训练越稳定,可能允许使用更大的学习率,从而加速收敛。

一个常见的误解是“全局批次大则显存占用大”。实际工程中,我们经常见到这样的配置:单卡mbs=1,累积16步,用64张卡,gbs=1024。此时单卡的激活显存仅相当于处理1个样本的量,远小于模型状态显存。因此,通过调整mbs和累积步数,我们可以在不改变gbs的情况下灵活控制激活显存。

序列长度从 2048 翻倍到 4096,激活显存大约增加多少?(提示:注意力部分 O(L²),其他 O(L))

序列长度L对激活显存的影响是训练长文本时必须精算的成本。激活显存按来源可拆分为两部分:注意力相关的二次项,和其他线性项。

注意力部分的二次增长:标准自注意力需要计算一个形状为 [batch, heads, L, L] 的注意力分数矩阵(或softmax之后的概率矩阵)。这个矩阵在反向传播时需要被保存(或用重计算避免)。其显存占用量正比于 。当L从2048增加到4096,L²变为原来的4倍。假设原序列长度下注意力矩阵占用 M_attn GB,翻倍后变为 4 × M_attn GB,增量 3 × M_attn

其他部分的线性增长:Q、K、V、FFN中间激活等张量,其形状通常是 [batch, L, hidden_dim] 或类似,与L成正比。这部分显存从 M_linear 变为 2 × M_linear,增量 M_linear

总增长幅度取决于M_attn和M_linear的相对比例。在较短序列下,M_linear可能占主导,增长近似线性;但在长序列下,M_attn成为绝对大头,增长接近二次。以GPT-3为例,2048长度下注意力矩阵可占激活显存的40%~60%;翻倍到4096后,注意力矩阵部分膨胀4倍,可能占据总激活显存的70%以上,总激活显存大约变为原来的2.5~3倍。具体数字需要根据模型隐藏维度、头数等参数计算,但二次增长的趋势是确定的。

因此,训练长序列时,如果不采用FlashAttention等优化,显存成本会急剧膨胀,这也是FlashAttention成为标配的根本驱动力。

如果使用 FlashAttention,激活显存中的注意力矩阵是否还需要存储?省了多少?

FlashAttention通过分块计算和在线softmax技术,在前向传播时完全不存储完整的注意力矩阵。它每次只加载一个小块的Q和K到SRAM中,计算出该块的局部注意力分数并立即进行softmax的增量更新,然后丢弃该块的中间矩阵。反向传播时,它利用保存的softmax归一化因子和Q、K、V,在SRAM中重新计算所需的注意力矩阵块,从而避免了在HBM(高带宽显存)中存储完整的 [L, L] 矩阵。

因此,使用FlashAttention后,注意力矩阵那部分O(L²)的激活显存被完全消除。节省的量取决于序列长度和模型配置。在长序列场景下(如L=4096),这部分节省可达数十GB。以LLaMA-7B为例,L=2048时注意力矩阵约占激活显存的40%,使用FlashAttention后这部分直接清零,激活总显存下降约40%。如果L翻倍到4096,原注意力矩阵占比更高,节省效果更显著。

但要注意,FlashAttention并不会减少Q、K、V矩阵的显存,也不影响FFN等其他激活。它专注于解决注意力矩阵这个最大的内存杀手。

FlashAttention 结合 Gradient Checkpointing 能省多少显存?以 GPT-3 为例说明。

FlashAttention和Gradient Checkpointing是两种互补的显存优化技术,组合使用可以将激活显存压缩到极致。

  • FlashAttention:消除注意力矩阵的O(L²)存储,仅保留Q、K、V等线性张量。

  • Gradient Checkpointing:在前向传播时,只保留每个Transformer层的输入张量(检查点),丢弃层内所有中间激活。反向传播时,从检查点重新执行该层的前向计算来恢复这些激活。这进一步削减了Q、K、V、FFN中间输出等线性激活的存储。

以GPT-3 175B模型为例(L=2048, batch=1, 96层, hidden_dim=12288, 96头):

  • 无任何优化时:每层注意力矩阵约占用 96头 × 2048² × 2 bytes ≈ 768 MB,96层累计约72 GB(但实际因各层串行,峰值出现在单层,这里为总激活量)。加上QKV和FFN等线性激活,单层总激活峰值可能超过1 GB,全模型激活总量达数十GB。

  • 仅FlashAttention:注意力矩阵存储降至0,线性激活仍保留。单层峰值降至约300-400 MB,全模型总激活量降为原来的40%~50%。

  • 仅Gradient Checkpointing:注意力矩阵仍存在,但每层中间激活不存储,只保留层输入。单层激活峰值仍是注意力矩阵主导,但全模型总激活量大幅下降(因为不堆积各层中间结果)。

  • 两者结合:注意力矩阵被FlashAttention消除,同时Gradient Checkpointing让每层仅需保存输入张量。单层激活峰值降到极低(仅层输入张量,约 batch×L×hidden_dim×2 bytes ≈ 8 MB),96层叠加的激活峰值也仅约为 96 × 8 MB ≈ 768 MB。相比无优化时数十GB的激活量,节省了约95%以上。这也是为什么大模型能在有限显存下训练的根本原因。

什么是“Activation Offloading”?将激活值卸载到 CPU 的代价是什么?

Activation Offloading 是一种极端的显存优化技术。当GPU显存连经过Gradient Checkpointing压缩后的激活值都放不下时,可以将激活值从GPU显存拷贝到CPU内存中暂存。在反向传播需要时,再从CPU拷贝回GPU。

这种方法的核心是用CPU内存的容量来换GPU显存。CPU内存通常远大于GPU显存(例如一台服务器可能有1TB内存,而GPU只有80GB),且成本低得多。因此,理论上可以训练无限长的序列。

代价:CPU和GPU之间的数据传输通过PCIe总线,其带宽(如PCIe 4.0 x16 约32 GB/s)远低于GPU内部带宽(HBM约2 TB/s),更无法与GPU计算速度匹配。每次前向产生的激活值需要异步拷贝到CPU,反向时再异步拷贝回来,这会产生巨大的延迟。具体来说:

  • 训练速度严重下降:通常会使单步训练时间增加50%~200%,甚至更多,取决于激活值的大小和计算/通信重叠的程度。

  • CPU内存压力:大量激活值堆积在CPU内存,可能导致CPU OOM。需要精心管理CPU内存,及时释放已消费的激活值。

  • 实现复杂:需要细粒度的异步拷贝和流水线调度,才能部分隐藏传输延迟。

这种技术通常作为最后手段,在显存极端受限(如单卡训练大模型、超长序列)且无法增加GPU时使用。

在分布式训练中,如何计算总显存占用?各卡是否有差异?

分布式训练中,总显存占用取决于并行策略。不同策略下各卡的显存占用可能相同,也可能差异显著。

数据并行 + ZeRO:这是最常见的情况。每张卡的显存占用基本相同,计算公式为:

text

显存_per_GPU = 模型状态_per_GPU + 激活值_per_GPU

  • 模型状态_per_GPU = (总模型状态 / DP数) 根据ZeRO阶段而定。若ZeRO-3,则参数、梯度、优化器状态都被均分。

  • 激活值_per_GPU = micro_batch_size产生的激活值(各卡独立计算,但相同)。

  • 各卡显存完全对称。

张量并行 (TP):单层内权重被切分,激活值也被切分。各卡显存基本对称,但通信缓冲区占用可能不同。

流水线并行 (PP):不同层分配到不同设备,各卡存储的层数不同。如果层均匀分配,显存也接近对称;但如果首尾层有嵌入层等大参数模块,可能分配不均。此外,PP的第一张卡需要保存输入的嵌入层,最后一张卡需要保存输出的LM head,这些额外参数可能导致显存不均衡。

序列并行 (SP):与TP结合时,激活值沿序列维度切分,各卡存储部分激活,显存对称。

异构设备:如果使用不同型号GPU(如混合A100和V100),显存占用也不同,且通信带宽受限。一般不推荐。

总显存占用:集群总显存占用 = sum(各卡显存占用)。但分布式训练关注的是单卡峰值,因为任何一张卡OOM都会导致整个训练失败。所以设计时需保证每张卡的峰值显存在其容量之内。

张量并行 (TP) 如何影响单卡显存?以 Megatron 的 MLP 切分为例说明。

张量并行将单层内的参数矩阵切分到多张GPU上,从而降低单卡的参数和激活值存储。

以 Megatron-LM 对 MLP 层的切分为例。MLP 由两层全连接组成:第一层从 hidden_dim 升维到 4×hidden_dim,第二层再降维回来。设 hidden_dim=H,升维维度=4H。

不切分时,单卡存储完整的两个权重矩阵:W1: [H, 4H]W2: [4H, H],参数量为 H×4H + 4H×H = 8H²(忽略偏置)。

Megatron的切分方式:

  • W1 沿列切分成两个子矩阵 W1_a [H, 2H]W1_b [H, 2H],分别放在GPU0和GPU1上。输入X(形状为 [batch, L, H])被广播到两张卡。

  • 每张卡独立计算 Y_a = GeLU(X @ W1_a)Y_b = GeLU(X @ W1_b)

  • W2 沿行切分为 W2_a [2H, H]W2_b [2H, H]。每张卡计算 Z_a = Y_a @ W2_aZ_b = Y_b @ W2_b

  • 通过 AllReduce 将 Z_aZ_b 相加,得到最终输出。

显存影响:

  • 参数量:每张卡只存储 H×2H + 2H×H = 4H² 的参数,是原来的一半。

  • 激活值:中间激活 Y 的形状从 [batch, L, 4H] 降为 [batch, L, 2H],显存也减半。

  • 梯度:每张卡只计算自己那部分参数的梯度,梯度显存也减半。

  • 因此,TP=2 可将MLP层的参数和激活显存降低约50%。TP度越大,节省越多,但通信量也增大。通常TP只用在节点内(NVLink高速互联),跨节点通信会成为瓶颈。

流水线并行 (PP) 是否减少单卡显存?为什么?PP 主要解决什么问题?

流水线并行将模型的不同层分配到不同的GPU上,每张GPU只负责其中一部分层的计算和存储。因此,PP确实减少单卡显存。单卡的参数和优化器状态仅包含其负责的那几层,而不是全部层。例如,一个48层的模型,用PP=4切分,每张卡只存12层的参数和优化器状态,显存大约降为原来的1/4(忽略嵌入层等非均匀分布)。

但PP不减少激活值显存。每张卡仍然要为它负责的层处理完整的micro batch数据,激活值峰值取决于该层的大小和微批次大小,与未切分时相同(只是激活值只存在于该卡负责的层中,而不是全模型)。

PP主要解决的问题是模型太大,单卡放不下所有层。它与ZeRO不同,ZeRO通过分片优化器状态和参数来解决显存问题,但每张卡仍要“看到”所有层的计算(通过AllGather)。PP则是从根本上将模型“拆开”,每张卡物理上只拥有部分层,属于模型并行范畴。PP常与TP和DP组合使用,构成3D并行,以训练万亿参数模型。

PP的缺点是有流水线气泡,设备利用率下降。1F1B调度策略可部分缓解。

序列并行 (SP) 主要减少哪部分显存?它与 TP 结合时效果如何?

序列并行将输入序列的长度维度切分到多张GPU上,主要减少的是激活值显存。

在标准Transformer中,自注意力需要计算整个序列的注意力矩阵(O(L²)显存),而序列并行后,每张卡只存储自己负责那部分序列的激活值。具体来说,激活张量(如Q、K、V、FFN中间输出)的形状 [batch, L, hidden_dim] 沿L维度被切分,每张卡只持有 L/N 长度的激活。注意力矩阵也被切分或重计算。因此,激活显存随序列长度L的减少而线性下降(若注意力矩阵通过重计算则更是如此)。

与TP结合时效果更佳:TP已经将hidden_dim维度切分,SP再将L维度切分,二者正交叠加。例如,TP=2将激活显存减半(沿hidden_dim),SP=2再将剩余的激活显存减半(沿L),总激活显存降至原来的1/4。这种组合极大提升了长序列训练的能力。Megatron-LM将TP和SP结合,在训练超长文本时显存占用大幅下降。

但SP会引入额外的通信:在注意力计算前需要AllGather收集完整序列的K和V(或使用Ring Attention避免全量收集)。这部分通信量与L成正比,因此SP更适合极长序列、计算量主要由注意力主导的场景。

解释 ZeRO-Infinity 如何利用 NVMe 硬盘扩展显存。

ZeRO-Infinity 是 ZeRO 的终极扩展,它打通了 GPU 显存、CPU 内存和 NVMe 固态硬盘之间的数据迁移通路,将可用“显存”池从 GPU 扩展到整个异构存储系统。

工作原理:

  • 参数、梯度、优化器状态均可卸载到 CPU 内存(这已经是 ZeRO-Offload 的能力)。但当模型极大,连 CPU 内存都不够时,ZeRO-Infinity 将数据进一步卸载到 NVMe SSD。

  • 前向传播需要某层参数时,从 NVMe 读取到 CPU 内存,再传输到 GPU;反向传播同理。计算出的梯度也先暂存 CPU,需要时再写入 NVMe。

  • 优化器更新在 CPU 上执行,利用 CPU 的大内存和 NVMe 的大容量,GPU 仅负责计算密集的前向和反向传播。

NVMe 的角色:充当一个超大容量的低速“显存”。NVMe 的带宽(约 3~7 GB/s)远高于普通 SSD,但仍比 CPU 内存慢一个数量级,更无法与 GPU HBM 相比。因此,ZeRO-Infinity 会带来显著的训练减速。但它使得在有限 GPU(如单卡或数卡)上训练超大规模模型(如 100B+)成为可能,是“用空间和时间换规模”的极致体现。

为了最小化性能损失,ZeRO-Infinity 采用激进的预取和重叠策略:在计算当前层时,后台异步从 NVMe 加载下一层参数到 CPU 缓存;梯度也异步写回 NVMe。通过精巧的流水线调度,将数据传输时间部分隐藏在计算时间之后。即便如此,对于 NVMe 卸载,训练速度通常只有纯 GPU 训练的 10%~30%。

在微调阶段,使用 LoRA 训练时,显存节省主要体现在哪里?

LoRA(Low-Rank Adaptation)的核心思想是“冻结主模型,只训练旁路”。它不直接修改预训练权重,而是在需要适配的层旁边插入一对低秩矩阵A和B,训练时只更新这两个小矩阵。这一设计带来了显存占用的根本性改变。

第一,梯度的显存急剧缩减。

全参数微调时,反向传播需要为模型的每一个参数计算并存储梯度。一个7B参数的模型,仅梯度(FP16)就需要占用14GB显存。这些梯度要一直保留到优化器更新完成,是显存的常驻消耗。而LoRA冻结了主模型的所有参数,这意味着主模型的参数不需要梯度。反向传播时,计算图只需要追溯到LoRA层的低秩矩阵,只为这极少量的参数生成梯度。假设rank=16,LoRA的可训练参数通常只有全模型的0.1%左右,梯度占用从14GB骤降到几十MB,几乎可以忽略不计。

第二,优化器状态的显存同样坍塌。

Adam优化器需要为每一个可训练参数保存一阶动量m和二阶动量v,两者通常以FP32存储,占用的显存是参数本身的8倍。全参数微调7B模型时,m和v合起来需要约56GB显存,这是训练中最大的一笔开销。LoRA只有极少量的可训练参数,对应的优化器状态也缩减到几乎为零。这是LoRA能够将微调门槛降低到消费级显卡的核心原因。

第三,主模型权重以只读方式存在,无需冗余。

在全参数微调中,往往需要保留一份FP32的主权重副本用于精确更新,这又是一笔与模型参数相当的显存开销。LoRA的原始权重被冻结,不需要梯度,也不需要FP32副本。前向传播直接使用FP16的原始权重,反向传播时这些权重不产生梯度,不需要额外存储。主模型权重以一种“只读”的方式参与计算,不产生额外的显存负担。

第四,激活值可以选择性优化。

虽然前向传播仍然需要产生激活值用于反向传播,但LoRA的旁路结构让激活值的存储也有优化空间。因为主模型不变,某些中间层的输出可以在反向传播时重新计算而不是存储,进一步压缩激活值占用。配合Gradient Checkpointing,激活值可以被压到极低水平。

综上,LoRA将微调的显存开销从“模型参数、梯度、优化器状态”的三座大山,削减到仅剩“模型参数的只读存储”和“极少量可训练参数的相关状态”。这使得原本需要多张A100才能完成的7B模型全参数微调,变成单张消费级显卡就能跑的任务。

量化训练 (QLoRA) 如何进一步降低显存?4-bit 量化和双重量化的作用。

如果说LoRA解决了“梯度和优化器状态”的显存问题,那么QLoRA则进一步向“模型参数本身”开刀。在LoRA的基础上,QLoRA通过将预训练模型权重量化到4-bit,并引入双重量化技术,将模型权重的显存压缩到极限。

4-bit NormalFloat (NF4) 量化:

传统的4-bit整数量化采用均匀分段,将连续的浮点数值映射到16个离散值上。但神经网络的权重通常服从正态分布——大部分权重集中在均值附近,两端尾部的值较少。均匀量化会在密集区域浪费精度,在稀疏区域过度分配。

NF4专门针对正态分布设计。它假设数据服从标准正态分布,然后在累积分布函数上均匀分位,使得每个量化区间内包含的数据量大致相等。在分布的密集区域,NF4提供更精细的分段;在尾部区域,分段更粗糙。这样,4-bit的表示能力被最大限度地利用在“大多数”权重上,重建精度远高于均匀量化。

显存收益非常直接:原本FP16占2字节的每个权重,被压缩为4-bit,即0.5字节。模型权重显存直接降至原来的四分之一。一个65B参数的模型,FP16权重需要约130GB显存,NF4量化后仅需约32.5GB。这是从“需要多张A100”到“单张A100即可承载”的跨越。

双重量化(Double Quantization):

量化本身会产生额外的开销。为了能够反量化,需要为每个量化块存储缩放常数和零点。这些量化参数通常以FP32存储。当模型很大时,量化参数本身也会占用可观的显存——在65B模型上可能达到几GB。

双重量化对这些量化参数本身再进行一次量化。具体来说,它对第一轮量化产生的缩放常数进行第二轮的FP8量化。由于缩放常数的数量远少于原始权重,这轮量化几乎不损失精度,却能额外节省约3~5GB的显存。在显存边缘的情况下,这可能是OOM和成功运行之间的差距。

分页优化器(Paged Optimizer):

QLoRA的第三个关键技术是将优化器状态卸载到CPU内存中。它借鉴了操作系统的分页机制,将GPU显存作为CPU内存的缓存。当需要更新某个参数块时,才将对应的优化器状态页从CPU调入GPU,更新完成后再写回CPU。这样,即使GPU显存不足,也能利用CPU的大容量内存来存储优化器状态,仅在当前步骤需要时才占用GPU空间。

混合精度训练中,master weights 保存在 FP32,多占多少显存?

混合精度训练的标准流程是:前向传播和反向传播使用FP16的权重和激活值,以利用Tensor Core加速;但在优化器更新时,需要一个FP32的高精度权重副本来累积微小的梯度更新。这个FP32副本就是master weights。

它的显存开销非常直接:每个参数需要额外存储4字节的FP32副本。对于10B参数的模型,就是 10×10⁹ × 4 bytes = 40GB。对于7B模型,约28GB。这部分显存是混合精度训练为数值稳定性付出的代价。

为什么必须要FP32?FP16的尾数只有10位,能表示的精度约3位有效数字。当梯度很小(训练后期常见)时,FP16可能无法表示梯度和权重的比值,导致更新被截断为零,训练停滞。FP32的23位尾数提供了足够的精度来累积这些微小更新,保证训练的持续进行。

在ZeRO优化中,master weights同样被分片。使用ZeRO-2且8张卡时,这40GB被均分到每张卡,单卡仅需5GB,不再是瓶颈。

为什么 BF16 训练时,master weights 有时可以不用 FP32?

BF16(Brain Float 16)与FP16的根本区别在于它的指数位数。BF16有8位指数,与FP32相同,因此它的数值表示范围与FP32完全一致,最大可以表示约3.4×10³⁸,最小正规数约1.2×10⁻³⁸。这意味着在训练过程中,无论是激活值的前向传播,还是梯度的反向传播,BF16都不会因为数值超出范围而溢出。

FP16的痛点在于它只有5位指数,表示范围窄(最大约65504)。一旦某个中间值超出这个范围,就会变成Inf,导致训练崩溃。因此FP16必须保留一份FP32的master weights来兜底。

BF16的动态范围与FP32一致,溢出的风险被消除了。剩下的问题是精度:BF16的尾数只有7位,约2位有效数字,远低于FP32。理论上面临梯度被截断的风险。但大量实践表明,随机梯度下降本身的噪声远大于这个精度损失。LLaMA系列、OPT等大模型都在BF16下直接训练,不保留FP32主副本,收敛性和最终性能几乎没有差异。

因此,BF16训练可以省去FP32 master weights,每参数显存从6字节(FP16权重+FP32主副本)降至2字节(仅BF16权重),节省了约67%的权重相关显存。这是BF16相比FP16的又一个重大优势。

训练时是否可以使用 INT8 优化器?有哪些现成实现?对精度的影响。

使用INT8优化器的动机很明确:Adam的优化器状态(一阶动量m和二阶动量v)以FP32存储,占模型参数量的8倍。对于7B模型,这就是56GB显存。如果能将m和v压缩到INT8,显存将骤降至14GB。

现成实现:

bitsandbytes库提供了AdamW8bit优化器,是目前应用最广泛的INT8优化器。它的核心机制是:将优化器状态的FP32张量分块,每个块独立计算自己的缩放因子,然后块内的值被量化为INT8存储。在更新时,反量化回FP32进行计算,然后再量化回INT8。

DeepSpeed提供了1-bit Adam,将动量压缩到极致的1-bit表示,主要用于降低通信开销,但也有显存节省效果。

对精度的影响:

优化器状态本质上是在平滑地累积历史梯度信息。每个训练步,动量的变化量远小于动量本身,因此量化引入的相对误差非常小。在大多数任务上,INT8优化器的精度损失微乎其微,困惑度上升通常在1%以内,下游任务性能几乎无差异。

但在某些特殊场景下需要注意:极小模型可能因为状态量太少而受量化噪声影响较大;训练初期动量尚未稳定时,量化误差可能有一定干扰;对精度要求极高的任务(如某些科学计算或金融预测),需要谨慎评估。

总体来说,INT8优化器是显存紧张时非常可靠的选择,与混合精度和LoRA等技术组合使用,可将优化器状态显存压缩到原来的十分之一以下。

给定一个 13B 模型,FP16,用 8 张 80GB A100,DP=8,无其他并行,能否训练?需估算 batch size=1 时的显存。

纯数据并行意味着每张卡拥有完整的模型副本,各自计算梯度后同步。每张卡的显存需求完全一样。

模型参数(FP16):13B × 2 bytes = 26GB。每张卡都要存储完整的26GB权重。

梯度(FP16):反向传播时,每张卡计算自己那部分数据的梯度,同样需要26GB来存储。

优化器状态:Adam的m和v各一份FP32,加上一份FP32主副本,共12 bytes/param。13B × 12 = 156GB。这是最大的一笔开销,远超单卡80GB。

模型状态合计:26 + 26 + 156 = 208GB。仅这三项就已经远超80GB,OOM是必然的。

激活值:即使不考虑上述模型状态,激活值也是一笔不小的开销。以序列长度2048为例,13B模型的单层激活值(注意力矩阵、FFN中间输出等)约需15-25GB。即使使用梯度检查点,也需要额外10-15GB。这些在模型状态面前甚至显得次要。

结论:纯数据并行完全不可行。必须引入ZeRO至少第2阶段(分片优化器状态和梯度),或者使用模型并行和张量并行。在8卡上,ZeRO-3可将单卡模型状态压缩至约14GB,配合梯度检查点将激活值控制在10GB以内,总显存约24GB,远超80GB限制——所以实际上ZeRO-3可以让13B模型在8卡上舒适训练,单卡显存占用约30-40GB。

如果上面的设置 OOM,在不改变硬件的前提下,列举至少 5 种降低单卡显存的方法。

  1. 启用ZeRO-3:将参数、梯度、优化器状态全部分片到8张卡。单卡模型状态从208GB骤降至约 208/8 = 26GB,立刻从OOM变为可行。这是最根本、最有效的措施。

  2. 使用Gradient Checkpointing:以额外约15-25%的计算量为代价,将激活值存储量降低50%-70%。对于13B模型,激活值可从约20GB降至6-10GB,释放大量空间。

  3. 减小序列长度:注意力矩阵的显存与L²成正比。将序列长度从2048降至1024,注意力矩阵显存降为原来的1/4,激活值总量大幅下降。如果下游任务允许较短上下文,这是最直接的优化。

  4. 使用FlashAttention:完全消除注意力矩阵的O(L²)存储。在长序列下效果尤其显著,可节省数十GB激活值显存,且不增加计算量(甚至更快)。

  5. 使用CPU Offload:将优化器状态(甚至梯度)卸载到CPU内存。虽然PCIe传输会拖慢训练(30%-70%不等),但在显存是唯一瓶颈的情况下,这是用时间换空间的有效手段。

  6. 使用LoRA微调代替全参数训练:如果任务是微调而非预训练,LoRA将可训练参数量压缩到全量的0.1%,梯度、优化器状态显存几乎归零。

什么是“显存碎片” (Memory Fragmentation)?如何影响训练?PyTorch 的分配器如何缓解?

显存碎片指的是显存中存在大量小的、不连续的空闲块。这些碎片的总容量可能足够分配一个新的大张量,但因为它们不连续,无法被一次性使用,最终导致分配失败。

在大模型训练中,激活值张量的形状频繁变化(不同层的输出维度不同,FFN中间层扩展4倍再收缩)。每次分配和释放都会在显存中留下形状各异的“空洞”。长期训练后,碎片累积,可能出现这样的诡异现象:监控显示显存还有20GB空闲,但尝试分配一个10GB的连续张量却报错OOM。

PyTorch的缓存分配器通过预申请和复用机制来缓解碎片。它不会在张量释放时立即将显存归还给CUDA驱动,而是将空闲置入自己的缓存池。下次需要相似大小的张量时,直接从池中分配,避免了频繁的系统调用和碎片产生。

然而,如果训练中频繁出现大小交替的张量(比如动态序列长度),缓存池的效果会打折扣。PyTorch 2.0引入的expandable_segments特性更进一步:允许分配器在物理显存允许时,动态扩大已分配的显存段,将原本分散的空闲块“拼接”起来,极大地缓解了碎片问题。

环境变量 PYTORCH_CUDA_ALLOC_CONF 中的 expandable_segments 有什么作用?

PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True 是PyTorch 2.0为解决显存碎片问题引入的重要特性。

在传统分配器下,PyTorch从CUDA驱动申请一块固定大小的显存段。随着训练进行,段内产生了大量碎片,即使总空闲量足够,也可能无法满足一个大张量的连续分配需求。这种情况下,分配器只能向CUDA驱动申请新的显存段,导致显存占用膨胀,最终OOM。

启用expandable_segments后,分配器可以在段内空闲总量足够的情况下,动态地扩展现有显存段的边界,将分散的空闲块合并为一个大的连续块。这相当于对显存进行了“碎片整理”,只是这个过程是在分配器层面透明完成的,无需移动数据。

这个特性对大模型训练尤其有价值。训练循环中产生的激活值张量大小差异巨大,碎片化严重。expandable_segments能让这些碎片被有效回收利用,显存利用率显著提高,OOM风险大幅降低。目前建议在所有大模型训练脚本中默认开启此选项。

训练过程中,显存占用逐渐增加是什么原因?如何检测并解决内存泄漏?

训练过程中显存持续增长,最终OOM,是典型的“内存泄漏”症状。与训练初期的OOM不同,泄漏发生在训练已经稳定运行一段时间后。

常见原因:

一是在训练循环中无意保留了张量的引用。例如,为了记录每个batch的loss,将loss值存入一个Python列表。但loss是一个标量张量,它持有整个计算图的引用,导致反向传播后计算图无法释放,激活值和梯度全部堆积在显存中。

二是一些中间计算结果被意外保存。例如可视化的注意力图、调试用的中间激活值、未及时detach的生成结果等。

三是第三方库或自定义算子的内部缓存。某些算子(如FlashAttention的旧版本、某些自定义CUDA扩展)会维护内部状态或缓存,这些缓存可能随训练步数增长。

四是DDP或NCCL的通信缓冲区。在梯度同步过程中,通信库会分配临时缓冲区。如果通信模式频繁变化,可能导致缓冲区逐渐膨胀。

检测方法:

使用torch.cuda.memory_allocated()记录每一步训练后的显存占用,观察是否有持续上升的趋势。正常情况下,除前几步外,显存应保持稳定。如果发现持续上涨,在训练循环中逐段插入torch.cuda.empty_cache()memory_summary(),定位是哪个操作导致了增长。

更系统的方法是使用PyTorch的memory profiler或NVIDIA的Nsight Systems,捕获完整的显存时间线,可视化每次分配和释放。

解释“二次峰值”现象:为什么反向传播后期显存会突然降低?

在训练的一个完整前向-反向周期中,显存占用不是一条平稳的直线,而是呈现出先升后降的“山峰”形状。

前向传播阶段,每一层计算出的激活值都被保留在显存中,等待反向传播时使用。随着前向的推进,激活值不断累积,显存占用逐步攀升,到最后一层前向完成时达到最高峰。

进入反向传播后,显存并不会立刻下降,因为第一层反向传播(即输出层)仍需消费最后一层前向的激活值。但随着反向传播向浅层推进,已计算完梯度的层,其对应的激活值就被释放了。到反向传播的中后期,大量激活值已被消费和释放,显存开始快速下降。到整个反向传播完成时,仅剩梯度、优化器状态等持久状态,显存降至谷底。

这就是“二次峰值”现象——显存峰值出现在前向完成、反向刚开始的时刻,随后在反向后期显著下降。理解这个模式对于优化显存峰值有帮助:如果要降低峰值,要么减少前向存储(如Gradient Checkpointing),要么在反向早期及时释放(如激活值流式消费)。

为什么在开启 Gradient Checkpointing 后,显存使用不再是简单的求和?

Gradient Checkpointing打破了“激活值总量=各层激活之和”的简单累加关系。

在没有检查点时,前向传播的每一层激活值都被完整保存,显存占用等于所有层激活值的总和。开启检查点后,模型只保存少数“检查点”张量(通常每层保存其输入张量),层内的中间激活(Q、K、V、注意力矩阵、FFN中间输出等)全部丢弃。

反向传播到某一层时,需要该层的中间激活来计算梯度。这些激活值并不在显存中,而是从最近的检查点(即该层的输入)重新执行前向计算,临时生成所需的中间激活。计算完这一层的梯度后,这些临时激活又被释放。

因此,显存占用不再等于所有激活值的静态总和,而是等于检查点张量的存储加上当前重计算层临时产生的激活。显存峰值出现在重计算某一层时——该层产生的临时激活叠加上所有层的检查点。这个峰值远低于全量存储,但具体数值取决于检查点放置策略和层的计算特征,无法用简单的线性叠加来估算。

分析 ZeRO 各阶段在单机 8 卡训练 7B 模型时的显存占用分布。

以LLaMA-7B为例,FP16混合精度,假设激活值经过Gradient Checkpointing后约8GB。先计算单卡原始需求:

  • 模型参数(FP16):7B × 2 = 14GB

  • 梯度(FP16):14GB

  • 优化器状态(FP32 m+v+master):7B × 12 = 84GB

  • 模型状态合计:112GB

  • 激活值(GC后):8GB

  • 单卡总需求:120GB,远超80GB

ZeRO-1(分片优化器状态):

  • 优化器状态被8卡均分:84/8 = 10.5GB

  • 参数和梯度仍为全量:14 + 14 = 28GB

  • 模型状态:38.5GB

  • 激活值:8GB

  • 单卡总显存:46.5GB,训练可行

ZeRO-2(进一步分片梯度):

  • 梯度被8卡均分:14/8 = 1.75GB

  • 优化器状态:10.5GB

  • 参数仍全量:14GB

  • 模型状态:26.25GB

  • 激活值:8GB

  • 单卡总显存:34.25GB,更加宽裕

ZeRO-3(进一步分片参数):

  • 参数被8卡均分:14/8 = 1.75GB

  • 梯度:1.75GB

  • 优化器状态:10.5GB

  • 模型状态:14GB

  • 激活值:8GB

  • 单卡总显存:22GB,极度充裕

随着ZeRO阶段升高,单卡显存占用递减。但通信量递增:ZeRO-1仅需一次小规模AllGather更新参数,ZeRO-2增加了梯度的ReduceScatter,ZeRO-3更是在每层前向和反向各需要一次全参数AllGather。在8卡单机内,NVLink带宽足够支撑这些通信开销,ZeRO-3是训练7B模型的舒适选择。