【问题标题】:MPI with C: Passive RMA synchronizationMPI 与 C:被动 RMA 同步
【发布时间】:2013-09-15 06:18:07
【问题描述】:

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

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

在我的并行程序中,我区分了主进程(在我的情况下为 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 定期检查是否有可用的 slave。因此我创建了一个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 的情况下,根本不授予锁,而在 get 的情况下,仅当从属进程完成其工作并在屏障中等待时才授予锁。在我看来,我的想法一定有一个严重的错误。只有当目标进程处于同步整个通信器的屏障中时才能实现锁定,这不可能是被动 RMA 的想法。然后我就可以进行标准的发送/接收操作了。我想要实现的是,进程 0 一直在委派工作,并且能够通过从属设备的 RMA 确定它可以委派给谁。 有人可以帮我解释一下如何让进程 0 中断以允许其他进程获得锁吗?

提前谢谢你!

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

【问题讨论】:

  • 你从MPI_Win_lock收到什么错误代码?
  • 另请注意,主进程中schedule[] 的值不需要反映远程MPI_Put 调用设置的值,因为您缺少在等级0 中锁定/解锁窗口的调用。§ 11.7(MPI 2.2)定义了 RMA 操作的语义,它允许推迟远程更改的可见性,直到本地进程锁定窗口。
  • 我没有得到错误代码,锁不是由相应的进程给的,所以我创建了一个死锁。此外,等级 0 不需要调用锁定/解锁,因为它是被动 RMA 并且仅由调用进程同步,该调用进程将是从属之一。这就是栅栏同步的区别。因此,在 §11.7(并在 §11.4.3 中充分解释)中声明“相同”而不是“匹配”调用是实现目标同步的点。
  • §11.7, p. 365, ll。 1-4 - “当对 MPI_WIN_WAIT、MPI_WIN_FENCE 或 MPI_WIN_LOCK 的后续调用由窗口所有者。” 另见示例 11.12。
  • 无论如何,鉴于您没有提供有关如何分配 0 级窗口、具体 MPI 实现和正在使用的网络互连的详细信息,因此只能推测问题的根源。跨度>

标签: c locking mpi unlock


【解决方案1】:

首先,被动 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);

第 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 操作的本地部分的进度。 MPI 提供 portable 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 或 accumage 调用的更新在进程内存中的私有副本中变得可见。

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

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

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

...

锁:如果可能发生冲突,对窗口的更新会受到排他锁的保护。非冲突访问(例如只读访问或累积访问)是 受共享锁保护,本地访问和 RMA 访问都

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

MPI_Put(&amp;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 <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

【讨论】:

  • pt2pt模块从何而来? Open-MPI github repo 显然在 osc 模块中没有“pt2pt”。 github.com/open-mpi/ompi/tree/master/ompi/mca/osc
  • @Praxeolitic,它是从编写答案时开始的生产版本的一部分 - 1.6:svn.open-mpi.org/trac/ompi/browser/tags/v1.6-series/v1.6.5/ompi/…。也存在于早期版本中。自 1.8 以来的版本丢弃 pt2pt.
  • @HristoIliev:“当调用 MPI_WIN_(UN)LOCK 时,[..] 不会在目标端进行 RMA 操作,这会导致类似死锁的行为 [..]” - 你是知道为什么会这样吗?我也观察到了这种行为,虽然我的代码不依赖它,但这种行为会显着降低性能。
  • @cschwan:我不知道。这是特定于实现的东西。可能rdma 模块在 1.6 中是不完整的。我还没有测试过最新版本。
【解决方案2】:

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

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

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

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

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

完整信息可以在here找到。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-09-29
    • 1970-01-01
    • 2022-01-01
    • 1970-01-01
    • 2010-12-03
    • 2012-06-20
    • 2013-02-19
    • 2016-09-11
    相关资源
    最近更新 更多