Global Tensor和实习总结|OneFlow学习笔记
撰文|李响
1
前言
2
Global Tensor
2.1 OneFlow 分布式全局视角的基础保证
2x2
的 Global Tensor 在 4 种 SBP 映射(每一种都被称为 SBP Signature)下被映射为 2 个局部张量(Local Tensor),分别为 split(0),split(1),broadcast 和 partial-sum,分别是按维度分割、复制并广播和按位置相加的思路。X
和一个权重张量 W
,Y=X*W
的 SBP Signature 可以从 X
和 W
的 Signature 中推断出来。如下表,是合法的合法 SBP 组合。当然,SBP Signature 是不需要被我们显式指定的,OneFlow 存在自动推导机制。>>> placement1 = flow.placement("cuda", ranks=[0, 1, 2, 3]) # 1D SBP 配置集群
>>> placement2 = flow.placement("cuda", ranks=[[0, 1], [2, 3]]) # 2D SBP 配置集群
(broadcast, split(0))
的 2D SBP。这时,在设备阵列上,第 0 维做 broadcast 操作;在第 1 维做 split(0)。ranks=[[0, 1], [2, 3]])
,假设看作是 ranks = [Dim0_DeviceGroupA, Dim0_DeviceGroupB]
,Dim0_DeviceGroupA = [0, 1]
,Dim0_DeviceGroupB=[2, 3]
。那么 rank0 的数据就是先把Global Tensor 完整数据 broadcast 到第 0 维的两个设备小组 Dim0_DeviceGroupA 和 Dim0_DeviceGroupB;然后再把 Dim0_DeviceGroupA 小组得到的数据 split(0) 到第 1 维即 Dim0_DeviceGroupA 内的 rank 0 和 rank 1(ranks=[[0, 1]]
),对于 Dim0_DeviceGroupB 也同理。>>> sbp = (flow.sbp.broadcast, flow.sbp.split(0))
>>> tensor_to_global = tensor.to_global(placement=placement, sbp=sbp)
2.2 SBP 自动转换
split(0)
转换为 broadcast
,相当于做了一次 all-gather 操作。其实很好理解,这部分内容更详细的解释对应 OneFlow 论文(https://arxiv.org/pdf/2110.15032.pdf)的 3.2 节,比如每种操作的通信成本大小计算。2.3 to_global 方法
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)
matmul
的输出 SBP 本来是 split(0)
,但是下一层算子 matmul
的输入,被转成了 broadcast
。to_global
方法将 split(0)
转换为 broadcast
,也就是代码参数中的 sbp=y0_sbp
。2.4 GlobalTensor 类代码跟踪
ConsistentTensor
,在 OneFlow v0.7.0 中已经叫做 GlobalTensor。相关的代码也可以在这个位置开始追踪。在 ConsistentTensor
中,有指向 ConsistentTensorImpl
的指针,EagerConsistentTensorImpl
和 EagerConsistentTensorImpl
又分别继承 ConsistentTensorImpl
,代码位置在 github.com/Oneflow-Inc/ 。值得注意的是, ConsistentTensor
的实现类 ConsistentTensorImpl
里,代码中存在 ConsistentTensorMeta
的成员指针,TensorMeta
系列类维护了除包括 Tensor 的设备、形状和数据类型等基本变量外,包括 placement 和 SBP 的信息。MirroredTensor
,就是实际存储各个设备 Tensor data 的地方。EagerConsistentTensorImpl
成员中有指向 MirroredTensor
的指针,同样的,MirroredTensor
持有指向 MirroredTensorImpl
的指针,MirroredTensorImpl
的实现类中则持有指向 TensorStorage
的指针,Tensor 中的数据最终是存在于 TensorStorage
成员中,代码位置在https://github.com/Oneflow-Inc/oneflow/blob/baa761916262c72414b744538afcc56b01906a09/oneflow/core/eager/eager_blob_object.h#L322.5 如何做 Global Ops 的执行测试
@autotest(n=1, check_graph=False)
def _test_matmul(test_case, placement, x_sbp, y_sbp):
x = random_tensor(ndim=2, dim0=8, dim1=16).to_global(placement=placement, sbp=x_sbp)
y = random_tensor(ndim=2, dim0=16, dim1=8).to_global(placement=placement, sbp=y_sbp)
return torch.matmul(x, y)
class TestMatMulModule(flow.unittest.TestCase):
@globaltest
def test_matmul(test_case):
for placement in all_placement():
for x_sbp in all_sbp(placement, max_dim=2):
for y_sbp in all_sbp(placement, max_dim=2):
_test_matmul(test_case, placement, x_sbp, y_sbp)
if __name__ == "__main__":
unittest.main()
3
总结
刚入职时,首先是提交 OneFlow 仓库几个简单的 PR,熟悉 CI 的流程,还有性能测试、使用 OneFlow 完成多卡训练 U-Net 网络和 python 代码搬运等一些简单的工作,更快的适应工作节奏。在这期间,学习了 ONNX,认真读了 github.com/BBuf/onnx_le 下的一系列文章,并且参与了 OneFlow2ONNX 的一些工作。
实习中期,自己修复了 OneFlow 一些算子代码的 bug,并且自学了 C++ 的新标准和 CUDA 优化,这时才觉得自己大学三年好像什么都没学到一样,是一个没有感情的 Java、408 考试机器。此外,开发了一些算子,也写了很多 bug,这里不得不感谢 OneFlow 的保安 zzk(郑泽康)。搭建了 libai 和 flowvision 仓库 RTD 文档的 CI,负责相关文档的 Oncall 工作。
实习后期,完成了 nn.Graph 支持 Local Ops 单测的项目,也就是不改代码实现同一段代码的 Graph 和 Eager 模式的自动化执行测试,这里面要解决很多子问题,但是通过一点一点的 hack,基本完成了这个任务。基于此,我对静态图(Graph)的构建、编译流程、训练和调试会更加熟悉。
在 OneFlow 仓库大概提了 30+ 的PR,参与开源有非常实际的好处,收获比每天实习做软件黑盒测试要大,比如成为Committer或提交了多少PR,面试时就可以把这些放在简历上,但如果没有参与开源的经历,就得从其他方面花费很大精力去证明自己的能力。
在 OneFlow 工作,总体上还是挺轻松的。在新手村期间,一直有同事帮助,让我不至于面对很多不懂的技术而痛苦。有时间还想能有机会回 OneFlow 一直实习,如果反思自己,就是真的还需要好好沉淀一下碎片化的知识。
接下来一小段时间开始做实验和写论文啦,I'm a fight until free~