3238 字
16 分钟
MPI 并行指南

MPI 并行指南#


MPI是一个跨平台的通信协议,用于编写并行计算机,支持点对点和广播。MPI是一个信息传递应用程序接口,包括协议和语义说明,他们指明其如何在各种实现中发挥其特性。MPI的目标是高性能,大规模性和可移植性。MPI在今天仍为高性能计算的主要模型。

MPI 是一个跨语言的通讯协议,用于编写并行计算机。支持点对点和广播。MPI 是一个信息传递应用程序接口,包括协议和和语义说明,他们指明其如何在各种实现中发挥其特性。MPI的目标是高性能,大规模性,和可移植性。MPI 在今天仍为高性能计算的主要模型。

MPI 可以大概分为两大类:Open MPI 和 MPICH 及其衍生。

MPICH 实现的比较全面,但是不支持 InfiniBand 这种比较新的硬件和协议。但是它的衍生品例如 Intel MPI 和 MVAPICH 支持比较新的硬件与协议。

Open MPI 也实现了 MPI 标准,并且应用广泛。它的进程管理比 MPICH 相对比较好。

Open MPI 与 OpenMP 是不同的概念。

1、Hello world 代码案例#

让我们来看一下这节课的代码吧,完整的代码在 mpi_hello_world.c。 下面是一些重点内容的摘录。

#include <mpi.h>
#include <stdio.h>
int main(int argc, char** argv) {
// 初始化 MPI 环境
MPI_Init(NULL, NULL);
// 通过调用以下方法来得到所有可以工作的进程数量
int world_size;
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
// 得到当前进程的秩
int world_rank;
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
// 得到当前进程的名字
char processor_name[MPI_MAX_PROCESSOR_NAME];
int name_len;
MPI_Get_processor_name(processor_name, &name_len);
// 打印一条带有当前进程名字,秩以及
// 整个 communicator 的大小的 hello world 消息。
printf("Hello world from processor %s, rank %d out of %d processors\n",
processor_name, world_rank, world_size);
// 释放 MPI 的一些资源
MPI_Finalize();
}

你应该已经注意到搭建一个 MPI 程序的第一步是引入 #include <mpi.h> 这个头文件。然后 MPI 环境必须以以下代码来初始化:

MPI_Init(int* argc, char*** argv)

在 MPI_Init 的过程中,所有 MPI 的全局变量或者内部变量都会被创建。举例来说,一个通讯器 communicator 会根据所有可用的进程被创建出来(进程是我们通过 mpi 运行时的参数指定的),然后每个进程会被分配独一无二的秩 rank。当前来说,MPI_Init 接受的两个参数是没有用处的,不过参数的位置保留着,可能以后的实现会需要用到。

在 MPI_Init 之后,有两个主要的函数被调用到了。这两个函数是几乎所有 MPI 程序都会用到的。

MPI_Comm_size(
MPI_Comm communicator,
int* size)

MPI_Comm_size 会返回 communicator 的大小,也就是 communicator 中可用的进程数量。在我们的例子中,MPI_COMM_WORLD(这个 communicator 是 MPI 帮我们生成的)这个变量包含了当前 MPI 任务中所有的进程,因此在我们的代码里的这个调用会返回所有的可用的进程数目。

MPI_Comm_rank( MPI_Comm communicator, int* rank)

MPI_Comm_rank 这个函数会返回 communicator 中当前进程的 rank。 communicator 中每个进程会以此得到一个从0开始递增的数字作为 rank 值。rank 值主要是用来指定发送或者接受信息时对应的进程。

我们代码中使用到的一个不太常见的方法是:

MPI_Get_processor_name( char* name, int* name_length)

MPI_Get_processor_name 会得到当前进程实际跑的时候所在的处理器名字。 代码中最后一个调用是:

MPI_Finalize 是用来清理 MPI 环境的。这个调用之后就没有 MPI 函数可以被调用了。

2、运行 MPI hello world 程序#

现在查看以下代码文件以及代码所在的文件夹,你会看到一个 makefile。

>>> git clone https://github.com/mpitutorial/mpitutorial
>>> cd mpitutorial/tutorials/mpi-hello-world/code
>>> cat makefile
EXECS=mpi_hello_world
MPICC?=mpicc
all: ${EXECS}
mpi_hello_world: mpi_hello_world.c
${MPICC} -o mpi_hello_world mpi_hello_world.c
clean:
rm ${EXECS}

