在 MPI 的 3D 过程分解中交换 2D 晕圈的子数组数据类型的数量

Number of subarray data types for exchanging 2D halos in 3D process decomposition in MPI

假设一个维度为 GX*GY*GZ 的全局立方体,在每个进程中使用 3D 笛卡尔拓扑将其分解为 3D 个大小为 PX*PY*PZ 的立方体。添加 Halos 以交换数据,这变成 (PX+2)*(PY+2)*(PZ+2)假设我们使用子数组数据类型进行 2D halo 交换 - 我们是否需要定义 12 子数组类型?

我的理由是:对于 YZ 平面,我们创建一个用于发送的子数组类型和一个用于接收的子数组类型,因为要在子数组数据类型本身中指定起始坐标。但是有 2 YZ 平面,这导致 4 子数组数据类型。尽管全局和局部数据大小保持不变,但由于起始索引 - 我们需要定义 4 不同的子数组类型。 使用矢量数据类型发送这些平面中的四个,使用子阵列数据类型发送其余两个平面不是更好吗?

您在这里有三种数据访问模式 - sending/receiving 子域的 X 面、Y 面和 Z 面 - 所以你需要 描述这些模式的三种不同方式。您使用哪种类型和多少类型来描述这在很大程度上取决于您找到表达和使用这些模式的最清晰方式。

假设您在本地有 PX=8、PY=5、PZ=7,因此包括光环在内,本地子域为 10x7x9。这是在 C 中,所以我们假设数据存储在一些连续的数组 arr[ix][iy][iz] 中,以便值 (ix,iy,1) 和 (ix,iy,2) 是连续的(偏移一项大小 - 对于双精度来说 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 为 left/right 和 up/down 勾勒出网格的面,x 显示在相邻的面板中。为简单起见,我们将在 send/receive.

中包含角单元格

将 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 ] - 这是从 [0][PY][0] 开始的 (PY+2)*(PZ+2) 值的跳跃,较早开始,并取另一个 PZ+2 连续值,依此类推。您可以简单地表达为:

  • MPI_Type_vector of count PX+2, blocklen (PZ+2), and stride of (PY+2)*(PZ+2), or
  • MPI_Type_subarray,切片子大小为 [PX+2,1,PZ+2],从 [0,PY,0]
  • 开始

它们完全等价,没有性能差异。

现在,让我们考虑接收此数据:

       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 of count PX+2, blocklen (PZ+2), and stride of (PY+2)*(PZ+2), or
  • 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, ... );

以上正是您使用相应矢量类型的方式 - 将其指向 send/receive 的第一项。

如果您选择使用子数组类型,使用它的任何一种方式都非常好,您会在各种软件中看到这两种选择。这只是你更清晰的问题 - 每个模式 4 种类型(取决于偏移量),或者在 send/receive 中明确使用偏移量。我个人认为 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 of count 1, blocklen (PY+2)*(PZ+2), and stride of anything, 或者count PY+2, blocklen PZ+2, and stride of PZ+2 ,或任何等效组合
  • 一个子数组,切片子大小为 [1,PY+2,PZ+2],从适当的位置开始

对于 z 面:

  • MPI_Type_vector of count (PX+2)*(PY+2), blocklen 1, and stride of PZ+2
  • 一个子数组,切片子大小为 [PX+2,PY+2,1],从适当的位置开始。

所以,一切都归结为清晰。子数组类型在所有方向上看起来最相似,区别也相当明显;然而,如果我向您展示了一堆在同一段代码中声明的向量类型,您必须在白板上画一些草图以确保我没有不小心将它们调换。子数组也最容易泛化——如果你转向一个现在每边都需要 2 个光环单元的方法,比如说,或者不发送角单元,对子数组的修改是微不足道的,而你必须做一些工作来用向量构建一些东西。