MPI并行程序设计简介 曙光信息产业(北京)有限公司 2018年11月
讲座内容提示 基本概念 实例 基本的MPI 点到点通信(Point to point) MPI程序的编译和运行 MPI中API的主要内容,为MPI最基本,最重要的内容 MPI程序的编译和运行 实例 2018年11月
参考文献 MPI--the complete reference. Marc Snir, MIT Press, 1998. ISBN 0262692155, 0262692163. Using MPI : portable parallel programming with the message-passing interface, William Gropp, MIT Press, 1999. 2nd edition. ISBN 0262571323. Using MPI-2 : advanced features of the message-passing interface. William Gropp, MIT Press, 1999. ISBN 0262571331. 高性能计算并行编程技术-MPI并行程序设计,都志辉,清华大学出版社, 2001年8月。 2018年11月
并行编程标准 多线程库标准 编译制导标准 消息传递库标准 – Win32 API. – POSIX threads. – OpenMP – 可移植共享存储并行编程标准. 消息传递库标准 – MPI – PVM 本讨论的重点 2018年11月
消息传递并行程序设计 消息传递并行程序设计 并行计算粒度大,特别适合于大规模可扩展并行算法 指用户必须通过显式地发送和接收消息来实现处理机间的数据交换。 在这种并行编程中,每个并行进程均有自己独立的地址空间,相互之间访问不能直接进行,必须通过显式的消息传递来实现。 这种编程方式是大规模并行处理机(MPP)和机群(Cluster)采用的主要编程方式。 并行计算粒度大,特别适合于大规模可扩展并行算法 由于消息传递程序设计要求用户很好地分解问题,组织不同进程间的数据交换,并行计算粒度大,特别适合于大规模可扩展并行算法. 消息传递是当前并行计算领域的一个非常重要的并行程序设计方式 2018年11月
什么是MPI? Massage Passing Interface:是消息传递函数库的标准规范,由MPI论坛开发,支持Fortran和C 2018年11月
MPI的发展过程 发展的两个阶段 MPI 1.1: 1995 MPICH:是MPI最流行的非专利实现,由Argonne国家实验室和密西西比州立大学联合开发,具有更好的可移植性. MPI 1.2~2.0:动态进程, 并行 I/O, 远程存储访问、支持F90和C++(1997). 2018年11月
为什么要用MPI? 高可移植性 MPI已在IBM PC机上、MS Windows上、所有主要的Unix工作站上和所有主流的并行机上得到实现。使用MPI作消息传递的C或Fortran并行程序可不加改变地运行在IBM PC、MS Windows、Unix工作站、以及各种并行机上。 2018年11月
:从简单入手 —Init和Finalize 下面我们首先分别以C语言和Fortran语言的形式给出一个最简单的MPI并行程序Hello (下页). 该程序在终端打印出Hello World!字样. “Hello World”:一声来自新生儿的问候. 2018年11月
Hello world(C) #include <stdio.h> #include "mpi.h“ main( int argc, char *argv[] ) { MPI_Init( &argc, &argv ); printf( "Hello, world!\n" ); MPI_Finalize(); } 2018年11月
Hello world(Fortran) program main include ‘mpif.h’ integer ierr call MPI_INIT( ierr ) print *, 'Hello, world!' call MPI_FINALIZE( ierr ) end 2018年11月
C和Fortran中MPI函数约定 C Fortran MPI函数的参数被标志为以下三种类型: 必须包含mpi.h. MPI 函数返回出错代码或 MPI_SUCCESS成功标志. MPI-前缀,且只有MPI以及MPI_标志后的第一个字母大写,其余小写. Fortran 必须包含mpif.h. 通过子函数形式调用MPI,函数最后一个参数为返回值. MPI-前缀,且函数名全部为大写. MPI函数的参数被标志为以下三种类型: IN:参数在例程的调用中不会被修正. OUT:参数在例程的调用中可能会被修正. INOUT:参数有初始值,且在例程的调用中可能会被修正 2018年11月
MPI初始化-MPI_INIT int MPI_Init(int *argc, char **argv) MPI_INIT(IERROR) MPI_INIT是MPI程序的第一个调用,它完成MPI程序的所有初始化工作。所有的MPI程序的第一条可执行语句都是这条语句。 启动MPI环境,标志并行代码的开始. 并行代码之前,第一个mpi函数(除MPI_Initialized()外). 要求main必须带参数运行,否则出错. 2018年11月
MPI结束-MPI_FINALIZE int MPI_Finalize(void) MPI_FINALIZE(IERROR) MPI_FINALIZE是MPI程序的最后一个调用,它结束MPI程序的运行,它是MPI程序的最后一条可执行语句,否则程序的运行结果是不可预知的。 标志并行代码的结束,结束除主进程外其它进程. 之后串行代码仍可在主进程(rank = 0)上运行(如果必须). 2018年11月
MPI程序的的编译与运行 %小写o mpif77 hello.f 或 mpicc hello.c 默认生成a.out的可执行代码. mpif77 –o hello hello.f 或 mpicc –o hello hello.c 生成hello的可执行代码. mpirun –np 4 a.out mpirun –np 4 hello 4 指定np的实参,表示进程数,由用户指定. a.out / hello 要运行的MPI并行程序. %小写o np: The number of process. 2018年11月
:运行我们的MPI程序! [dair@node01 ~]$ mpicc -o hello hello.c [dair@node01 ~]$ ./hello () [0] Aborting program ! Could not create p4 procgroup. Possible missing fileor program started without mpirun. [dair@node01 ~]$ mpirun -np 4 hello () Hello World! [dair@node01 ~]$ 计算机打印字符 我们输入的命令 2018年11月
:Hello是如何被执行的? SPMD: Single Program Multiple Data(SIMD) :::: rsh\ssh #include "mpi.h" #include <stdio.h> main( int argc, char *argv[] ) { MPI_Init( &argc, &argv ); printf( "Hello, world!\n" ); MPI_Finalize(); } #include "mpi.h" #include <stdio.h> main( int argc, char *argv[] ) { MPI_Init( &argc, &argv ); printf( "Hello, world!\n" ); MPI_Finalize(); } #include "mpi.h" #include <stdio.h> main( int argc, char *argv[] ) { MPI_Init( &argc, &argv ); printf( "Hello, world!\n" ); MPI_Finalize(); } #include "mpi.h" #include <stdio.h> main( int argc, char *argv[] ) { MPI_Init( &argc, &argv ); printf( "Hello, world!\n" ); MPI_Finalize(); } #include "mpi.h" #include <stdio.h> main( int argc, char *argv[] ) { MPI_Init( &argc, &argv ); printf( "Hello, world!\n" ); MPI_Finalize(); } Hello World! 2018年11月
:开始写MPI并行程序 —Comm_size和Comm_rank 任务由多少个进程来进行并行计算? 我是哪一个进程? 2018年11月
MPI 提供了下列函数来回答这些问题: 用MPI_Comm_size 获得进程个数 p int MPI_Comm_size(MPI_Comm comm, int *size); 用MPI_Comm_rank 获得进程的一个叫rank的值,该 rank值为0到p-1间的整数,相当于进程的ID int MPI_Comm_rank(MPI_Comm comm, int *rank); 2018年11月
更新的Hello World(c) #include <stdio.h> #include "mpi.h" main( int argc, char *argv[] ) { int myid, numprocs; MPI_Init( &argc, &argv ); MPI_Comm_rank( MPI_COMM_WORLD, &myid ); MPI_Comm_size( MPI_COMM_WORLD, &numprocs ); printf(“I am %d of %d\n", myid, numprocs ); MPI_Finalize(); } 2018年11月
更新的Hello World(F77) program main include ‘mpif.h’ integer ierr, myid, numprocs call MPI_INIT( ierr ) call MPI_COMM_RANK( MPI_COMM_WORLD, myid, ierr ) call MPI_COMM_SIZE( MPI_COMM_WORLD, numprocs, ierr ) print *, ‘I am', myid, ‘of', numprocs call MPI_FINALIZE( ierr ) end 2018年11月
:运行结果 [dair@node01 ~]$ mpicc –o hello1 hello1.c [dair@node01 ~]$ mpirun -np 4 hello1 I am 0 of 4 I am 1 of 4 I am 2 of 4 I am 3 of 4 [dair@node01 ~]$ 计算机打印字符 我们输入的命令 2018年11月
:写MPI并行通信程序 --Send和Recv Greeting执行过程 2018年11月
有消息传递greetings(c) #include <stdio.h> #include "mpi.h" main(int argc, char* argv[]) { int numprocs, myid, 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); 2018年11月
有消息传递greetings(c) if (myid != 0) { strcpy(message, "Hello World!"); MPI_Send(message,strlen(message)+1, MPI_CHAR, 0,99, MPI_COMM_WORLD); } else {/* myid == 0 */ for (source = 1; source < numprocs; source++) { MPI_Recv(message, 100, MPI_CHAR, source, 99, MPI_COMM_WORLD, &status); printf("%s\n", message); } MPI_Finalize(); } /* end main */ 2018年11月
解剖greetings程序 头文件: mpi.h/mpif.h. int MPI_Init(int *argc, char ***argv) 并行代码之前,第一个mpi函数(除MPI_Initialize()外). 要求main必须带参数运行,否则出错. 通信域(通信空间): MPI_COMM_WORLD: 一个通信空间是一个进程组和一个上下文的组合.上下文可看作为组的超级标签,用于区分不同的通信域. 在执行函数MPI_Init之后,一个MPI程序的所有进程形成一个缺省的组,这个组的通信域即被写作MPI_COMM_WORLD. 该参数是MPI通信操作函数中必不可少的参数,用于限定参加通信的进程的范围. 2018年11月
解剖greetings程序 int MPI_Comm_size ( MPI_Comm comm, int *size ) 指定一个communicator,也指定了一组共享该空间的进程, 这些进程组成该communicator的group. int MPI_Comm_rank ( MPI_Comm comm, int *rank ) 得到本进程在通信空间中的rank值,即在组中的逻辑编号(从0开始). int MPI_Finalize() 标志并行代码的结束,结束除主进程外其它进程. 之后串行代码仍可在主进程(rank = 0)上运行(如果必须). 2018年11月
讲座内容提示 基本的MPI 深入MPI 实例 基本概念 点到点通信(Point to point) MPI程序的编译和运行 MPI中API的主要内容,为MPI最基本,最重要的内容 MPI程序的编译和运行 深入MPI 用户自定义(/派生)数据类型(User-defined(Derived) data type) 事实上MPI的所有数据类型均为MPI自定义类型 支持异构系统 允许消息来自不连续的或类型不一致的存储区(结构,数组散元) 集合通信(Collective) 数据移动,数据聚集,同步 基于point to point 构建 MPI环境管理函数 组,上下文和通信空间/通信域的管理 实例 2018年11月
Point to Point通信 单个进程对单个进程的通信,重要且复杂 术语 Blocking(阻塞) :一个例程须等待操作完成才返回,返回后用户可以重新使用调用中所占用的资源. Non-blocking(非阻塞):一个例程不必等待操作完成便可返回,但这并不意味着所占用的资源可被重用. Local(本地):过程的完成仅依赖于本地正在执行的进程。 Non-local(非本地):如果过程的完成要求其他进程的 MPI 过程完成。 2018年11月
Blocking Send int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm); IN buf 发送缓冲区的起始地址 IN count 要发送信息的元素个数 IN datatype 发送信息的数据类型 IN dest 目标进程的rank值 IN tag 消息标签 IN comm 通信域 2018年11月
Blocking Receive int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status); OUT buf 发送缓冲区的起始地址 IN count 要发送信息的元素个数 IN datatype 发送信息的数据类型 IN dest 目标进程的rank值 IN tag 消息标签 IN comm 通信域 OUT status status对象,包含实际接收到的消息的有关信息 2018年11月
MPI消息 MPI消息包括信封和数据两个部分,信封指出了发送或接收消息的对象及相关信息,而数据是本消息将要传递的内容 数据:<起始地址、数据个数、数据类型> 信封:<源/目的、标识、通信域> 2018年11月
2018年11月
消息数据 由count个类型为datatype的连续数据空间组成, 起始地址为buf 不是以字节数, 而是以元素的个数指定消息的长度 MPI基本数据类型相应于宿主语言的基本数据类型 2018年11月
MPI基本数据类型 2018年11月
消息信封 MPI标识一条消息的信息包含四个域: Source: 发送进程隐式确定,由进程的rank值唯一标识 Destination: Send函数参数确定 Tag: Send函数参数确定,用于识别不同的消息 (0,UB),UB:MPI_TAG_UB>=32767. Communicator: 缺省MPI_COMM_WORLD Group:有限/N,有序/Rank [0,1,2,…N-1] Contex:Super_tag,用于标识该通讯空间. 2018年11月
status参数 当使用MPI_ANY_SOURCE或/和MPI_ANY_TAG接收消息时如何确定消息的来源source 和 tag值呢? 在C中,结构,status.MPI_SOURCE, status.MPI_TAG. 在Fortran中,数组,source=status(MPI_SOURCE), tag=status(MPI_TAG). Status还可用于返回实际接收到消息的长度 int MPI_Get_count(MPI_Status status, MPI_Datatype datatype,int* count) IN status 接收操作的返回值. IN datatype 接收缓冲区中元素的数据类型. OUT count 接收消息中的元素个数. 2018年11月
消息匹配 接收buffer必须至少可以容纳count个由datatype参数指明类型的数据. 如果接收buf太小, 将导致溢出、出错. 参数匹配 dest,tag,comm/ source,tag,comm Source == MPI_ANY_SOURCE:接收任意处理器来的数据(任意消息来源). Tag == MPI_ANY_TAG:匹配任意tag值的消息(任意tag消息). Source = destination 是允许的, 但是不安全的, 可能导致死锁。 消息传送被限制在同一个communicator. 在send函数中必须指定唯一的接收者(Push/pull通讯机制). 2018年11月
分析greetings #include <stdio.h> #include "mpi.h“ main(int argc, char* argv[]) { int numprocs; /*进程数,该变量为各处理器中的同名变量, 存储是分布的 */ int myid; /*我的进程ID,存储也是分布的 */ MPI_Status status; /*消息接收状态变量,存储也是分布的 */ char message[100]; /*消息buffer,存储也是分布的 */ /*初始化MPI*/ MPI_Init(&argc, &argv); /*该函数被各进程各调用一次,得到自己的进程rank值*/ MPI_Comm_rank(MPI_COMM_WORLD, &myid); /*该函数被各进程各调用一次,得到进程数*/ MPI_Comm_size(MPI_COMM_WORLD, &numprocs); 2018年11月
分析greetings if (myid != 0) { /*建立消息*/ sprintf(message, "Greetings from process %d!",myid); /* 发送长度取strlen(message)+1,使\0也一同发送出去*/ MPI_Send(message,strlen(message)+1, MPI_CHAR, 0,99,MPI_COMM_WORLD); } else { /* my_rank == 0 */ for (source = 1; source < numprocs; source++) MPI_Recv(message, 100, MPI_CHAR, source, 99, MPI_COMM_WORLD,&status); printf(“%s\n", message); /*关闭MPI,标志并行代码段的结束*/ MPI_Finalize(); } /* End main */ 2018年11月
? Greetings执行过程 % 假设进程数为3 问题:进程1和2谁先开始发送消息?谁先完成发送? (进程0) (进程1) (进程2) (进程0) (进程1) (进程2) (rank=0) (rank=1) (rank=2) . Recv(); . Send(); . Send() ? % 问题:进程1和2谁先开始发送消息?谁先完成发送? 2018年11月
运行greetings [dair@node01 ~]$ mpicc –o greeting greeting.c [dair@node01 ~]$ mpirun -np 4 greeting Greetings from process 1! Greetings from process 2! Greetings from process 3! [dair@node01 ~]$ 计算机打印字符 我们输入的命令 2018年11月
最基本的MPI MPI调用借口的总数虽然庞大,但根据实际编写MPI的经验,常用的MPI调用的个数非常有限。上面介绍的是6个最基本的MPI函数。 MPI_Init(…); MPI_Comm_size(…); MPI_Comm_rank(…); MPI_Send(…); MPI_Recv(…); MPI_Finalize(); MPI_Init(…); … 并行代码; MPI_Fainalize(); 只能有串行代码; 2018年11月
现在您已经能够用MPI进行并行编程了! 2018年11月
实例分析:求PI 2018年11月
串行代码 h=1.0/(double)n; sum=0.0; for (i=1; i<=n; i++) { x=h*((double)i – 0.5); sum += f(x); } pi=h*sum; double f(double a) { return (4.0/(1.0+a*a)); } 2018年11月
并行代码 h=1.0/(double)n; sum=0.0; for (i=myid+1; i<=n; i+=numprocs) { double f(double a) { return (4.0/(1.0+a*a)); } h=1.0/(double)n; sum=0.0; for (i=myid+1; i<=n; i+=numprocs) { x=h*((double)i – 0.5); sum += f(x); } mypi=h*sum; MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); 2018年11月
cpi.c #include "mpi.h" #include <stdio.h> #include <math.h> double f( double ); double f( double a ); { return (4.0 / (1.0 + a*a)); } 2018年11月
cpi.c int main( int argc, char *argv[]) { int done = 0, n, myid, numprocs, i; double PI25DT = 3.141592653589793238462643; double mypi, pi, h, sum, x; double startwtime = 0.0, endwtime; int namelen; char processor_name[MPI_MAX_PROCESSOR_NAME]; MPI_Init(&argc,&argv); MPI_Comm_size(MPI_COMM_WORLD,&numprocs); MPI_Comm_rank(MPI_COMM_WORLD,&myid); MPI_Get_processor_name(processor_name,&namelen); fprintf(stderr,"Process %d on %s\n", myid, processor_name); 2018年11月
cpi.c n = 0; while (!done) { if (myid == 0) if (n==0) n=100; else n=0; startwtime = MPI_Wtime(); } MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD); 2018年11月
cpi.c if (n == 0) done = 1; else { h = 1.0 / (double) n; sum = 0.0; for (i = myid + 1; i <= n; i += numprocs) { x = h * ((double)i - 0.5); sum += f(x); } mypi = h * sum; 2018年11月
cpi.c MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); if (myid == 0) { printf("pi is approximately %.16f, Error is %.16f\n", pi, fabs(pi - PI25DT)); endwtime = MPI_Wtime(); printf("wall clock time = %f\n", endwtime-startwtime); } MPI_Finalize(); return 0; 2018年11月
谢谢! 2018年11月