在MPI中进行3D进程分解时,用于交换2D边界的子阵列数据类型数量。

5
假设存在一个全局的GX*GY*GZ尺寸的立方体,使用3D笛卡尔拓扑结构将其分解为每个进程上大小为PX*PY*PZ的3D立方体。加上数据交换的Halos后,它变成了(PX+2)*(PY+2)*(PZ+2)。假设我们使用Subarray数据类型进行2D Halo交换-我们需要定义12个Subarray类型吗?
我的推理是:对于YZ平面,我们创建一个用于发送和一个用于接收的Subarray类型,因为起始坐标必须在Subarray数据类型内指定。但有2个YZ平面,这导致需要定义4个Subarray数据类型。尽管全局和本地数据大小保持不变,但由于起始索引的不同,我们需要定义4种不同的Subarray类型。使用矢量数据类型发送其中四个平面和使用Subarray数据类型发送其余两个是否更好?

Petsc的DM结构可以表示分布在某些处理器上的三维规则数组数据:这可能是您要查找的内容。请查看DMDACreate3d()。Petsc的文档第2.4节也很有趣:Halos在Petsc中被称为ghost points。 - francis
感谢@francis的回复,但这不是我想要的。我想交换3D数据的XY、YZ、ZX平面。Petsc及其文档对于我正在制作的这个简单MPI程序来说过于复杂了。 - Gaurav Saxena
当我为YZ平面(X恒定)定义MPI_Type_create_subarray时,它可以正常工作。具体来说,X轴是上下方向,Y轴是左右方向,Z轴是朝向/远离您(即MPI_ORDER_C)。但对于其他平面,它无法正常工作,这就是问题所在。 - Gaurav Saxena
1个回答

6
你这里有三种数据访问模式——发送/接收子域的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);

/* Send lower yface */
MPI_Send(&(arr[0][0][0]), 1, send_down_yface_t, ... );
/* Send upper yface */
MPI_Send(&(arr[0][0][0]), 1, send_up_yface_t, ... );
/* Receive lower face */
MPI_Recv(&(arr[0][0][0]), 1, recv_down_yface_t, ... );
/* Receive upper face */
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);
/* ... */
/* Send lower yface */
MPI_Send(&(arr[0][1][0]), 1, yface_t, ... );
/* Send upper yface */
MPI_Send(&(arr[0][PY][0]), 1, yface_t, ... );
/* Receive lower face */
MPI_Recv(&(arr[0][0][0]), 1, yface_t, ... );
/* Receive upper face */
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个空心单元的方法,或者不发送角单元,则对子数组进行修改是微不足道的,而要构建带有矢量的内容需要做一些工作。

感谢您耐心的解释。但是我在句子“你需要发送一个y-face到上面的邻居的数据看起来像”(请参见第一张图之前的句子)中迷失了。您能否确认起始坐标是否根据图形正确?您是从上方向下看这个图形吗?在我理解整个答案之前,我真的需要理解这个句子。非常感谢! - Gaurav Saxena
@GauravSaxena - 他们是正确的,但我重新制作了图表,希望更清晰易懂。希望能有所帮助。 - Jonathan Dursi
再次感谢。我从答案中学到了两件事。(1)可以使用子数组/向量 (2)在这种情况下,子数组更对称。我成功地使用子数组传递了二维数据。我将尝试使用向量 - 让我的大脑多动一点!非常感谢您的回答 :)。 - Gaurav Saxena
我一直回到这个答案。非常好的答案,它帮助了我很多! - Gaurav Saxena

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接