第四讲 消息传递编程接口 MPI 一 MPI 编程基础
主要内容 MPI 安装 程序编译与运行 MPI 编程基础 MPI 程序基本结构 MPI 数据类型 消息发送和接收 MPI 一些常用函数
MPI 介绍 Message Passing Interface 消息传递编程标准, 目前最为通用的并行编程方式 提供一个高效 可扩展 统一的并行编程环境 MPI 是一个库, 不是一门语言,MPI 提供库函数 / 过程供 C/FORTRAN 调用 MPI 是一种标准或规范的代表而不特指某一个对它的具体实现 迄今为止所有的并行计算机制造商都提供对 MPI 的支持 MPI 是一种消息传递编程模型, 最终目的是服务于进程间通信这一目标
MPI 介绍 MPI 的目标 较高的通信性能 ; 较好的程序可移植性 ; 强大的功能 MPI 的实现 免费版本 1994 年公布 MPI 1.0 标准,1998 年公布 2.0 标准 MPI 1.0:MPICH 1.2.7 MPI 2.0:MPICH2 1.0.7 MPI 商业版本 一些厂商也提供商业版的 MPI 系统, 许多是在 MPICH 的基础上优化产生的
MPI 下载与安装 MPICH 下载 http://www.mpich.org/ MPICH 的安装 参考 MPICH Install Guide MPICH 的使用 参考 MPICH User Guide
MPI 编程基本概念 MPI 进程 MPI 程序中一个独立参与通信的个体 MPI 进程组 MPI 程序中由部分或全部进程构成的有序集合 每个进程都被赋予一个所在进程组中唯一的序号 (rank), 用于在该组中标识该进程, 称为进程号, 取值从 0 开始
MPI 进程与通信器 MPI 通信器 / 通信子 (Communicator) MPI 程序中进程间的通信必须通过通信器进行 通信器分为域内通信器 ( 同一进程组内的通信 ) 和域间通信器 ( 不同进程组的进程间的通信 ) MPI 程序启动时自动建立两个通信器 : MPI_COMM_WORLD : 包含程序中所有 MPI 进程 MPI_COMM_SELF : 有单个进程独自构成, 仅包含自己 MPI 程序中, 一个 MPI 进程由一个进程组和在该组中的进程号唯一确定 ; 或由一个通信器和在该通信器中的进程号唯一确定 进程号是相对进程组或通信器而言的, 同一进程在不同的进程组或通信器中可以有不同的进程号
MPI 消息 进程号是在进程组或通信器被创建时赋予的 空进程 :MPI_PROC_NULL 与空进程通信时不做任何操作 消息 (message) 一个消息指进程间进行的一次数据交换 一个消息由通信器 源地址 目的地址 消息标签 和数据构成
第一个 MPI Fortran 程序 program main include 'mpif.h' character * (MPI_MAX_PROCESSOR_NAME) proc_name integer myid, numprocs, namelen, rc, ierr call MPI_INIT( ierr ) call MPI_COMM_RANK( MPI_COMM_WORLD, myid, ierr ) call MPI_COMM_SIZE( MPI_COMM_WORLD, numprocs, ierr ) call MPI_GET_PROCESSOR_NAME(proc_name, namelen, ierr) write(*,10) myid, numprocs, proc_name 10 FORMAT('Hello, I am Proc. ',I2,' of ',I1,' on ', 20A) call MPI_FINALIZE(ierr) end mpif.h 是 MPI 相对于 FORTRAN 的头文件
MPI 程序分析 所有包含 MPI 调用的程序必须包含 MPI 头文件 MPI_MAX_PROCESSOR_NAME 是 MPI 预定义的宏, 即 MPI 所允许的机器名字的最大长度 MPI 程序的开始和结束必须是 MPI_INIT 和 MPI_FINALIZE, 分别完成 MPI 的初始化和结束工作 MPI_COMM_RANK 得到本进程的进程号 MPI_COMM_SIZE 得到所有参加运算的进程的个数 MPI_GET_PROCESSOR_NAME 得到运行本进程所在的结点的主机名 进程号取值范围为 0,, np-1
MPI 程序分析 在单个结点 (c1) 上, 开 4 个进程的运行结果 Hello, I am Proc. 1 of 4 on c1 Hello, I am Proc. 0 of 4 on c1 Hello, I am Proc. 2 of 4 on c1 Hello, I am Proc. 3 of 4 on c1 在四个结点上, 开 4 个进程的运行结果 Hello, I am Proc. 1 of 4 on c1 Hello, I am Proc. 0 of 4 on c2 Hello, I am Proc. 2 of 4 on c4 Hello, I am Proc. 3 of 4 on c3
MPI 程序执行过程 启动编译生成的可执行文件 hello 进程 0 进程 1 进程 2 进程 3 MPI_INIT MPI_COMM_RANK myid=0 MPI_GET_PROCESSOR_NAME proc_name=c1 namelen=2 Write hello, I am proc. 0 of 4 on c1............ MPI_INIT MPI_COMM_RANK myid=4 MPI_GET_PROCESSOR_NAME proc_name=c1 namelen=2 Write hello, I am proc. 3 of 4 on c1 MPI_FINALIZE MPI_FINALIZE 程序运行结束
第一个 MPI C 程序 #include "mpi.h" #include <stdio.h> #include <math.h> mpi.h 是 MPI 相对于 C 语言的头文件 int main(argc,argv) int argc; char *argv[]; { int myid, np; int namelen; char proc_name[mpi_max_processor_name]; MPI_Init(&argc,&argv); MPI_Comm_rank(MPI_COMM_WORLD,&myid); MPI_Comm_size(MPI_COMM_WORLD,&numprocs); MPI_Get_processor_name(proc_name,&namelen); fprintf(stderr,"hello, I am proc. %d of %d on %s\n", myid, np, proc_name); MPI_Finalize(); }
MPI 编程的一些惯例 MPI 的所有常量 变量与函数 / 过程均以 MPI_ 开头 MPI 的 C 语言接口为函数,FORTRAN 接口为 SUBROUTINE, 且对应接口的名称相同 在 C 程序中, 所有常数的定义除下划线外一律由大写字母组成, 在函数和数据类型定义中, 接 MPI_ 之后的第一个字母大写, 其余全部为小写字母, 即 MPI_Xxxx_xxx 形式 对于 FORTRAN 程序,MPI 函数全部以过程方式调用, 一般全用大写字母表示, 即 MPI_XXXX_XXX 形式 (FORTRAN 不区分大小写 ) 除 MPI_WTIME 和 MPI_WTICK 外, 所有 C 函数调用之后都将返回一个错误信息码, 而 MPI 的所有 FORTRAN 子程序中都有一个哑元参数 (IERR) 代表错误码
MPI 编程的一些惯例 由于 C 语言的函数调用机制是值传递, 所以 MPI 的所有 C 函数中的输出参数用的都是指针 MPI 是按进程组 (Process Group) 方式工作 : 所有 MPI 程序在开始时均被认为是在通信器 MPI_COMM_WORLD 所拥有的进程组中工作, 之后用户可以根据自己的需要, 建立其它的进程组 所有 MPI 的通信一定要在通信器中进行 FORTRAN 的数组下标缺省以 1 开始, 而 C 语言的数组是以 0 开始
MPI 数据类型 MPI 除了提供 C 语言函数和 FORTRAN 子程序之外, 还定义了一组常量和 C 变量数据类型 MPI 常量命名规则 : 全部大写 MPI 变量数据类型命名规则 : 以 MPI_ 开头, 后面跟 C 语言或 FORTRAN 原始数据类型名称 MPI 数据类型 : 原始数据类型和自定义数据类型
MPI 原始数据类型 FORTRAN 77 数据类型 MPI datatype MPI_INTEGER MPI_REAL MPI_DOUBLE_PRECISION MPI_COMPLEX MPI_DOUBLE_COMPLEX MPI_LOGICAL MPI_CHARACTER MPI_BYTE MPI_PACKED Fortran datatype INTEGER REAL DOUBLE PRECISION COMPLEX DOUBLE COMPLEX LOGICAL CHARACTER(1)
MPI 原始数据类型 C MPI datatype MPI_CHAR MPI_SHORT MPI_INT MPI_LONG MPI_UNSIGNED_CHAR MPI_UNSIGNED_SHORT MPI_UNSIGNED_INT MPI_UNSIGNED_LONG MPI_FLOAT MPI_DOUBLE MPI_LONG_DOUBLE MPI_BYTE MPI_PACKED C datatype signed char signed short int signed int signed long int unsigned char unsigned short int unsigned int unsigned long int float double long double
MPI 原始数据类型 某些 MPI 系统还支持一些附加数据类型 附加的 MPI 数据类型 MPI_LONG_LONG_INT long 相应的 C 数据类型 long int 附加的 MPI 数据类型 MPI_REAL2 MPI_REAL4 MPI_REAL8 MPI_INTEGER1 MPI_INTEGER2 MPI_INTEGER4 相应的 FORTRAN77 数据类型 REAL*2 REAL*4 REAL*8 INTEGER*1 INTEGER*2 INTEGER*4
MPI 最常用的六个接口 MPI_INIT MPI_FINALIZE MPI_COMM_RANK MPI_COMM_SIZE MPI_SEND MPI_RECV
MPI_INIT MPI_INIT:MPI 初始化 参数 C F77 无 int MPI_Init(int *argc, char ***argv) MPI_INIT(IERR) INTEGER IERR 该函数初始化 MPI 并行程序的执行环境, 它必须在调用所有其它 MPI 函数 ( 除 MPI_INITIALIZED) 之前被调用, 并且在一个 MPI 程序中, 只能被调用一次
MPI_FINALIZE MPI_FINALIZE: 结束 MPI 系统 参数 C F77 无 int MPI_Finalize(void) MPI_FINALIZE(IERR) INTEGER IERR 该函数清除 MPI 环境的所有状态 即一但它被调用, 所有 MPI 函数都不能再调用, 其中包括 MPI_INIT
MPI_COMM_RANK MPI_COMM_RANK(comm, rank) 参数 C F77 IN comm 通信器 OUT rank 本进程在通信器 comm 中的进程号 int MPI_Comm_rank(MPI_Comm comm, int *rank) MPI_COMM_RANK(COMM, RANK, IERR) INTEGER COMM, RANK, IERR 该函数返回本进程在指定通信器中的进程号
MPI_COMM_SIZE MPI_COMM_SIZE(comm, size) 参数 C F77 IN comm 通信器 OUT size 该通信器 comm 中的进程数 int MPI_Comm_size(MPI_Comm comm, int *size) MPI_COMM_SIZE(COMM, SIZE, IERR) INTEGER COMM, SIZE, IERR 该函数返回指定通信器所包含的进程数
MPI_SEND 点对点通信 MPI_SEND(buf,count,datatype,dest,tag,comm) 参数 C F77 IN buf 所发送消息的首地址 IN count 将发送的数据的个数 IN datatype 发送数据的数据类型 IN dest 接收消息的进程的标识号 IN tag 消息标签 IN comm 通信器 int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm) MPI_SEND(BUF,COUNT,DATATYPE,DEST,TAG, COMM, IERR) <type> BUF(*) INTEGER COUNT,DATATYPE,DEST,TAG,COMM,IERR
MPI_SEND 点对点通信 MPI_SEND(buf,count,datatype,dest,tag,comm) 阻塞型消息发送接口 MPI_SEND 将缓冲区中 count 个 datatype 类型的数据发给进程号为 dest 的目的进程 这里 count 是元素个数, 即指定数据类型的个数, 不是字节数, 数据的起始地址为 buf 本次发送的消息标签是 tag, 使用标签的目的是把本次发送的消息和本进程向同一目的进程发送的其它消息区别开来 其中 dest 的取值范围为 0~np-1 ( np 表示通信器 comm 中的进程数 ) 或 MPI_PROC_NULL,tag 的取值为 0~ MPI_TAG_UB 该函数可以发送各种类型的数据, 如整型 实型 字符等 点对点通信是 MPI 通信机制的基础
MPI_RECV 点对点通信 MPI_RECV(buf,count,datatype,source,tag,comm,status) 参数 C F77 OUT buf 接收消息数据的首地址 IN count 接收数据的最大个数 IN datatype 接收数据的数据类型 IN source 发送消息的进程的标识号 IN tag 消息标签 IN comm 通信器 OUT status 返回状态 int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag,mpi_comm comm,mpi_status *status) MPI_RECV(BUF, COUNT, DATATYPE, SOURCE, TAG, COMM, STATUS, IERR) <type> BUF(*) INTEGER COUNT, DATATYPE, SOURCE, TAG, COMM, IERR, STATUS(MPI_STATUS_SIZE)
MPI_RECV 点对点通信 MPI_RECV(buf,count,datatype,source,tag,comm,status) 阻塞型消息接收接口 从指定的进程 source 接收不超过 count 个 datatype 类型的数据, 并把它放到缓冲区中, 起始位置为 buf, 本次消息的标识为 tag 这里 source 的取值范围为 0 ~np-1, 或 MPI_ANY_SOURCE, 或 MPI_PROC_NULL,tag 的取值为 0~MPI_TAG_UB 或 MPI_ANY_TAG 接收消息时返回的状态 STATUS, 在 C 语言中是用结构定义的, 在 FORTRAN 中是用数组定义的, 其中包括 MPI_SOURCE,MPI_TAG 和 MPI_ERROR 此外 STATUS 还包含接收消息元素的个数, 但它不是显式给出的, 需要用到后面给出的函数 MPI_GET_COUNT
MPI 程序示例 例 : 将每个进程中某个数据发送给下一个进程, 并从前一个进程接收一个数据, 即 0 号进程给 1 号进程发送一个数据, 并从 np-1 号进程接收一个数据,1 号进程从 0 号进程接收一个数据, 并向 2 号进程发送一个数据, 以此类推 实现方式一 : 0 号进程先发送后接收, 其它进程先接收后发送 例 :ex4sendrecv01.f, ex4sendrecv02.f 实现方式二 : 奇数号进程先发送后接收, 偶数号进程先接收后发送 上机作业 在使用阻塞型函数传递消息时要避免死锁!
MPI 发送接收 MPI_SEND(buf,count,datatype,dest,tag,comm) 消息数据 消息信封 MPI_RECV(buf,count,datatype,source,tag,comm,status) 消息数据 消息信封 发送数据类型 通信函数中的数据类型 接收的数据类型要一致! C: 结构 status.mpi_source status.mpi_tag status.mpi_error FORTRAN: 数组 status(mpi_source) status(mpi_tag) status(mpi_error)
发送与接收组合进行 MPI_SENDRECV(sendbuf, sendcount, sendtype, dest, sendtag, recvbuf, recvcount, recvtype, source, recvtag, comm, status) 参数略 C F77 int MPI_Sendrecv(void *sendbuf, int sendcount, MPI_Datatype sendtype, int dest, int sendtag, void *recvbuf, int recvcount, MPI_Datatype recvtype, int source, int recvtag, MPI_Comm comm, MPI_Status *status) MPI_SENDRECV(SENDBUF, SENDCOUNT, SENDTYPE, DEST, SENDTAG, RECVBUF, RECVCOUNT, RECVTYPE, SOURCE, RECVTAG, COMM, STATUS,IERR) <type> INTEGER SENDBUF(*), RECVBUF(*) SENDCOUNT, SENDTYPE, DEST, SENDTAG, RECVCOUNT, RECVTYPE, SOURCE, RECVTAG, COMM, IERR, STATUS(MPI_STATUS_SIZE)
MPI_SENDRECV MPI_SENDRECV(sendbuf, sendcount, sendtype, dest, sendtag, recvbuf, recvcount, recvtype, source, recvtag, comm, status) 这是发送消息和接收消息组合在一起的一个函数, 好处是不用考虑先发送还是先接收消息, 从而可以避免消息传递过程中的死锁 sendbuf 和 recvbuf 必须指向不同的缓冲区 例 :ex4sendrecv03.f
MPI_SENDRECV_REPLACE MPI_SENDRECV_REPLACE(buf, datatype, dest, sendtag, source, recvtag, comm, status) 功能与 MPI_SENDRECV 类似, 但收发消息使用的是同一个缓冲区 例 :ex4sendrecv04.f
MPI 程序示例 例 : 计算 π 的值 1 n 4 π = d x h f( x ) 0 2 i x + 1 采用等步长中矩形公式, 其中 n 为积分区间数,h = 1/n 为步长,x i = (i + 0.5) h 为积分区间的中点 假设采用 p 的进程同时计算, 各自计算其中的一部分, 然后再将结果加起来 i= 1 例 :ex4pi.f mpif77 -Wno-globals o ex4pi ex4pi.f
MPI_GET_COUNT MPI_GET_COUNT(status, datatype, count) 参数 C F77 IN status IN datatype 接收操作的返回状态接收操作使用的数据类型 OUT count 接收到的数据个数 int MPI_Get_count(MPI_Status *status, MPI_Datatype datatype, int *count) MPI_GET_COUNT(STATUS,DATATYPE,COUNT,IERR) INTEGER DATATYPE,COUNT,IERR, STATUS(MPI_STATUS_SIZE) 查询实际接收到的数据信息
MPI 异常终止 MPI_ABORT(comm, errorcode) 参数 IN comm OUT errorcode 通信器 错误码 C F77 int MPI_Abort(MPI_Comm comm,int errorcode) MPI_ABORT(COMM, ERRORCODE, IERR) INTEGER COMM, ERRORCODE, IERR 异常终止 MPI 程序的执行,MPI 系统会尽量设法终止通信器中的所有进程
MPI 计时函数 MPI_WTIME() 参数 C F77 无 double MPI_Wtime(void) DOUBLE PRECISION MPI_WTIME() 该函数返回当前的墙钟时间
MPI 计时函数 MPI_WTICK() 参数 C F77 无 double MPI_Wtick(void) DOUBLE PRECISION MPI_WTICK() 该函数返回 MPI_WTIME 的时钟精度, 单位为秒 MPI_WTIME 和 MPI_WTICK 是 C 中仅有的两个返回双精度值而非整型错误码的 MPI 函数 ; 它们也是 FORTRAN 中仅有的两个 FUNCTION 形式的接口!
获取结点主机名 MPI_GET_PROCESSOR_NAME(name, namelen) 参数 C F77 OUT name 结点主机名 OUT namelen 主机名的长度 int MPI_Get_processor_name(char *name, int *namelen) MPI_GET_PROCESSOR_NAME(NAME, NAMELEN, IERR) CHARACTER(*) NAME INTEGER NAMELEN, IERR 该函数返回进程所在结点的主机名
获取 MPI 版本号 MPI_MPI_GET_VERSION(version, subver) 参数 C F77 OUT version 主版本号 OUT subver 次版本号 int MPI_Get_version(int *version, int *subver ) MPI_GET_VERSION(VERSION, SUBVER, IERR) INTEGER VERSION, SUBVER, IERR 该函数返回 MPI 的版本号