作为NCCL的小白,阅读了NCCL有关Tree的源码,参考过CSDN许多大佬的blog,奈何自己悟性有限,始终参悟不了NCCL的Tree的概念。
结合了github中issue的解答,自己在环境上通过log一步一步把Tree里的关系画出来,发现自己好像明白了一些东西,整理了一下对Tree的理解。
NCCL里Tree的概念其实是节点内的chain和节点间Tree的组合。
Ring的概念也是一样的,节点内的Ring和节点间Ring的组合。
1、Tree的位置
Tree只在节点之间生效,这个说法在任何地方都是这么说的,但是讲的不太清楚。
Tree确实只是在节点之间生效的,但是描述这个Tree的元素并不是节点编号,而是每个节点中的GPU编号(代码中是用NODE编号来组装的双二叉树,但是通过channel的tree.up和tree.down画出来图,却发现其实真正的节点是GPU,查看log:NCCL INFO Trees ),这些GPU编号是全局的。
(1)在每个节点内部,会产生一个描述节点内所有GPU的chain,这个chain很类似于Ring中的环,但是这个chain是个单向的,有头有尾的那种chain。
这个chain可以理解为有一个尾节点,然后一直指向root rank节点,这样每个节点上都有自己的一个root rank节点。这些root rank节点组织成Tree。
(2)ncclTopoPreset的最后有一步复制channel的操作,这个操作其实就是在节点内部产生一样的chain,这样两个一样的chain会组成两颗互补的二叉树,也就说的双二叉树。
下面用issue中假设的场景来画图描述一下Tree的结构
Understand the tree topology · Issue #671 · NVIDIA/nccl · GitHub
上图可以看出来,每一行代表一个节点中的GPU编号,所有GPU编号是全局的。
每一行的最右边的GPU代表了这个节点里的root rank,这些root rank组成了Tree(注意每个节点中的root rank和Tree的root rank不要弄混了,这里我也不好表达,就这么描述吧)
Tree在NCCL中的定义:
// The root of each tree only has one node down (+1 intra-node).
#define NCCL_MAX_TREE_ARITY_TOP 2
// Nodes inside the binary tree can have to two nodes down (+1 intra-node).
#define NCCL_MAX_TREE_ARITY 3
struct ncclTree {
int depth;
int up;
int down[NCCL_MAX_TREE_ARITY];
};
up就代表了当前这个rank的上一个rank,在chain里就是本节点内的上一个rank,在Tree里就是节点间的上一个rank。
down总共有三个元素,为什么二叉树会有三个元素,是因为down[0]代表了节点内chain中本GPU的下一个GPU,而down[1]和down[2]才是在Tree上用的。代表了节点间本GPU的叶子GPU。
设计的挺巧妙的,但是乍一看不好理解。下面图里能看出来,在Tree上的GPU的down都是down[1]和down[2],而down[0]都是在节点内部的chain上用的。
2、三种Tree的对比
NCCL中定义的三种Tree
#define NCCL_TOPO_PATTERN_BALANCED_TREE 1 // Spread NIC traffic between two GPUs (Tree parent + one child on first GPU, second child on second GPU)
#define NCCL_TOPO_PATTERN_SPLIT_TREE 2 // Spread NIC traffic between two GPUs (Tree parent on first GPU, tree children on the second GPU)
#define NCCL_TOPO_PATTERN_TREE 3 // All NIC traffic going to/from the same GPU
其中NCCL_TOPO_PATTERN_TREE 是最简单的,在Tree的组织上也比较容易理解
issue中的解释也不好描述,直接看看图吧,就比较清晰一些了。
依旧以issue中4机32卡的情况画一下Tree的形状
(1)NCCL_TOPO_PATTERN_TREE
在basic Tree中,每个NODE的GPU 0负责做节点内的UP和DOWN操作,比如:0, 16, 8, 24
(2)NCCL_TOPO_PATTERN_SPLIT_TREE
在split tree中,负责节点间UP操作的还是GPU 0,但是负责DOWN操作变成了GPU 1,比如rank 1, 17 。。。
(3)NCCL_TOPO_PATTERN_BALANCED_TREE
在balance tree中,负责节点间UP操作的还是GPU 0,但是负责DOWN操作变成了GPU 0和GPU 1,分别负责向Tree中两个子节点做DOWN
3、单tree和双Tree的对比
为啥说单Tree会造成带宽的浪费,而使用双Tree就可以把双向带宽打满呢?
我个人分析了一下,自己的的理解不一定正确。参考issue中的讨论。
Question: NCCL Tree algorithm behaviour · Issue #919 · NVIDIA/nccl · GitHub
这个问题的理解其实比较微妙,可以固定在一次数据传递过程中进行分析。
(貌似奇数个RANK和偶数个RANK的结论稍微有点不一样,奇数个RANK貌似更能充分利用双向带宽,下面的解析是用偶数个RANK进行分析的)
Tree 1
Tree 2
看上图分析
前面分析了在内核里通过Tree上不同的节点有不同的操作,完成数据的传递,需要Reduce+Broadcast两个步骤。下面就传递完一块数据的内部实现来分析。
Reduce:从Tree的叶子节点开始向上进行reduce,叶子节点只发不收,只使用了发送带宽,而没有用到接收带宽,中间节点肯定是即收又发,所以是使用了双向带宽,到了Tree的root节点,只收不发,只使用了接收带宽。
Broadcast:在来看Broadcast阶段,从Tree的root节点向下进行Broadcast,root节点只发不收,只使用了发送带宽,中间节点肯定是即收又发,所以是使用了双向带宽,到了叶子节点只收不发,只使用了接收带宽。
只使用Tree 1的情形:
所有的数据都通过Tree 1进行Reduce+Broadcast操作
Reduce过程:4,12,20,28只使用了发送带宽,8,16,24使用了发送带宽和接收带宽,0只使用了接收带宽。
Broadcast过程:0只使用了发送带宽,8,16,24使用了发送带宽和接收带宽,4,12,20,28只使用了接收带宽。
Node | reduce | reduce | Broadcast | 总 | ||
---|---|---|---|---|---|---|
收 | 发 | 收 | 发 | 收 | 发 | |
0 | 1 | 1 | 1 | 1 | ||
4 | 1 | 1 | 1 | 1 | ||
8 | 1 | 1 | 1 | 1 | 2 | 2 |
12 | 1 | 1 | 1 | 1 | ||
16 | 1 | 1 | 1 | 1 | 2 | 2 |
20 | 1 | 1 | 1 | 1 | ||
24 | 1 | 1 | 1 | 1 | 2 | 2 |
28 | 1 | 1 | 1 | 1 |
可见root节点和叶子没有充分使用全部的发送接收带宽,正好这些节点占一半,所以就相当于浪费了一半带宽
使用两个Tree的情形:
一半数据通过Tree 1进行Reduce+Broadcast操作,另一半数据通过Tree 2进行Reduce+Broadcast操作
Tree 1中:
Reduce过程:4,12,20,28只使用了发送带宽,4,12,20使用了发送带宽和接收带宽,0只使用了接收带宽。
Broadcast过程:0只使用了发送带宽,4,12,20使用了发送带宽和接收带宽,4,12,20,28只使用了接收带宽。
Tree 2中:
Reduce过程:0,8,16,24只使用了发送带宽,8,16,24使用了发送带宽和接收带宽,28只使用了接收带宽。
Broadcast过程:28只使用了发送带宽,8,16,24使用了发送带宽和接收带宽,0,8,16,24只使用了接收带宽。
Node | Tree1 | Tree2 | 总 | |||||||
---|---|---|---|---|---|---|---|---|---|---|
reduce | reduce | Broadcast | Broadcast | reduce | reduce | Broadcast | Broadcast | |||
收 | 发 | 收 | 发 | 收 | 发 | 收 | 发 | 收 | 发 | |
0 | 1 | 1 | 1 | 1 | 2 | 2 | ||||
4 | 1 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | ||
8 | 1 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | ||
12 | 1 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | ||
16 | 1 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | ||
20 | 1 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | ||
24 | 1 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | ||
28 | 1 | 1 | 1 | 1 | 2 | 2 |
把完整的数据分成两半,每个tree处理一半,则从上图可以看出,两个Tree的root节点只收发了一倍的完整数据量,其他节点都收发了1.5倍的完整数据量。
在一次数据传递过程中,除了两个root节点,其他的所有节点都完全使用了双向带宽。
是这样的吗?
还是说全部节点都没有带宽浪费呢?
4、通过Tree进行Allreduce的过程
因为NCCL里只对Allreduce实现了Tree算法,Tree的Allreduce是分成了两块,一块是做reduce,一块是做Broadcast,跟Ring算法不太一样。
reduce阶段:从每个节点的每个chain的末尾那个chain开始向上reduce,一直reduce到每个节点 的root rank,root rank继续向上reduce,一直reduce到整个Tree的root rank
Broadcast阶段:从整个Tree的root rank向下做Broadcast,每个节点的root rank也在节点内顺着chain向下做Broadcast
5、Tree的建立
包括B-Tree的算法,没有仔细的研究,算法功底不行。。。。
(后续补充)
6、参考
https://developer.nvidia.com/blog/massively-scale-deep-learning-training-nccl-2-4/
https://github.com/NVIDIA/nccl/issues/919
https://github.com/NVIDIA/nccl/issues/545
Understand the tree topology · Issue #671 · NVIDIA/nccl · GitHub