Menu

Mpi编程实训

转载自超算习堂的在线实训。

MPI简介

MPI,全称Message Passing Interface(消息传递接口),是业界定义的一种消息传递标准,用于编写并行计算的程序,广泛应用于高性能计算领域。

标准定义了库函数核心的基本语法和语义,在C, C++和Fortran语言中也可以写出具有消息传递功能的程序。

MPI标准有许多经过良好测试且效率较高的的实现,这些实验是开源的,可供公众使用。

配置MPI环境

为了正常编译MPI代码,需要安装C, C++与Fortran的编译环境。

Ubuntu缺省情况下,并没有提供这些语言的编译环境,因此需要手动安装。如果单独安装这些编译环境非常麻烦。幸运的是,build-essential工具提供了许多与编译相关的软件包,包括gcc/g++/gfortran等编译器、libc6-dev等必要的库与其他工具。于是,我们只需要通过包管理器安装build-essential即可。

MPICH是MPI标准的一种重要的实现,可以免费从网上下载。MPICH的开发与MPI规范的制订是同步进行的,因此MPICH最能反映MPI的变化和发展。MPICH是MPI最流行的非专利实现,由Argonne国家实验室和密西西比州立大学联合开发,具有更好的可移植性,现阶段多流行的是MPICH2。

apt命令的 -y 选项默认安装过程中同意所有的默认选择。

1
sudo apt install -y build-essential mpich

第一个MPI程序

首先,我们应该先包含进一个头文件<mpi.h>,我们使用的函数都在其中。另外,在这之后,MPI程序和普通的C程序的区别在于有一个开始的函数和结束的函数来标识MPI部分,再在这个部分进行你想要进行的操作,现在就来尝试一下! 以下是第一段程序helloworld.c的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
int MPI_Init(int *argc, char **argv);//通过MPI_Init函数进入MPI环境并完成所有的初始化工作,标志并行代码的开始。
int MPI_Finalize(void);//通过MPI_Finalize函数从MPI环境中退出,标志并行代码的结束,如果不是MPI程序最后一条可执行语句,则运行结果不可知。
*/
#include <mpi.h>
#include <stdio.h>
int main(int argc, char **argv)
{
	//your code here
	MPI_Init(&argc, &argv);
	puts("Hello World!");
	MPI_Finalize();
	//end of your code

	return 0;
}

按下面的指令编译运行(以下都以四个进程执行为例)。

1
2
mpicc helloworld.c -o helloworld
mpirun -np 4 ./mpihelloworld

得到输出。

1
2
3
4
Hello World!
Hello World!
Hello World!
Hello World!

获取进程数量

在MPI编程中,我们常常需要获取指定通信域的进程个数,以确定程序的规模。

一组可以相互发送消息的进程集合叫做通信子,通常由MPI_Init()在用户启动程序时,定义由用户启动的所有进程所组成的通信子,缺省值为 MPI_COMM_WORLD 。这个参数是MPI通信操作函数中必不可少的参数,用于限定参加通信的进程的范围。

使用函数MPI_Comm_size获取通信域中的进程个数并打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
int MPI_Comm_size(MPI_Comm comm, int *rank);//获取指定通信域的进程个数。其中,第一个参数是通信子,第二个参数返回进程的个数
*/
#include <stdio.h>
#include <mpi.h>

int main(int argc, char **argv)
{
	int numprocs;
	MPI_Init(&argc, &argv);

	//your code here
	MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
	//end of your code

	printf("Hello World! The number of processes is %d\n",numprocs);

	MPI_Finalize();
	return 0;
}
/*
Hello World! The number of processes is 4
Hello World! The number of processes is 4
Hello World! The number of processes is 4
Hello World! The number of processes is 4
*/

获取进程id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
int MPI_Comm_rank(MPI_Comm comm, int *rank);//获得当前进程在指定通信域中的编号,将自身与其他程序区分。其中,第一个参数是通信子,第二个参数返回进程的编号。
*/
#include <stdio.h>
#include <mpi.h>

int main(int argc, char **argv)
{
	int myid, numprocs;
	MPI_Init(&argc, &argv);

  MPI_Comm_size(MPI_COMM_WORLD, &numprocs);

	//your code here
	MPI_Comm_rank(MPI_COMM_WORLD, &myid);
	//end of your code

	printf("Hello World!I'm rank %d of %d\n", myid, numprocs);

	MPI_Finalize();
	return 0;
}
/*
Hello World!I'm rank 3 of 4
Hello World!I'm rank 0 of 4
Hello World!I'm rank 1 of 4
Hello World!I'm rank 2 of 4
*/

获取处理器名

有时候在实际处理中我们可能需要将进程迁移至不同的处理器,而MPI提供了获取处理器名的函数以简单地允许这种行为。

注意在MPI中不需要定义这种迁移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
int MPI_Get_processor_name ( char *name, int *resultlen);//实际节点的唯一说明字;在name中返回结果的长度;
*/
#include <stdio.h>
#include <mpi.h>

