数组结构的 MPI-3 共享内存
MPI-3 Shared Memory for Array Struct
我有一个简单的 C++ 结构,它基本上包装了一个标准的 C 数组:
struct MyArray {
T* data;
int length;
// ...
}
其中 T
是数字类型,如 float
或 double
。 length
是数组中元素的个数。通常我的数组非常大(数万到数千万个元素)。
我有一个 MPI 程序,我想在其中公开 MyArray
的两个实例,比如 a_old
和 a_new
,作为通过 MPI 3 共享内存的共享内存对象。上下文是每个 MPI 等级从 a_old
读取。然后,每个 MPI rank 写入 a_new
的某些索引(每个 rank 只写入它自己的一组索引 - 没有重叠)。最后,必须在所有行列上设置 a_old = a_new
。 a_old
和 a_new
大小相同。现在,我正在通过将每个等级的更新值与其他等级同步 (Isend/Irecv
) 来使我的代码正常工作。然而,由于数据访问模式,我没有理由需要承担消息传递的开销,而是可以拥有一个共享内存对象并在 a_old = a_new
之前放置一个屏障。我认为这会给我更好的性能(尽管如果我错了请纠正我)。
我很难找到使用 MPI 3 执行共享内存的完整代码示例。大多数站点仅提供参考文档或不完整的片段。有人可以通过一个简单且 完整 的代码示例来完成我想要实现的事情(通过 MPI 共享内存更新和同步数字数组)吗?我了解创建共享内存通信器和 windows、设置栅栏等的主要概念,但是看到一个将它们放在一起的示例确实有助于我的理解。
另外,我应该提一下,我只会 运行 我的代码在一个节点上,所以我不需要担心跨节点需要共享内存对象的多个副本;对于我的 MPI 进程所在的单个节点,我只需要一份数据副本 运行。尽管如此,在这种情况下,像 OpenMP 这样的其他解决方案对我来说并不可行,因为我有大量的 MPI 代码并且不能为了我想共享的一两个数组而重写所有内容。
这是提供您的描述的代码。在评论中,我对代码的描述很少。通常它会呈现动态 RMA Window 并且必须分配内存并达到 window。
MPI_Win_lock_all(0, win)
来自 Open MPI Documentation 的描述:
启动对 win 中所有进程的 RMA 访问纪元,锁类型为 MPI_LOCK_SHARED。在纪元期间,调用进程可以使用 RMA 操作访问 win 中所有进程上的 window 内存。
在我使用 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;
}
通过 MPI-3 使用共享内存相对简单。
首先,您使用 MPI_Win_allocate_shared
:
分配共享内存 window
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
然后被所有其他进程用来找出共享内存块开始的虚拟地址 space 中的位置。该地址可能因等级而异,因此不应传递绝对指针。
您现在可以简单地分别从 a_old.data
加载和存储到 a_new.data
。由于您案例中的行列在不相交的内存位置集上工作,因此您实际上不需要锁定 window。使用 window 锁来实现例如a_old
或其他需要同步的操作的受保护初始化。您可能还需要明确告诉编译器不要重新排序代码并发出内存栅栏,以便在例如之前完成所有未完成的 load/store 操作。你打电话给 MPI_Barrier()
.
a_old = a_new
代码建议将一个数组复制到另一个数组。相反,您可以简单地交换数据指针并最终交换大小字段。由于只有数组的数据在共享内存块中,因此交换指针是一个本地操作,即不需要同步。假设两个数组长度相等:
T *temp;
temp = a_old.data;
a_old.data = a_new.data;
a_new.data = temp;
您仍然需要一个屏障来确保所有其他进程在继续之前已完成处理。
最后,只需释放 window:
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的理解还很薄弱
我有一个简单的 C++ 结构,它基本上包装了一个标准的 C 数组:
struct MyArray {
T* data;
int length;
// ...
}
其中 T
是数字类型,如 float
或 double
。 length
是数组中元素的个数。通常我的数组非常大(数万到数千万个元素)。
我有一个 MPI 程序,我想在其中公开 MyArray
的两个实例,比如 a_old
和 a_new
,作为通过 MPI 3 共享内存的共享内存对象。上下文是每个 MPI 等级从 a_old
读取。然后,每个 MPI rank 写入 a_new
的某些索引(每个 rank 只写入它自己的一组索引 - 没有重叠)。最后,必须在所有行列上设置 a_old = a_new
。 a_old
和 a_new
大小相同。现在,我正在通过将每个等级的更新值与其他等级同步 (Isend/Irecv
) 来使我的代码正常工作。然而,由于数据访问模式,我没有理由需要承担消息传递的开销,而是可以拥有一个共享内存对象并在 a_old = a_new
之前放置一个屏障。我认为这会给我更好的性能(尽管如果我错了请纠正我)。
我很难找到使用 MPI 3 执行共享内存的完整代码示例。大多数站点仅提供参考文档或不完整的片段。有人可以通过一个简单且 完整 的代码示例来完成我想要实现的事情(通过 MPI 共享内存更新和同步数字数组)吗?我了解创建共享内存通信器和 windows、设置栅栏等的主要概念,但是看到一个将它们放在一起的示例确实有助于我的理解。
另外,我应该提一下,我只会 运行 我的代码在一个节点上,所以我不需要担心跨节点需要共享内存对象的多个副本;对于我的 MPI 进程所在的单个节点,我只需要一份数据副本 运行。尽管如此,在这种情况下,像 OpenMP 这样的其他解决方案对我来说并不可行,因为我有大量的 MPI 代码并且不能为了我想共享的一两个数组而重写所有内容。
这是提供您的描述的代码。在评论中,我对代码的描述很少。通常它会呈现动态 RMA Window 并且必须分配内存并达到 window。
MPI_Win_lock_all(0, win)
来自 Open MPI Documentation 的描述:
启动对 win 中所有进程的 RMA 访问纪元,锁类型为 MPI_LOCK_SHARED。在纪元期间,调用进程可以使用 RMA 操作访问 win 中所有进程上的 window 内存。
在我使用 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;
}
通过 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
然后被所有其他进程用来找出共享内存块开始的虚拟地址 space 中的位置。该地址可能因等级而异,因此不应传递绝对指针。
您现在可以简单地分别从 a_old.data
加载和存储到 a_new.data
。由于您案例中的行列在不相交的内存位置集上工作,因此您实际上不需要锁定 window。使用 window 锁来实现例如a_old
或其他需要同步的操作的受保护初始化。您可能还需要明确告诉编译器不要重新排序代码并发出内存栅栏,以便在例如之前完成所有未完成的 load/store 操作。你打电话给 MPI_Barrier()
.
a_old = a_new
代码建议将一个数组复制到另一个数组。相反,您可以简单地交换数据指针并最终交换大小字段。由于只有数组的数据在共享内存块中,因此交换指针是一个本地操作,即不需要同步。假设两个数组长度相等:
T *temp;
temp = a_old.data;
a_old.data = a_new.data;
a_new.data = temp;
您仍然需要一个屏障来确保所有其他进程在继续之前已完成处理。
最后,只需释放 window:
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的理解还很薄弱