MPI与C:被动RMA同步



由于到目前为止我还没有找到问题的答案,而且我正处于为这个问题疯狂的边缘,所以我只是问了一个折磨我心灵的问题;-)

我正在对我已经编程的节点消除算法进行并行化。目标环境是一个集群。

在我的并行程序中,我区分主进程(在我的情况下为0级)和工作从进程(除0以外的每个级别)。我的想法是,主人会跟踪哪些奴隶可用,然后派他们去工作。因此,出于其他一些原因,我试图建立一个基于被动RMA的锁定-解锁序列的工作流。我使用一个名为schedule的整数数组,其中数组中表示秩的每个位置对于工作进程为0,对于可用进程为1(因此,如果schedule[1]=1,则有一个可用于工作)。如果一个进程完成了它的工作,它会在主进程上的数组中放入1,以表示它的可用性。我尝试的代码如下:

MPI_Win_lock(MPI_LOCK_EXCLUSIVE,0,0,win); // a exclusive window is locked on process 0
printf("Process %d:t exclusive lock on process 0 startedn",myrank);
MPI_Put(&schedule[myrank],1,MPI_INT,0,0,1,MPI_INT,win); // the line myrank of schedule is put into process 0
printf("Process %d:t put operation calledn",myrank);
MPI_Win_unlock(0,win); // the window is unlocked

它工作得很好,尤其是当主进程与锁末端的屏障同步时,因为主进程的输出是在put操作之后进行的。

作为下一步,我试图让master定期检查是否有可用的奴隶。因此,我创建了一个while循环来重复,直到每个过程都表明它的可用性(我重复一遍,这是程序在教我原理,我知道实现仍然没有达到我想要的效果)。循环是在一个基本变体中,只是打印我的数组时间表,然后在函数fnz中检查是否有除master之外的其他工作过程:

while(j!=1){
printf("Process %d:t following schedule evaluated:n",myrank);
for(i=0;i<size;i++)printf("%dt",schedule[i]);//print the schedule
printf("n");
j=fnz(schedule);
}

然后这个概念就破灭了。在反转过程并通过主进程从从属进程获取所需信息,而不是将其从从属进程放入主进程的put之后,我发现我的主要问题是获取锁:解锁命令没有成功,因为在put的情况下根本没有授予锁,而在get的情况下,只有当从属进程完成工作并在障碍物中等待。在我看来,我的想法肯定有一个严重的错误。只有当目标进程处于同步整个通信器的屏障中时,才能实现锁定,这不可能是被动RMA的想法。然后我就可以使用标准的发送/接收操作了。我想实现的是,流程0一直在委派工作,并能够通过对奴隶的RMA来确定它可以委派给谁。请有人帮我解释一下如何中断进程0以允许其他进程获得锁定?

提前谢谢!

更新:我不确定你是否使用过锁,只是想强调一下,我完全能够获得远程内存窗口的更新副本。如果我从奴隶那里获得可用性,只有当奴隶在屏障中等待时,才会授予锁。所以我要做的是,进程0执行锁定-解锁,而进程1和2正在模拟工作,因此进程2的占用时间明显比进程1长。因此,我期望进程0打印一个时间表(0,1,0),因为进程0根本没有被问到它是否在工作,进程1已经完成了工作,进程2仍然在工作。在下一步中,当进程2准备好时,我期望输出(0,1,1),因为从进程都准备好了进行新的工作。我得到的是,从进程只有在屏障中等待时才授予进程0的锁,所以我得到的第一个也是唯一一个输出是我期望的最后一个输出,这向我表明,当每个进程完成其工作时,锁是首先授予的。因此,如果有人能告诉我目标流程何时可以授予锁定,而不是试图混淆我对被动RMA的了解,我将非常感谢

首先,被动RMA机制不会以某种方式神奇地插入远程进程的内存,因为没有多少MPI传输具有真正的RDMA功能,即使是那些具有RDMA功能的传输(例如InfiniBand)也需要目标的大量非被动参与才能进行被动RMA操作。这在MPI标准中进行了解释,但以通过RMA窗口公开的内存的公共和私有副本的非常抽象的形式进行了解释。

使用MPI-2实现可工作和可移植的被动RMA需要几个步骤。

步骤1:目标进程中的窗口分配

出于可移植性和性能原因,应该使用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);

步骤2:目标的内存同步