我的 makefile 会去找 MPICC 这个环境变量。如果你把 MPICH2 装在了本地文件夹里面而不是全局 PATH 下面, 手动设置一下 MPICC 这个环境变量,把它指向你的 mpicc 二进制程序。mpicc 二进制程序其实只是对 gcc 做了一层封装,使得编译和链接所有的 MPI 程序更方便。

Terminal window
>>> export MPICC=/home/kendall/bin/mpicc
>>> make
/home/kendall/bin/mpicc -o mpi_hello_world mpi_hello_world.c

当你的程序编译好之后,它就可以被执行了。不过执行之前你也许会需要一些额外配置。比如如果你想要在好几个节点的集群上面跑这个 MPI 程序的话,你需要配置一个 host 文件(不是 /etc/hosts)。如果你在笔记本或者单机上运行的话,可以跳过下面这一段。

需要配置的 host 文件会包含你想要运行的所有节点的名称。为了运行方便,你需要确认一下所有这些节点之间能通过 SSH 通信,并且需要根据设置认证文件这个教程配置不需要密码的 SSH 访问。 我的 host 文件看起来像这样:

>>> cat host_file
cetus1
cetus2
cetus3
cetus4

为了用我提供的脚本来运行这个程序,你应该设置一个叫 MPI_HOSTS 的环境变量,把它指向 host 文件所在的位置。我的脚本会自动把这个 host 文件的配置项加到 MPI 启动命令里。如果单机跑的话就不用设置这个环境变量。另外如果你的 MPI 没有装到全局环境的话,你还需要指定 MPIRUN 这个环境变量指向你的 mpirun 二进制程序。

准备就绪之后你就可以使用这个项目的我提供的 python 脚本来执行程序。脚本在 tutorials 目录下面,这个脚本可以用来跑我们这个教程里面提到的所有程序(而且它会帮你先编译一下程序)。你可以在 mpitutorial 这个文件夹的根目录下执行以下命令:

>>> export MPIRUN=/home/kendall/bin/mpirun
>>> export MPI_HOSTS=host_file
>>> cd tutorials
>>> ./run.py mpi_hello_world
/home/kendall/bin/mpirun -n 4 -f host_file ./mpi_hello_world
Hello world from processor cetus2, rank 1 out of 4 processors
Hello world from processor cetus1, rank 0 out of 4 processors
Hello world from processor cetus4, rank 3 out of 4 processors
Hello world from processor cetus3, rank 2 out of 4 processors

跟预想的一样,这个 MPI 程序运行在了我提供的所有节点上面。每个进程都被分配了一个单独的 rank,跟进程的名字一起打印出来了。你可以看到,在我们的输出的结果里,进程之间的打印顺序是任意的,因为我们的代码里并没有涉及到同步的操作。

我们可以在打印的内容上面那条看到脚本是如何调用 mpirun 这个程序的。mpirun 是 MPI 的实现用来启动任务的一个程序。进程会在 host 文件里指定的所有机器上面生成,MPI 程序就会在所有进程上面运行。我的脚本自定地提供了一个 -n 参数告诉 MPI 程序我要运行 4 个进程。你可以试着修改脚本来使用更多进程运行 MPI 程序。当心别把你的操作系统玩崩了。:-)

你可能会问,“我的节点都都是双核的机器,我怎么样可以让 MPI 先在每个节点上的每个核上生成进程,再去其他的机器?” 其实方案很简单。修改一下你的 host 文件,在每个节点名字的后面加一个冒号和每个处理器有的核数就行了。比如,我在 host 文件里指定我的每个节点有2个核。

>>> cat host_file
cetus1:2
cetus2:2
cetus3:2
cetus4:2

当我再次运行我的脚本,哇!,MPI 任务只在我的两个节点上生成了4个进程。

>>> ./run.py mpi_hello_world
/home/kendall/bin/mpirun -n 4 -f host_file ./mpi_hello_world
Hello world from processor cetus1, rank 0 out of 4 processors
Hello world from processor cetus2, rank 2 out of 4 processors
Hello world from processor cetus2, rank 3 out of 4 processors
Hello world from processor cetus1, rank 1 out of 4 processors

