一种分布式深度学习编程新范式:Global Tensor
1
Global Tensor概述
2
创建Global Tensor
randn
算子为例,创建一个 Python 文件 test_randn_global.py
,加入以下内容:import oneflow as flow
# Place a global tensor on cuda device of rank(process) 0 and 1
placement = flow.placement(type="cuda", ranks=[0, 1])
# Each rank's local data is a part data as a result of spliting global data on dim 0
sbp = flow.sbp.split(dim=0)
# Create a global tensor by randn
x = flow.randn(4, 5, placement=placement, sbp=sbp)
# Print local data
print("Local data of global tensor:\n ", x.to_local().numpy())
# Print global data
print("Global data of global tensor:\n ", x.numpy())
placement
表示 Global Tensor 分布的物理设备,参数 type
指定了物理设备的类型,这里使用“cuda”
表示 GPU 设备,参数 ranks
指定了设备 ID。对于没有 2 张 GPU 的用户,在这里可以将 type
指定为 "cpu"
,这样可以使用 CPU 模拟多个设备,下文的代码同样适用。sbp
表示 Global Tensor 分布的方式,代码中的 sbp = flow.sbp.split(dim=0)
表示把 Global Tensor 在维度 0 均匀切分。to_local()
可以从 Global Tensor 中获取它在当前 rank 的 Local Tensor,因为 Global Tensor 在每个 rank 都内含了一个 Local Tensor 作为实际存在的本地分量。export MASTER_ADDR=127.0.0.1 MASTER_PORT=17789 WORLD_SIZE=2 RANK=0 LOCAL_RANK=0
export MASTER_ADDR=127.0.0.1 MASTER_PORT=17789 WORLD_SIZE=2 RANK=1 LOCAL_RANK=1
test_randn_global.py
,观察 Global Tensor 的创建结果:python3 test_randn_global.py
Local data of global tensor:
[[-0.07157125 -0.92717147 1.5102768 1.4611115 1.014263 ]
[-0.1511031 1.570759 0.9416077 0.6184639 2.4420679 ]]
Global data of global tensor:
[[-0.07157125 -0.92717147 1.5102768 1.4611115 1.014263 ]
[-0.1511031 1.570759 0.9416077 0.6184639 2.4420679 ]
[-0.38203463 0.453836 0.9136015 2.35773 -0.3279942 ]
[-0.8570119 -0.91476554 -0.06646168 0.50022084 -0.4387695 ]]
Local data of global tensor:
[[-0.38203463 0.453836 0.9136015 2.35773 -0.3279942 ]
[-0.8570119 -0.91476554 -0.06646168 0.50022084 -0.4387695 ]]
Global data of global tensor:
[[-0.07157125 -0.92717147 1.5102768 1.4611115 1.014263 ]
[-0.1511031 1.570759 0.9416077 0.6184639 2.4420679 ]
[-0.38203463 0.453836 0.9136015 2.35773 -0.3279942 ]
[-0.8570119 -0.91476554 -0.06646168 0.50022084 -0.4387695 ]]
3
由Local Tensor得到Global Tensor
import oneflow as flow
x = flow.randn(2, 5).cuda()
print(x.is_local) # True
print(x.is_global) # False
placement = flow.placement(type="cuda", ranks=[0, 1])
sbp = flow.sbp.split(0)
x_global = x.to_global(placement=placement, sbp=sbp)
print(x_global.shape) # (4, 5)
print(x.is_local) # True
print(x_global.is_global) # True
shape=(2,5)
的 Local Tensor,即 x。to_global
变换后,就得到一个名为 x_global
的 Global Tensor。x_global
的 shape 变为了 (4, 5)
,这是 Global Tensor 的 shape(global shape)。x_global
在第 0 维 split 而得到 x
。to_global
方法根据如上关系可以从 x.shape
推理出 x_global.shape
:把两个 GPU 上的 Local Tensor x
在第 0 维拼接后得到 x_global
。4
由Global Tensor得到Local Tensor
import oneflow as flow
placement = flow.placement(type="cuda", ranks=[0, 1])
sbp = flow.sbp.split(0)
x = flow.randn(4, 5, placement=placement, sbp=sbp)
print(x.to_local())
x.to_local()
时,两个不同的 rank 将分别得到一个 shape 为 (2, 5)
的本地分量 tensor。tensor([[-0.2730, 1.8042, 0.0721, -0.5024, -1.2583],
[-0.3379, 0.9371, 0.7981, -0.5447, -0.5629]],
dtype=oneflow.float32)
tensor([[ 0.6829, 0.4849, 2.1611, 1.4059, 0.0934],
[-0.0301, -0.6942, -0.8094, -1.3050, -0.1778]],
dtype=oneflow.float32)
to_local()
没有任何参数,因为 Global Tensor 已经通过 placement 和 SBP 确定好了它的本地分量,所以直接取本地分量对应的 Local Tensor 就好。5
由Global Tensor转成另一个Global Tensor
参数
type
指定了物理设备的类型,cuda
表示 GPU 设备内存,cpu
表示 CPU 设备内存;参数
ranks
指定了进程 ID 集合,因为隐含了一个 Rank 对应一个物理设备,所以ranks
就是设备 ID 集合; 实际上ranks
是一个由 rank id 组成 nd-array,支持高维设备排布。
S 即 split(dim),局部和全局是切分关系, 表示在 dim 维度做了切分的数据分布关系; B 即 broadcast,局部和全局是广播关系,表示做了广播的数据分布关系; P 即 partial_sum,局部和全局是部分关系,表示做了 element-wise 累加的数据分布关系。
to_global
有 placement
和 sbp
两个参数,这两个参数即期望转换成的新全局数据分布类型。import oneflow as flow
x = flow.randn(2, 5).cuda()
placement = flow.placement(type="cuda", ranks=[0, 1])
sbp = flow.sbp.split(0)
x_global = x.to_global(placement=placement, sbp=sbp)
print(x_global.shape) # (4, 5)
print(x_global.to_local())
sbp_b = flow.sbp.broadcast
x_global_b = x_global.to_global(placement=placement, sbp=sbp_b)
print(x_global_b.shape) # (4, 5)
print(x_global_b.to_local())
x_global
到 x_global_b
的全局数据分布类型变化就是 sbp 从 flow.sbp.split(0)
变成了 flow.sbp.broadcast
。它们的 global shape 都是 (4, 5)
,但是本地分量从一个分片变成了一个完整的数据,这个变化可以从对 to_local()
的打印结果观察到。to_global
变换完成了对 local tensor 的归并。通常来讲,SPMD 编程模式要求用户手写一个 all-gather
集合通信来完成。而在 OneFlow Global View 中,只需做一下类型转换。numpy()
方法。对于任意的 Global Tensor 如 x_global
,x_global.numpy()
等价于 x_global.to_global(spb=flow.sbp.broadcast).to_local().numpy()
,即内部隐含了一次将原 Global Tensor 转成 SBP 为 flow.sbp.broadcast() 的 Global Tensor,然后进行一次 to_local 操作,最后对这个 Local Tensor 调用 numpy()
方法。所以 x_global.numpy()
得到的是一个完整的数据。6
Global Tensor参与计算
import oneflow as flow
placement = flow.placement(type="cuda", ranks=[0, 1])
x = flow.randn(4, 5, placement=placement, sbp=flow.sbp.split(dim=0))
w = flow.randn(5, 8, placement=placement, sbp=flow.sbp.broadcast)
y = flow.matmul(x, w)
print(y.is_global) # True
print(y.shape) # (4, 8)
print(y.sbp) # (flow.sbp.split(dim=0))
print(y.to_local().numpy())
x
和 w
,它们参与 oneflow.matmul
计算得到 y
。flow.matmul
执行 Global Tensor 时,在接口上并无特殊之处。可以认为 OneFlow 中的算子都是多态的。即根据输入,决定自己的行为:如果算子的输入是 Local Tensor,那么算子会按照普通的单机单设备执行模式进行计算;
如果算子的输入是 Global Tensor,那么算子会采用 Global View(多机多设备)模式进行计算;
flow.matmul
这一算子可以顺利执行的前置条件是:输入的 x
和 w
的 placement 相同。y
同样是一个 Global Tensor 。flow.matmul
对输入 x
和 w
做计算时,会自动进行输出数据的 placement 和 SBP 的推理,规则如下:Placement:输出和输入的 placement 相同;
SBP:输出的 SBP 的推理规则,因算子类型而异,这个推理规则是 OneFlow 内置的,详情可见: SBP Signature
flow.sbp.split(0)
和 flow.sbp.broadcast
相乘的输出数据会被推理成 flow.sbp.split(0)
。x
在每个 rank 上是一个分片数据,w
是一个完整的数据,二者矩阵乘法得到的 y
是一个分片的数据。看到这里,了解常见并行执行方式的用户可以发现:这里实现了一个数据并行的前向计算,x
是切片的数据,w
是完整的参数。7
结语
Global View 提供的 SPSD 编程视角;
Global Tensor 的跨进程可见的执行特点;
Global Tensor 和 Local Tensor 的互转;
通过 Global Tensor 的全局数据分布类型转换来实现分布式通信;
OneFlow 算子的多态特性支持了 Global Tensor 的执行。
扩展阅读:OneFlow 多机多卡启动和依赖的环境变量
n 机 m 卡
的环境,就对应 n * m
个进程。每个进程都有一个进程 rank 编号,Global Tensor 中的 placement 参数中的 ranks 对应的就是这个 rank 编号。2 机 2 卡
为例, 0 号机器中两张卡分别对应编号 0 和 1,第 1 号机器中两张卡分别对应编号 2 和 3。此时 flow.placement(type="cuda", ranks=[2])
可以唯一标识第 1 号机器中的第 0 卡。n 机 m 卡
的环境,flow.placement(type="cuda", ranks=[k])
唯一标识第 k / n
号机器的第 k % m
张卡。MASTER_ADDR
:多机训练的第 0 号机器的 IP;MASTER_PORT
:多机训练的第 0 号机器的监听端口,不与已经占用的端口冲突即可;WORLD_SIZE
:整个集群中计算设备的数目,因为目前还不支持各个机器上显卡数目不一致,因此WORLD_SIZE
的数目实际上是 $ 机器数目 \times 每台机器上的显卡数目$。创建 Global Tensor 中的示例是单机 2 卡的情况,因此WORLD_SIZE=2
;RANK
:集群内所有机器下的进程编号;LOCAL_RANK
:单个机器内的进程编号。
RANK
和 LOCAL_RANK
的区别:在单机训练(单机单卡或单机多卡)时,两者相等;
在多机训练时,每台机器上的
LOCAL_RANK
的上限,就是每台机器上的计算设备的数目;RANK
的上限,就是所有机器上所有计算设备的总和,它们的编号均从 0 开始(因为编号从 0 开始,所以不包含上限)。
2 机 2 卡
为例,每张显卡的 LOCAL_RANK
与 RANK
对应情况如下: