【MPI】组和通信器
引言
在并行计算中,MPI(Message Passing Interface)是一个广泛使用的标准,它提供了一系列的功能来在分布式内存系统中管理进程间的通信。虽然在简单的并行应用程序中,MPI_COMM_WORLD
是最常见的通讯器(communicator),但是当程序规模增大时,使用默认的通讯器进行全局通信变得不再合适。此时,创建多个通讯器以管理不同的进程子集显得尤为重要。本文将详细介绍如何使用 MPI 的 MPI_Comm_split
和 MPI_Comm_create_group
函数来创建和管理自定义通讯器,并深入探讨 MPI 中的组(group)操作及其使用场景。
1. 通讯器的基本概念
1.1 什么是通讯器?
在 MPI 中,通讯器是一个逻辑上分组的进程集合,它用于标识一组可以彼此通信的进程。一个通讯器不仅仅是进程的集合,它还具有一个上下文(context),用于区分同样的操作在不同通讯器中的执行。通过通讯器,MPI 可以确保不同组的进程之间的消息传递不会发生冲突。
例如,默认通讯器 MPI_COMM_WORLD
包含了所有启动 MPI 程序的进程,而在某些应用中,我们可能只关心其中一部分进程之间的通信。
1.2 MPI_Comm_split
的作用
MPI_Comm_split
是 MPI 中最常见的创建新通讯器的方式。它通过基于特定规则(如颜色和秩)将一个现有的通讯器划分为多个子通讯器。
其函数原型如下:
int MPI_Comm_split(
MPI_Comm comm, // 原始通讯器
int color, // 确定进程所属子通讯器的标识
int key, // 子通讯器中的秩(rank)
MPI_Comm *newcomm // 新创建的子通讯器
);
- comm: 原始通讯器,例如
MPI_COMM_WORLD
。 - color: 确定进程属于哪个子通讯器,具有相同
color
值的进程将属于同一子通讯器。如果为MPI_UNDEFINED
,则该进程不会参与任何子通讯器。 - key: 在新通讯器中的秩,通常通过原始秩来设置。
- newcomm: 创建的新通讯器。
1.3 示例:按行划分网格
假设我们有一个 4x4 的进程网格,使用 MPI_Comm_split
来按行划分进程。每一行的进程将被分配到一个新的通讯器。
以下是具体的实现:
#include <iostream>
#include <mpi.h>
// MPI_Comm_split、MPI_Comm_free ----------------------------
int main() {
MPI_Init(NULL, NULL);
int world_size, rank;
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
int color = rank / 4;
MPI_Comm row_comm;
MPI_Comm_split(MPI_COMM_WORLD, color, rank, &row_comm);
int row_rank, row_size;
MPI_Comm_rank(row_comm, &row_rank);
MPI_Comm_size(row_comm, &row_size);
std::cout << "WORLD RANK/SIZE: " << rank << "/" << world_size
<< " \t ROW RANK/SIZE: "<< row_rank << "/" << row_size << std::endl;
// 最后,我们使用 MPI_Comm_free 释放通讯器。 这似乎不是一个重要的步骤,但与在其他任何程序中使用完内存后释放内存一样重要。
// 当不再使用 MPI 对象时,应将其释放,以便以后重用。 MPI 一次可以创建的对象数量有限,如果 MPI 用完了可分配对象,则不释放对象可能会导致运行时错误。
MPI_Comm_free(&row_comm);
MPI_Finalize();
return 0;
}
在这个示例中,我们将进程按照 4x4 网格划分,使用 MPI_Comm_split
为每一行创建一个新的通讯器。输出结果如下:
WORLD RANK/SIZE: 0/16 ROW RANK/SIZE: 0/4
WORLD RANK/SIZE: 1/16 ROW RANK/SIZE: 1/4
WORLD RANK/SIZE: 2/16 ROW RANK/SIZE: 2/4
WORLD RANK/SIZE: 3/16 ROW RANK/SIZE: 3/4
WORLD RANK/SIZE: 4/16 ROW RANK/SIZE: 0/4
...
可以看到,每一行的进程具有相同的 color
值,因此它们被分配到同一个新的通讯器 row_comm
中。
2. 更高级的通讯器创建方法
除了 MPI_Comm_split
,MPI 还提供了一些更灵活的函数来创建和管理通讯器。例如,MPI_Comm_dup
用于创建一个现有通讯器的副本,而 MPI_Comm_create_group
则用于通过现有的进程组来创建新的通讯器。
2.1 MPI_Comm_dup
示例
MPI_Comm_dup(MPI_COMM_WORLD, &new_comm);
MPI_Comm_dup
函数用于创建一个现有通讯器的副本,与原通讯器具有相同的组和上下文。它的主要用途是在多个库或模块之间避免对同一通讯器的冲突。
2.2 MPI_Comm_create
示例
#include <iostream>
#include <mpi.h>
#include <cstdio>
// MPI_Comm_group、MPI_Group_incl、MPI_Comm_create、MPI_Group_free---------------------------------------
int main() {
MPI_Init(NULL, NULL);
int world_size, rank;
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Group world_group, new_group;
MPI_Comm new_comm;
// 获取原组
MPI_Comm_group(MPI_COMM_WORLD, &world_group);
// 从原组创建子组
int ranks[3] = {0, 1,3};
// MPI_Group_incl:从现有组中选择一部分进程创建一个新的组。
// int MPI_Group_incl(MPI_Group group, int n, const int ranks[], MPI_Group *newgroup);
MPI_Group_incl(world_group, 3, ranks, &new_group);
// 根据新组创建新通讯器
MPI_Comm_create(MPI_COMM_WORLD, new_group, &new_comm);
// 如果某个进程未被选中加入新通讯器,则该函数会返回 MPI_COMM_NULL,表示该进程不属于新创建的通讯器。
if (new_comm != MPI_COMM_NULL) {
int new_rank, new_size;
MPI_Comm_rank(new_comm, &new_rank);
MPI_Comm_size(new_comm, &new_size);
printf("World Rank: %d, New Rank: %d, New Size: %d\n", rank,
new_rank, new_size);
MPI_Comm_free(&new_comm); // 释放新通讯器
}
// MPI_Group_free 用于释放一个由 MPI 创建的组 (MPI_Group) 对象,并将其设置为 MPI_GROUP_NULL,防止资源泄漏
MPI_Group_free(&new_group);
MPI_Group_free(&world_group);
MPI_Finalize();
return 0;
}
3. 组的操作和使用
3.1 组的概念
MPI 中的 组(Group) 是一个进程集合,类似于集合理论中的概念。每个组都包含了一些进程的秩,并且通过 MPI_Comm_group
可以从现有通讯器中获取到进程组。
// 从通讯器中获取对应的组。
MPI_Group group;
MPI_Comm_group(MPI_COMM_WORLD, &group);
// 获取当前进程在组内的 rank(组内的进程编号)
int rank;
MPI_Group_rank(group, &rank);
//获取组中进程的数量。
int size;
MPI_Group_size(group, &size);
3.2 组操作:并集和交集
MPI 提供了多种操作来对组进行处理,其中最常见的是并集(union)和交集(intersection)操作。
- 并集(Union):两个组的并集是包含所有元素的集合。
- 交集(Intersection):两个组的交集仅包含同时出现在两个组中的元素。
例如,使用 MPI_Group_union
来获取两个组的并集:
MPI_Group_union(group1, group2, &newgroup);
4. 示例:使用组来创建通讯器
假设我们想要从 MPI_COMM_WORLD
中选择特定的进程(比如所有素数秩的进程),并根据这些进程创建一个新的通讯器:
#include <iostream>
#include <mpi.h>
#include <cstdio>
// MPI_Comm_group、MPI_Group_rank、MPI_Group_size、MPI_Group_union、MPI_Group_intersection、MPI_Comm_create-----------------
int main() {
MPI_Init(NULL,NULL);
int world_rank, world_size;
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
// 获取 MPI_COMM_WORLD 的组
MPI_Group world_group;
MPI_Comm_group(MPI_COMM_WORLD, &world_group);
// 定义两个子组
int group1_ranks[] = {0, 1, 2}; // 子组1包含 rank 0, 1, 2
int group2_ranks[] = {1, 2, 3}; // 子组2包含 rank 1, 2, 3
// 创建子组1和子组2
MPI_Group group1, group2;
MPI_Group_incl(world_group, 3, group1_ranks, &group1);
MPI_Group_incl(world_group, 3, group2_ranks, &group2);
// 打印组内 rank 和大小
int group1_rank, group1_size;
MPI_Group_rank(group1, &group1_rank);
MPI_Group_size(group1, &group1_size);
if (group1_rank != MPI_UNDEFINED) {
printf("Rank %d in MPI_COMM_WORLD is Rank %d in Group1 of size %d\n",
world_rank, group1_rank, group1_size);
}
// 并集操作:创建一个包含子组1和子组2的联合组
MPI_Group union_group;
MPI_Group_union(group1, group2, &union_group);
// 交集操作:创建一个包含子组1和子组2交集的组
MPI_Group intersection_group;
MPI_Group_intersection(group1, group2, &intersection_group);
// 基于并集组创建一个新的通讯器
MPI_Comm union_comm;
MPI_Comm_create(MPI_COMM_WORLD, union_group, &union_comm);
if (union_comm != MPI_COMM_NULL) {
int union_rank, union_size;
MPI_Comm_rank(union_comm, &union_rank);
MPI_Comm_size(union_comm, &union_size);
printf(
"Rank %d in MPI_COMM_WORLD is Rank %d in Union Communicator of size "
"%d\n",
world_rank, union_rank, union_size);
MPI_Comm_free(&union_comm);
}
// 基于交集组创建一个新的通讯器
MPI_Comm intersection_comm;
MPI_Comm_create(MPI_COMM_WORLD, intersection_group,
&intersection_comm);
if (intersection_comm != MPI_COMM_NULL) {
int intersection_rank, intersection_size;
MPI_Comm_rank(intersection_comm, &intersection_rank);
MPI_Comm_size(intersection_comm, &intersection_size);
printf(
"Rank %d in MPI_COMM_WORLD is Rank %d in Intersection Communicator of "
"size %d\n",
world_rank, intersection_rank, intersection_size);
MPI_Comm_free(&intersection_comm);
}
// 释放所有组
MPI_Group_free(&group1);
MPI_Group_free(&group2);
MPI_Group_free(&union_group);
MPI_Group_free(&intersection_group);
MPI_Group_free(&world_group);
MPI_Finalize();
return 0;
}
5. 小结
在大规模并行应用中,使用多个通讯器来管理不同进程子集的通信是提高效率的关键。通过 MPI_Comm_split
和 MPI_Comm_create
等函数,开发者可以灵活地将进程分组并为每个组创建独立的通讯器,从而减少通信冲突并提高并行效率。