数度学习模型规模越来越大,因为内存墙的存在,单一设备的算力及容量受限于物理电路,持续提高芯片集成越来越困难,难以跟上模型扩大的需求。
以下内容整理自 OneFlow官网
简单的机器堆叠并不一定会带来算力的增长。它不仅需要多个设备进行计算,还涉及到设备之间的数据传输,只有协调好集群中的计算与通信,才能做高效的分布式训练。
数据并行,将数据 x 进行切分,每个设备上模型 w 是完整的、一直的。如下图所示,x 被按照第 0 维度平均切分到 2 个设备上,两个设备上都有完整的 w。
这样,在两台设备上,分别得到的输出,都只是逻辑上输出的一半(形状为 ),将两个设备上的输出拼接到一起,才能得到逻辑上完整的输出
注意,因为数据被分发到了2个设备上,因此反向传播过程,各自设备上得到的 ∂ l o s s ∂ w \frac{\partial loss}{\partial w} ∂w∂loss会不一样,如果直接使用各个设备上的梯度更新各自的模型,会造成2个设备上的 模型不一致,训练就失去了意义(到底用哪个模型好呢?)
数据并行策略下,在反向传播过程中,需要对各个设备上的梯度进行 AllReduce,以确保各个设备上的模型始终保持一致.
当数据集较大,模型较小的时候,由于反向过程中为同步梯度产生的通信代价较小,此时选择数据并行一般比较有优势。
当神经网络过于巨大,无法在一个设备上存放时,除了上述的模型并行的策略外,还可以选择流水并行。 流水并行指将网络切为多个阶段,并分发到不同的计算设备上,各个计算设备之间以“接力”的方式完成训练。
4层网络被切分到2个计算设备上,其中 GPU0 上进行 T1 与 T2 的运算,GPU1 上进行 T3 与 T4 的计算。
GPU0 上完成前两层的计算后,它的输出被当作 GPU1 的输入,继续进行后两层的计算
网络的训练中,也可以将多种并行策略混用,以 GPT-3 为例,以下是它训练时的设备并行方案:
它首先被分为 64 个阶段,进行流水并行。每个阶段都运行在 6 台 DGX-A100 主机上。在6台主机之间,进行的是数据并行训练;每台主机有 8 张 GPU 显卡,同一台机器上的8张 GPU 显卡之间是进行模型并行训练。
在OneFlow的全局视角下,几个重要概念: Placement、SBP与 SBP Signature
SBP 描述了全局视角下的数据与物理设备上的数据的映射关系,当进行分布式训练时,OneFlow 根据数据的 SBP 属性,将数据分发到各个物理设备,进行计算,并输出结果。
举个具体例子,比如以下代码中,上一层算子 matmul 的输出 SBP 本来是 split(0),但是下一层算子 matmul 的输入,被转成了 broadcast。此时,上一层的输出与下一层的输入,它们的 SBP 其实就不一致了。
import oneflow as flow
P0 = flow.placement("cuda", ranks=[0, 1])
P1 = flow.placement("cuda", ranks=[2, 3])
a0_sbp = flow.sbp.split(0)
b0_sbp = flow.sbp.broadcast
y0_sbp = flow.sbp.broadcast
b1_sbp = flow.sbp.split(1)
A0 = flow.randn(4, 5, placement=P0, sbp=a0_sbp)
B0 = flow.randn(5, 8, placement=P0, sbp=b0_sbp)
Y0 = flow.matmul(A0, B0)
Y0 = Y0.to_global(placement=P1, sbp=y0_sbp)
B1 = flow.randn(8, 6, placement=P1, sbp=b1_sbp)
Y2 = flow.matmul(Y0, B1)
全局视角与物理视角的映射
import oneflow as flow
placement = flow.placement("cuda", [0,1])
sbp = flow.sbp.split(0)
x = flow.randn(4,5,placement=placement, sbp=sbp)
x.shape
oneflow.Size([4, 5])
to_local()
Returns the local component of this global tensor in the current rank.
x.to_local()
tensor([[ 2.9186e-01, -3.9442e-01, 4.7072e-04, -3.2216e-01, 1.7788e-01],
[-4.5284e-01, 1.2361e-01, -3.5962e-01, 2.6651e-01, 1.2951e+00]],
device='cuda:0', dtype=oneflow.float32)
可以先创建 local tensor,再利用 Tensor.to_global 方法,将 local tensor 转为 global tensor
在 1D SBP 的场景下,通过 oneflow.placement 接口配置集群,比如使用集群中的第 0~3 号 GPU 显卡:
>>> placement1 = flow.placement("cuda", ranks=[0, 1, 2, 3])
ranks 可以是多维数组:
placement2 = flow.placement("cuda", ranks=[[0, 1], [2, 3]])
设备被划分成了 2X2设备阵列
当 placement 中的集群是 2 维的设备阵列时;SBP 也必须与之对应,是一个长度为 2 的 tuple,这个tuple中的第 0 个、第 1 个 元素,分别描述了 Global Tensor 张量在设备阵列第 0 维、第 1 维的分布。
>>> a = flow.Tensor([[1,2],[3,4]])
>>> placement = flow.placement("cuda", ranks=[[0, 1], [2, 3]])
>>> sbp = (flow.sbp.broadcast, flow.sbp.split(0))
>>> a_to_global = a.to_global(placement=placement, sbp=sbp)
此图的最左边是全局视角的数据,最右边是设备阵列上各个设备的数据。可以看到,从第 0 维的角度看,它们都是 broadcast 的关系:
而从第 1 维的角度看,它们都是 split(0) 的关系
以 (broadcast, split(0)) 为例:
假定我们给 x 设置了 2D SBP 为:(broadcast, split(0)), 给 设置 2D SBP 为 (split(1), broadcast),那么,在 2D SBP 的背景下, x X w = y 运算,得到 y 的 SBP 属性为 (split(1), split(0)) 。
( b r o a d c a s t , s p l i t ( 0 ) ) X ( s p l i t ( 1 ) , b r o a d c a s t ) = ( s p l i t ( 1 ) , s p l i t ( 0 ) ) (broadcast, split(0)) X (split(1), broadcast) = (split(1), split(0)) (broadcast,split(0))X(split(1),broadcast)=(split(1),split(0))
python3 -m oneflow.distributed.launch [启动选项] 训练脚本.py
注意 oneflow.distributed.launch 的主要作用,是待用户完成分布式程序后,让用户可以更方便地启动分布式训练。它省去了配置集群中环境变量 的繁琐。
oneflow.distributed.launch 并不决定 并行策略,并行策略是由设置数据、模型的分发方式、在物理设备上的放置位置决定的。
OneFlow中 提供了两种数据并行方式。
DistributedSampler 会在每个进程中实例化 Dataloader,每个 Dataloader 实例会加载完整数据的一部分,自动完成数据的分发。
将需要使用的 placement 与 sbp 设置提前准备好:
BROADCAST = [flow.sbp.broadcast]
P0 = flow.placement("cuda", ranks=[0])
P1 = flow.placement("cuda", ranks=[1])
P0 、P1 分别代表集群的第 0 个 GPU 和第 1 个 GPU。
通过调用 nn.Module.to_global 或 Tensor.to_global 就可以将模型或张量分配到指定的计算设备上运行,将一个网络拆分为多个流水阶段(stage)