
randn 算子为例,创建一个 Python 文件 test_randn_global.py,加入以下内容:import oneflow as flow# Place a global tensor on cuda device of rank(process) 0 and 1placement = 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 0sbp = flow.sbp.split(dim=0)# Create a global tensor by randnx = flow.randn(4, 5, placement=placement, sbp=sbp)# Print local dataprint("Local data of global tensor:\n ", x.to_local().numpy())# Print global dataprint("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=0export MASTER_ADDR=127.0.0.1 MASTER_PORT=17789 WORLD_SIZE=2 RANK=1 LOCAL_RANK=1test_randn_global.py,观察 Global Tensor 的创建结果:python3 test_randn_global.py
Local data of global tensor:[][]]Global data of global tensor:[][][][]]
Local data of global tensor:[][]]Global data of global tensor:[][][][]]
import oneflow as flowx = flow.randn(2, 5).cuda()print(x.is_local) # Trueprint(x.is_global) # Falseplacement = 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) # Trueprint(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。import oneflow as flowplacement = 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],[]],dtype=oneflow.float32)
tensor([[ 0.6829, 0.4849, 2.1611, 1.4059, 0.0934],[]],dtype=oneflow.float32)
to_local() 没有任何参数,因为 Global Tensor 已经通过 placement 和 SBP 确定好了它的本地分量,所以直接取本地分量对应的 Local Tensor 就好。参数 type 指定了物理设备的类型,cuda 表示 GPU 设备内存, cpu 表示 CPU 设备内存;
参数 ranks 指定了进程 ID 集合,因为隐含了一个 Rank 对应一个物理设备,所以 ranks 就是设备 ID 集合; 实际上 ranks 是一个由 rank id 组成 nd-array,支持高维设备排布。
to_global 有 placement 和 sbp 两个参数,这两个参数即期望转换成的新全局数据分布类型。import oneflow as flowx = 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.broadcastx_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() 得到的是一个完整的数据。import oneflow as flowplacement = 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) # Trueprint(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 是完整的参数。Global View 提供的 SPSD 编程视角;
Global Tensor 的跨进程可见的执行特点;
Global Tensor 和 Local Tensor 的互转;
通过 Global Tensor 的全局数据分布类型转换来实现分布式通信;
OneFlow 算子的多态特性支持了 Global Tensor 的执行。
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 对应情况如下:

原文作者:OneFlow;如有侵权,请联系管理员删除。