这个问题有一个众所周知的解决方案,名为ghost cells 或halos。这个想法是根据模板用一层或多层额外的单元围绕每个子阵列。在每次迭代开始时,每个进程通过与其最近的邻居交换数据来同步光环的状态,该操作称为光环交换。光晕提供了计算所有内部单元格的新状态所需的数据,但它们缺乏自身更新所需的数据,因此这些单元格的内容在一次迭代后变得“旧”,这就是为什么它们有时被称为“幽灵” .
This blog post 清楚地表达了这个想法,但使用的语言是 Julia。由于 About 页面上说可以在其他地方重复使用内容,所以我无耻地借用了以下插图:
该图仅显示进程之间的边界上的光晕,但实际上,如果您在所有四个边上都有光晕并且只更新需要的光晕,因为它使代码更加对称并降低了其复杂性,这样会更容易。
您的特定情况下的分解如下:
1 2 3 4
5 6 7 8
1 2 3 4
0 6 0 8
先成为
1 2 | 3 4
5 6 | 7 8
----+----
1 2 | 3 4
0 6 | 0 8
添加光环:
x x x x | x x x x
x 1 2 x | x 3 4 x
x 5 6 x | x 7 8 x
x x x x | x x x x
--------+--------
x x x x | x x x x
x 1 2 x | x 3 4 x
x 0 6 x | x 0 8 x
x x x x | x x x x
现在,在模板操作之前,您需要执行光环交换。它按如下所示的步骤进行。交换沿每个维度成对执行。顺序并不重要。
1.1) 向东水平交换
每个进程将其最右边的列发送到其右侧的进程。接收器将数据放在它的左晕列中:
x x [x] x | [x] x x x x x [x] x | [x] x x x
x 1 [2] x | [x] 3 4 x x 1 [2] x | [2] 3 4 x
x 5 [6] x | [x] 7 8 x x 5 [6] x | [6] 7 8 x
x x [x] x | [x] x x x x x [x] x | [x] x x x
----------+---------- --> ----------+----------
x x [x] x | [x] x x x x x [x] x | [x] x x x
x 1 [2] x | [x] 3 4 x x 1 [2] x | [2] 3 4 x
x 0 [6] x | [x] 0 8 x x 0 [6] x | [6] 0 8 x
x x [x] x | [x] x x x x x [x] x | [x] x x x
1.2) 向西水平交换
每个进程将其最左边的列发送到它左边的进程。接收器将数据放在它的右晕列中:
x x x [x] | x [x] x x x x x [x] | x [x] x x
x 1 2 [x] | 2 [3] 4 x x 1 2 [3] | 2 [3] 4 x
x 5 6 [x] | 6 [7] 8 x x 5 6 [7] | 6 [7] 8 x
x x x [x] | x [x] x x x x x [x] | x [x] x x
----------+---------- --> ----------+----------
x x x [x] | x [x] x x x x x [x] | x [x] x x
x 1 2 [x] | 2 [3] 4 x x 1 2 [3] | 2 [3] 4 x
x 0 6 [x] | 6 [0] 8 x x 0 6 [0] | 6 [0] 8 x
x x x [x] | x [x] x x x x x [x] | x [x] x x
2.1) 向南垂直交换
每个进程将其底行发送给它下面的进程。接收器将数据放置在其顶部的光环行中:
[x x x x | x x x x] [x x x x | x x x x]
x 1 2 3 | 2 3 4 x x 1 2 3 | 2 3 4 x
[x 5 6 7 | 6 7 8 x] [x 5 6 7 | 6 7 8 x]
x x x x | x x x x x x x x | x x x x
--------+-------- --> --------+--------
[x x x x | x x x x] [x 5 6 7 | 6 7 8 x]
x 1 2 3 | 2 3 4 x x 1 2 3 | 2 3 4 x
[x 0 6 0 | 6 0 8 x] [x 0 6 0 | 6 0 8 x]
x x x x | x x x x x x x x | x x x x
2.2) 向北垂直交换
每个进程将其顶行发送给它上面的进程。接收器将数据放置在其底部的光环行中:
x x x x | x x x x x x x x | x x x x
[x 1 2 3 | 2 3 4 x] [x 1 2 3 | 2 3 4 x]
x 5 6 7 | 6 7 8 x x 5 6 7 | 6 7 8 x
[x x x x | x x x x] [x 1 2 3 | 2 3 4 x]
--------+-------- --> --------+--------
x 5 6 7 | 6 7 8 x x 5 6 7 | 6 7 8 x
[x 1 2 3 | 2 3 4 x] [x 1 2 3 | 2 3 4 x]
x 0 6 0 | 6 0 8 x x 0 6 0 | 6 0 8 x
[x x x x | x x x x] [x x x x | x x x x]
这些操作中的每一个都可以通过一个 MPI 调用来实现 - MPI_Sendrecv。发送部分发送本地数据行或列,接收部分将其接收到本地晕行或列中。
四次交换后的最终结果是:
x x x x | x x x x
x 1 2 3 | 2 3 4 x
x 5 6 7 | 6 7 8 x
x 1 2 3 | 2 3 4 x
--------+--------
x 5 6 7 | 6 7 8 x
x 1 2 3 | 2 3 4 x
x 0 6 0 | 6 0 8 x
x x x x | x x x x
您可能会注意到,即使是每个光环中的角元素也包含人们期望的正确对角元素。这样做的美妙之处在于,您可以简单地添加更多步骤,将其扩展到所需的任意维度,并且所有元素都会自动在光环区域中找到它们的正确位置。
您现在可以在本地获得所有 6 个 neigbhours,并且可以继续对它们的值求平均值。请注意,您只使用光环中的值,不要更新它们。
一些补充说明:
-
无论您是否有周期性边界条件,这都有效。在周期性边界条件下,当进行右移时,P2 是 P1 的右邻居,P1 是 P2 的右邻居。此外,P1 是 P2 的左邻居,P2 是 P1 的左邻居。垂直方向也是如此。
-
您不需要特殊的代码来处理边界上的进程。如果一个进程没有右(或左、上或下)邻居,只需向/从MPI_PROC_NULL 发送或接收消息。即,代码如下:
int right = compute_right_rank();
int left = compute_left_rank();
int up = compute_top_rank();
int down = compute_bottom_rank();
MPI_Sendrecv(right_column, 1, columndt, right, 0,
left_halo, 1, columndt, left, 0,
MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Sendrecv(left_column, 1, columndt, left, 0,
right_halo, 1, columndt, right, 0,
MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Sendrecv(bottom_row, 1, rowdt, down, 0,
top_halo, 1, rowdt, up, 0,
MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Sendrecv(top_column, 1, rowdt, up, 0,
bottom_halo, 1, rowdt, down, 0,
MPI_COMM_WORLD, MPI_STATUS_IGNORE);
这里,compute_right_rank() 应该返回 rank + 1 如果右边有排名,否则返回 MPI_PROC_NULL。发送到MPI_PROC_NULL 或从它接收是无操作的,即没有任何反应。它允许您在没有ifs 的情况下编写代码。
columndt 是一种 MPI 数据类型,对应于数组中的一列。您可以使用MPI_Type_vector 构建它。 rowdt 是一种 MPI 数据类型,表示数组中的一整行。使用MPI_Type_contiguous 构造它。
-
如果排名在笛卡尔通信器中,计算邻居的排名非常容易。 MPI_Cart_create 和 MPI_Cart_shift 是你最好的朋友。您还可以使用 MPI 邻居集合来进一步减少 MPI 调用的数量。
-
由于边界条件不是周期性的,因此未填充底部等级的底部光晕。最右边行列的右光晕、最高行列的顶部光晕和最左边行列的左光晕也是如此。您可能希望使用特殊值预填充它们,例如 0。这个价值永远不会改变,因为没有沟通可以把东西放在那里。
-
如果您的计算是迭代的,您必须在每次迭代之前执行光环。如果光环交换与计算相比太慢,您可以增加光环的厚度并使其厚两层或多层。使用外层更新内层中的值。三层厚的晕轮需要每三次迭代交换一次。