int main(int argc, char **argv)
{
	int len;
	char name[MPI_MAX_PROCESSOR_NAME];
	MPI_Init(&argc, &argv);

	//your code here
	MPI_Get_processor_name (name, &len);
	//end of your code

	printf("Hello, world. I am %s.\n", name);

	MPI_Finalize();
	return 0;
}
/*
Hello, world. I am 60e876622717.
Hello, world. I am 60e876622717.
Hello, world. I am 60e876622717.
Hello, world. I am 60e876622717.
*/

运行时间

在实际编程中,计时是一个很实用的功能。 在MPI编程我们可以使用MPI_Wtime函数在并行代码中计算运行时间,用MPI_Wtick来查看精度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
double MPI_Wtime();//返回一个用浮点数表示的秒数, 它表示从过去某一时刻到调用时刻所经历的时间
double MPI_Wtick();//返回MPI_WTIME的精度,单位是秒,可以认为是一个时钟滴答所占用的时间
*/
#include<stdio.h>
#include<mpi.h>

int main(int argc, char **argv)
{
	int myid, numprocs;
	double start, finish;

	MPI_Init(&argc, &argv);

	MPI_Comm_rank(MPI_COMM_WORLD, &myid);
	MPI_Comm_size(MPI_COMM_WORLD, &numprocs);

	//your code here
	start=MPI_Wtime();
	printf("The precision is: %.9lf\n",MPI_Wtick());
	finish=MPI_Wtime();
	//your code here

	printf("Hello World!I'm rank %d of %d, running %f seconds.\n", myid, numprocs, finish-start);

	MPI_Finalize();
	return 0;
}
/*
The precision is: 0.000000001
Hello World!I'm rank 3 of 4, running 0.000031 seconds.
The precision is: 0.000000001
Hello World!I'm rank 0 of 4, running 0.000019 seconds.
The precision is: 0.000000001
Hello World!I'm rank 1 of 4, running 0.000026 seconds.
The precision is: 0.000000001
Hello World!I'm rank 2 of 4, running 0.000022 seconds.
*/

同步

在实际工作中,我们常常会因为许多原因需要进行同步操作。 例如,希望保证所有进程中并行代码在某个地方同时开始运行,或者在某个函数调用结束之前不能返回。 这时候我们就需要使用到MPI_Barrier函数。

在此示例程序中,可能是否调用函数不影响最终输出,但这并不意味着效果相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
int MPI_Barrier(MPI_Comm comm);//阻止调用直到communicator中所有进程已经完成调用,就是说,任意一次进程的调用只能在所有communicator中的成员已经开始调用之后进行。
*/
#include<stdio.h>
#include<mpi.h>

int main(int argc, char **argv)
{
	int myid, numprocs;
	double start, finish;

	MPI_Init(&argc, &argv);

    MPI_Comm_rank(MPI_COMM_WORLD, &myid);
    MPI_Comm_size(MPI_COMM_WORLD, &numprocs);

	//your code here
	MPI_Barrier(MPI_COMM_WORLD);
	//end of your code

	start = MPI_Wtime();

	printf("The precision is: %f\n", MPI_Wtick());

	finish = MPI_Wtime();

	printf("Hello World!I'm rank %d of %d, running %f seconds.\n", myid, numprocs, finish-start);

	MPI_Finalize();
	return 0;
}
/*
The precision is: 0.000000
Hello World!I'm rank 0 of 4, running 0.000023 seconds.
The precision is: 0.000000
Hello World!I'm rank 1 of 4, running 0.000016 seconds.
The precision is: 0.000000
Hello World!I'm rank 2 of 4, running 0.000017 seconds.
The precision is: 0.000000
Hello World!I'm rank 3 of 4, running 0.000015 seconds.
*/

消息传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*
int MPI_Send(
	void *msg_buf_p,//发送缓冲区的起始地址;
	int msg_size,//缓冲区大小
	MPI_Datatype msg_type,//发送信息的数据类型
	int dest,//目标进程的id值
	int tag,//消息标签
	MPI_Comm communicator);//通信子
int MPI_Recv(
	void *msg_buf_p,//缓冲区的起始地址;
	int buf_size,//缓冲区大小;
	MPI_Datatype msg_type,//发送信息的数据类型;
	int source,//目标进程的id值;
	int tag,//消息标签;
	MPI_Comm communicator,//通信子;
	MPI_Status *status_p);//status_p对象,包含实际接收到的消息的有关信息
*/
#include <stdio.h>
#include <string.h>
#include <mpi.h>

int main(int argc, char **argv)
{
	int myid, numprocs, source;
	MPI_Status status;
	char message[100];

	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &myid);
    MPI_Comm_size(MPI_COMM_WORLD, &numprocs);

    if(myid != 0) {
    	strcpy(message, "hello world!");

    	//your code here
    	MPI_Send(message, strlen(message)+1, MPI_CHAR, 0, myid, MPI_COMM_WORLD);
    	//end of your code
	}
	else { //myid == 0
		for(source=1; source<numprocs; source++) {
			//your code here
			MPI_Recv(message, 100, MPI_CHAR, source, source, MPI_COMM_WORLD, &status);
			//end of your code

			printf("%s\n", message);
		}
	}

	MPI_Finalize();
	return 0;
}
/*
hello world!
hello world!
hello world!
*/

地址偏移量

待续