MPI标准禁止同时访问窗口中的同一位置(MPI-2.2规范中的§11.3):

在窗如果一个位置是通过放置或累积操作更新的,那么在目标完成更新操作之前,加载或其他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操作的本地部分的进程。MPI提供可移植RMA,即使在使用不支持RDMA的传输(如TCP/IP或共享内存)时也是如此,这需要在目标上进行大量主动工作(称为进展)才能支持"被动"RMA。一些库提供了可以在后台进行操作的异步进展线程,例如,当配置有--enable-opal-multi-threads(默认情况下禁用)时打开MPI,但依赖于这种行为会导致程序不可移植。这就是为什么MPI标准允许put操作的以下宽松语义(§11.7,第365页):

6。最晚当窗口所有者在进程内存中的专用副本上执行对MPI_WIN_WAIT、MPI_WIN_FENCE或MPI_WIN_LOCK的后续调用时,通过对公用窗口副本的put或accumulate调用进行的更新会在该副本中可见。

如果put或accumulate访问与锁同步,那么一旦更新过程执行MPI_WIN_UNLOCK,公共窗口副本的更新就完成了另一方面,进程内存中私有副本的更新可能会延迟,直到目标进程在该窗口上执行同步调用(6)。因此,对进程内存的更新总是可以延迟,直到进程执行合适的同步调用如果使用围栏或启动后完全等待同步,则对公共窗口副本的更新也可以延迟,直到窗口所有者执行同步调用。只有当使用了锁同步时,才有必要更新公共窗口副本,即使窗口所有者没有执行任何相关的同步调用。

这也在标准的同一部分的示例11.12中进行了说明(第367页)。事实上,如果master代码中的锁定/解锁调用被注释掉,Open MPI和Intel MPI都不会更新schedule[]的值。MPI标准进一步建议(§11.7,第366页):

给用户的建议用户可以按照以下规则编写正确的程序:

锁定:如果窗口的更新可能发生冲突,则它们将受到独占锁定的保护。非冲突访问(如只读访问或累积访问)是受共享锁保护,用于本地访问和RMA访问

步骤3:在原点向MPI_PUT提供正确的参数

CCD_ 6将把所有内容传送到目标窗口的第一个元素中。给定目标处的窗口是用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,则正确的输入为:

MPI_Put(&one, 1, MPI_INT, 0, rank * sizeof(int), 1, MPI_INT, win);

步骤4:处理实施细节

上面的详细程序使用"英特尔MPI"开箱即用。使用Open MPI时,必须特别小心。该库是围绕一组框架和实现模块构建的。osc(单边通信)框架有两种实现方式——rdmapt2pt。默认值(在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 RMAn");
// Release the window
MPI_Win_free(&win);
// Free the allocated memory
MPI_Free_mem(schedule);
free(oldschedule);
printf("Master donen");
}
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 RMAn", rank);
// Release the window
MPI_Win_free(&win);
printf("Worker %d donen", 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

如果我使用较新版本的Open MPI库,Hristo Lliev的答案会非常有效。

然而,在我们目前使用的集群上,这是不可能的,对于旧版本,最终解锁调用存在死锁行为,正如Hhristo所描述的那样。添加选项--mca osc pt2pt确实在某种意义上解决了死锁,但MPI_Win_unlock调用似乎仍然没有完成,直到拥有访问变量的进程自己锁定/解锁窗口。当你的工作完成时间非常不同时,这不是很有用。

因此,从实用的角度来看,尽管严格来说,我不谈被动RMA同步的话题(对此我深表歉意),但我想指出一种变通方法,即为那些坚持使用旧版本的Open MPI库的人使用外部文件,这样他们就不必像我那样浪费太多时间:

您基本上创建了一个外部文件,其中包含关于哪个(从)进程执行哪个作业的信息,而不是一个内部数组。这样,你甚至不必有一个只用于奴隶记账的主流程:它还可以执行一项工作。不管怎样,每个过程都可以在这个文件中查看下一步要做的工作,并可能确定一切都完成了。

现在重要的一点是,多个进程不会同时访问此信息文件,因为这可能会导致工作重复或更糟。MPI中窗口的锁定和解锁在这里最容易通过使用锁定文件来模仿:该文件是由当前访问信息文件的进程创建的。其他进程必须等待当前进程完成,方法是稍微延迟一段时间检查锁定文件是否仍然存在。

完整的信息可以在这里找到。

最新更新