3、进阶MPI编程#

3.1、MPI初始化#

#include "mpi.h"
#include <stdio.h>
int main(int argc, char* argv[])
{
//=========================================================
// MPI Indix
// =======================================================
int myid,numprocs,namelen;
char processor_name[MPI_MAX_PROCESSOR_NAME];
// Basic MPI
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&myid); // rank id
MPI_Comm_size(MPI_COMM_WORLD,&numprocs); // total rank
MPI_Get_processor_name(processor_name,&namelen); // get the meachine name
if(myid==0)
printf("Number of MPI thread is %d\n",numprocs);
MPI_Finalize();
}

在这段代码里,有几句比较重要:

  • MPI_Init(NULL, NULL) 在这一句之前,所有的进程都是完全相互独立的。
  • MPI_COMM_WORLD 是预定义好的通信器 (Communicator),通信器是一个包含了进程的集合,同一个通信器里面的进程可以相互通信。
  • MPI_Comm_rank(MPI_COMM_WORLD, &world_rank) 获取每个进程的在通信器里面的 Rank,也就是编号
  • MPI_Finalize() 这一句释放掉所有被 MPI 占用的资源。
  • 实际上 MPICC 的后端还是 GCC。MPICC 只是添加了动态链接库。

3.2、进程间通讯#

3.2.1、P2P 接收与发送#

  • MPI_Send 用来发送信息

  • MPI_Recv 用来接收信息

    int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm);
    int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status);

参数说明

  • buf 内存地址,用于接收/发送消息的存储。
  • count 传送消息的数量
  • datatype 传输数据类型,除了 C 自带的类型以外,MPI 还定义了一些类型
  • dest 接收节点
  • source 发送节点
  • tag 信息的标签
  • comm 通信器
  • status 接收消息的状态

传递信息可以看作是一个 Message Envelope.每个 Send 要确保有对应的 Recv。除此以外,还要注意死锁问题,每个 Recv 都能即使获取到发送的东西。

值得一提的是,接收消息的长度跟发送消息的长度是可以不同的,但是最好保证接收消息的长度 大于等于 发送的长度,以免溢出。

#include <stdio.h>
#include <mpi.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
MPI_Init(&argc, &argv);
int numP, myId;
MPI_Comm_rank(MPI_COMM_WORLD, &myId);
MPI_Comm_size(MPI_COMM_WORLD, &numP);
if(numP % 2 != 0) {
if(!myId) {
printf("ERROR: the number of ping-pong processors must be even number!\n");
}
MPI_Abort(MPI_COMM_WORLD, EXIT_FAILURE);
}
int partner_id;
int odd = myId & 1;
if(odd) {
partner_id = myId - 1;
} else {
partner_id = myId + 1;
}
int a, b;
if(odd == 0) {
a = -1;
printf("Process %d sends token %d to process %d\n", myId, a, partner_id);
MPI_Send(&a, 1, MPI_INT, partner_id, 0, MPI_COMM_WORLD);
MPI_Recv(&b, 1, MPI_INT, partner_id, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("Process %d receives token %d from process %d\n", myId, b, partner_id);
} else {
a = 1;
MPI_Recv(&b, 1, MPI_INT, partner_id, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("Process %d receives token %d from process %d\n", myId, b, partner_id);
printf("Process %d sends token %d to process %d\n", myId, a, partner_id);
MPI_Send(&a, 1, MPI_INT, partner_id, 0, MPI_COMM_WORLD);
}
MPI_Finalize();
}

Result

Terminal window
Process 2 sends token -1 to process 3
Process 0 sends token -1 to process 1
Process 1 receives token -1 from process 0
Process 1 sends token 1 to process 0
Process 0 receives token 1 from process 1
Process 3 receives token -l from process 2
Process 3 sends token 1 to process 2
Process 2 receives token 1 from process 3

3.2.2 阻塞通信与非阻塞通信#

刚才说的是阻塞通信,在发送出去/接收到之前,程序会停止运行。相对的,也存在非阻塞通信。如下:

  • MPI_ISend 用来发送信息

  • MPI_IRecv 用来接收信息

    int MPI_ISend(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request);
    int MPI_IRecv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request);

