OneFlow源码解析:Eager模式下的SBP Signature推导
作者|郑建华
更新|赵露阳
OneFlow 的 Global Tensor 有两个必要属性:
Placement:决定了 tensor 数据分布在哪些设备上。 SBP:决定了 tensor 数据在这些设备上的分布方式。例如:
split:将切分后的不同部分放到不同设备;同时指定切分的 axis。 broadcast:将数据复制到各个设备。
如果参与运算的 tensor 的 SBP 不一样,结果 tensor 的 SBP 是什么呢?例如下面的代码:
# 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
import oneflow as flow
P0 = flow.placement("cpu", ranks=[0, 1])
t1 = flow.Tensor([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]], placement=P0, sbp=flow.sbp.split(0))
# t1 = flow.Tensor([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]], placement=P0, sbp=flow.sbp.broadcast)
t2 = flow.Tensor([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]], placement=P0, sbp=flow.sbp.split(1))
t3 = t1 + t2
# oneflow.placement(type="cpu", ranks=[0, 1])
print(t3.placement)
# (oneflow.sbp.split(dim=0),)
print(t3.sbp)
计算结果t3的 SBP 不需要用户手动指定,系统可以自动推导出t3.sbp为S(0)。这个过程中的一个核心步骤,就是 SBP Signature 的推导。
1
SBP相关概念
1.1 SBP
split表示物理设备上的 Tensor,是将全局视角的 Tensor 切分得到的。切分时,需要指定切分的维度。物理设备上的 Tensor ,经过拼接,可以还原得到全局视角的 Tensor 。 broadcast表示全局视角下的 Tensor,会复制并广播到所有的物理设备上。 partial 表示全局视角下的 Tensor 与物理设备上的 Tensor 的 形状相同,但是物理设备上的值,只是全局视角下 Tensor 的 一部分。以 partial sum 为例,如果我们将集群中所有设备的张量按位置相加,那么就可以还原得到全局视角的 Tensor。除了 sum 外,min、max 等操作也适用于 partial。
1.2 SBP Signature
SBP Signature即SBP签名,是OneFlow中独创且很重要的概念。本节以下文字摘自SBP Signature的官方文档:
对于一个孤立的 Tensor,我们可以随意设置它的 SBP 属性。但是,对于一个有输入、输出数据的算子,我们却不可以随意设置它的输入、输出的 SBP 属性。这是因为随意设置一个算子输入输出的 SBP 属性,可能不符合全局视角下算子的运算法则。
对于某个算子,其输入输出的一个特定的、合法的 SBP 属性组合,称为这个算子的一个 SBP Signature。
算子作者根据算子的运算法则,在开发算子时,就已经罗列并预设好该算子所有可能的 SBP Signature。
某一层算子只要有输入的 SBP 属性,OneFlow 就可以根据 SBP Signature 推导出该层算子输出的 SBP 属性。
所谓的 SBP Signature 自动推导,指的是:在给定所有算子的所有合法的 SBP Signature 的前提下,OneFlow 有一套算法,会基于传输代价为每种合法的 SBP Signature 进行打分,并选择传输代价最小的那个 SBP Signature。这样使得系统的吞吐效率最高。
如果 OneFlow 自动选择的 SBP Signature,上一层算子的输出与下一层算子的输入的 SBP 属性不匹配时,那怎么办呢?OneFlow 会检测到这种不一致,并且在上游的输出和下游的输入间插入一类算子,做相关的转换工作。这类自动加入做转换的算子,就称为 Boxing 算子。
每个算子都需要设置相应的SBP签名,用于描述数据(Tensor)的分布方式。
SBP签名包括算子的全部输入、输出的SBP。缺少(部分)输入,或(部分)输出,不能构成签名。
所以SbpSignature.bn_in_op2sbp_parallel是一个map结构,key就是各个input和output的标识。
输入与输出的SBP签名组合,在算子的运算法则下必须是合法的,算子的作者需要列出合法SBP签名的候选集。 如果输入数据(input tensor)的SBP与该算子合法的SBP签名不一致,则为了得到该算子正确计算所需要的数据(tensor),OneFlow 会在上游的输出和下游的输入间插入boxing算子(可能包含nccl等集合通信操作),做自动转换工作,这类自动转换的过程,就称为 Boxing。例如,eager global模式下的interpreter在GetBoxingOutput方法中完成Boxing过程。
1.3 NdSbp 及 NdSbpSignature
在 ranks 的第一维度执行广播,将数据分别拷贝到group 0(rank [0, 1])和group 1(rank [2, 3])。 在 ranks 的第二维度分别执行split(0)。
例如,对于group 0,将上一步中分配给它的数据按行拆分成(1,2)和(3,4)分别给device 0和device 1。
2
placement.hierarchy
hierarchy 数组的长度是 ranks 的维数。 hierarchy 数组的元素值,是 ranks 对应维度的 size。 构造 hierarchy 的 C++ 代码可参考GetRanksShape。
import oneflow as flow
placements = [
flow.placement("cpu", ranks=[ 0, 1, 2, 3, 4, 5]),
flow.placement("cpu", ranks=[[0, 1, 2], [3, 4, 5]]),
]
for p in placements:
print(p.hierarchy)
# outputs:
# [6]
# [2, 3]
3
tensor add 是哪个算子?
4
一维 SBP 的推导过程
shape dtype nd_sbp placement consumer_nd_sbp_constraint 不同的 tensor 对象,只要这些元信息相同,就可以复用同一个推导结果。
4.1 GlobalTensorInferCache 中的推导准备
4.1.1 推导 output 的 shape 和 dtype
user_op_expr.InferLogicalTensorDesc
logical_tensor_desc_infer_fn_->AddNOp::InferLogicalTensorDesc out.shape = in[0].shape dtype_infer_fn_->AddNOp::InferDataType out.data_type = in[0].data_type
4.1.2 构造 UserOp
对于 AddNOp 来说,key是in_0, in_1, out_0,value 是 inputs[0].placement。
# for each input index i
blob_descs[i].shape = inputs[i].shape
blob_descs[i].stride = inputs[i].stride
blob_descs[i].data_type = inputs[i].data_type
# for each input index i
hints[i].parallel_desc = inputs[i].parallel_desc
hints[i].blob_desc = blob_descs[i]
hints[i].nd_sbp = inputs[i].nd_sbp
4.2 Operator 中的推导准备
先只看 1D SBP 的情况。调用传入的 NdSbpInferHint4Ibn 函数对象,查到 GlobalTensorInferCache 中创建的 NdSbpInferHint,转为 NdSbpInferHint 并存到 map 中。因为是一维的,所以只需要取 sbp_parallel 的第一个元素。然后调用 InferSbpSignature(名字中少了 Nd),将推导结果写到 SbpSignature。
无论是一维还是多维,结果的类型都是 NdSbpSignature。所以要将 SbpSignature 转为 NdSbpSignature。
SbpInferHint4Ibn 是将传入的 map 数据封装到函数对象中,用于查询输入输出的元信息。
CalcOrderValue4SbpSig给每个 SbpSignature 计算一个序值,用于对签名进行排序。
4.3 SbpSignature 的推导
获取候选集 过滤不合适的签名 排序
4.3.1 SbpSignature 的候选集
对输入 tensor 的 shape 的每个 axis i,所有的 input/output 都创建一个 split(i)。
对于 tensor add 来说,input/output 的 shape 一样才能直接计算,所以 split 的 axis 也都一样。
所有的 input/output 都创建一个 partialsum。
broadcast 的情况会在 Operator 中默认设置,因为理论上所有inputs/outputs都应该支持以broadcast的方式进行运算。
{"sbp_signature":[{"bn_in_op2sbp_parallel":{"in_0":{"split_parallel":{"axis":"0"}},"in_1":{"split_parallel":{"axis":"0"}},"out_0":{"split_parallel":{"axis":"0"}}}},{"bn_in_op2sbp_parallel":{"in_0":{"split_parallel":{"axis":"1"}},"in_1":{"split_parallel":{"axis":"1"}},"out_0":{"split_parallel":{"axis":"1"}}}},{"bn_in_op2sbp_parallel":{"in_0":{"partial_sum_parallel":{}},"in_1":{"partial_sum_parallel":{}},"out_0":{"partial_sum_parallel":{}}}},{"bn_in_op2sbp_parallel":{"in_0":{"broadcast_parallel":{}},"in_1":{"broadcast_parallel":{}},"out_0":{"broadcast_parallel":{}}}}]}
4.3.2 过滤不合适的签名
FilterAndCheckValidSbpSignatureListByLogicalShape中,对于每个输入tensor ibn,签名中 ibn 的 split axis,必须小于 tensor ibn 的 shape axes 数量。换句话说,如果 tensor 是二维的,就无法接受split(2),只能是split(0)或split(1)。
FilterSbpSignatureList的作用是检验sbp_sig_conf约束,也就是从GlobalTensorInferCache一路传过来的参数nd_sbp_constraints。这个过滤规则要求,符合条件的签名,其内容必须包含sbp_sig_conf。
4.3.3 签名排序
优先按 OrderValue 比较
OrderValue 相等时,按 CopyCost 比较
二者都是较小的值优先。
如果 sbp 一致,cost 为0
partial_sum 和 broadcast 的 cost 都是一个超大的数字。
否则 cost 等于 input tensor 的数据传输字节数量。
4.4 推导结果
{"bn_in_op2nd_sbp":{"in_0":{"sbp_parallel":[{"split_parallel":{"axis":"0"}}]},"in_1":{"sbp_parallel":[{"split_parallel":{"axis":"0"}}]},"out_0":{"sbp_parallel":[{"split_parallel":{"axis":"0"}}]}}}
5
NdSbp 的推导过程
调用 GetValidNdSbpSignatureList 获取有效的签名
剔除不能包含 nd_sbp_constraints 的签名
贪心搜索较优的签名
GetNdSbpSignatureList: 获取全部签名
FilterNdSbpSignatureListByLogicalShape: 过滤不合适的签名
5.1 NdSbp 签名的候选集
GetSbpSignaturesIf: 得到一维的签名(和 1D SBP 的情况相同)
DfsGetNdSbpSignature: 根据一维签名拓展到多维
从形式上看,NdSbpSignature 是先按 bn 组织数据。但是从数据分布的过程看,是先按SbpSignature组织数据。一个 NdSbpSignature 等价于 SbpSignature 数组。NdSbp中的每个槽位,都表示一个 1D Sbp 的数据分布(所有的 input/output一起分布)。
比如第 0 个槽位,就是在r1和r2这两个 sub group 之间分布数据,这个分布必须是一个有效的 1D SbpSignature(所有的 input/output一起分布)。
第 1 个槽位,对于r1,就是将分配给它的数据子集,再根据一个 SbpSignature 进行分布(所有的 input/output一起分布)。
所以,只需要按 SbpSignature整体 填满两个槽位就行。每个槽位各有 n 种可能,一共有 n*n 个候选签名。这样生成的候选集是完整的,不会漏掉候选项。这应该就是 direct product of 1D sbp signatures 的含义。
6
模块间协作关系
参考资料
oneflow v0.9.1(https://github.com/Oneflow-Inc/oneflow/tree/0ea44f45b360cd21f455c7b5fa8303269f7867f8/oneflow)
SBP Signature(https://docs.oneflow.org/master/parallelism/02_sbp.html#sbp-signature)
2D SBP(https://docs.oneflow.org/master/parallelism/04_2d-sbp.html)
placement api(https://oneflow.readthedocs.io/en/master/tensor_attributes.html?highlight=placement#oneflow-placement)
https://segmentfault.com/a/1190000042625900
OneFlow v0.9.0正式发布 开源ChatGPT要来了;软件2.0智能革命 比快更快,开源Stable Diffusion刷新作图速度 OneEmbedding:单卡训练TB级推荐模型不是梦 GLM训练加速:性能最高提升3倍,显存节省1/3 “一键”模型迁移,性能翻倍,多语言AltDiffusion推理速度超快 点击“阅读原文”,欢迎Star、试用OneFlow最新版本