单GPU训练
- 流程:前向传播,反向传播,参数优化。
- batch size:影响模型收敛速度训练效率。较小初期容易收敛但是noise大。较大则可能浪费计算资源。(在lm这里一般是衡量token数量)
- 大的batch_size浪费算力资源和样本,但是grad估计会更加正确
- 小的batch_size能在初期尽量探索更多内容,但是梯度噪声大也影响性能。
- 最优batch size:模型性能对batch size数值不敏感,空间较大,容错很高(这里是需要调整超参数需要考虑的点)
- 内存消耗来源:模型权重,梯度,优化器状态,激活值
- 精度:FP32/BF16/FP8的显存不同,mixed precision会影响各部分的显存分布
- 模型权重,梯度,优化器状态占用较多,一般状态下,7b模型训练占用远超单卡GPU容量
- 激活值占用:batch size,sequence length,等
- 分析:PyTorch Profiler分析
- 内存优化方法:
- 激活重计算(Activation Recomputation)
- 前向传播时保留关键部分激活值,剩余内容在backward时重算,降低显存消耗(但是增加计算复杂度(?)
- Full(全部checkpoint),显存节省但是计算开销较大。
- Selective(选择部分checkpoint),比如选择注意力部分。大幅减少显存消耗,但是计算量增加较少
- 较新模型往往用其他方式选择checkpoint(也许是启发性的,或者甚至可以用一个模型来评估选择什么部分?)
- 往往在显存有限但是算力充足的场景使用
- 前向传播时保留关键部分激活值,剩余内容在backward时重算,降低显存消耗(但是增加计算复杂度(?)
- 梯度累积(Gradient Accumulation)
- 大的batch拆分成多个micro batch,分别forward + backward传播后累积梯度,统一优化。
- 优点:单步显存消耗减少的情况下实现大batch训练,且和Activation Recomputation兼容
- 缺点:每次优化需要多次forward+ backward,减慢训练速度
- 大的batch拆分成多个micro batch,分别forward + backward传播后累积梯度,统一优化。
- 激活重计算(Activation Recomputation)
Data Parallelism
- 核心思想是,多个GPU上跑同样的模型进行前向传播和反向传播,然后梯度同步(all reduce),最后optimize之后的权重仍然是相同的。
🥲
这里和之前的某篇文章(μFuzz提供的fuzz方法似乎很像),于是瓶颈和优化点都差不多(?也不是)
- 朴素的并行计算方法方法是,在所有GPU backward之后再同步梯度,但是通信时GPU空闲,效率不高。
- 针对朴素的DP的优化:
- 计算与通信重叠的优化
- backward每一层之后立刻callback进行all reduce梯度(加和所有卡上的同参数梯度,并求均值等)
在nn.Module计算backward完成后通过hook可以自动同步每一层。
- 问题:尾部效应,还是受到最慢的一张卡的最后一层backward速度的影响。且,每层all reduce会产生小规模通信,效率较为低下
- 梯度分桶的优化
- 打包发送(在上面这个操作的基础下),从而减少小规模通信
- 梯度累积下的数据并行优化:
- 梯度累积是,我们喂进去多次小batch的内容,且在最后一次小batch喂完了之后才发送all reduce内容。(这不废话吗)
- 计算与通信重叠的优化
- 一个好像没啥用的公式:global batch size = micro batch size * grad_acc * dp。
- 更大batch size时,优先增加dp,然后增加 grad_acc。
- 1D维度(只区分不同数据)下并行训练的配置流程,瓶颈和优化方向
- 配置:全局 batch size → sequence → 根据dp得到 grad_acc。
- 瓶颈:通信延迟,较大模型无法存放在单卡中。
- 优化:tensor/context/pipeline并行,或者参数分片等更高维度的技术
- DeepSpeed ZeRO:分片优化器?
- 大概思想:优化器状态,梯度,模型参数分片(shard)到多卡中。
- 三种分片阶段
- ZeRO-1:分片optimzer状态,参数和梯度都完整保存
- ZeRO-2:分片optimizer状态和gradient
- ZeRO-3:分片optimizer,gradient,parameters。
- 瓶颈仍然是通信开销。越彻底的分片,通信开销越大。
- ZeRO-1:
- forward + backward没有变化
- 使用reduce-scatter发送gradient分片到各卡,使用本地的optimizer和gradient更新自己负责的分片内容
值得注意的是,Adam,SGD等Optimizer在数值上是独立的,不存在集合计算的内容,所以可以分块使用。
- optimizer之后,通过all gather的方式收集每张卡的内容统一发送回去。
- ZeRO-2
- 由于模型是完整的,forward和backward没有变化
- 不需要all reduce,而是reduce-scatter得到梯度分片?(然后就可以把本地计算出来的其他gradient删掉了)
- 每张卡保存更新本地参数分片。
- 通信过程与ZeRO-1类似
- ZeRO-3
- 计算前向反向传播时,每层都all-gather拿到当前层的参数,计算完成释放。(呃呃呃总感觉有点抽象啊)这个通信瓶颈太大了。
- 后面和ZeRo-2相同。
- 瓶颈:通信!还TM是通信!(DP规模越大,显存越节省,可以训练更大的模型,但是DP规模过大时,通信延迟和带宽成为瓶颈)