request 用于标记通信任务。在使用非阻塞通信传来的数据之前,需要使用 MPI_Wait 确认是否数据已经传入缓冲区。

非阻塞通信还有如下的控制与测试函数:

  • MPI_Cancel 用于取消非阻塞通信。
  • MPI_Test 用于测试非阻塞通信是否已经结束。
  • MPI_Wait 用于等待非阻塞通信数据。
#include <stdio.h>
#include <mpi.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
MPI_Init(&argc, &argv);
int numP, myId;
MPI_Comm_rank(MPI_COMM_WORLD, &myId);
MPI_Comm_size(MPI_COMM_WORLD, &numP);
int next_id = (myId + 1) % numP, prev_id = (myId - 1 + numP) % numP;
MPI_Request rq_send, rq_recv;
int a = 1, b;
MPI_Isend(&a, 1, MPI_INT, next_id, 0, MPI_COMM_WORLD, &rq_send);
MPI_Irecv(&b, 1, MPI_INT, prev_id, 0, MPI_COMM_WORLD, &rq_recv);
MPI_Wait(&rq_recv, MPI_STATUS_IGNORE);
printf("%d receive token %d from %d\n" myId, b, prev_id);
MPI_Barrier(MPI_COMM_WORLD);
a = -1;
MPI_Isend(&a, 1, MPI_INT, prev_id, 0, MPI_COMM_WORLD, &rq_send);
MPI_Irecv(&b, 1, MPI_INT, next_id, 0, MPI_COMM_WORLD, &rq_recv);
MPI_Wait(&rq_recv, MPI_STATUS_IGNORE);
printf("%d receive token %d from %d\n" myId, b, next_id);
MPI_Barrier(MPI_COMM_WORLD);
MPI_Finalize();
}

结果

Terminal window
5 receive token 1 from 4
6 receive token 1 from 5
4 receive token 1 from 3
1 receive token 1 from 0
0 receive token 1 from 6
2 receive token 1 from 1
3 receive token 1 from 2
1 receive token -1 from 2
4 receive token -1 from 5
0 receive token -1 from 1
6 receive token -1 from 0
5 receive token -1 from 6
3 receive token -1 from 4
2 receive token -1 from 3

3.2.3 聚合通信 (Collective Communication)#

除了点对点通信之外,MPI 还有其他的通信方式如图所示。

Image 2

#include <stdio.h>
#include <mpi.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
MPI_Init(&argc, &argv);
int numP, myId;
MPI_Comm_rank(MPI_COMM_WORLD, &myId);
MPI_Comm_size(MPI_COMM_WORLD, &numP);
int n = atoi(argv[1])
MPI_Barrier(MPI_COMM_WORLD);
double start = MPI_Wtime();
int myCount = 0;
int total;
int prime;
for(int i = 2 + myId; i <= n; i += numP) {
prime = 1;
for(int j = 2; j < i; ++j) {
if(i % j == 0) {
prime = 0;
break;
}
}
myCount += prime;
}
MPI_Reduce(&myCount, &total, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);
double end = MPI_Wtime();
if(!myId) {
printf("%d primes between 1 and %d\n", total, n);
printf("Time with %d processes: %.3lf seconds\n", numP, end - start);
}
MPI_Finalize();
}

MPI_Reduce 中有预定义好的 MPI_Ops 例如 MPI_MAX, MPI_SUM 等等。

mpirun -np 10 ./prime 100000

Terminal window
9592 primes between 1 and 100000
Time with 10 processes: 0.265 seconds

mpirun -np 1 ./prime 100000

Terminal window
9592 primes between 1 and 100000
Time with 1 processes: 1.012 seconds

image-20240611154152399

Reference#

【1】 https://mpitutorial.com/tutorials/mpi-hello-world/zh_cn/

【2】MPI Hello World · MPI Tutorial

【3】MPI编程入门(Fortran) (cndaqiang.github.io)

【4】cnblogs.com/zhangyazhou/p/13376262.html

【5】https://jinbridger.github.io/docs/hpc/openmpi-programming-101/

MPI 并行指南
https://tuoyuangui.github.io/website/posts/typora/指南/mpi并行指南/
作者
椭圆规/tuoyuangui
发布于
2022-10-12
许可协议
CC BY-NC-SA 4.0