在这里我们定义分布式计算为使用众多CPU/GPU的并行计算,这些CPU/GPU的内存地址不能被其他CPU/GPU直接读写,只有通过网线在指定的通信协议(MPI)下进行间接操作。更具体的介绍可以参考我的下述文章。
hillyuan:有限元并行计算简介32 赞同 · 11 评论文章
当你需要快速得到你的大规模计算问题的答案,比如你的200万节点的震动解析需要在两天内得到答案的时候,采用单机内的并行计算往往实现不了这个目标。由于现在的设计人员往往需要在更改设计后经过一次模拟验证,根据我个人的面谈调查,模拟验证计算在一个晚上完成是往往可以接受的。但是如果模拟验证计算一天内还不能完成,不少人会觉得难以容忍的,这时设计人员的应对手段往往是采用分布式计算(还有一个可能是要求我们修改程序,让程序快起来^O^)。除此以外,另一个可能性就是你的计算问题大到最豪华的单机内存都容不下时,此时分布式计算是你唯一的选择。这样的问题可能是大气环流,地震海啸,整车碰触问题等等。
最后,如的标题所述,本文讲的是有限元的分布式计算。但是本文所述概念同样适应与如FDM,FVM等的分布式计算。
在此我们要考虑上图由5个节点,4个单元构成的有限元网格。虽然简单,但是用来介绍分布式计算的基本概念应该足够了。
为了便于表述,我们导入记号(几何形状维度,编号)来标记上述几何实体。如节点3标记为(0,3),单元1标记为(1,1)。如此,采用1CPU计算时,内存中保存的几何实体为
(0,0),(0,1),(0,2),(0,3),(0,4)
(1,0),(1,1),(1,2),(1,3)
其全体刚度阵的形状为
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | × | × | |||
1 | × | × | × | ||
2 | × | × | |||
3 | × | × | × | ||
4 | × | × |
在上表中,数字表示自由度编号,×表示非零成分。
现在我们把上述数据分布在两个CPU,把单元0,1放在CPU0,把单元2,3放在CPU1中。则此时的数据分布为:
CPU0
(0,0:0),(0,1:1),(0,2:2)
(1,0:0),(1,1:1)
CPU1
(0,0:3),(0,1:4) | (0,2:2)
(1,0:2),(1,1:3)
请注意,在此我们把前述几何实体的标记方法改为(几何形状维度,局部编号:全体编号)以对应分布式数据的变化。其中全体编号为网格文件中定义的几何实体的原有编号,局部编号为在每个CPU内重新排序的编号。各个CPU实际上是采用局部编号来进行计算。
分布式数据的另一个重要概念是持有(owned)数据和非持有(ghost)数据的概念。注意到上面的数据分布中,节点2被CPU0和CPU1所共有,我们规定CPU0持有该节点,CPU1可以参照该节点,是其ghost节点(绝大多数软件都设定序号低的CPU持有数据)。为了区分持有数据和非持有数据,在上面的记号中,我们增加了一个区分符|,在该区分符后得数据为ghost数据。
此时个CPU中的刚度阵为(表中数据为局部自由度编号:全体自由度编号)
CPU0
0:0 | 1:1 | 2:2 | 3:3 | |
---|---|---|---|---|
0:0 | × | × | ||
1:1 | × | × | × | |
2:2 | × | × |
CPU1
0:3 | 1:4 | 2:2 | |
---|---|---|---|
0:3 | × | × | × |
1:4 | × | × | |
2:2 | × | × |
CPU0持有全局自由度0,1,2,那么意味着它也持有全体刚度阵的0,1,2行。但是你要注意到全体刚度阵(2,3)的数据并不能在CPU0的计算得出,它是由CPU1计算得出,需要通过数据通信传输到CPU0. 相应的,在后续的求解过程中CPU1的ghost行往往并不介入计算。
从上面的描述我们可以看出分布式计算往往要经过两步才能完成
各CPU中的计算
CPU间的通信。
在具体的程序实现中往往提供相应的函数要求CPU间的通信。比如在petsc中的
MatAssemblyBegin和MatAssemblyEnd
在Trilinos中的
resumeFill和fillComplete
在FrontISTR中的
hecmw_update
Interconnected network间的数据传输速度是分布式计算的罩门,因为它往往比CPU内部的数据传输速度低n个数量级。如何减少数据传输量是分布式计算编程中的重中之重。下面的例子是如何通过改变数据分布设计来减少通信数据量。
CPU0
(0,0:0),(0,1:1),(0,2:2) | (0,3:3)
(1,0:0),(1,1:1) | (1,2:2)
CPU1
(0,0:3),(0,1:4) | (0,2:2)
(1,0:2),(1,1:3)
在这里,我们把节点3和单元2增添为CPU0的ghost量,如此全体刚度阵的0,1,2行的数值可以从CPU0处直接计算得出,不需要与CPU1的数据通信。
CPU0
0:0 | 1:1 | 2:2 | 3:3 | |
---|---|---|---|---|
0:0 | × | × | ||
1:1 | × | × | × | |
2:2 | × | × | × | |
3:3 | × | × |
当然这种方法的不良后果是CPU0使用内存和计算量的增加。但是考虑到Interconnected network间的通信速度,这种牺牲往往是划算的。这种在CPU界面处多取一层ghost单元的做法也是很常见的有限元实装方法。
MPI提供了通信标准,最常用的函数
MPI_Sendrecv (sendbuf,sendcount,sendtype,dest,sendtag,recvbuf, recvcount recvtype source recvtag comm status ierr recvcount,recvtype,source,recvtag,comm,status,ierr)
用户通过指定传出(send)和接受(dest)方,数据种类,大小来完成数据传输。
网格如何在CPU上分配是减少CPU间数据传输量的主要方法。网格的配分(partition)方法一般要满足如下原则
各CPU的计算量(往往可以但不限于由CPU持有的单元,节点数来判断)大致相当
CPU间的共有节点(在你采用边单元的时候是共有边,当你采用面单元或FVM时是共有面)数要尽量小。
这是一个标准的图论(graph)优化问题,数学家们已经提供了不少优秀的开源软件。
Obtaining ParMETIS | Karypis Labglaros.dtc.umn.edu/gkhome/metis/parmetis/download
https://github.com/sandialabs/Zoltangithub.com/sandialabs/Zoltan
对于有限元软件的开发者(至少对我而言),有用的就好。
分布式计算原理可说简单,但是其实装却是极其繁复。即使是要仅仅实现CPU间的多对多通信都要死掉不少脑细胞。更不要说考虑网格界面的滑动,单元的消失和生成,网格重新划分等等。好在经过几十年的大浪淘沙,当下已有很优秀的开源软件可用。如果倒回二十年前就没有这么轻松了。
GitHub - trilinos/Trilinos: Primary repository for the Trilinos Projectgithub.com/trilinos/Trilinos
Trilinos下属的 Tpetra软件(用于替代旧式的Epetra: Linear Algebra Services Package软件)实装了分布式数据管理。
Tpetra使用一个Map类来管理CPU间的通信
Tpetra::map<> mymap(numGlobalEntries, numLocalEntries, indexBase, comm);
Tpetra::vector<> x(mymap);
用户可以通过指定一个各CPU的大致的数据量来定义一个Map类,然后通过该map类定义的分布式数据(vector,matrix)都会置于map的管理下,原则上所有的CPU间的通信都不要用户来具体操作。
Tpetra提供了数种map类和数据类型(上面的Map,vector类都含有数个template类型),以便用户对于计算过程做细小粒度的控制。
https://gitlab.com/petsc/petscgitlab.com/petsc/petsc
使用petsc来生成一维数组
Vec v;
VecCreate(comm, &v);
VecSetSizes(v,m,M);
用户需要指定数组全体的长度M或/和在本CPU上的长度m。对于二维数组
Mat A;
MatCreate(comm, &A);
MatSetSizes(A,m,n,M,N);
用户需要指定矩阵全体的长度M,N或/和在本CPU上的长度m,n。这里用户指定的长度可以通过petsc优化处理(往往使用上面提到的开源软件metis, scotch)。数据通信也完全交给petsc来处理。
petsc没有提供像trilinos那样多的细节管理。但是使用起来也相对简单。特别是对只是熟悉Fortran,c这样的结构化程序的用户而言。Trilinos则用到了非常多的现代c 功能。