消息传递接口(MPI)全面解析
1. MPI 概述
MPI(Message Passing Interface)是集群中用于消息传递的软件库,是高性能计算领域的事实上的标准。它于 1994 年最初为 Fortran 和 C 语言开发,从其函数接口仍能看出这一发展脉络,数据类型的处理较为底层。不过,MPI 提供了一套相当完整的集体通信操作,这是大多数其他替代方案所欠缺的。它是性能、可移植性和通用性之间精心权衡的标准化成果。
MPI 有多个版本,MPI - 1 涵盖了许多常用功能;1997 年的 MPI - 2 增加了高性能 I/O 和单边通信功能,能实现部分共享内存程序的功能,但会有一定性能开销;2012 年的 MPI - 3 则增加了非阻塞集体操作。
2. MPI 程序基础:“Hello World”示例
一个 MPI 程序本质上是一个普通的“顺序”(Fortran、C 或 C++)程序,它包含
mpi.h
头文件并调用其中声明的 MPI 函数。这类程序遵循单程序多数据(SPMD)的并行编程方法,会在并行机器的所有处理元素(PE)上并行执行。
以下是一个简单的 MPI “Hello World”示例代码:
#include <iostream>
#include <mpi.h>
int main(int argc, char** argv)
{
int p, i;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &p);
MPI_Comm_rank(MPI_COMM_WORLD, &i);
std::cout << "PE " << i << " out of " << p << std::endl;
MPI_Finalize();
}
在这个示例中,
MPI_Init
函数用于初始化 MPI 库,同时初始化全局变量
MPI_COMM_WORLD
,该变量存储一个通信器对象,描述了程序可用的 PE 集合。
MPI_Comm_size
和
MPI_Comm_rank
函数分别用于获取 PE 的总数
p
和当前 PE 的编号
i
。最后,
MPI_Finalize
函数用于结束 MPI 环境。
需要注意的是,程序的整体输出并不唯一,不同 PE 的输出字符或行可能会混乱,也可能每个 PE 的输出被写入一个单独的文件。
在 MPI 术语中,PE 被称为进程,一个 MPI 进程通常对应主机操作系统中的一个进程。每个节点运行的进程数量取决于 MPI 程序的启动方式,有以下几种常见选择:
- 每个核心(或硬件线程)运行一个进程:这种方式方便,因为 MPI 可以处理所有并行性。
- 每个节点只运行一个进程:此时节点应运行多线程共享内存并行程序,并且建议任何时候每个节点只有一个线程进行 MPI 调用,以避免一些 MPI 实现出现问题。
- 中间方案:例如每个插槽运行一个 MPI 进程,可明确考虑非统一内存访问(NUMA)效应。
启动 MPI 程序的方式因环境而异:
- 在单台机器的核心上运行时,以 Linux 系统和 OpenMPI v3.0.0 为例,编译程序的命令为:
mpic++ example.cpp -o example
其中
mpic++
是一个脚本,会使用适当的参数调用 GNU 编译器。运行程序使用四个进程的命令为:
mpirun -np 4 example
- 在超级计算机上启动程序则更复杂,通常需要编写一个配置文件,描述要调用的程序、使用的节点数量以及每个节点上运行的进程数量,然后将该配置文件传递给作业调度器,由其分配资源并在所有节点上启动程序。
3. 点对点通信
在 MPI 中,假设消息
m
是一个包含
k
个整数的数组,伪代码操作
send(i, m)
可以转换为以下 MPI 调用:
MPI_Send(&m, k, MPI_INT, i, t, MPI_COMM_WORLD)
其中,整数
t
是消息标签,帮助接收者区分不同类型的消息;目标 PE
i
是全局通信器
MPI_COMM_WORLD
内的一个排名。也可以使用其他通信器来表示 PE 的子集。还可以通过将常量
MPI_INT
替换为其他预定义常量(如
MPI_CHAR
、
MPI_SHORT
、
MPI_LONG
、
MPI_FLOAT
、
MPI_DOUBLE
)来使用不同的数据类型,甚至支持用户自定义数据类型。
MPI 提供了多种发送操作变体:
-
MPI_Send
:普通发送操作,返回时消息缓冲区
m
可重用,不影响消息传递。
-
MPI_Ssend
:除了普通发送的功能外,还保证接收者已开始实际接收消息。
-
MPI_Isend
和
MPI_Issend
:非阻塞或立即操作,有一个额外的返回参数返回请求对象。只有在调用等待请求完成的(阻塞)操作后,缓冲区才能重用。其优点是能立即返回,可同时发起多个通信,还能实现通信与内部工作的重叠。
-
MPI_Bsend
:也立即返回,并保证消息缓冲区可立即移除,但用户需要使用
MPI_Buffer_attach
操作提供额外的缓冲区内存,会导致额外的复制操作和一定开销。
对应的接收操作如下:
MPI_Recv(&m, k, MPI_INT, j, t, MPI_COMM_WORLD, &status)
其中,
j
可以指定期望接收消息的 PE,也可以设置为
MPI_ANY_SOURCE
以接收任何发送者的消息;
t
可以指定期望的标签,也可以是
MPI_ANY_TAG
。参数
k
指定消息缓冲区的分配长度,该缓冲区可能比实际接收的消息长,实际接收消息的长度可以通过
MPI_Get_count
操作从
status
变量(类型为
MPI_Status
)中读取。
status
对象的
status.MPI_TAG
和
status.MPI_SOURCE
字段分别表示接收消息的标签和发送者。
当接收者无法确定消息长度的上限时,可以先调用
MPI_Probe
操作,它会返回一个包含消息长度(以及标签和源 PE)的状态信息。此外,还有非阻塞接收操作
MPI_Irecv
。
最后,
MPI_Sendrecv
操作对应伪代码中的
send(···) ∥ receive(···)
操作。
为了处理非阻塞操作
MPI_Isend
和
MPI_Irecv
,还需要使用
MPI_Wait
、
MPI_Waitany
、
MPI_Waitall
和
MPI_Test
、
MPI_Testany
、
MPI_Testall
等操作。这些操作接收
MPI_Isend
和
MPI_Irecv
返回的请求对象,
wait
操作会阻塞直到指定请求完成,
test
操作不阻塞,允许在通信操作在后台执行时进行计算。
4. 集体通信
MPI 支持多种集体通信操作,这是其相较于其他并行处理框架的一大优势。以下是 MPI 中可用的集体通信操作总结表格:
| 操作名称 | MPI 函数名 | 相关章节 |
| ---- | ---- | ---- |
| 广播 | MPI_Bcast | 13.1 |
| 归约 | MPI_Reduce | 13.2 |
| 全归约 | MPI_Allreduce | 13.2 |
| 前缀和 | MPI_Scan | 13.3 |
| 屏障 | MPI_Barrier | 13.4.2 |
| 收集 | MPI_Gather(v) | 13.5 |
| 全收集 | MPI_Allgather(v) | 13.5 |
| 散布 | MPI_Scatter(v) | 13.5 |
| 全对全 | MPI_Alltoall(v) | 13.6 |
从 MPI 3.0 开始,还支持第 13.7 节中介绍的异步集体操作。不过,并非所有 MPI 实现都能高效实现所有集体操作,因此在实际应用中,仔细的性能分析和必要时手动重新实现所需操作对于获得良好性能非常重要。
集体操作中,消息大小不规则的操作名称以
v
结尾,这意味着接收者需要指定消息的长度,通常需要单独传输消息长度信息。
以下是一个集体操作的示例调用:
MPI_Reduce(&c, &sum, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD)
该操作会对局部变量
c
的值进行求和归约,最终结果存储在编号为 0 的 PE 的变量
sum
中。除了
MPI_SUM
,MPI 还支持许多其他预定义的归约操作,也允许用户自定义操作。
5. 商业产品、商标和软件许可
常见的商业产品和商标包括 Microsoft® Windows®、Oracle® Java®、IBM® RS/6000® 等。书中的代码等内容遵循开源的 BSD - 3 - 条款许可证。
BSD 3 - 条款许可证规定:
- 源代码的再分发必须保留上述版权声明、条件列表和免责声明。
- 二进制形式的再分发必须在文档和/或其他分发材料中复制上述版权声明、条件列表和免责声明。
- 未经版权所有者及其贡献者的明确书面许可,不得使用其名称来认可或推广从该软件派生的产品。
并且该软件按“原样”提供,不提供任何明示或暗示的保证,版权所有者和贡献者对因使用该软件而导致的任何直接、间接、偶然、特殊、惩戒性或后果性损害不承担责任。
通过以上内容,我们对 MPI 的基本概念、编程基础、通信方式以及相关许可有了较为全面的了解,MPI 在高性能计算领域有着广泛的应用,掌握其使用方法对于开发高效的并行程序至关重要。
消息传递接口(MPI)全面解析
6. 操作流程总结
为了更清晰地展示 MPI 编程中的关键操作流程,下面通过 mermaid 格式的流程图来呈现。
graph TD;
A[程序开始] --> B[MPI_Init 初始化];
B --> C[获取进程总数 p 和当前进程编号 i];
C --> D{操作类型};
D -->|点对点通信| E[MPI_Send 或 MPI_Recv 等操作];
D -->|集体通信| F[MPI_Reduce 等集体操作];
E --> G{是否非阻塞操作};
G -->|是| H[使用 MPI_Wait 或 MPI_Test 处理请求];
G -->|否| I[继续后续操作];
F --> I[继续后续操作];
I --> J[MPI_Finalize 结束 MPI 环境];
J --> K[程序结束];
7. 操作步骤梳理
在实际使用 MPI 进行编程时,我们可以将关键操作步骤进行梳理,形成一个清晰的列表:
1.
初始化 MPI 环境
:使用
MPI_Init
函数,该函数会初始化全局变量
MPI_COMM_WORLD
,它描述了程序可用的进程集合。
2.
获取进程信息
:通过
MPI_Comm_size
获取进程总数,使用
MPI_Comm_rank
获取当前进程的编号。
3.
选择通信方式
:
-
点对点通信
:根据需求选择合适的发送和接收函数,如
MPI_Send
、
MPI_Recv
等。若使用非阻塞操作,还需使用
MPI_Wait
或
MPI_Test
处理请求。
-
集体通信
:根据具体的集体操作需求,选择如
MPI_Reduce
、
MPI_Bcast
等函数。
4.
结束 MPI 环境
:使用
MPI_Finalize
函数结束 MPI 环境。
8. 不同通信方式对比
为了更好地理解 MPI 中不同通信方式的特点,下面通过表格进行对比:
| 通信方式 | 特点 | 适用场景 | 注意事项 |
| ---- | ---- | ---- | ---- |
| 点对点通信 | 实现单个进程之间的消息传递,有多种发送和接收操作变体 | 进程间需要一对一通信的场景,如数据交换、任务分配等 | 非阻塞操作需要额外处理请求;不同发送操作有不同的保证机制 |
| 集体通信 | 涉及多个进程的同步操作,支持广播、归约等多种操作 | 需要多个进程协同完成的任务,如数据汇总、同步等 | 并非所有实现都能高效执行所有集体操作,可能需要手动优化 |
9. 代码示例综合分析
下面我们对之前提到的“Hello World”示例代码进行更深入的分析:
#include <iostream>
#include <mpi.h>
int main(int argc, char** argv)
{
int p, i;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &p);
MPI_Comm_rank(MPI_COMM_WORLD, &i);
std::cout << "PE " << i << " out of " << p << std::endl;
MPI_Finalize();
}
-
头文件包含
:
#include <iostream>用于标准输入输出,#include <mpi.h>包含 MPI 库的头文件。 -
主函数参数
:
int main(int argc, char** argv)是标准的 C++ 主函数格式,MPI_Init会使用这些参数进行初始化。 -
进程信息获取
:
MPI_Comm_size和MPI_Comm_rank分别获取进程总数和当前进程编号。 -
输出信息
:
std::cout << "PE " << i << " out of " << p << std::endl;输出当前进程的编号和总进程数。 -
结束 MPI 环境
:
MPI_Finalize用于结束 MPI 环境,释放相关资源。
10. 总结与展望
MPI 作为高性能计算领域的事实标准,为并行编程提供了强大的支持。通过本文,我们详细介绍了 MPI 的基本概念、编程基础、通信方式以及相关许可等内容。在实际应用中,我们需要根据具体的任务需求选择合适的通信方式,并注意不同操作的特点和注意事项。
未来,随着计算机硬件的不断发展和并行计算需求的增加,MPI 可能会不断演进和完善。例如,可能会进一步优化非阻塞操作和集体操作的性能,以更好地适应大规模并行计算的需求。同时,与其他并行编程模型的结合也可能会成为一个研究方向,以提供更灵活、高效的并行编程解决方案。
通过不断学习和实践 MPI 编程,我们可以开发出更高效、更强大的并行程序,为解决各种复杂的科学计算和工程问题提供有力支持。
超级会员免费看
6860

被折叠的 条评论
为什么被折叠?



