# MPI 与并行计算(一):并行环境及编程模型
# 1. 什么是 MPI
Massage Passing Interface:是消息传递函数库的标准规范,由 MPI 论坛开发,支持 Fortran 和 C。
- 一种新的库描述,不是一种语言。共有上百个函数调用接口,在 Fortran 和 C 语言中可以直接对这些函数进行调用。
- MPI 是一种标准或规范的代表,而不是特指某一个对它的具体实现。迄今为止所有的并行计算机制造商都提供对 MPI 的支持。
- Intel MPI
- OpenMPI
- mpich
- MPI 是一种消息传递编程模型,并成为这种编程模型的代表和事实上的标准。
# 2. MPI 的发展过程
- MPI 1.1:1995
- MPICH:是 MPI 最流行的非专利实现,由 Argonne 国家实验室和密西西比州立大学联合开发,具有更好的可移植性.
- MPI 1.2~2.0:动态进程, 并行 I/O, 支持 F90 和 C++(1997)
# 3. 为什么要用 MPI
- 高可移植性:MPI 已在 IBM PC 机上、 MS Windows 上、所有主要的 Unix 工作站上和所有主流的并行机上得到实现。使用 MPI 作消息传递的 C 或 Fortran 并行程序可不加改变地运行在 IBMPC、 MS Windows、 Unix 工作站、以及各种并行机上。
# 4. 并行编程模式
- 隐式并行:借助编译器和运行时环境的支持发掘程序的并行性,对串行程序进行并行化。
- 数据并行:数据并行依靠所处理的数据集合无关性,借助数据划分来驱动程序之间的并行执行。
- 消息传递:消息传递模型可以通过如下的几个概念加以定义:
- 一组仅有本地内存空间的进程
- 进程之间通过发送和接收消息进行通信
- 进程之间需要使用协同操作完成数据传递,如发送操作必须要求有与之配对的接收操作
- 共享变量:并行代码分别驻留在不同的处理器上,通过读写公共存储器中的共享变量进行同步和通信,一般适合在多核系统,SMP 系统上运行。分布式存储系统可以在运行时库支持下通过自定义机制以共享变量的方式运行。
# 5. MPI 的工作模式
- 运行方式:以串行方式编写,运行时分别执行不同的块。
- 资源分配:所有程序元素只要没有进行显示区分,无论是代码、函数、全局变量还是局部变量,都默认的由全部进程共同拥有,所有进程看到的虽然是相同的名字,但在“物理”上却彼此无关。
- 显示区分:就是指程序员需在程序设计阶段通过显示的条件判断来指定在不同进程上运行不同的代码块。这正是 MPI 程序的特点,也恰好是难点之一。
注释
一个典型的 MPI 程序代码模式如下:
// Inititalization code block
if (rank == process1) {
// define works to be carried out by process1
} else if (rank == process2) {
// define works to be carried out by process2
} else if (rank == process3) {
// define works to be carried out by process3
} else if (rank == process4) {
// define works to be carried out by process4
}...else if (rank == processn) {
// define works to be carried out by processn
} else {
// define works to be carried out by all processes
}
# 6. MPI 消息传递通信的基本概念
消息:一个消息可以比做一个封信。需要定义消息的内容以及消息的发送与接收者。前者称为消息的缓冲(Message Buffer),后者称为消息信封(Message Envelop)。在 MPI 中,消息缓冲由三元组<起始地址,数据个数,数据类型>来标识,而消息信封则是由三元组<源/目标进程,消息标签,通讯域>来标识。如下为
MPI_Send
的消息缓冲和消息信封。缓冲区:MPI 环境定义了 3 种缓冲区:应用缓冲区,系统缓冲区和用户向系统注册的缓冲区。
- 应用缓冲区:保存将要发送或接收的消息内容即上述的消息缓冲。
- 系统缓冲区:MPI 环境为通信所准备的存储空间。
- 用户缓冲区:指用户向系统注册的缓冲区,用户使用某些 API(如
MPI_Bsend
)时,在程序中显示申请的存储空间,然后注册到 MPI 环境中供通信所用。
通信子:MPI 环境管理进程及通信的基本设施。
MPI_COMM_WORLD
就是 MPI 环境启动时默认创建的通信子。对某个进程的操作必须放在通信子内方可有效。进程号:进程号即进程的
rank
,指在某个通信子内某个进程号 rank 为 num。在一个通信子内,每一个进程都有它唯一的 num,这个标识号是在进程初始化时由系统分配,从 0 开始编号。进程组:定义一个通信子,也就指定了一组共享该空间的进程,这些进程组成了该通信子的进程组(group)。
通信协议:MPI 环境依据实现的策略不同,可能采用如下一种或几种协议。
- 立即通信协议,总是假定目标进程具有保存消息数据的能力。
- 集中通信协议,在目标准备好之后,才可以执行发送动作。
- 短消息协议,消息数据与信封封装在一起发送。
# 7. MPI 程序编译、运行
- MPI 环境安装:
- 更新 apt 源:
sudo apt-get update
- 安装 build-essential:
sudo apt-get install -y build-essential
- 安装 mpich:
sudo apt-get install -y mpich
- 更新 apt 源:
- MPI 程序编译:mpicc
Intel MPI Mpich/OpenMPI Fortran mpiifort mpi90 C mpiicc mpicc C++ mpiicpc mpicxx
注释
mpicc 编译示例
mpicc -o mpi mpi.c
- MPI 程序运行:mpirun
- 使用 mpirun 来运行 mpi 程序(intel mpi、mpich、openmpi 等)
- 用法:mpirun -n 进程数 可执行文件名
- 示例:
mpirun -n 2 ./example
注释
第一个 MPI 程序示例 - Hello World
// mpi.c
#include <mpi.h>
#include <stdio.h>
int main(int argc, char **argv)
{
MPI_Init(&argc, &argv);
printf("Hello World!\n");
MPI_Finalize();
return 0;
}
- 编译与运行 mpi.c
mpicc -o mpi mpi.c
mpirun -n 2 ./mpi
警告
注意: root 用户运行 mpirun 时,需要加上–allow-run-as-root 参数,否则会报错。
# 8. MPI 的四个基本接口
接口名 | 功能 |
---|---|
MPI_Init(&argc, &argv) | 初始化 MPI 环境,MPI 系统将通过 argc, argv 得到命令行参数 |
MPI_Comm_rank(MPI_COMM_WORLD, &myrank) | 缺省的通信子为 MPI_COMMON_WORLD,获得进程所在缺省通信子的编号,赋值给 myrank |
MPI_Comm_size(MPI_COMM_WORLD, &nprocs) | 获得缺省通信子中进程的个数,赋值给 nprocs |
MPI_Finalize() | 一般放在程序最后一行,如果没有此行,MPI 程序将不会终止 |
- MPI 初始化:MPI_Init
- int MPI_Init(int *argc, char ***argv)
- MPI_Init 是 MPI 程序的第一个调用,它完成 MPI 程序的所有初始化工作。所有的 MPI 程序的第一条可执行语句都是这条语句。
- 启动 MPI 环境,标志并行代码的开始.
- 要求 main 必须带参数运行,否则出错
- MPI 结束:MPI_Finalize
- int MPI_Finalize(void)
- MPI_FINALIZE 是 MPI 程序的最后一个调用,它结束 MPI 程序的运行,它是 MPI 程序的最后一条可执行语句,否则程序的运行结果是不可预知的。
- 标志并行代码的结束,结束除主进程外其它进程。
- 之后串行代码仍可在主进程(rank = 0)上运行(如果必须)。
- 进程号:MPI_Comm_rank
- int MPI_Comm_rank(MPI_Comm comm, int *rank)
- MPI_COMM_RANK 是 MPI 程序中的一个重要函数,它返回调用进程在通信子中的进程号,即 rank。
- 通信子:MPI_COMM_WORLD
- 进程号:rank
- 进程数:MPI_Comm_size
- int MPI_Comm_size(MPI_Comm comm, int *size)
- MPI_COMM_SIZE 是 MPI 程序中的一个重要函数,它返回通信子中的进程数,即 size。
- 通信子:MPI_COMM_WORLD
- 进程数:size
注释
示例 2:打印进程 ID 和进程数
// mpi.c
#include <mpi.h>
#include <stdio.h>
int main(int argc, char **argv)
{
int myrank, nprocs;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
MPI_Comm_size(MPI_COMM_WORLD, &nprocs);
printf("Hello World! I'm %d of %d\n", myrank, nprocs);
MPI_Finalize();
return 0;
}
- 结果:
root@ubuntu:~# mpicc -o mpi mpi.c
root@ubuntu:~# mpirun -n 2 ./mpi
Hello World! I'm 0 of 2
Hello World! I'm 1 of 2