您在这里有三种数据访问模式 - 发送/接收子域的 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 为左/右和上/下绘制网格的各个面,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 ] - 这是从 [0][PY][0] 跳转 (PY+2)*(PZ+2) 值,开始较早,并取另一个 PZ+2 连续值,依此类推。你可以简单地表达为:
- 计数 PX+2、blocklen (PZ+2) 和 (PY+2)*(PZ+2) 的步幅的 MPI_Type_vector,或
- 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 值。我们可以这样描述:
- 计数 PX+2、blocklen (PZ+2) 和 (PY+2)*(PZ+2) 的步幅的 MPI_Type_vector,或
- 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-type 方法要清楚得多,但是对于这个问题没有明确的正确答案。
至于是使用 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、blocklen (PY+2)*(PZ+2) 和任何东西的步幅,或计数 PY+2、blocklen PZ+2 和 PZ+2 的步幅,或任何等效组合
- 一个子数组,切片子大小为 [1,PY+2,PZ+2],从适当的位置开始
对于 z 面:
- 计数 (PX+2)*(PY+2)、blocklen 1 和 PZ+2 步幅的 MPI_Type_vector
- 一个子数组,切片子大小为 [PX+2,PY+2,1],从适当的位置开始。
所以,这一切都归结为清晰。子数组类型在各个方向之间看起来最相似,并且差异相当明显;然而,如果我向您展示了一组在同一段代码中声明的向量类型,您将不得不在白板上画一些草图,以确保我没有意外地切换它们。子数组也最容易泛化 - 如果您移动到现在每侧需要 2 个光环单元的方法,例如,或者不发送角单元,则对子数组的修改是微不足道的,而您必须做一些工作用向量构建一些东西。