你这里有三种数据访问模式——发送/接收子域的X面、Y面和Z面,因此你需要三种不同的描述方法。你使用哪一种及使用多少种类型来描述它们,很大程度上取决于你发现哪种表达方式最清晰并且最适合使用这些模式。
假设本地 PX=8,PY=5,PZ=7,因此包括边界,本地子域为10x7x9。在 C 语言中,我们假设数据存储在一些连续的数组
arr[ix][iy][iz]
中,因此前进值(ix, iy, 1)和(ix, iy, 2)是相邻的(偏移一个项目大小,例如double类型偏移8个字节),值(ix, 1, iz)和(ix, 2, iz)偏移(PZ+2) [即9]个值,(1, iy, iz)和(2, iy, iz)偏移(PY+2)*(PZ+2) [=7*9=63]个值。
因此,让我们看看这是如何运作的,勾勒出网格的面,其中 z/y 是左/右和上/下,而 x 则显示在相邻的面板中。为简单起见,我们将包括所发送/接收的角落单元。
向上邻居发送 y 面需要发送的数据如下:
x = 0 x = 1 ... x = 9 Local Grid Size:
+---------+ +---------+ +---------+ PX = 8
6 | | | | | | PY = 5
5 |@@@@@@@@@| |@@@@@@@@@| |@@@@@@@@@| PZ = 7
4 ^| | ^| | ^| |
3 || | || | || |
2 y| | y| | y| |
1 | | | | | |
0 | | | | | |
+---------+ +---------+ +---------+
012345678 012345678 ... 012345678
z-> z-> z->
那么,它将从 [0][PY][0](例如,[0][5][0])开始,并延伸到 [PX+1][PY][PZ+1]。因此,您将从 [0][PY][0]...[0][PY][PZ+1] 开始,这些是 PZ+2 个连续值,然后转到 [1][PY][0]——它跳过了 (PY+2)*(PZ+2) 个值,从更早的起点 [0][PY][0] 开始,取另外 PZ+2 个连续值,以此类推。您可以简单地表示为:
- 使用计数为 PX+2、块长度为(PZ+2)和步幅为(PY+2)*(PZ+2)的 MPI_Type_vector,或
- 使用从 [0,PY,0] 开始,切片子大小为[PX+2,1,PZ+2] 的 MPI_Type_subarray
它们完全等效,没有性能差异。
现在,让我们考虑接收这些数据:
x = 0 x = 1 ... x = 9 Local Grid Size:
+---------+ +---------+ +---------+ PX = 8
6 | | | | | | PY = 5
5 | | | | | | PZ = 7
4 ^| | ^| | ^| |
3 || | || | || |
2 y| | y| | y| |
1 | | | | | |
0 |@@@@@@@@@| |@@@@@@@@@| |@@@@@@@@@|
+---------+ +---------+ +---------+
012345678 012345678 ... 012345678
z-> z-> z->
至关重要的是,所需的数据模式完全相同:PZ+2个值,然后跳过从最后一个块的开头算起的(PY+2)*(PZ+2)个值,再加上PZ+2个值。我们可以描述为:
- MPI_Type_vector,计数为PX+2,块长度为(PZ+2),步幅为(PY+2)*(PZ+2)
- MPI_Type_subarray,切片子大小为[PX+2,1,PZ+2],从[0,0,0]开始
唯一不同的是子数组类型的起始位置。但这并没有像看起来那么大的差异!
当您实际在发送或接收中使用子数组类型时,将向例程传递指向某些数据的指针,然后给它一个子数组类型,其中包含一些起始位置和切片描述。MPI然后向前跳转到该起始位置,并使用由该切片描述描述的数据布局。
因此,虽然定义和使用四种子数组类型是完全可以的:
MPI_Type_create_subarray(ndims=3, sizes=[PX+2,PY+2,PZ+2], subsizes=[PX+2,1,PZ+2],
starts=[0,0,0],... &recv_down_yface_t);
MPI_Type_create_subarray(...all the same...
starts=[0,1,0],... &send_down_yface_t);
MPI_Type_create_subarray(...all the same...
starts=[0,PY,0],... &send_up_yface_t);
MPI_Type_create_subarray(...all the same...
starts=[0,PY+1,0],... &recv_up_yface_t);
MPI_Send(&(arr[0][0][0]), 1, send_down_yface_t, ... );
MPI_Send(&(arr[0][0][0]), 1, send_up_yface_t, ... );
MPI_Recv(&(arr[0][0][0]), 1, recv_down_yface_t, ... );
MPI_Recv(&(arr[0][0][0]), 1, recv_up_yface_t, ... );
这里声明了四个等效的模式,它们有不同的起始点。您也可以只定义一个模式,并将其指向您需要的数据的不同起始点:
MPI_Type_create_subarray(ndims=3, sizes=[PX+2,PY+2,PZ+2], subsizes=[PX+2,1,PZ+2],
starts=[0,0,0],... &yface_t);
MPI_Send(&(arr[0][1][0]), 1, yface_t, ... );
MPI_Send(&(arr[0][PY][0]), 1, yface_t, ... );
MPI_Recv(&(arr[0][0][0]), 1, yface_t, ... );
MPI_Recv(&(arr[0][PY+1][0]), 1, yface_t, ... );
上述的方法就是使用相应的向量类型的方式 - 将其指向要发送/接收的第一个项。
如果选择使用子阵列类型,则使用任何一种方式都可以,各种软件中都会看到这两种选择。只是在4个类型中进行选择(取决于偏移量),或者在发送/接收中明确使用偏移量。我个人认为1种类型的方法更清晰,但对于这个问题没有明确的正确答案。
至于是否使用MPI_Subarray或Vector(例如),最简单的方法是查看需要支持的其他两种模式:具有X面(这里您有几种更多的选项,因为它们是连续的:
- (PY+2)*(PZ+2)MPI_Doubles
- 1 MPI_Type_Contiguous of (PY+2)*(PZ+2) MPI_Doubles
- MPI_Type_vector,计数为1,块长为(PY+2)*(PZ+2),步长为任意值,或计数为PY + 2,块长为PZ + 2,步幅为PZ + 2,或任何等效组合
- 一个子数组,切片子大小为[1,PY+2,PZ+2],从适当的位置开始
对于z面:
- MPI_Type_vector,计数为(PX+2)*(PY+2),块长度为1,步幅为PZ+2
- 一个子数组,切片子大小为[PX+2,PY+2,1],从适当位置开始。
因此,这一切都归结于清晰度。子阵列类型在所有方向上看起来最相似,并且差别非常明显;而如果我向您展示了一堆在同一代码中声明的矢量类型,您将不得不在白板上进行一些草图以确保我没有意外地将它们弄反。子数组也最容易推广 - 如果您转移到现在需要每侧具有2个空心单元的方法,或者不发送角单元,则对子数组进行修改是微不足道的,而要构建带有矢量的内容需要做一些工作。
DM
结构可以表示分布在某些处理器上的三维规则数组数据:这可能是您要查找的内容。请查看DMDACreate3d()
。Petsc的文档第2.4节也很有趣:Halos在Petsc中被称为ghost points。 - francis