deepspeed 详解-源码分析

大模型火了之后,大模型的分布式训练自然而然成为了一个研究热点,其中 deepspeed 无疑是 最火爆的开源分布式训练框架之一。最近失业了,比较闲,正好整理一下 deepspeed 的实现细节, 帮助大家更深入的了解和学习 deepspeed 这个框架。 ps:deepspeed 无疑是非常好用和流行的,但代码写的实在一言难尽。

在开始讲代码细节前,先整理一下大模型分布式训练的关键逻辑和问题,这样更容易理解一些技术点到底是为什么。 很多博客上来就是 “大模型的训练有三种并行策略,分别是数据并行、流水线并行和张量并行”, 和国内的教材一个风格,显然这对于新手小白不是很友好, 这里我们先讲为什么,再说有什么。

单卡时代

Long long ago,一个深度学习模型并没有超过单个显卡的显存,其全部模型参数都可以加载到一个GPU中, 并且在单卡完成整个训练或者推理过程。这个时代,大家都能很愉快的玩耍。

多卡并行时代

后来随着显卡越来越便宜,训练数据量越来越多,人们逐渐开始研究如何利用多卡加速模型的训练,实现思路也很常规, 就是多张卡同时参与训练,每张卡都独立加载整个模型,并且独立进行前后向过程。通过把训练数据的一个大的 batch 分成多个小 batch,每张卡独立处理一个小 batch,最后再把各个卡上的梯度汇总整合起来,在一个主卡(主进程) 中计算新的参数值,然后再把新参数同步到各个卡中,这样实现数据的并行训练,所以称之为数据并行(Data Parallel,DP ) 。 pytorchData Parallel (DP)Distributed Data Parallel (DDP) 都是这种方法的实现, 其中 Data Parallel (DP) 早于 Distributed Data ParallelData Parallel (DP)单机多卡 的实现, Distributed Data Parallel多机多卡 的实现。

大模型时代

进入大模型时代后,一张卡的显存不足以加载完整的模型或者完成一个训练过程,上述的 DPDDP 的方案自然就失效了。 遇到问题就要想办法解决,那如何解决这个问题呢?先不用考虑那些论文,我们自己思考一下。

  1. 首先要弄清楚的是,消耗显存的都有哪些?

    1. 模型的参数。

    2. 前向过程中,一些中间计算结果以及激活值(即激活函数的执行结果)。

    3. 后向过程中,每个参数的梯度值。

    4. 优化器的状态。比如 adam 算法,需要为每个参数再保存一个一阶动量和二阶动量。

  2. 接下来,思考如何解决内存不足的问题。核心思路其实很简单,主要有两个方向:

    1. 先不把全部数据加载到 GPU 显存,暂时存放在别的地方,需要的时候再同步到 GPU 显存中,用完就扔掉。

    1. 把参数放到 CPU 内存中或者高速SSD中(支持NVMe的ssd,走的PCI-E总线),这就是 deepspeed 中的 offload 技术。

    2. 多张GPU卡,每张卡保存一部分,需要的时候再从其他卡同步过来,这就是参数分割。

    1. 降低内存的需求。原来每个参数都是 float32 类型,占用4个字节。

    1. 改成半精度,用2个字节的 float16 替代4个字节 float32,显存需求一下就降低一半。

    2. 用量化技术,用2个字节的 int16 或者1个字节的 int8 代替4字节的 float32

显然,每种方法都不是完美的,都有一定的局限性并且会引入新的问题,比如:

  1. 参数进行多卡分割或者 offload,比如会增加大量数据同步通信时间,不要小看这部分时间消耗,相对于 GPU 的显存访问速度而言, 多机器之间的网络通信、单机多卡之间通信、cpu内存到GPU内存的通信,这些都是巨大的延迟。

  2. 模型运行中,大量的浮点数乘法,产生很多很小的浮点数,降低参数精度,会造成数据溢出,导致出问题,即使不溢出,也损失了数据准确性。 模型训练时,梯度误差大,导致损失不收敛。模型推理时,误差变大,推理效果变差。

参数分割策略

说到分割参数,无论是多GPU之间分割参数,还是 offload 到CPU内存,都需要对参数进行分割分组。 这就涉及到多种划分策略。

  1. 按照模型的层(Layer)进行分割,保留每一层(Layer)为整体,不同层存储在不同的 GPU 中, 多个层(GPU)串行在一起,需要串行执行,这就是所谓的 流水线并行(Pipeline Parallel,PP)。时间效率很差, 并且如果某一层的参数量就很大并超过了单卡的显存就尴尬。当然可以通过异步执行一定程度解决时间效率差的问题,有兴趣的读者可以研读相关资料。

  2. 把参数张量切开,切开张量分开存储很容易,但切开之后,张量计算的时候怎么办?这里可以分两种策略。 1. 张量的计算过程也是可以切割,这样把一个大的张量,切分成多个小张量,每张 GPU 卡只保存一个小片段,每个小张量片段(GPU卡)独立进行相关计算,最后在需要的时候合并结果就行了。这种思路就称为 张量并行(Tensor Parallel,TP) , Megatron 就是走的这个路线。 2. 同样是把参数张量分割,每张卡只保存一个片段。但是需要计算的时候,每张卡都从其他卡同步其它片段过来,恢复完整的参数张量,再继续数据计算。Deepspeed 选取的这个策略,这个策略实现起来更简单一些。

降低精度

降低参数精度也有讲究,有些地方可以降低,有些地方就不能降低,所以一般是混合精度。 半精度还有另一个好处,就是 计算效率更高,两个字节的计算速度自然是高于4个字节的。 在模型训练过程中,参数的梯度是非常重要的,参数更新累积梯度变化时,如果精度损失太多会导致模型不收敛。 所以优化器的状态一般需要保留 float32 类型,具体参看下图。 有关混合精度更细节内容请参考论文 Mixed Precision Training [1]

../_images/mixed_precision.png

图 3 (图片来自论文 MIXED PRECISION TRAINING

实际上,GPU 显存不足的问题更多的是靠上面的参数分割来解决,半精度的应用更多的是为了提高计算速度。

流水线并行、张量并行,把模型一次完整的计算过程(前后向)分拆到多个 GPU 上进行, 所以这两者都被称为模型并行(Model Parallel,MP)。 而如果每张卡都能进行模型一次完整前后向计算,只是每张卡处理不同的训练数据批次(batch), 就称为数据并行(Data Parallel,DP)。 deepspeed 对参数进行了分割,每张卡存储一个片段,但在进行运算时, 每张卡都会恢复完整的参数张量,每张卡处理不同的数据批次, 因此 deepspeed 属于数据并行

最后总结一下, 针对大模型的训练有三种并行策略,理解起来并不复杂:

  • 数据并行:模型的计算过程没有分割,训练数据是分割并行处理的。

  • 模型并行:模型的计算过程被分割。
    • 流水线并行:模型按照层(Layer)切分。

    • 张量并行:把参数张量切分,并且将矩阵乘法分解后多 GPU 并行计算。

接下来是 deepspeed 的详解:

参考文献