【问题标题】:MPI-3 Shared Memory for Array Struct数组结构的 MPI-3 共享内存
【发布时间】:2016-11-07 21:35:07
【问题描述】:

我有一个简单的 C++ 结构,它基本上包装了一个标准 C 数组:

struct MyArray {
    T* data;
    int length;
    // ...
}

其中T 是数字类型,例如floatdoublelength 是数组中元素的数量。通常我的数组非常大(数万到数千万个元素)。

我有一个 MPI 程序,我想通过 MPI 3 共享内存公开两个 MyArray 实例,例如 a_olda_new,作为共享内存对象。上下文是每个 MPI 等级都从 a_old 读取。然后,每个 MPI 等级写入 a_new 的某些索引(每个等级只写入自己的索引集 - 没有重叠)。最后,必须在所有等级上设置a_old = a_newa_olda_new 大小相同。现在,我正在通过将 (Isend/Irecv) 每个等级的更新值与其他等级同步来使我的代码工作。但是,由于数据访问模式,我没有理由需要承担消息传递的开销,而是可以拥有一个共享内存对象并在a_old = a_new 之前放置一个屏障。我认为这会给我带来更好的表现(尽管如果我错了请纠正我)。

我很难找到使用 MPI 3 进行共享内存的完整代码示例。大多数站点仅提供参考文档或不完整的 sn-ps。有人可以引导我完成一个简单且完整的代码示例,它可以完成我想要实现的那种事情(通过 MPI 共享内存更新和同步数字数组)?我了解创建共享内存通信器和窗口、设置栅栏等的主要概念,但是看到一个将所有内容组合在一起的示例确实有助于我的理解。

另外,我应该提到我只会在一个节点上运行我的代码,所以我不需要担心需要跨节点的共享内存对象的多个副本;对于运行 MPI 进程的单个节点,我只需要一份数据副本。尽管如此,在这种情况下,像 OpenMP 这样的其他解决方案对我来说是不可行的,因为我有大量的 MPI 代码并且不能为了我想共享的一两个数组而重写所有内容。

