首先,被动 RMA 机制不会以某种方式神奇地插入远程进程的内存,因为没有多少 MPI 传输具有真正的 RDMA 功能,即使是那些具有真正 RDMA 功能的传输(例如 InfiniBand)也需要大量的非被动参与目标以允许发生被动 RMA 操作。这在 MPI 标准中进行了解释,但以非常抽象的形式,即通过 RMA 窗口公开的内存的公共和私有副本。
使用 MPI-2 实现工作和便携式无源 RMA 涉及几个步骤。
第一步:目标进程中的窗口分配
出于可移植性和性能原因,应使用以下方式分配窗口的内存MPI_ALLOC_MEM
:
int size;
MPI_Comm_rank(MPI_COMM_WORLD, &size);
int *schedule;
MPI_Alloc_mem(size * sizeof(int), MPI_INFO_NULL, &schedule);
for (int i = 0; i < size; i++)
{
schedule[i] = 0;
}
MPI_Win win;
MPI_Win_create(schedule, size * sizeof(int), sizeof(int), MPI_INFO_NULL,
MPI_COMM_WORLD, &win);
...
MPI_Win_free(win);
MPI_Free_mem(schedule);
第二步:目标内存同步
MPI 标准禁止同时访问窗口中的同一位置(MPI-2.2 规范中的第 11.3 节):
对窗口中的同一内存位置进行并发冲突访问是错误的;如果某个位置由 put 或 accumo 操作更新,则在目标上完成更新操作之前,加载或另一个 RMA 操作无法访问此位置。
因此,对目标的每次访问schedule[]
都必须受到锁的保护(共享,因为它只读取内存位置):
while (!ready)
{
MPI_Win_lock(MPI_LOCK_SHARED, 0, 0, win);
ready = fnz(schedule, oldschedule, size);
MPI_Win_unlock(0, win);
}
在目标处锁定窗口的另一个原因是为 MPI 库提供条目,从而促进RMA 操作的本地部分的进展。即使在使用不支持 RDMA 的传输(例如 TCP/IP 或共享内存)时, MPI 也提供可移植的 RMA,这需要在目标上完成大量主动工作(称为进程)以支持“被动”RMA。一些库提供了可以在后台进行操作的异步进程线程,例如 Open MPI 配置时--enable-opal-multi-threads
(默认禁用),但依赖这种行为会导致不可移植的程序。这就是为什么 MPI 标准允许 put 操作的以下宽松语义(第 11.7 节,第 365 页):
6. 最迟当窗口所有者在该窗口上执行对 MPI_WIN_WAIT、MPI_WIN_FENCE 或 MPI_WIN_LOCK 的后续调用时,通过对公共窗口副本的 put 或 accumm 调用进行的更新在进程内存中的私有副本中变得可见。
如果put 或accumulate 访问与锁同步,那么一旦更新过程执行MPI_WIN_UNLOCK,公共窗口副本的更新就完成了。另一方面,进程内存中私有副本的更新可能会延迟,直到目标进程在该窗口上执行同步调用(6)。因此,对进程内存的更新总是可以延迟到进程执行合适的同步调用。如果使用栅栏或启动后完成等待同步,也可以延迟对公共窗口副本的更新,直到窗口所有者执行同步调用。只有当使用锁同步时,才需要更新公共窗口副本,即使窗口所有者没有执行任何相关的同步调用。
这也在标准的同一部分(第 367 页)的示例 11.12 中进行了说明。事实上,Open MPI 和 Intel MPI都不会更新schedule[]
master 代码中的 lock/unlock 调用是否被注释掉的值。MPI 标准进一步建议(§11.7, p. 366):
给用户的建议。用户可以按照以下规则编写正确的程序:
...
lock:如果可能发生冲突,对窗口的更新会受到排他锁的保护。对于本地访问和 RMA 访问,非冲突访问(例如只读访问或累积访问)受共享锁保护。
MPI_PUT
第 3 步:在原点提供正确的参数
MPI_Put(&schedule[myrank],1,MPI_INT,0,0,1,MPI_INT,win);
会将所有内容转移到目标窗口的第一个元素中。鉴于创建目标窗口的正确调用disp_unit == sizeof(int)
是:
int one = 1;
MPI_Put(&one, 1, MPI_INT, 0, rank, 1, MPI_INT, win);
的本地值one
因此被转移到rank * sizeof(int)
目标窗口开始之后的字节中。如果disp_unit
设置为 1,正确的 put 将是:
MPI_Put(&one, 1, MPI_INT, 0, rank * sizeof(int), 1, MPI_INT, win);
第 4 步:处理实施细节
上述详细程序可与英特尔 MPI 开箱即用。对于 Open MPI,必须特别小心。该库是围绕一组框架和实现模块构建的。(osc
单向通信)框架有两种实现方式 -rdma
和pt2pt
. 默认值(在 Open MPI 1.6.x 和可能更早的版本中)是rdma
并且由于某种原因它在MPI_WIN_(UN)LOCK
调用时不会在目标端进行 RMA 操作,这会导致类似死锁的行为,除非进行另一个通信调用(MPI_BARRIER
在您的情况下) )。另一方面,pt2pt
模块按预期进行所有操作。因此,使用 Open MPI 必须像下面这样启动程序才能专门选择pt2pt
组件:
$ mpiexec --mca osc pt2pt ...
一个完整的 C99 示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <mpi.h>
// Compares schedule and oldschedule and prints schedule if different
// Also displays the time in seconds since the first invocation
int fnz (int *schedule, int *oldschedule, int size)
{
static double starttime = -1.0;
int diff = 0;
for (int i = 0; i < size; i++)
diff |= (schedule[i] != oldschedule[i]);
if (diff)
{
int res = 0;
if (starttime < 0.0) starttime = MPI_Wtime();
printf("[%6.3f] Schedule:", MPI_Wtime() - starttime);
for (int i = 0; i < size; i++)
{
printf("\t%d", schedule[i]);
res += schedule[i];
oldschedule[i] = schedule[i];
}
printf("\n");
return(res == size-1);
}
return 0;
}
int main (int argc, char **argv)
{
MPI_Win win;
int rank, size;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (rank == 0)
{
int *oldschedule = malloc(size * sizeof(int));
// Use MPI to allocate memory for the target window
int *schedule;
MPI_Alloc_mem(size * sizeof(int), MPI_INFO_NULL, &schedule);
for (int i = 0; i < size; i++)
{
schedule[i] = 0;
oldschedule[i] = -1;
}
// Create a window. Set the displacement unit to sizeof(int) to simplify
// the addressing at the originator processes
MPI_Win_create(schedule, size * sizeof(int), sizeof(int), MPI_INFO_NULL,
MPI_COMM_WORLD, &win);
int ready = 0;
while (!ready)
{
// Without the lock/unlock schedule stays forever filled with 0s
MPI_Win_lock(MPI_LOCK_SHARED, 0, 0, win);
ready = fnz(schedule, oldschedule, size);
MPI_Win_unlock(0, win);
}
printf("All workers checked in using RMA\n");
// Release the window
MPI_Win_free(&win);
// Free the allocated memory
MPI_Free_mem(schedule);
free(oldschedule);
printf("Master done\n");
}
else
{
int one = 1;
// Worker processes do not expose memory in the window
MPI_Win_create(NULL, 0, 1, MPI_INFO_NULL, MPI_COMM_WORLD, &win);
// Simulate some work based on the rank
sleep(2*rank);
// Register with the master
MPI_Win_lock(MPI_LOCK_EXCLUSIVE, 0, 0, win);
MPI_Put(&one, 1, MPI_INT, 0, rank, 1, MPI_INT, win);
MPI_Win_unlock(0, win);
printf("Worker %d finished RMA\n", rank);
// Release the window
MPI_Win_free(&win);
printf("Worker %d done\n", rank);
}
MPI_Finalize();
return 0;
}
具有 6 个进程的示例输出:
$ mpiexec --mca osc pt2pt -n 6 rma
[ 0.000] Schedule: 0 0 0 0 0 0
[ 1.995] Schedule: 0 1 0 0 0 0
Worker 1 finished RMA
[ 3.989] Schedule: 0 1 1 0 0 0
Worker 2 finished RMA
[ 5.988] Schedule: 0 1 1 1 0 0
Worker 3 finished RMA
[ 7.995] Schedule: 0 1 1 1 1 0
Worker 4 finished RMA
[ 9.988] Schedule: 0 1 1 1 1 1
All workers checked in using RMA
Worker 5 finished RMA
Worker 5 done
Worker 4 done
Worker 2 done
Worker 1 done
Worker 3 done
Master done