带有C的MPI:被动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 started\n",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 called\n",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("%d\t",schedule[i]);//print the schedule printf("\n"); j=fnz(schedule); } 

然后这个概念爆炸了。 在反转过程并获取所需的信息后,由主设备从奴隶获取而不是将其从从设备放入主设备我发现我的主要问题是获取锁:解锁命令不成功,因为在put的情况下,根本没有授予锁定,并且在获取锁定的情况下,仅当从属进程完成其工作并在屏障中等待时才授予锁定。 在我看来,我的思想中必定存在严重错误。 被动RMA的概念不能仅在目标进程处于同步整个通信器的障碍时才能实现锁定。 然后我可以继续执行标准的Send / Recv操作。 我想要实现的是,流程0始终在委派工作中工作,并且能够由奴隶的RMA识别它可以委派给谁。 可以请有人帮助我并解释我如何在进程0上rest以允许其他进程获取锁定?

先感谢您!

更新: 我不确定你是否曾经使用过锁,只是想强调我完全能够获得远程内存窗口的更新副本。 如果我从奴隶那里获得可用性,则只有当奴隶在障碍物中等待时才会授予锁定。 所以我需要工作的是,进程0执行lock-get-unlock,而进程1和2正在模拟工作,这样进程2的占用时间明显长于1。 我期望的结果是,进程0打印一个时间表(0,1,0),因为进程0根本不会被询问它是否正在工作,进程1是否完成了工作,进程2仍在工作。 在下一步中,当进程2准备就绪时,我期望输出(0,1,1),因为从站都准备好进行新的工作。 我得到的是奴隶只有当他们在障碍中等待时才给予进程0的锁定,所以我得到的第一个也是唯一的输出是我期望的最后一个,告诉我为每个人授予了锁定首先,当它完成它的工作。 所以如果有人可以告诉我什么时候锁定可以被目标进程授予,而不是试图混淆我对被动 RMA的了解,我将非常感激

首先,被动RMA机制并没有以某种方式神奇地进入远程进程的内存,因为没有多少MPI传输具有真正的RDMAfunction,甚至那些(例如InfiniBand)需要大量的非被动参与。目标是为了允许被动RMA操作发生。 这在MPI标准中进行了解释,但是以通过RMA窗口公开的内存的公共和私有副本的抽象forms进行了解释。

使用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节):

在窗口中对同一个内存位置进行并发冲突访问是错误的。 如果通过put或accumulate操作更新位置,则在目标完成更新操作之前,加载或其他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。 一些库提供可以在后台进行操作的异步进程线程,例如,配置了--enable-opal-multi-threads (默认情况下禁用)时打开MPI,但依赖此类行为会导致非可移植程序。 这就是为什么MPI标准允许put操作的以下宽松语义(§11.7,p.365):

6。 当窗口所有者在该窗口上执行对MPI_WIN_WAIT,MPI_WIN_FENCE或MPI_WIN_LOCK的后续调用时,最后通过对公共窗口副本的put或accumulate调用的更新在进程存储器中的私有副本中变得可见。

如果put或accumulate访问与锁同步,则更新过程执行MPI_WIN_UNLOCK后,公共窗口副本的更新即完成。 另一方面,可以延迟进程存储器中的私有副本的更新,直到目标进程在该窗口上执行同步调用(6)。 因此,对进程存储器的更新总是可以延迟,直到进程执行合适的同步调用。 如果使用fences或post-start-complete-wait同步,也可以延迟对公共窗口副本的更新,直到窗口所有者执行同步调用。 仅当使用锁同步时,即使窗口所有者未执行任何相关的同步调用,也必须更新公共窗口副本。

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

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

...

lock:如果窗口的更新可能会发生冲突,则会对它们进行更新保护。 非冲突访问(例如只读访问或累积访问)受共享锁保护, 包括本地访问和RMA访问

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

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 (单侧通信)框架有两种实现方式 - rdmapt2pt 。 默认(在Open MPI 1.6.x中,可能更早)是rdma ,由于某种原因,当MPI_WIN_(UN)LOCK时,它不会在目标端进行RMA操作,这会导致类似死锁的行为,除非另一个通信调用是(你的情况下MPI_BARRIER )。 另一方面, pt2pt模块按预期进行所有操作。 因此,对于Open MPI,必须像下面一样启动程序,以便专门选择pt2pt组件:

 $ mpiexec --mca osc pt2pt ... 

完整工作的C99示例代码如下:

 #include  #include  #include  #include  // 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 

如果我使用较新版本的Open-MPI库,Hristo Lliev的答案将完美无缺。

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

因此,从实用的角度来看,虽然严格地说是离开了被动RMA同步的主题(我为此道歉),但我想指出一种解决方法,它利用外部文件为那些坚持使用旧版本的人Open-MPI库让他们不必像我那样耗费太多时间:

您基本上创建一个外部文件,其中包含有关哪个(从属)进程执行哪个作业而不是内部数组的信息。 这样,您甚至不需要专门用于奴隶簿记的主进程:它也可以执行工作。 无论如何,每个进程都可以在这个文件中查看接下来要完成的工作,并可能确定一切都已完成。

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

完整信息可在此处找到。