【问题讨论】:

    标签: c++ mpi shared-memory openmpi


    【解决方案1】:

    在 MPI-3 中使用共享内存相对简单。

    首先,您使用MPI_Win_allocate_shared分配共享内存窗口:

    MPI_Win win;
    MPI_Aint size;
    void *baseptr;
    
    if (rank == 0)
    {
       size = 2 * ARRAY_LEN * sizeof(T);
       MPI_Win_allocate_shared(size, sizeof(T), MPI_INFO_NULL,
                               MPI_COMM_WORLD, &baseptr, &win);
    }
    else
    {
       int disp_unit;
       MPI_Win_allocate_shared(0, sizeof(T), MPI_INFO_NULL,
                               MPI_COMM_WORLD, &baseptr, &win);
       MPI_Win_shared_query(win, 0, &size, &disp_unit, &baseptr);
    }
    a_old.data = baseptr;
    a_old.length = ARRAY_LEN;
    a_new.data = a_old.data + ARRAY_LEN;
    a_new.length = ARRAY_LEN;
    

    这里,只有 rank 0 分配内存。哪个进程分配它并不重要,因为它是共享的。甚至可以让每个进程分配一部分内存,但由于默认情况下分配是连续的,因此两种方法是等效的。然后所有其他进程使用MPI_Win_shared_query 来找出共享内存块开始的虚拟地址空间中的位置。该地址可能因等级而异,因此不应传递绝对指针。

    您现在可以简单地分别从a_old.data 加载和存储到a_new.data。由于您的案例中的等级适用于不相交的内存位置集,因此您实际上并不需要锁定窗口。使用窗口锁来实现例如a_old 的受保护初始化或其他需要同步的操作。您可能还需要明确告诉编译器不要重新排序代码并发出内存围栏,以便在例如之前完成所有未完成的加载/存储操作。你打电话给MPI_Barrier()

    a_old = a_new 代码建议将一个数组复制到另一个数组。相反,您可以简单地交换数据指针并最终交换大小字段。由于只有数组的数据在共享内存块中,因此交换指针是本地操作,即不需要同步。假设两个数组长度相等:

    T *temp;
    temp = a_old.data;
    a_old.data = a_new.data;
    a_new.data = temp;
    

    您仍然需要一个屏障来确保所有其他进程都已完成处理,然后再继续。

    最后,简单地释放窗口:

    MPI_Win_free(&win);
    

    一个完整的例子(C)如下:

    #include <stdio.h>
    #include <mpi.h>
    
    #define ARRAY_LEN 1000
    
    int main (void)
    {
       MPI_Init(NULL, NULL);
    
       int rank, nproc;
       MPI_Comm_rank(MPI_COMM_WORLD, &rank);
       MPI_Comm_size(MPI_COMM_WORLD, &nproc);
    
       MPI_Win win;
       MPI_Aint size;
       void *baseptr;
    
       if (rank == 0)
       {
          size = ARRAY_LEN * sizeof(float);
          MPI_Win_allocate_shared(size, sizeof(int), MPI_INFO_NULL,
                                  MPI_COMM_WORLD, &baseptr, &win);
       }
       else
       {
          int disp_unit;
          MPI_Win_allocate_shared(0, sizeof(int), MPI_INFO_NULL,
                                  MPI_COMM_WORLD, &baseptr, &win);
          MPI_Win_shared_query(win, 0, &size, &disp_unit, &baseptr);
       }
    
       printf("Rank %d, baseptr = %p\n", rank, baseptr);
    
       int *arr = baseptr;
       for (int i = rank; i < ARRAY_LEN; i += nproc)
         arr[i] = rank;
    
       MPI_Barrier(MPI_COMM_WORLD);
    
       if (rank == 0)
       {
          for (int i = 0; i < 10; i++)
             printf("%4d", arr[i]);
          printf("\n");
       }
    
       MPI_Win_free(&win);
    
       MPI_Finalize();
       return 0;
    }
    

    免责声明:对此持保留态度。我对 MPI 的 RMA 的理解还是比较薄弱的。

    【讨论】:

    • 目前这对我很有帮助,处理类似的情况。你熟悉类似代码的 fortran 实现吗?
    • @Rain 它在 Fortran 中的工作方式相同。唯一重要的区别是您需要声明一个 Fortran 指针并将其与 MPI_Win_allocate_shared 返回的基指针地址相关联,使用类似于 Fortran 标准 iso_c_binding 模块中的 c_f_pointer() 的东西。
    • 谢谢!我仍然对 fortran 实现有一些疑问,即如何存储和索引用户定义的数据类型(而不是数组类型)。请参阅问题:stackoverflow.com/questions/68369535/…。在 C 中,我想出了一种使用指针算术的方法。在fortran中我想知道是否存在类似的代码?
    【解决方案2】:

    这是一个提供您描述的代码。在 cmets 中,我对代码进行了一些描述。通常,它呈现一个动态 RMA 窗口,并且必须将内存分配给该窗口。

    MPI_Win_lock_all(0, win)Open MPI Documentation的描述:

    对 win 中的所有进程启动 RMA 访问纪元,锁定类型为 MPI_LOCK_SHARED。在epoch期间,调用进程可以通过RMA操作访问win中所有进程的窗口内存。

    在我使用MPI_INFO_NULL 的地方,您可以使用 MPI_Info 对象向 MPI 提供附加信息,但这取决于您的内存访问模式。

    #include <stdio.h>
    #include <stdlib.h>
    #include <mpi.h>
    
    typedef struct MyArray {
        double* data;
        int length;
    }MyArray;
    
    #define ARRAY_SIZE 10
    
    int main(int argc, char *argv[]) {
        int rank, worldSize, i;
        MPI_Win win;
        MPI_Aint disp;
        MPI_Aint *allProcessDisp;
        MPI_Request *requestArray;
    
        MyArray myArray;
    
        MPI_Init(&argc, &argv);
        MPI_Comm_rank(MPI_COMM_WORLD, &rank);
        MPI_Comm_size(MPI_COMM_WORLD, &worldSize);
    
        MPI_Win_create_dynamic(MPI_INFO_NULL, MPI_COMM_WORLD, &win);
    
        allProcessDisp = malloc(sizeof(MPI_Aint) * worldSize);
    
        requestArray = malloc(sizeof(MPI_Request) * worldSize);
        for (i = 0; i < worldSize; i++) 
            requestArray[i] = MPI_REQUEST_NULL;
    
        myArray.data = malloc(sizeof(double) * ARRAY_SIZE);
        myArray.length = ARRAY_SIZE;
    
        //Allocating memory for each process share window space 
        MPI_Alloc_mem(sizeof(double) * ARRAY_SIZE, MPI_INFO_NULL, &myArray.data);
        for (i = 0; i < ARRAY_SIZE; i++)
            myArray.data[i] = rank;
    
        //attach the allocating memory to each process share window space 
        MPI_Win_attach(win, myArray.data, sizeof(double) * ARRAY_SIZE);
    
        MPI_Get_address(myArray.data, &disp);
    
        if (rank == 0) {
            allProcessDisp[0] = disp;
            //Collect all displacements
            for (i = 1; i < worldSize; i++) {
                MPI_Irecv(&allProcessDisp[i], 1, MPI_AINT, i, 0, MPI_COMM_WORLD, &requestArray[i]);
            }
            MPI_Waitall(worldSize, requestArray, MPI_STATUS_IGNORE);
            MPI_Bcast(allProcessDisp, worldSize, MPI_AINT, 0, MPI_COMM_WORLD);
        }
        else {
            //send displacement 
            MPI_Send(&disp, 1, MPI_AINT, 0, 0, MPI_COMM_WORLD);
            MPI_Bcast(allProcessDisp, worldSize, MPI_AINT, 0, MPI_COMM_WORLD);
        }
    
        // here you can do RMA operations 
        // Each time you need an RMA operation you start with 
        double otherRankData = -1.0;
        int otherRank = 1;
        if (rank == 0) {
            MPI_Win_lock_all(0, win);
            MPI_Get(&otherRankData, 1, MPI_DOUBLE, otherRank, allProcessDisp[otherRank], 1, MPI_DOUBLE, win);
            // and end with 
            MPI_Win_unlock_all(win);
            printf("Rank 0 : Got %.2f from %d\n", otherRankData, otherRank);
        }
    
        if (rank == 1) {
            MPI_Win_lock_all(0, win);
            MPI_Put(myArray.data, ARRAY_SIZE, MPI_DOUBLE, 0, allProcessDisp[0], ARRAY_SIZE, MPI_DOUBLE, win);
            // and end with 
            MPI_Win_unlock_all(win);
        }
    
        printf("Rank %d: ", rank);
        for (i = 0; i < ARRAY_SIZE; i++)
            printf("%.2f ", myArray.data[i]);
        printf("\n");
    
        //set rank 0 array
        if (rank == 0) {
            for (i = 0; i < ARRAY_SIZE; i++)
                myArray.data[i] = -1.0;
    
            printf("Rank %d: ", rank);
            for (i = 0; i < ARRAY_SIZE; i++)
                printf("%.2f ", myArray.data[i]);
            printf("\n");
        }
    
        free(allProcessDisp);
        free(requestArray);
        free(myArray.data);
    
        MPI_Win_detach(win, myArray.data);
        MPI_Win_free(&win);
        MPI_Finalize();
    
        return 0;
    }
    

    【讨论】:

    • 从 Hristo 开始就给了他答案,但这也是一个很好的例子,谢谢!
    • 没关系,@Hristo 的回答总是和上面那个一样好。
    • 你为什么不在 allProcessDisp 上使用 Allgather?
    猜你喜欢
    • 2013-01-11
    • 2020-06-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多