郎显宇 (lxy@sccas.cn) 中国科学院计算机网络信息中心 超级计算中心 MPI并行编程入门 郎显宇 (lxy@sccas.cn) 中国科学院计算机网络信息中心 超级计算中心 MPI truly is a portable standard Source code portability between machines running MPI this is the “beauty” of having a standard
参考材料 张林波等 《并行计算导论》 清华大学出版社 2006 莫则尧等 《消息传递并行编程环境MPI 》科学出版社 2001
目录 预备知识 MPI基础知识 点对点通信 聚合通信 数据类型 进程组与通信器 拓扑结构 文件输入输出(并行I/0) Point-to-Point improve performance Derived Datatypes send a structure in C send a record in F90 send a collection of non-uniform data types Collective communication group of processes that do something together Both math and communications functions Virtual topologies method of making a collection of processes act like they are in a particular shap
预备知识 并行计算 并行计算机体系结构 并行编程环境 消息传递接口MPI 进程与消息传递
并行计算 并行计算 并行机上进行任务分解,多处理器并行执行子任务 交叉学科,并行计算系统与实际应用的桥梁 包含并行计算机体系结构、并行算法、并行程序设计、并行 软件技术、并行性能优化与评价、并行应用等 并行计算的目的 减少计算时间 增加计算规模 并行计算的基本条件 并行计算机 应用问题具有并行度——开展并行算法设计 并行编程
并行计算机体系结构 共享存储 对称多处理共享存储并行机 SMP 均匀访存(UMA:Uniform Memory Access) 内存与结点分离,存储为所有结点共享 所有结点任意访问存储单元,且时间相同 访存竞争时,仲裁策略平等对待每个处理器 各处理器带有局部高速缓存cache 对称多处理共享存储并行机 SMP
并行计算机体系结构 共享存储 分布共享存储并行机 DSM 非均匀访存(NUMA: Non-uniform Memory Access) 内存模块局部在各个结点内,所有局部内存构成全局内存;即内存在物 理上分布,逻辑上共享,由硬件统一编址 所有结点任意访问各存储单元,但速度和时间不等 访存竞争时,仲裁策略对待每个处理器可能不等价 各处理器带有局部高速缓存cache,以及保持缓存一致性的协议,此又称 cache一致性的非均匀存储——ccNUMA 分布共享存储并行机 DSM
并行计算机体系结构 分布式存储 内存模块物理分布同前,每个结点拥有局部内存模块 各结点存储模块只能被局部CPU访问 访问其他结点内存,可通过消息传递实现 MPP、微机机群各结点之间
并行计算机体系结构 混和存储 结点内部是共享存储模型 结点间是分布式存储模型 微机机群、星群、工作站机群COW、MPP等 深腾7000
并行编程环境 分布式存储系统 共享存储系统 混合式存储系统 消息传递接口 MPI (“Message Passing Interface”) PVM (“Parallel Virtual Machine”) 共享存储系统 OpenMP SMP 多核处理器体系结构 混合式存储系统 MPI+OpenMP混合编程 通过socket网络通信实现 进程通过读写共享数据缓存区实现
并行编程——MPI & OpenMP OpenMP MPI
消息传递接口MPI 什么是MPI (Message Passing Interface) 是函数库规范,而不是并行语言;操作如同库函数调用 是一种标准和规范,而非某个对它的具体实现(MPICH等), 与编程语言无关 是一种消息传递编程模型,并成为这类编程模型的代表 什么是消息(message)? DATA+ENVELOPE MPI的目标 较高的通信性能 较好的程序可移植性 强大的功能
消息传递接口MPI MPI的产生 1992-1994年,MPI 1.1版本问世 1995-1997年,MPI 2.0版本出现 并行I/O 单边通信 动态进程管理等 MPI的语言绑定 Fortran(科学与工程计算) C/C++(系统和应用程序开发) 主要的MPI实现 并行机厂商提供 高校、科研部门 MPICH (http://www.mcs.anl.gov/mpi/mpich ) Open MPI (http://www.open-mpi.org/ ) MVAPICH (http://mvapich.cse.ohio-state.edu/ )
消息传递接口MPI MPI程序编译与运行 C: %mpicc -o mpiprog mpisrc.c Fortran 77: %mpif77 -o mpiprog mpisrc.f 程序运行 %mpirun (mpiexec) -np 4 mpiprog 程序执行过程中不能动态改变进程的个数 申请的进程数np与实际处理器个数无关
进程与消息传递 单个进程(process) 进程与程序相联,程序一旦在操作系统中运行即成为进程。进程拥 有独立的执行环境(内存、寄存器、程序计数器等) 操作系统分配资源的最小单位 串行应用程序编译形成的可执行代码,分为“指令”和“数据”两 个部分,并在程序执行时“独立地申请和占有”内存空间,且所有 计算均局限于该内存空间。 进程1 进程2 内存
进程与消息传递 单机内多个进程 多个进程可同时存在于单机内同一操作系统。操作系统负责 调度分时共享处理机资源(CPU、内存、存储、外设等) 进程间相互独立(内存空间不相交)。在操作系统调度下各 自独立地运行,例如多个串行应用程序在同一台计算机运行 进程间可以相互交换信息。例如数据交换、同步等待,消息 是这些交换信息的基本单位,消息传递是指这些信息在进程 间的相互交换,是实现进程间通信的唯一方式
进程与消息传递 包含于网络联接的不同处理器的多个进程 进程独立存在,并位于不同的处理器,由各自独立的操作系统调度,享 有独立的CPU和内存资源 进程间相互信息交换,可依靠消息传递 最基本的消息传递操作包括发送消息send、接受消息receive、进程同 步barrier、归约reduction等
进程与消息传递 进程与线程 进程 操作系统资源分配的最小单位 资源特征 + 执行特征 比喻为管家 至少包含一个线程 拥有自己的地址空间 线程 操作系统调度执行的最小单位 继承 执行特征 比喻为苦力 属于某个进程 使用进程的地址空间,有私有栈,各线程栈不相交
MPI基础知识 MPI重要概念 MPI函数一般形式 MPI原始数据类型 MPI程序基本结构 MPI几个基本函数
MPI重要概念 进程组(process group) 指MPI 程序的全部进程集合的一个有序子集,进程 组中每个进程被赋予一个在该组中唯一的序号(rank), 用于在该组中标识该进程 。序号的取值范围是[0,进程数- 1] 通信器/通信子(communicator) 基于某个进程组来定义,且在该进程组,进程间可以相互通信 任何MPI通信函数均必须通过通信器进行 MPI系统提供省缺的通信器MPI_COMM_WORLD,所有启动的MPI进程通过 调用函数MPI_Init()包含在该通信器内;各进程通过函数MPI_Comm_size()获 取通信器包含的(初始启动)的MPI进程个数 域内通信器(同一进程组内的通信)和域间通信器(不同进程组进程间的通 信,只具备点对点通信)
MPI重要概念 进程序号(rank) 用来在一个进程组或通信器中标识一个进程 MPI 程序中的进程由进程组或通信器内的序号唯一确定, 序号相对于进程组 或通信器而言(假设np个进程,标号0…np-1) 同一个进程在不同的进程组或通信器中可以有不同的序号,进程的序号是在 进程组或通信器被创建时赋予的 MPI 系统提供了一个特殊的进程序号MPI_PROC_NULL,它代表空进程(不存 在的进程), 与MPI_PROC_NULL 间的通信实际上没有任何作用 消息(message) 分为数据(data)和包装(envelope)两个部分 包装由接收进程序号/发送进程序号、消息标号和通信器三部分组成;数据包 含用户将要传递的内容
MPI重要概念 MPI_COMM_WORLD缺省通信器中进程{1,3,4}成为新 通信器的进程组,但是进程序号不同 SOME_OTHER_COMM 5 1 2 2 3 1 4 MPI_COMM_WORLD缺省通信器中进程{1,3,4}成为新 通信器的进程组,但是进程序号不同
MPI重要概念 MPI对象 MPI系统内部定义的数据结构,包括数据类型(如MPI_INT)、通信器( MPI_Comm)、通信请求(MPI_Request)等,它们对用户不透明。 MPI联接器(handles) 联接MPI对象的具体变量,用户可以通过它访问和参与相应 MPI对象的具体操作。例如,MPI系统内部提供的通信器MPI_COMM_WORLD。在 FORTRAN语言中,所有MPI联接器均必须说明为“整型变量INTEGER”
MPI函数一般形式 C: error = MPI_Xxxxx(parameter,...); MPI_Xxxxx(parameter,...); 整型错误码由函数值返回 除MPI_Wtime() 和MPI_Wtick()外, 所有MPI 的C 函数均返回一个整型 错误码。成功时返回MPI_SUCCESS=0,其他错误代码依赖于执行 Fortran 77 : CALL MPI_XXXXX(parameter,...,IERROR) 整型错误码由函数的参数返回 除MPI_WTIME() 和MPI_WTICK()外为子函数程序(function), Fortran77的所有MPI过程都是Fortran77的子例行程序(subroutine)
MPI原始数据类型 MPI_BYTE 一个字节 MPI_PACKED 打包数据 全部大写
MPI原始数据类型
MPI程序基本结构 #include <mpi.h> void main (int argc, char *argv[]) { MPI include file #include <mpi.h> void main (int argc, char *argv[]) { int np, rank, ierr; ierr = MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); MPI_Comm_size(MPI_COMM_WORLD,&np); /* Do Some Works */ ierr = MPI_Finalize(); } 变量定义 #include <mpi.h> void main (int argc, char *argv[]) { int np, rank, ierr; ierr = MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); MPI_Comm_size(MPI_COMM_WORLD,&np); /* Do Some Works */ ierr = MPI_Finalize(); } 退出 MPI 环境 #include <mpi.h> void main (int argc, char *argv[]) { int np, rank, ierr; ierr = MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); MPI_Comm_size(MPI_COMM_WORLD,&np); /* Do Some Works */ ierr = MPI_Finalize(); } MPI 环境初始化 #include <mpi.h> void main (int argc, char *argv[]) { int np, rank, ierr; ierr = MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); MPI_Comm_size(MPI_COMM_WORLD,&np); /* Do Some Works */ ierr = MPI_Finalize(); } 执行程序 进程间通信 #include <mpi.h> void main (int argc, char *argv[]) { int np, rank, ierr; ierr = MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); MPI_Comm_size(MPI_COMM_WORLD,&np); /* Do Some Works */ ierr = MPI_Finalize(); }
MPI程序基本结构 PROGRAM bones.f INCLUDE ‘mpif.h’ INTEGER ierror,rank,np CALL MPI_INIT(ierror) CALL MPI_COMM_RANK(MPI_COMM_WORLD,rank,ierror) CALL MPI_COMM_SIZE(MPI_COMM_WORLD,np,ierror) C … Do some work … CALL MPI_FINALIZE(ierror) END
MPI几个基本函数 Index MPI_Init MPI_Initialized MPI_Comm_size MPI_Comm_rank MPI_Finalize MPI_Abort MPI_Get_processor_name MPI_Get_version MPI_Wtime
MPI几个基本函数 初始化 MPI 系统 C: int MPI_Init(int *argc, char *argv[]) Fortran 77: MPI_INIT(IERROR) INTEGER IERROR 通常为第一个调用的MPI函数,除 MPI_Initialized 外 只被调用一次 在C接口中,MPI系统通过argc和argv得到命令行参数,并且 会把MPI系统专用的参数删除,留下用户的解释参数
MPI几个基本函数 检测 MPI 系统是否已经初始化 C: int MPI_Initialized(int *flag) Fortran 77: MPI_INIT(FLAG,IERROR) LOGICAL FLAG INTEGER IERROR 唯一可在 MPI_Init 前使用的函数 已经调用MPI_Init,返回flag=true,否则flag=false
MPI几个基本函数 得到通信器的进程数和进程在通信器中的标号 C: int MPI_Comm_size(MPI_Comm comm, int *size) int MPI_Comm_rank(MPI_Comm comm, int *rank) Fortran 77: MPI_COMM_SIZE(COMM, SIZE, IERROR) INTEGER COMM, SIZE, IERROR MPI_COMM_RANK(COMM, RANK, IERROR) INTEGER COMM, RANK, IERROR
MPI几个基本函数 退出 MPI 系统 C: int MPI_Finalize(void) Fortran 77: MPI_FINALIZE(IERROR) 每个进程都必须调用,使用后不准许调用任何MPI函数 若不执行MPI退出函数,进程可能被悬挂 用户在调用该函数前,应确保非阻塞通讯结束
MPI几个基本函数 异常终止MPI程序 C: int MPI_Abort(MPI_Comm comm, int errorcode) Fortran 77: MPI_ABORT(COMM, ERRORCODE, IERROR) INTEGER COMM, ERRORCODE, IERROR 在出现了致命错误而希望异常终止MPI程序时执行 MPI系统会设法终止comm通信器中所有进程 输入整型参数errorcode,将被作为进程的退出码返回给系统
MPI几个基本函数 获取处理器的名称 C: int MPI_Get_processor_name(char *name, int *resultlen) Fortran 77: MPI_GET_PROCESSOR_NAME(NAME, RESULTLEN, IERR) CHARACTER *(*) NAME INTEGER RESULTLEN, IERROR 在返回的name中存储进程所在处理器的名称 resultlen存放返回名字所占字节 应提供参数name不小于MPI_MAX_PRCESSOR_NAME个字 节的存储空间
MPI几个基本函数 获取 MPI 版本号 C: int MPI_Get_version(int *version, int *subversion) Fortran 77: MPI_GET_VERSION(VERSION,SUBVERSION,IERR) INTEGER VERSION, SUBVERSION, IERROR 若 mpi 版本号为2.0,则返回的version=2,subversion=0
MPI几个基本函数 获取墙上时间 C: double MPI_Wtime(void) Fortran 77: DOUBLE PRECISION MPI_WTIME() 返回调用时刻的墙上时间,用浮点数表示秒数 经常用来计算程序运行时间
Sample :Hello World - C C+MPI #include “mpi.h” #include <stdio.h> #include <math.h> void main(int argc, char *argv[ ]) { int myid, numprocs, namelen; char processor_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(processor_name,&namelen); printf("Hello World! Process %d of %d on %s\n",myid, numprocs, processor_name); MPI_Finalize(); }
Sample :Hello World 单处理器(tp5)运行4个进程 4个处理器(tp1,tp2,tp3,tp4)分别运行4个进程
Sample :Hello World
Sample :Hello World - Fortran Fortran+MPI program main include 'mpif.h' character * (MPI_MAX_PROCESSOR_NAME) processor_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(processor_name, namelen, ierr) write(*,*) 'Hello World! Process ',myid,' of ',numprocs,' on ', processor_name call MPI_FINALIZE(rc) end
点对点通信
点对点通信 定义 阻塞式点对点通信 编写安全的MPI程序 其他阻塞式点对点通信函数 阻塞式消息发送模式 非阻塞式点对点通信
MPI系统的通信方式都建立在点对点通信之上 定义 1 2 3 4 5 destination source communicator 两个进程之间的通信 源进程发送消息到目标进程 目标进程接受消息 通信发生在同一个通信器内,即域内通信器 进程通过其在通信器内的标号表示 MPI系统的通信方式都建立在点对点通信之上
阻塞式点对点通信 阻塞式通信与非阻塞式通信 通信类型 函数返回 对数据区操作 特性 阻塞式通信 非阻塞式通信 阻塞型函数需要等待 指定操作完成返回 或所涉及操作的数据 要被MPI系统缓存安全 备份后返回 函数返回后,对数 据区操作是安全的 程序设计相对 简单 使用不当容易 造成死锁 非阻塞式通信 调用后立刻返回,实 际操作在MPI后台执行 需调用函数等待或查 询操作的完成情况 函数返回后,即操 作数据区不安全。 可能与后台正进行 的操作冲突 可以实现计算 与通信的重叠 程序设计相对 复杂
阻塞式点对点通信 标准阻塞式通信 是否对发送数据进行缓存,由MPI系统决定,而非程序员 阻塞:发送成功,意味(1)消息被接收完毕;(2)或者消息被缓存 接收成功,意味消息已被成功接收
阻塞式点对点通信 消息(message)
阻塞式点对点通信 Index MPI_Send MPI_Recv MPI_Get_count MPI_Sendrecv MPI_Sendrecv_replace
阻塞式点对点通信 阻塞式消息发送 C: int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm) Fortran 77: MPI_SEND(BUF, COUNT, DATATYPE, DEST, TAG, COMM, IERROR) <type> BUF(*) INTEGER COUNT, DATATYPE, DEST, TAG, COMM, IERROR count 不是字节数,而是指定数据类型的个数 datatype可是原始数据类型,或为用户自定义类型 dest 取值范围是 0~np-1,或MPI_PROC_NULL (np是comm中的进程总数) tag 取值范围是 0~MPI_TAG_UB,用来区分消息 The arguments in the MPI_SEND call are “handles” briefly introduce arguments here - more detail will be given on next slide Introduce concept of memory address here
阻塞式点对点通信 阻塞式消息接收 C: int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status) Fortran 77: MPI_RECV(BUF, COUNT, DATATYPE, SOURCE, TAG, COMM, STATUS, IERROR) <type> BUF(*) INTEGER COUNT, DATATYPE, DEST, TAG, COMM, IERROR INTEGER STATUS(MPI_STATUS_SIZE) count是接受缓存区的大小,表示接受上界,具体接受长度可用 MPI_Get_count 获得 source 取值范围是 0~np-1,或MPI_PROC_NULL和 MPI_ANY_SOURCE tag 取值范围是 0~MPI_TAG_UB,或MPI_ANY_TAG Tag - the tag allows selectivity of the message at the receiving end
阻塞式点对点通信 status C中是一个数据结构为MPI_status的参数,用户可以直接访问 的三个域(共5个域) Fortran中是包含MPI_STATUS_SIZE元素的整型数组 C中使用前需要用户为其申请存储空间 (MPI_Status status;) C中引用时为 status.MPI_SOURCE … typedef struct { ... ...int MPI_SOURCE; 消息源地址 int MPI_TAG; 消息标号 int MPI_ERROR; ... ... 接收操作的错误码 } MPI_Status; STATUS(MPI_SOURCE) STATUS(MPI_TAG) STATUS(MPI_ERROR)
阻塞式点对点通信 查询接收到的消息长度 C: Fortran 77: int MPI_Get_count(MPI_Status status, MPI_Datatype datatype, int *count) Fortran 77: MPI_GET_COUNT(STATUS, DATATYPE, COUNT, IERR) INTEGER DATATYPE, COUNT, IERR, STATUS(MPI_STATUS_SIZE) 该函数在count中返回数据类型的个数,即消息的长度 count属于MPI_Status结构的一个域,但不能被用户直接访问
阻塞式点对点通信 消息传递成功 发送进程需指定一个有效的目标接收进程 接收进程需指定一个有效的源发送进程 接收和发送消息的进程要在同一个通信器内 接收和发送消息的 tag 要相同 接收缓存区要足够大
阻塞式点对点通信 任意源进程(接收操作可以接受任意进程的消息) 任意标号(接收操作可以接受任意标号的消息) MPI_ANY_TAG MPI_ANY_SOURCE 任意标号(接收操作可以接受任意标号的消息) MPI_ANY_TAG 真实的源进程与消息标号可以访问接受函数中的status 参数获得
编写安全的MPI程序 Pro0发送消息到Pro1,同时,Pro1发送消息到Pro0 A C B D 死锁
编写安全的MPI程序 A C B D 不安全
编写安全的MPI程序 A C B D 正确
其他阻塞式点对点通信函数 捆绑发送和接收 C: 当前进程把一次发送函数和一次接收函数合并在一起,执行无先后;两个函 数都执行完捆绑函数才返回 发送缓存区和接收缓存区须互不相交 发送与接收使用同一个通信域 由捆绑发送接收调用发出的消息可被普通接收操作接收;一个捆绑发送接收 调用可以接受一个普通的发送操作所发送的消息 C: int MPI_Sendrecv(void *sendbuff,int sendcount,MPI_Datatype sendtype,int dest,int sendtag,void *recvbuff,int recvcount, MPI_Datatype recvtype,int source,int recvtag, MPI_Comm comm,MPI_Status *status)
其他阻塞式点对点通信函数 捆绑发送和接收 Fortran 77: 语义上等同于一个发送和一个接收操作结合,但此函数可以有效避免在单独 MPI_SENDRECV(SENDBUFF,SENDCOUNT,SENDTYPE,DEST,SENDTAG,RECVBUFF, RECVCOUNT, RECVTYPE, SOURCE, RECVTAG,COMM, STATUS, IERR) <type> SENDBUFF(*), RECVBUFF(*) INTEGER SENDCOUNT,SENDTYPE,DEST,SENDTAG,RECVCOUNT,RECVTYPE,SOURCE, RECVTAG,COMM,IERR INTEGER STATUS(MPI_STATUS_SIZE) 语义上等同于一个发送和一个接收操作结合,但此函数可以有效避免在单独 发送和接收操作过程中,由于调用次序不当而造成的死锁。MPI系统会优化通 信次序,从而最大限度避免错误发生。
其他阻塞式点对点通信函数 捆绑发送和接收,收发使用同一缓存区 C: Fortran 77: int MPI_Sendrecv_replace(void *buff,int count,MPI_Datatype datatype, int dest, int sendtag,int source, int recvtag,MPI_Comm comm, MPI_Status *status) Fortran 77: MPI_SENDRECV_REPLACE(BUFF,COUNT,DATATYPE,DEST,SENDTAG,SOURCE,RECVTAG, COMM, STATUS, IERR) <type> BUFF(*) INTEGER COUNT, DATATYPE, DEST, SENDTAG, SOURCE, RECVTAG, COMM, IERR INTEGER STATUS(MPI_STATUS_SIZE) 等价于当前进程先执行一个发送函数再执行一个接收函数;MPI系统保证其 消息发出后再接收信息 MPI_Sendrecv送收使用不同的缓存区;该函数使用同一缓存区
Sample - Fortran MPI_SENDRECV代替MPI_SEND和MPI_RECV CALL MPI_COMM_RANK(comm, rank, ierr) IF(rank.EQ.0) THEN CALL MPI_SENDRECV(sendbuf, count, MPI_REAL, 1, tag, + recvbuf, count, MPI_REAL, 1, tag, comm, status, ierr) IF(rank.EQ.1) THEN CALL MPI_SENDRECV(sendbuf, count, MPI_REAL, 0, tag, + recvbuf, count, MPI_REAL, 0, tag, comm, status, ierr)
阻塞式消息发送模式 按着发送方式和接收方状态要求的不同分类 四个函数拥有完全一样的入口参数 共用一个标准的消息接受函数 发送返回后,发送缓存区可以被释放或者重新使用
标准消息发送函数 (MPI_Send) 发送操作不管接收操作是否启动,都可以开始 发送返回的条件 当消息小于MPI系统为每个进程设置的最大消息缓存区MPI_BUFFER_SIZE,发送数据被 MPI系统缓存,此时不要求接收操作收到发送数据 不缓存,则数据被接收到接收缓存区
缓存消息发送函数 (MPI_Bsend) 发送操作不管接收操作是否启动,都可以开始 进程直接对缓存区进行控制,用户直接对通信缓存区进行申请、使用、释放 发送消息前必须有足够的缓存区可用,否则发送失败 缓存发送返回后,不意味申请的缓存区可自由使用,须等待消息发送出去方可 优势:发送操作在缓存了发送数据后,可以立刻返回
缓存消息发送函数 (MPI_Bsend) 缓存区申请提交 缓存区释放 可调用MPI_Type_size函数来确定数据类型所占字节数 同一时刻,一个进程只能定义一个缓存区,即进程再定义另一个,需要释放已经定义的 缓存区释放 此函数为阻塞式调用,等到该缓存消息发送后才释放返回
Sample - Fortran REAL * (*) BUF INTEGER SIZE, TOTALSIZE, count CALL MPI_TYPE_SIZE(MPI_REAL,SIZE,ierr) TOTALSIZE=count*SIZE + 2*MPI_BSEND_OVERHEAD ! 必须如此 (各进程都有自己的缓存) CALL MPI_BUFFER_ATTACH(BUF,TOTALSIZE,ierr) IF(rank.EQ.0) THEN CALL MPI_BSEND(sendbuf, count, MPI_REAL, 1, tag,comm,ierr) CALL MPI_RECV(recvbuf, count, MPI_REAL, 1, tag, comm, status, ierr) IF(rank.EQ.1) THEN CALL MPI_BSEND(sendbuf, count, MPI_REAL, 0, tag,comm,ierr) CALL MPI_RECV(recvbuf, count, MPI_REAL, 0, tag, comm, status, ierr)
同步消息发送函数 (MPI_Ssend) 发送操作不管接收操作是否启动,都可以开始 发送返回条件,需在标准模式上确认接收方已经开始接收数据 发送数据被存入系统缓存区,需接收方开始接收,发送才能返回 发送数据没有被系统缓存,则需要消息发送完毕后,发送才能返回 优势:这种模式发送和接收最为安全
就绪消息发送函数 (MPI_Rsend) 发送操作必须要求接收操作启动,才可以开始 优势:减少消息发送接收时间开销,可能获得好的计算性能 启动接受操作,意味着接收进程正等待接收发送的消息 若发送操作启动而相应接收操作没有启动,发送操作将出错 优势:减少消息发送接收时间开销,可能获得好的计算性能
阻塞式消息发送模式 发送模式 发送启动,是否 要求接收启动 发送返回 特性 标准发送 否 缓冲发送 同步发送 就绪发送 是 消息被系统缓存 消息被接收完毕 缓冲发送 足够的缓存空间且消息被缓存 发送操作立即 返回 同步发送 消息被缓存并开始被接收 发收同步,最 为安全 就绪发送 是 接收已启动 同与标准发送 减少通讯开销 获得较好性能
非阻塞式点对点通信 阻塞式通信与非阻塞式通信 通信类型 函数返回 对数据区操作 特性 阻塞式通信 非阻塞式通信 阻塞型函数需要等待 指定操作完成返回 或所涉及操作的数据 要被MPI系统缓存安全 备份后返回 函数返回后,对数 据区操作是安全的 程序设计相对 简单 使用不当容易 造成死锁 非阻塞式通信 调用后立刻返回,实 际操作在MPI后台执行 需调用函数等待或查 询操作的完成情况 函数返回后,即操 作数据区不安全。 可能与后台正进行 的操作冲突 可以实现计算 与通信的重叠 程序设计相对 复杂
非阻塞式点对点通信
非阻塞式点对点通信 Index MPI_Isend/MPI_Irecv MPI_Wait/MPI_Waitany/MPI_Waitall/MPI_Waitsome MPI_Test/MPI_Testany/MPI_Testall/MPI_Testsome MPI_Probe/MPI_Iprobe MPI_Request_free MPI_Cancel MPI_Test_cancelled
非阻塞式点对点通信 非阻塞式发送 该函数仅提交了一个消息发送请求,并立即返回,并不意味发送成功 MPI系统会在后台完成消息发送 函数为该发送操作创建了一个请求,描述非阻塞通信状况,通过request变量 返回 request可供之后查询和等待函数使用
非阻塞式点对点通信 非阻塞式接收 该函数仅提交了一个消息发送请求,并立即返回,并不意味接收成功 MPI系统会在后台完成消息接收 函数为该接收操作创建了一个请求,通过request变量返回 request可供之后查询和等待函数使用
非阻塞式点对点通信 等待、检测一个通信请求的完成 MPI_Wait 阻塞等待通信函数完成后返回;MPI_Test检测某通信,不论其是 否完成,都立刻返回。如果通信完成,则flag=true 当等待或检测的通信完成时,通信请求request被设置成 MPI_REQUEST_NULL 考察接收请求,status返回与MPI_Recv一样;发送请求,则不确定 MPI_Test返回时,当flag=false, status不被赋值
Sample - Fortran IF(rank.EQ.0) THEN CALL MPI_ISEND(sendbuf, count, MPI_REAL, 1, tag, comm, request, ierr) CALL MPI_RECV(recvbuf, count, MPI_REAL, 1, tag, comm, status, ierr) CALL MPI_WAIT(request, status, ierr) IF(rank.EQ.1) THEN CALL MPI_ISEND(sendbuf, count, MPI_REAL, 0, tag, comm, request, ierr) CALL MPI_RECV(recvbuf, count, MPI_REAL, 0, tag, comm, status, ierr)
非阻塞式点对点通信 等待、检测一组通信请求中某一个的完成 count表示通信请求的个数 array_of_requests是一组非阻塞通信的请求 index存储一个成功完成的通信在array_of_requests中的位置 flag表示是否有任意一个通信请求完成,若有flag=true 完成的通信请求request被自动赋值MPI_REQUEST_NULL MPI_Testany返回时,当flag=false, status不被赋值
非阻塞式点对点通信
非阻塞式点对点通信 等待、检测一组通信请求的全部完成 count表示通信请求的个数 array_of_requests是一组非阻塞通信的请求 array_of_statuses返回该组通信完成的状态 flag表示全部通信是否完成,若完成flag=true MPI_Testall返回时,当flag=false, array_of_statuses不被赋值
非阻塞式点对点通信
非阻塞式点对点通信 等待、检测一组通信请求的部分完成 MPI_Waitsome等待至少一个通信完成才返回 outcount表示通信成功完成的个数 array_of_indices存储完成的通信在array_of_requests中的位置 array_of_statuses返回完成通信的状态,其他不被赋值 MPI_Testsome返回时若没有一个通信完成,则outcount=0 MPI_Testsome返回时,当flag=false, array_of_statuses不被赋值
非阻塞式点对点通信
非阻塞式点对点通信 消息探测(阻塞型) 阻塞等待,只有在MPI系统探测到符合source/tag条件的消息时才返回 source/tag可以取MPI_ANY_SOURCE/MPI_ANY_TAG 返回的status与MPI_Recv的status完全相同 为接收消息前即可对接收消息进行探测,进而决定如何接收该消息
非阻塞式点对点通信 消息探测(非阻塞型) 函数调用后即返回 当在 MPI系统探测到符合source/tag条件的消息时,flag=true; 若flag=true,返回的status与MPI_Recv的status完全相同;若flag=false,则 对status不作定义 source/tag可以取MPI_ANY_SOURCE/MPI_ANY_TAG
Sample - Fortran …… CALL MPI_COMM_RANK(comm,rank,ierr) IF (rank .EQ. 0) THEN CALL MPI_SEND(i,1,MPI_INTEGER,2,0,comm,ierr) ELSE IF (rank.EQ.1) THEN CALL MPI_SEND(x,1,MPI_REAL,2,0,comm,ierr) ELSE IF (rank .EQ.2 ) THEN DO i=1,2 CALL MPI_PROBE(MPI_ANY_SOURCE, 0, comm, status, ierr) IF (status(MPI_SOURCE) = 0) THEN CALL MPI_RECV(i,1,MPI_INTEGER,0,0,status,ierr) ELSE CALL MPI_RECV(x,1,MPI_REAL,1,0,status,ierr) END IF END DO END IF……
非阻塞式点对点通信 通信请求的释放(阻塞型) 调用MPI_Wait/Test可间接释放完成的通信请求,此函数则直接释放通信 请求及所占内存空间 如果通信尚未完成,则阻塞等待,并完成释放后返回 该函数返回,通信请求request被设置成MPI_REQUEST_NULL
非阻塞式点对点通信 通信请求的取消(非阻塞型) MPI_Cancel取消已调用的非阻塞通信,用此命令来释放非阻塞操作 所占用的资源 命令调用后立刻返回,但调用并不意味相应的通信被取消。该操作调 用时,若相应非阻塞通信已经开始,它会正常完成,不受影响;若没 有开始,则释放通信占用资源,该通信被取消 即使调用取消操作,也需等待、查询函数来释放该非阻塞通信的请求
非阻塞式点对点通信 检测一个通信操作是否被取消(非阻塞型) 函数调用后立即返回 如果一个非阻塞通信已经被执行了取消操作,则该通信的MPI_Wait 和MPI_Test将释放相应的通信对象,并且在返回结果status中指明该 通信是否被取消 如果MPI_Test_cancelled返回结果flag=true,则表明此通信已经被成 功取消,否则该通信还没有被取消
点对点通信函数总表
聚合通信
聚合通信 定义 三种通信方式 聚合函数列表 同步 广播 收集 散发 全散发收集 归约
定义 communicator 5 2 1 3 4 一个通信器的所有进程参与,所有进程都调用聚合通信函数 4 一个通信器的所有进程参与,所有进程都调用聚合通信函数 MPI系统保证聚合通信函数与点对点调用不会混淆 聚合通信不需要消息标号 聚合通信函数都为阻塞式函数
三种通信方式 一对多 多对一 多对多
聚合函数列表 MPI_Barrier MPI_Bcast MPI_Gather/MPI_Gatherv MPI_Allgather/MPI_Allgatherv MPI_Scatter/MPI_Scatterv MPI_Alltoall/MPI_Alltoallv MPI_Reduce/MPI_Allreduce/MPI_Reduce_scatter MPI_Scan
同步 该函数用于进程同步,当且仅当 comm 通信器内的所 有进程均调用该函数后,各个进程才能继续执行;否 则先到达的进程必须空闲等待其他未到达进程。
√ Sample - Fortran …… CALL MPI_COMM_RANK(COMM,RANK,IERR) IF(RANK.EQ.0) THEN CALL WORK0(……) ELSE CALL WORK1(……) CALL MPI_BARRIER(COMM,IERR) CALL WORK2(……) …… CALL MPI_COMM_RANK(COMM,RANK,IERR) IF(RANK.EQ.0) THEN CALL WORK0(……) CALL MPI_BARRIER(COMM,IERR) ELSE CALL WORK1(……) CALL WORK2(……) √
广播
广播 通信器中root进程将自己buffer内的数据发给通信器内所有进程 非root进程用自己的buffer接收数据
Sample - C #include<mpi.h> int main (int argc, char *argv[]) { int rank; double param; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); if(rank==5) param=23.0; MPI_Bcast(¶m,1,MPI_DOUBLE,5,MPI_COMM_WORLD); printf("P:%d after broadcast parameter is %f\n",rank,param); MPI_Finalize(); } Program Output P:0 after broadcast parameter is 23.000000 P:6 after broadcast parameter is 23.000000 P:5 after broadcast parameter is 23.000000 P:2 after broadcast parameter is 23.000000 P:3 after broadcast parameter is 23.000000 P:7 after broadcast parameter is 23.000000 P:1 after broadcast parameter is 23.000000 P:4 after broadcast parameter is 23.000000
收集 (MPI_Gather) ROOT
收集(MPI_Gather) 所有进程(包括根进程)将sendbuf的数据传输给根进程;根进 程按着进程号顺序依次接收到recvbuf 发送与接收的数据类型相同;sendcount和recvcount相同 非根进程接收消息缓存区被忽略,但需要提供
收集(MPI_Gatherv) A B C D A B C D
收集(MPI_Gatherv) 每个进程发送的数据个数不同 根进程接收不一定连续存放 数组recvcounts和displs的元素个数等于进程总数,并与进程顺序对应 数据的个数与位移都以recvtype为单位
Sample – Fortran 进程 i 向进程 0 发送 100-i 个整型数, 每隔100个整型数依次存储消息 1 3 4 …… INTEGER A(100),RBUF(10000),SIZE,ROOT,RTYPE,RANK,RECS(100),DISP(100) CALL MPI_COMM_SIZE(COMM,SIZE,IERR) CALL MPI_COMM_RANK(COMM,RANK,IERR) IF(100*SIZE .GT. 10000) THEN PRINT*, “NOT ENOUGH RECEIVING BUF” CALL MPI_FINALIZE(IERR) ELSE ROOT=0 IF(RANK.EQ.0) THEN DO I=0,SIZE-1 RECS(I)=100-I DISP(I)=I*100 ENDDO ENDIF CALL MPI_GATHERV(A,100-RANK,MPI_INTEGER,RBUF,RECS,DISP,MPI_INTERGER,ROOT,COMM,IERR) 进程 i 向进程 0 发送 100-i 个整型数, 每隔100个整型数依次存储消息 1 3 4
收集(MPI_Allgather)
收集(MPI_Allgather)
收集(MPI_Allgatherv)
散发(MPI_Scatter)
散发(MPI_Scatter) 根进程有np个数据块,每块包含sendcount个类型为sendtype的数据; 根进程将这些数据块按着进程号顺序依次散发到各个进程(包含根进程) 的recvbuf 发送与接收的数据类型相同;sendcount和recvcount相同 非根进程发送消息缓存区被忽略,但需要提供
散发(MPI_Scatterv) A B C D A B C D
散发(MPI_Scatterv) 根进程向各个进程发送的数据个数不等 根进程散发各个进程的数据,其缓存区不一定连续 数组recvcounts和displs的元素个数等于进程总数,并与进程顺序对应 数据的个数与位移都以sendtype为单位
Sample - C 根进程向所有进程次序分发1个数组元素 #include <mpi.h> int main (int argc, char *argv[]) { int rank,size,i,j; double param[400],mine; int sndcnt,revcnt; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); MPI_Comm_size(MPI_COMM_WORLD,&size); revcnt=1; if(rank==3) for(i=0;i<size;i++) param[i]=23.0+i; sndcnt=1; } MPI_Scatter(param,sndcnt,MPI_DOUBLE,&mine,revcnt,MPI_DOUBLE,3,MPI_COMM_WORLD); printf("P:%d mine is %f\n",rank,mine); MPI_Finalize(); 根进程向所有进程次序分发1个数组元素 Program Output P:0 mine is 23.000000 P:1 mine is 24.000000 P:2 mine is 25.000000 P:3 mine is 26.000000
各进程依次为根进程分发 MPI_Scatter; 各进程依次为根进程收集MPI_Gather 全散发收集(MPI_Alltoall) A B C D E F G H J N I K L M O P 各进程依次为根进程分发 MPI_Scatter; 各进程依次为根进程收集MPI_Gather
全散发收集(MPI_Alltoall) Do I=0,NPROCS-1 CALL MPI_SCATTER(SENDBUF(I), SENDCOUNT, SENDTYPE, + RECVBUF+I*RECVCOUNT*extent(RECVTYPE), RECVCOUNT, RECVTYPE, I, COMM, IERR) ENDDO
全散发收集(MPI_Alltoallv) 任意行散发,参照sdispls 任意列收集,参照rdispls
全散发收集(MPI_Alltoallv) 每个进程如同根进程一样,执行一次MPI_Scatterv发送 Do I=0,NPROCS-1 CALL MPI_SCATTERV(SENDBUF(I), SENDCOUNTS, SDISPLS, SENDTYPE, + RECVBUF+RDISPLS(I)*extent(RECVTYPE), RECVCOUNTS(I), RECVTYPE, I,…) ENDDO 每个进程如同根进程一样,执行一次MPI_Gatherv接收 Do I=0,NPROCS-1 CALL MPI_GATHERV(SENDBUF+SDISPLS(I)*extent(RECVTYPE), SENDCOUNTS(I), + SENDTYPE, RECVBUF(I), RECVCOUNTS, RDISPLS, RECVTYPE, I,…) ENDDO
归约
归约(MPI_Reduce) 各进程提供数据(sendbuf,count,datatype) 归约结果存放在root进程的缓存区recvbuf
归约
归约
归约
Sample - C (1.000000,0) (2.000000,1)…(16.000000,15) 数对的归约操作 #include <mpi.h> /* Run with 16 processes */ int main (int argc, char *argv[]) { int rank, root=7; struct double value; int rank; } in, out; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); in.value=rank+1; in.rank=rank; MPI_Reduce(&in,&out,1,MPI_DOUBLE_INT,MPI_MAXLOC,root,MPI_COMM_WORLD); if(rank==root) printf("P :%d max=%lf at rank %d\n",rank,out.value,out.rank); MPI_Reduce(&in,&out,1,MPI_DOUBLE_INT,MPI_MINLOC,root,MPI_COMM_WORLD); if(rank==root) printf("P :%d min=%lf at rank %d\n",rank,out.value,out.rank); MPI_Finalize(); } 数对的归约操作 (1.000000,0) (2.000000,1)…(16.000000,15) Program Output P:7 max = 16.000000 at rank 15 P:7 min = 1.000000 at rank 0
Sample - Fortran Program Output P:2 min=1 at rank 0 PROGRAM MaxMin C Run with 8 processes INCLUDE 'mpif.h' INTEGER err, rank, size integer in(2),out(2) CALL MPI_INIT(err) CALL MPI_COMM_RANK(MPI_WORLD_COMM,rank,err) CALL MPI_COMM_SIZE(MPI_WORLD_COMM,size,err) in(1)=rank+1 in(2)=rank call MPI_REDUCE(in,out,1,MPI_2INTEGER,MPI_MAXLOC, 7,MPI_COMM_WORLD,err) if(rank.eq.7) print *,"P:",rank," max=",out(1)," at rank ",out(2) call MPI_REDUCE(in,out,1,MPI_2INTEGER,MPI_MINLOC, 2,MPI_COMM_WORLD,err) if(rank.eq.2) print *,"P:",rank," min=",out(1)," at rank ",out(2) CALL MPI_FINALIZE(err) END Program Output P:2 min=1 at rank 0 P:7 max=8 at rank 7
全归约(MPI_Allreduce)
归约散发(MPI_Reduce_scatter)
归约散发(MPI_Reduce_scatter)
前缀归约(MPI_Scan)
归约 创建新运算 func是用户提供的用于完成运算的外部函数 commute用来指明所定义的运算是否满足交换律 一个运算创建后和MPI预定义的运算一样使用
归约 用户自定义函数 invec与inoutvec分别指出要被归约的数据所在缓冲的首地址 函数返回时,运算结果储存在inoutvec中 datatype指出归约对象的数据类型 len给出了invec与inoutvec中包含的元素个数(相当于归约函数中的 count)
归约
Sample - Fortran source={ 1,4,9,16,25,36,49,64} Program Output PROGRAM UserOP C Run with 8 processes INCLUDE 'mpif.h' INTEGER err, rank, size integer source, reslt EXTERNAL digit LOGICAL commute INTEGER myop CALL MPI_INIT(err) CALL MPI_COMM_RANK(MPI_WORLD_COMM,rank,err) CALL MPI_COMM_SIZE(MPI_WORLD_COMM,size,err) commute= true 满足交换律 call MPI_OP_CREATE(digit,commute,myop,err) source=(rank+1)**2 call MPI_BARRIER(MPI_COM_WORLD,err) call MPI_SCAN(source,reslt,1,MPI_INTEGER,myop,MPI_COMM_WORLD,err) print *,"P:",rank," my result is ",reslt CALL MPI_OP_FREE(myop,err) CALL MPI_FINALIZE(err) END integer function digit(in,inout,len,type) integer len,type integer in(len),inout(len) do i=1,len inout(i)=mod((in(i)+inout(i)),10) end do digit=5 end source={ 1,4,9,16,25,36,49,64} Program Output P:6 my result is 0 P:5 my result is 1 P:7 my result is 4 P:1 my result is 5 P:3 my result is 0 P:2 my result is 4 P:4 my result is 5 P:0 my result is 1