目录
一、environment setup for MPI programming
五、collective communication broadcast/reduce pi estimation in MPI and MPI+OMP
一、environment setup for MPI programming
安装mpich:首先在课程平台下载mpich的安装包“MPICH2 for Win64 (mpich2-1.4.1p1-win-x86-64.msi)”。以管理员的身份打开命令提示符,进入文件安装包所在目录后使用指令“msiexec /package mpich2-1.4.1p1-win-x86-64.msi”,安装mpich。安装完毕后,添加目录下的bin文件所在路径到电脑的环境变量中:右键“我的电脑”à选择“属性”à选择“高级系统设置”à“环境变量”——将bin文件路径复制到系统变量和环境变量的path目录下,如果已经存在,不需要复制,不存在的要新建并添加路径。使用带有管理员身份的电脑账户通过cmd运行注册文件,输入电脑账号密码。最后输入“mpiexec -validate” ,验证是否安装成功,显示“it should return SUCCESS”,则意味着安装成功。
验证测试mpich:以管理员的身份运行wmpiexec窗口,选择examples目录下自带的cpi.exe文件进行测试,点击execute开始执行,显示输出“Enter the number of intervals: (0 quits) “则成功。
在vs2022配置mpich:打开创建好的项目,点击项目à属性àC/C++à附加包含目录à添加安装mpich的路径下的include目录。项目à属性à链接器à常规à附加库目录à添加安装的mpich路径下的lib文件夹目录。项目à属性à链接器à输入à附加依赖项à添加mpi.lib
MPI (Message Passing Interface) 是一种并行计算中常用的编程模型,而 MPICH2 是其中一个开源实现。它利用的是Shared distributed memory,即每个存储空间的地址都是0开始的,有多个存储空间,每个memory离每个cpu的距离都不一样的近,每个cpu都可以调用所有的memory。而上一次使用的openMP则是Non-shared-memory,即一个存储空间,地址从零开始后面地址连续,内容不对每个线程共享。
在 Windows 系统上安装MPICH2的过程中需要管理员权限,否则将安装失败,这是因为安装过程中需要进行系统设置和文件访问等操作,而这些操作需要管理员权限才能完成,比如配置系统环境变量和安装MPICH2相关的服务和驱动程序。
二、MPI Environment
1. #include<stdio.h>
2. #include"mpi.h"
3. int main()
4. {
5. int size, rank, namelength;
6. char name[MPI_MAX_PROCESSOR_NAME];
7. MPI_Init(NULL, NULL);
8. MPI_Comm_size(MPI_COMM_WORLD, &size);
9. MPI_Comm_rank(MPI_COMM_WORLD, &rank);
10. MPI_Get_processor_name(name, &namelength);
11. printf("size=%d rank=%d name=%s len=%d \n", size, rank, name, namelength);
12. printf("myid=%d\n", rank);
13. fflush(stdout);
14. MPI_Finalize();
15. return 0;
16. }
任何一个MPI程序在调用MPI函数之前,首先调用的是初始化函数MPI_Init(int *argc, char ***argv),以建立MPU的执行环境;MPI_Comm_size(MPI_Comm, int *size)函数可以获取通信域包含的进程数;MPI_Comm_rank(MPI_Comm comm, int *rank)函数可以获取当前进程标识;同时MPI程序的最后一个调用必须为:MPI_Finalize()函数以结束MPI程序的执行。
在配置好执行mpi程序后,输出显示当前mpi程序的运行环境。在本机vs中执行时默认线程数为1,所以输出显示为通信域包含线程数1,当前进程标号为0,当前电脑用户名及名字长度,和myid(即当前执行本条指令的进程号)。而在华为泰山服务器中运行时,由于运行时可以直接设置线程数,当设置为4时,4个线程并行执行程序一次,输出四次通信域进程数4,分别输出当前进程标号0-3,服务器名kunpeng191及长度10,和myid。
三、blocking Send/Receive
1. #include<stdio.h>
2. #include"mpi.h"
3. int main()
4. {
5. MPI_Status status;
6. char string[] = "xxxxx";
7. char str[] = "HELLO";
8. int myid;
9. MPI_Init(NULL, NULL);
10. MPI_Comm_rank(MPI_COMM_WORLD, &myid);
11. printf("myid=%d\n", myid);
12. if (myid == 2)
13. MPI_Send(str, 5, MPI_CHAR, 7, 1234, MPI_COMM_WORLD);
14. if (myid == 7) {
15. MPI_Recv(string, 5, MPI_CHAR, 2, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
16. printf("Got %s from P%d, tag %d\n", string, status.MPI_SOURCE, status.MPI_TAG);
17. fflush(stdout);
18. }
19. MPI_Finalize();
20. return 0;
21. }
函数MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)(buf:发送缓存的起始地址、count:发送的数据个数、datatype:数据类型、dest:目的进程标识号、Tag:消息标志、comm:通信域)可以令源进程将缓存中的数据发送到目的进程;而函数MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)(buf:接收缓存的起始地址、count:最多可接收的数据个数、datatype:数据类型、source:发送数据的进程标识号、Tag:消息标志、comm:通信域、Status:返回状态)可以使得目的进程从源进程接受数据,存入缓存中。
这段程序代码在第二个进程中将“HELLO”作为数据发送到第七个进程中。由于本机执行时默认为1个线程工作,所以没有进行数据的收发,而在服务器上指定16线程工作时,第二进程向第七进程发送,在第七进程接受数据并输出。
四、nonblocking send/receive
1. #include<stdio.h>
2. #include"mpi.h"
3. #define N 100000
4. int main()
5. {
6. int numtasks, rank, dest, source, tag = 1234;
7. char inmsg[] = "xxxxx", outmsg[] = "HELLO";
8. MPI_Status stats[2];
9. MPI_Request reqs[2];
10. MPI_Init(NULL, NULL);
11. MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
12. MPI_Comm_rank(MPI_COMM_WORLD, &rank);
13. if (rank == 0){
14. dest = 1;
15. MPI_Isend(&outmsg, 5, MPI_CHAR, dest, tag, MPI_COMM_WORLD, &reqs[0]);
16. printf("Task %d: Send %s while inmas=%s \n", rank, outmsg, inmsg);
17. fflush(stdout);
18. MPI_Wait(&reqs[0], &stats[0]);
19. printf("Task %d: send %s while inmsg=%s reqs[0]=%d \n", rank, outmsg, inmsg, reqs[0]);
20. fflush(stdout);
21. }
22. else if (rank == 1) {
23. source = 0;
24. MPI_Irecv(&inmsg, 5, MPI_CHAR, source, tag, MPI_COMM_WORLD, &reqs[1]);
25. printf("Task %d: Received %s \n", rank, inmsg);
26. fflush(stdout);
27. MPI_Wait(&reqs[1], &stats[1]);
28. printf("Task %d: Received %s reqs[1]=%d\n", rank, inmsg, reqs[1]);
29. fflush(stdout);
30. }
31. MPI_Finalize();
32. return 0;
33. }
Mpi程序在进行数据的收发时,在成功进行数据接受前,线程将不会进行其他的工作,此时线程的资源将会被部分浪费,在这种情况下,可以使用非阻塞通信来重叠计算和通信,从而使应用程序更高效。使用函数MPI_Isend (*buf, count, datatype, destination, tag, comm, MPI_Request *request)进行发送数据,并使用函数MPI_Irecv(*buf, count, datatype, source, tag, comm, MPI_Request *request)接受数据,而如果MPI未实现进度线程,则在调用函数MPI_Wait(MPI_Request *request, MPI_Status *status)之前将不会收到任何消息。
非阻塞式的通信,使得发送和计算可以重叠,发送被接受即刻计算,节约资源,最大化利用线程资源。
五、collective communication broadcast/reduce pi estimation in MPI and MPI+OMP
//MPI
1. #include<stdio.h>
2. #include"mpi.h"
3. #define N 100000
4. int main()
5. {
6. int myid, numproces, i, n;
7. double mypi, pi, h, sum, x, start, end;
8. n = N;
9. MPI_Init(NULL, NULL);
10. MPI_Comm_size(MPI_COMM_WORLD, &numproces);
11. MPI_Comm_rank(MPI_COMM_WORLD, &myid);
12. MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
13. h = 1.0 / n;
14. sum = 0.0;
15. start = MPI_Wtime();
16. for (i = myid + 1; i <= N; i += numproces) {
17. x = h * ((double)i - 0.5);
18. sum += (4.0 / (1.0 + x * x));
19. }
20. mypi = h * sum;
21. MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
22. if (myid == 0) {
23. end = MPI_Wtime();
24. printf("pi is approximately %.16f\n", pi);
25. printf("time is %.16f\n", end - start);
26. fflush(stdout);
27. }
28. MPI_Finalize();
29.
30. return 0;
31. }
//MPI+OMP
1. #include<stdio.h>
2. #include<omp.h>
3. #include"mpi.h"
4. #define N 100000
5. #define NUM_THREADS 16
6. int main()
7. {
8. int myr, numprocs, i, myn, fi, li, n;
9. double my_pi = 0.0, pi, h, x, mypi, start, end;
10. MPI_Init(NULL, NULL);
11. MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
12. MPI_Comm_rank(MPI_COMM_WORLD, &myr);
13. start = MPI_Wtime();
14. n = N;
15. h = 1.0 / n;
16. myn = n / numprocs;
17. fi = myr * myn;
18. li = fi + myn;
19. omp_set_num_threads(NUM_THREADS);
20. #pragma omp parallel for reduction(+:my_pi)private(x,i)
21. for (i = fi; i < li; i++)
22. {
23. x = (i + 0.5) * h;
24. my_pi = my_pi + 4.0 / (1.0 + x * x);
25. }
26. MPI_Reduce(&my_pi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
27. if (myr == 0)
28. {
29. mypi = h * pi;
30. end = MPI_Wtime();
31. printf("pi is approximately %.16f\n", mypi);
32. printf("time is %.16f\n", end - start);
33. fflush(stdout);
34. }
35. MPI_Finalize();
36. return 0;
37. }
函数MPI_Bcast(void* buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm)(buffer:通信消息缓存的起始地址、count:数据个数、datatype:数据类型、root:根进程的标识号、信息源进程标识号、comm:通信域)可以将根进程的消息广播到组中的所有其他进程,实现对于所有进程中的某数据的值更新,其既是发送方调用,也是接收方调用。函数MPI_Reduce(void* sendbuf, void* recvbuf, int count, MPI_Datatype, MPI_Op op, int root, MPI_Comm comm)(sendbuf:发送消息缓存的起始地址、recvbuf:接收消息缓存的起始地址、count:发送缓存中的数据个数、op:归约操作符、root:根进程序列号、comm:通信域)可以将每个进程缓冲区中的数据按给定的操作进行计算,并将计算结果返回到根进程的输出缓冲区中。
可以使用函数MPI_Wtime()来获取并计算程序运行时间。其返回值为double类型数据。
MPI中预置的归约操作符号如下图:(用户也可以使用MPI_Op_create自定义归约操作)
服务器中无法使用函数omp_set_num_threads来设定工作线程数,将会出现如下图所示报错。
服务器中的工作线程数可以直接通过执行编译后的文件时,使用的指令来修改,如mpiexec -n 16 ./mpiomp.out,其中的16即为线程数。
实验过程中发现两台服务器并行执行使用MPI求pi值的程序时,时间并无明显变化,原因应该为运算时间较短,无明显差距。
使用MPI+OMP的程序求pi值时,程序运行时间大于单独使用MPI求pi值,两种并行计算库的同步使用在较为简单的求取pi值上无明显优势,多台服务器并行执行MPI+OMP程序也无速度的明显提升。