机群应用开发 并行编程原理及程序设计 Parallel Programming: Fundamentals and Implementation 占杰 zhanjie@dawningcomcn 曙光信息产业有限公司 2010 年 1 月 2010 年 1 月 1
参考文献 黄铠, 徐志伟著, 陆鑫达等译 可扩展并行计算技术, 结构与编程 北京 : 机械工业出版社, P33~56,P227~237, 2000 陈国良著 并行计算 结构 算法 编程 北京 : 高等教育出版社,1999 Barry Wilkinson and Michael Allen Parallel Programming(Techniques and Applications using Networked Workstations and Parallel Computers) Prentice Hall, 1999 李晓梅, 莫则尧等著 可扩展并行算法的设计与分析 北京 : 国防工业出版社,2000 张宝琳, 谷同祥等著 数值并行计算原理与方法 北京 : 国防工业出版社,1999 都志辉著 高性能计算并行编程技术 MPI 并行程序设计 北京 : 清华大学出版社, 2001 2010 年 1 月 2
相关网址 MPI: http://wwmpi-forumorg, http://wwwmcsanlgov/mpi Pthreads: http://wwworeillycom PVM: http://wwwepmornlgov/pvm/ OpemMP: http://wwwopenmporg 网上搜索 :wwwgooglecom 2010 年 1 月 3
MPI 并行程序设计 Parallel Programming with the Massage Passing Interface (MPI) 2010 年 1 月 4
参考文献 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 2 nd edition ISBN 0262571323 Using MPI-2 : advanced features of the messagepassing interface William Gropp, MIT Press, 1999 ISBN 0262571331 高性能计算并行编程技术 -MPI 并行程序设计, 都志辉, 清华大学出版社, 2001 年 8 月 2010 年 1 月 5
并行编程标准 本讨论的重点 多线程库标准 Win32 API POSIX threads 编译制导标准 OpenMP 可移植共享存储并行编程标准 消息传递库标准 MPI PVM 2010 年 1 月 6
消息传递并行程序设计 消息传递并行程序设计指用户必须通过显式地发送和接收消息来实现处理机间的数据交换 在这种并行编程中, 每个并行进程均有自己独立的地址空间, 相互之间访问不能直接进行, 必须通过显式的消息传递来实现 这种编程方式是大规模并行处理机 (MPP) 和机群 (Cluster) 采用的主要编程方式 并行计算粒度大, 特别适合于大规模可扩展并行算法由于消息传递程序设计要求用户很好地分解问题, 组织不同进程间的数据交换, 并行计算粒度大, 特别适合于大规模可扩展并行算法 消息传递是当前并行计算领域的一个非常重要的并行程序设计方式 2010 年 1 月 7
什么是 MPI? Massage Passing Interface: 是消息传递函数库的标准规范, 由 MPI 论坛开发, 支持 Fortran 和 C 一种新的库描述, 不是一种语言 共有上百个函数调用接口, 在 Fortran 和 C 语言中可以直接对这些函数进行调用 MPI 是一种标准或规范的代表, 而不是特指某一个对它的具体实现 MPI 是一种消息传递编程模型, 并成为这种编程模型的代表和事实上的标准 2010 年 1 月 8
MPI 的发展过程 发展的两个阶段 MPI 11: 1995 MPICH: 是 MPI 最流行的非专利实现, 由 Argonne 国家实验室和密西西比州立大学联合开发, 具有更好的可移植性 MPI 12~20: 动态进程, 并行 I/O, 远程存储访问 支持 F90 和 C++(1997) 2010 年 1 月 9
为什么要用 MPI? 高可移植性 MPI 已在 IBM PC 机上 MS Windows 上 所有主要的 Unix 工作站上和所有主流的并行机上得到实现 使用 MPI 作消息传递的 C 或 Fortran 并行程序可不加改变地运行在 IBM PC MS Windows Unix 工作站 以及各种并行机上 2010 年 1 月 10
MPI 基础 基本概念点到点通信 (Point to point) MPI 中 API 的主要内容, 为 MPI 最基本, 最重要的内容 MPI 程序的编译和运行 2010 年 1 月 11
: 从简单入手 Init 和 Finalize 下面我们首先分别以 C 语言和 Fortran 语言的形式给出一个最简单的 MPI 并行程序 Hello ( 下页 ) 该程序在终端打印出 Hello World! 字样 2010 年 1 月 12
Hello world(c) #include <stdioh> #include "mpih main( int argc, char *argv[] ) { MPI_Init( &argc, &argv ); printf( "Hello, world!\n" ); MPI_Finalize(); } 2010 年 1 月 13
Hello world(fortran) program main include mpifh integer ierr call MPI_INIT( ierr ) print *, 'Hello, world!' call MPI_FINALIZE( ierr ) end 2010 年 1 月 14
C 和 Fortran 中 MPI 函数约定 C 必须包含 mpih MPI 函数返回出错代码或 MPI_SUCCESS 成功标志 MPI_ 前缀, 且只有 MPI 以及 MPI_ 标志后的第一个字母大写, 其余小写 Fortran 必须包含 mpifh 通过子函数形式调用 MPI, 函数最后一个参数为返回值 MPI_ 前缀, 且函数名全部为大写 MPI 函数的参数被标志为以下三种类型 : IN: 参数在例程的调用中不会被修正 OUT: 参数在例程的调用中可能会被修正 INOUT: 参数有初始值, 且在例程的调用中可能会被修正 2010 年 1 月 15
MPI 初始化 -MPI_INIT int MPI_Init(int *argc, char **argv) MPI_INIT(IERROR) MPI_INIT 是 MPI 程序的第一个调用, 它完成 MPI 程序的所有初始化工作 所有的 MPI 程序的第一条可执行语句都是这条语句 启动 MPI 环境, 标志并行代码的开始 并行代码之前, 第一个 mpi 函数 ( 除 MPI_Initialized() 外 ) 要求 main 必须带参数运行, 否则出错 2010 年 1 月 16
MPI 结束 -MPI_FINALIZE int MPI_Finalize(void) MPI_FINALIZE(IERROR) MPI_FINALIZE 是 MPI 程序的最后一个调用, 它结束 MPI 程序的运行, 它是 MPI 程序的最后一条可执行语句, 否则程序的运行结果是不可预知的 标志并行代码的结束, 结束除主进程外其它进程 之后串行代码仍可在主进程 (rank = 0) 上运行 ( 如果必须 ) 2010 年 1 月 17
MPI 程序的的编译与运行 mpif77 hellof 或 mpicc helloc 默认生成 aout 的可执行代码 mpif77 o hello hellof 或 mpicc o hello helloc 生成 hello 的可执行代码 mpirun np 4 aout mpirun np 4 hello 4 指定 np 的实参, 表示进程数, 由用户指定 aout / hello 要运行的 MPI 并行程序 小写 o np: The number of process 2010 年 1 月 18
: 运行我们的 MPI 程序! [dair@node01 ~]$ mpicc -o hello helloc [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! Hello World! Hello World! Hello World! 计算机打印字符 [dair@node01 ~]$ 我们输入的命令 2010 年 1 月 19
:Hello 是如何被执行的? SPMD: Single Program Multiple Data #include "mpih" #include <stdioh> main( int argc, char *argv[] ) { MPI_Init( &argc, &argv ); printf( "Hello, world!\n" ); MPI_Finalize(); } #include "mpih" #include <stdioh> "mpih" #include <stdioh> "mpih" main( #include <stdioh> "mpih" intmain( argc, #include <stdioh> char intmain( *argv[] argc, ) { char intmain( *argv[] argc, ) MPI_Init( { char int &argc, *argv[] &argv ) ); printf( MPI_Init( { "Hello, char &argc, *argv[] world!\n" &argv ) ); ); MPI_Finalize(); printf( MPI_Init( { "Hello, &argc, world!\n" &argv ); ); } MPI_Finalize(); printf( MPI_Init( "Hello, &argc, world!\n" &argv ); ); } MPI_Finalize(); printf( "Hello, world!\n" ); } MPI_Finalize(); } rsh\ssh Hello World! Hello World! Hello World! Hello World! 2010 年 1 月 20
: 开始写 MPI 并行程序 Comm_size 和 Comm_rank 在写 MPI 程序时, 我们常需要知道以下两个问题的答案 : 任务由多少个进程来进行并行计算? 我是哪一个进程? 2010 年 1 月 21
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); 2010 年 1 月 22
更新的 Hello World(c) #include <stdioh> #include "mpih" 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(); 2010 年 1 月 23
更新的 Hello World(F77) program main include mpifh 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 2010 年 1 月 24
: 运行结果 [dair@node01 ~]$ mpicc o hello1 hello1c [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 ~]$ 计算机打印字符我们输入的命令 2010 年 1 月 25
: 写 MPI 并行通信程序 --Send 和 Recv 进程 0 rank=0 进程 1 rank=1 进程 2 rank=2 进程 3 rank=3 Recv() Send() Send() Send() Greeting 执行过程 2010 年 1 月 26
有消息传递 greetings(c) #include <stdioh> #include "mpih" 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); 2010 年 1 月 27
有消息传递 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 */ 2010 年 1 月 28
解剖 greetings 程序 头文件 : mpih/mpifh int MPI_Init(int *argc, char ***argv) 启动 MPI 环境, 标志并行代码的开始 并行代码之前, 第一个 mpi 函数 ( 除 MPI_Initialize() 外 ) 要求 main 必须带能运行, 否则出错 通信域 ( 通信空间 ): MPI_COMM_WORLD: 一个通信空间是一个进程组和一个上下文的组合 上下文可看作为组的超级标签, 用于区分不同的通信域 在执行函数 MPI_Init 之后, 一个 MPI 程序的所有进程形成一个缺省的组, 这个组的通信域即被写作 MPI_COMM_WORLD 该参数是 MPI 通信操作函数中必不可少的参数, 用于限定参加通信的进程的范围 2010 年 1 月 29
解剖 greetings 程序 int MPI_Comm_size ( MPI_Comm comm, int *size ) 获得通信空间 comm 中规定的组包含的进程的数量 指定一个 communicator, 也指定了一组共享该空间的进程, 这些进程组成该 communicator 的 group int MPI_Comm_rank ( MPI_Comm comm, int *rank ) 得到本进程在通信空间中的 rank 值, 即在组中的逻辑编号 ( 从 0 开始 ) int MPI_Finalize() 标志并行代码的结束, 结束除主进程外其它进程 之后串行代码仍可在主进程 (rank = 0) 上运行 ( 如果必须 ) 2010 年 1 月 30
MPI 基础 基本概念点到点通信 (Point to point) MPI 中 API 的主要内容, 为 MPI 最基本, 最重要的内容 MPI 程序的编译和运行 2010 年 1 月 31
Point to Point 通信 单个进程对单个进程的通信, 重要且复杂 术语 Blocking( 阻塞 ) : 一个例程须等待操作完成才返回, 返回后用户可以重新使用调用中所占用的资源 Non-blocking( 非阻塞 ): 一个例程不必等待操作完成便可返回, 但这并不意味着所占用的资源可被重用 Local( 本地 ): 过程的完成仅依赖于本地正在执行的进程 Non-local( 非本地 ): 如果过程的完成要求其他进程的 MPI 过程完成 2010 年 1 月 32
MPI 消息 MPI 消息包括信封和数据两个部分, 信封指出了发送或接收消息的对象及相关信息, 而数据是本消息将要传递的内容 数据 :< 起始地址 数据个数 数据类型 > 信封 :< 源 / 目的 标识 通信域 > 2010 年 1 月 33
消息信封 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, 用于标识该通讯空间 2010 年 1 月 34
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 通信域 2010 年 1 月 35
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 对象, 包含实际接收到的消息的有关信息 2010 年 1 月 36
2010 年 1 月 37
status 参数 当使用 MPI_ANY_SOURCE 或 / 和 MPI_ANY_TAG 接收消息时如何确定消息的来源 source 和 tag 值呢? 在 C 中, 结构,statusMPI_SOURCE, statusmpi_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 接收消息中的元素个数 2010 年 1 月 38
消息数据 由 count 个类型为 datatype 的连续数据空间组成, 起始地址为 buf 不是以字节数, 而是以元素的个数指定消息的长度 count 可以是零, 这种情况下消息的数据部分是空的 MPI 基本数据类型相应于宿主语言的基本数据类型 2010 年 1 月 39
MPI 基本数据类型2010 年 1 月 40
什么是缓冲区? 1 应用程序中说明的变量, 在消息传递语句中又用作缓冲区的起始位置 2 由系统 ( 不同用户 ) 创建和管理的某一存储区域, 在消息传递过程中用于暂存放消息 也被称为系统缓冲区 3 用户可设置一定大小的存储区域, 用作中间缓冲区以保留可能出现在其应用程序中的任意消息 A M B A M 进程 P 进程 Q 用户缓冲区 T A M S B 进程 P 进程 Q 系统缓冲区 进程 P 进程 Q 用户指定缓冲区 2010 年 1 月 41 B
消息匹配 接收 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 通讯机制 ) 2010 年 1 月 42
MPI 阻塞通信流程 2010 年 1 月 43
分析 greetings #include #include <stdioh> <stdioh> #include #include "mpih "mpih main(int main(int argc, argc, char* char* argv[]) argv[]) { { int int numprocs; numprocs; /* /* 进程数进程数, 该变量为各处理器中的同名变量, 该变量为各处理器中的同名变量,, 存储是分布的存储是分布的 */ */ int int myid; myid; /* /* 我的进程我的进程 ID, ID, 存储也是分布的存储也是分布的 */ */ MPI_Status MPI_Status status; status; /* /* 消息接收状态变量消息接收状态变量, 存储也是分布的, 存储也是分布的 */ */ char char message[100]; message[100]; /* /* 消息消息 buffer, buffer, 存储也是分布的存储也是分布的 */ */ /* /* 初始化初始化 MPI*/ MPI*/ MPI_Init(&argc, MPI_Init(&argc, &argv); &argv); /* /* 该函数被各进程各调用一次该函数被各进程各调用一次, 得到自己的进程, 得到自己的进程 rank rank 值 */ */ MPI_Comm_rank(MPI_COMM_WORLD, MPI_Comm_rank(MPI_COMM_WORLD, &myid); &myid); /* /* 该函数被各进程各调用一次该函数被各进程各调用一次, 得到进程数, 得到进程数 */ */ MPI_Comm_size(MPI_COMM_WORLD, MPI_Comm_size(MPI_COMM_WORLD, &numprocs); &numprocs); 2010 年 1 月 44
分析 greetings if if (myid (myid!=!= 0) 0) { { /* /* 建立消息建立消息 */ */ sprintf(message, sprintf(message, "Greetings "Greetings from from process process %d!",myid); %d!",myid); /* /* 发送长度取发送长度取 strlen(message)+1, strlen(message)+1, 使 \0 \0 也一同发送出去也一同发送出去 */ */ MPI_Send(message,strlen(message)+1, MPI_Send(message,strlen(message)+1, MPI_CHAR, MPI_CHAR, 0,99,MPI_COMM_WORLD); 0,99,MPI_COMM_WORLD); } } else else { { /* /* my_rank my_rank == == 0 */ */ for for (source (source = = 1; 1; source source < < numprocs; numprocs; source++) source++) { { MPI_Recv(message, MPI_Recv(message, 100, 100, MPI_CHAR, MPI_CHAR, source, source, 99, 99, MPI_COMM_WORLD,&status); MPI_COMM_WORLD,&status); printf( %s\n", printf( %s\n", message); message); } } } } /* /* 关闭关闭 MPI, MPI, 标志并行代码段的结束标志并行代码段的结束 */ */ MPI_Finalize(); MPI_Finalize(); } } /* /* End End main main */ */ 2010 年 1 月 45
Greetings 执行过程 假设进程数为 3 ( 进程 0) ( 进程 1) ( 进程 2) (rank=0) (rank=1) (rank=2) Recv(); Recv();? Send(); Send() 问题 :: 进程 1 和 2 谁先开始发送消息? 谁先完成发送? 2010 年 1 月 46
运行 greetings [dair@node01 ~]$ mpicc o greeting greetingc [dair@node01 ~]$ mpirun -np 4 greeting Greetings from process 1! Greetings from process 2! Greetings from process 3! [dair@node01 ~]$ 计算机打印字符 我们输入的命令 2010 年 1 月 47
最基本的 MPI MPI 调用接口的总数虽然庞大, 但根据实际编写 MPI 的经验, 常用的 MPI 调用的个数非常有限 上面介绍的是 6 个最基本的 MPI 函数 1 MPI_Init( ); 2 MPI_Comm_size( ); 3 MPI_Comm_rank( ); 4 MPI_Send( ); 5 MPI_Recv( ); 6 MPI_Finalize(); MPI_Init( ); 并行代码 ;; MPI_Fainalize(); 只能有串行代码 ;; 2010 年 1 月 48
现在您已经能 够用 MPI 进行并 行编程了! 2010 年 1 月 49
4 种阻塞通信模式 由发送方体现 (send 语句 ) 阻塞通信中接收语句相同, MPI_Recv 按发送方式的不同, 消息或直接被 copy 到接收者的 buffer 中或被拷贝到系统 buffer 中 标准模式 Standard 最常用的发送方式 MPI_Send( ) B: 缓冲模式 Buffer 发到系统缓冲区 MPI_Bsend( ) S: 同步模式 Synchronous 任意发出, 不需系统缓冲区 MPI_Ssend( ) R: 就绪模式 Ready 就绪发出, 不需系统缓冲区 MPI_Rsend( ) 2010 年 1 月 50
标准模式 Standard -- 直接送信或通过邮局送信 由 MPI 决定是否缓冲消息 没有足够的系统缓冲区时或出于性能的考虑,MPI 可能进行直接拷贝 : 仅当相应的接收完成后, 发送语句才能返回 MPI 缓冲消息 : 发送语句的相应的接收语句完成前返回发送的结束 == 消息已从发送方发出, 而不是滞留在发送方的系统缓冲区中 非本地的 : 发送操作的成功与否依赖于接收操作最常用的发送方式 Process 0 间Process 1 ( 执(Rank = 0) 行(Rank = 1) 顺x; 序y; ) 数据传送 MPI_Send(&x,1); MPI_Recv(&y,1); 数据在发送方 buffer 与接收方 buffer 间直接拷贝时2010 年 1 月 51
缓冲模式 Buffer -- 通过邮局送信 ( 应用系统缓冲区 ) 前提 : 用户显式地分配用于缓冲消息的系统缓冲区 MPI_Buffer_attach(*buffer, *size) 发送是本地的 : 完成不依赖于与其匹配的接收操作 发送的结束仅表明消息进入系统的缓冲区中, 发送方缓冲区可以重用, 而对接收方的情况并不知道 缓冲模式在相匹配的接收未开始的 情况下, 总是将送出的消息放在缓冲区内, 这样发送者可以很快地继续计算, 然后由系统处理放在缓冲区中的消息 占用内存, 一次内存拷贝 其函数调用形式为 : MPI_BSEND( ) B 代表缓冲 Process 0 (Rank = 0) x; MPI_Bsend(&x,1); Process 1 (Rank = 1) y; MPI_Recv(&y,1); 2010 年 1 月 52 系统缓冲区时通过系统缓冲区传送消息 间( 执行顺序)
同步模式 Synchronous 本质特征 : 收方接收该消息的缓冲区已准备好, 不需要附加的系统缓冲区任意发出 : 发送请求可以不依赖于收方的匹配的接收请求而任意发出成功结束 : 仅当收方已发出接收该消息的请求后才成功返回, 否则将阻塞 意味着 : 发送方缓冲区可以重用收方已发出接收请求非本地的其函数调用形式为 : MPI_SSEND( ) S 代表同步 -- 握手后才送出名片 Process 0 (Rank = 0) x; MPI_Ssend(&x,1); 时间( 执行顺序) 请求发送确认消息请求发送 同步发送与接收上图 : 发送超前于接收下图 : 发送滞后于接收 Process 1 (Rank = 1) y; MPI_Recv(&y,1); 2010 年 1 月 53 x; MPI_Ssend(&x,1); 确认消息 y; MPI_Recv(&y,1);
就绪模式 Ready -- 有客户请求, 才提供服务 发送请求仅当有匹配的接收后才能发出, 否则出错 在就绪模式下, 系统默认与其相匹配的接收已经调用 接收必须先于发送 它依赖于接收方的匹配的接收请求, 不可以任意发出 其函数调用形式为 : MPI_RSEND( ) R 代表就绪 Process 0 (Rank = 0) x; MPI_Rsend(&x,1); 时间( 执行顺序) 请求发送确认消息 Process 1 (Rank = 1) y; MPI_Recv(&y,1); 接收必须先于发送 ( 只有客户发出服务请求, 才提供服务 ) 2010 年 1 月 54
标准通信模式 缓存通信模式 同步通信模式 就绪通信模式 2010 年 1 月 55
非阻塞通信 阻塞发送将发生阻塞, 直到通讯完成 非阻塞可将通讯交由后台处理, 通信与计算可重叠 用户发送缓冲区的重用 : 非阻塞的发送 : 仅当调用了有关结束该发送的语句后才能重用发送缓冲区, 否则将导致错误 ; 对于接收方, 与此相同, 仅当确认该接收请求已完成后才能使用 所以对于非阻塞操作, 要先调用等待 MPI_Wait() 或测试 MPI_Test() 函数来结束或判断该请求, 然后再向缓冲区中写入新内容或读取新内容 发送语句的前缀由 MPI_ 改为 MPI_I, I:immediate: 标准模式 :MPI_Send( )->MPI_Isend( ) Buffer 模式 :MPI_Bsend( )->MPI_Ibsend( ) 2010 年 1 月 56
非阻塞发送与接收 int MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request) IN buf 发送缓冲区的起始地址 IN count 发送缓冲区的大小 ( 发送元素个数 ) IN datatype 发送缓冲区数据的数据类型 IN dest 目的进程的 rank 值 IN tag 消息标签 IN comm 通信空间 / 通信域 OUT request 非阻塞通信完成对象 ( 句柄 ) MPI_Ibsend/MPI_Issend/MPI_Irsend: 非阻塞缓冲模式 / 非阻塞同步模式 / 非阻塞就绪模式 int MPI_Irecv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request* request) 2010 年 1 月 57
非阻塞通信对象 所有的非阻塞发送或接收通信都会返回一个 非阻塞通信对象, 程序员可以通过对这一对象的查询, 可以识别各种通信操作, 并判断相应的非阻塞操作是否完成 使用 MPI_Cancle() 可以取消已调用的非阻塞操作, 但仍必须调用 MPI_Wait() 或 MPI_Test() 操作来释放非阻塞通信对象 2010 年 1 月 58
非阻塞标准发送和接收 2010 年 1 月 59
通信的完成 发送的完成 : 代表发送缓冲区中的数据已送出, 发送缓冲区可以重用 它并不代表数据已被接收方接收 数据有可能被缓冲 ; 同步模式 : 发送完成 == 接收方已初始化接收, 数据将被接收方接收 ; 接收的完成 : 代表数据已经写入接收缓冲区 接收者可访问接收缓冲区,status 对象已被释放 它并不代表相应的发送操作已结束 通过 MPI_Wait() 和 MPI_Test() 来判断通信是否已经完成 ; 2010 年 1 月 60
非阻塞通信的检测与完成 2010 年 1 月 61
MPI_Wait() 及应用示例 int MPI_Wait(MPI_Request* request, MPI_Status * status); 当 request 标识的通信结束后,MPI_Wait() 才返回 如果通信是非阻塞的, 返回时 request = MPI_REQUEST_NULL; 函数调用是非本地的 ; MPI_Request request; MPI_Status status; int x,y; if(rank == 0){ MPI_Isend(&x,1,MPI_INT,1,99,comm,&request) MPI_Wait(&request,&status); }else{ MPI_Irecv(&y,1,MPI_INT,0,99,comm,&request) MPI_Wait(&request,&status); } 2010 年 1 月 62
MPI_Test() 及应用示例 //int MPI_Test(MPI_Request *request,int *flag, MPI_Status *status); MPI_Request request; MPI_Status status; int x,y,flag; if(rank == 0){ MPI_Isend(&x,1,MPI_INT,1,99,comm,&request) while(!flag) MPI_Test(&request,&flag,&status); }else{ MPI_Irecv(&y,1,MPI_INT,0,99,comm,&request) while(!flag) MPI_Test(&request,&flag,&status); } 2010 年 1 月 63
消息探测 --Probe 函数 ( 适用于阻塞与非阻塞 ) MPI_Probe() 和 MPI_Iprobe() 函数探测接收消息的内容 用户根据探测到的消息内容决定如何接收这些消息, 如根据消息大小分配缓冲区等 前者为阻塞方式, 即只有探测到匹配的消息才返回 ; 后者为非阻塞, 即无论探测到与否均立即返回 int MPI_Probe(int source, int tag, MPI_Comm comm, MPI_Status* status) int MPI_Iprobe(int source, int tag, MPI_Comm comm, int*flag, MPI_Status* status) IN source 数据源的 rank, 可以是 MPI_ANY_SOURCE IN tag 数据标签, 可以是 MPI_ANY_TAG IN comm 通信空间 / 通信域 OUT flag 布尔值, 表示探测到与否 ( 只用于非阻塞方式 ) OUT status status 对象, 包含探测到消息的内容 2010 年 1 月 64
MPI_Probe 应用示例 int x; float y; MPI_Comm_rank(comm, &rank); if(rank ==0) /*0->2 发送一 int 型数 */ MPI_Send(100,1,MPI_INT,2,99,comm); else if(rank == 1) /*1->2 发送一 float 型数 */ MPI_Send(1000,1,MPI_FLOAT,2,99,comm); else /* 根进程接收 */ for(int i=0;i<2;i++) { MPI_Probe(MPI_ANY_SOURCE,0,comm,&status);/*Blocking*/ if (statusmpi_source == 0) MPI_Recv(&x,1,MPI_INT,0,99,&status); else if(statusmpi_source == 1) MPI_Recv(&y,1,MPI_FLOAT,0,99,&status); } 2010 年 1 月 65
重复非阻塞通信 如果一个通信会被重复执行, 比如一个循环内的通信调用, 每次都对所需的通信对象进行初始化和释放的效率是很低的 重复非阻塞通信可在通信参数和 MPI 内部对象之间建立固定的联系, 降低不必要的开销 2010 年 1 月 66
步骤 1 通信的初始化, 比如 MPI_SEND_INIT 2 启动通信,MPI_START 3 完成通信,MPI_WAIT 4 释放非阻塞通信对象, MPI_REQUEST_FREE 2010 年 1 月 67
MPI_SEND_INIT 2010 年 1 月 68
MPI_RECV_INIT 2010 年 1 月 69
MPI_START 重复非阻塞通信在创建后处于非活动状态, 要使用 MPI_START() 或 MPI_STARTALL() 将它激活 2010 年 1 月 70
避免死锁 deadlock 发送和接收是成对出现的, 忽略这个原则很可能会产生死锁 造成死锁的通信调用次序 2010 年 1 月 71
不安全的通信调用次序 2010 年 1 月 72
安全的通信调用次序 2010 年 1 月 73
MPI_Sendrecv 捆绑发送和接收 : 在一条 MPI 语句中同时实现向其它进程的数据发送和从其它进程接收数据操作 该操作由通信系统来实现, 系统会优化通信次序, 从而有效地避免不合理的通信次序, 最大限度避免死锁的产生 2010 年 1 月 74
MPI_Sendrecv 函数原型 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) 数据轮换 P(n-1) pi p0 p2 p1 2010 年 1 月 75
MPI_Sendrecv 用法示意 int a,b; MPI_Status status; int dest = (rank+1)%p; int source = (rank + p -1)%p; /*p 为进程个数 */ MPI_Sendrecv( &a, 1, MPI_INT, dest, 99, &b 1, MPI_INT, source, 99, MPI_COMM_WORLD, &status); 该函数被每一进程执行一次 2010 年 1 月 76
空进程 rank = MPI_PROC_NULL 的进程称为空进程使用空进程的通信不做任何操作 向 MPI_PROC_NULL 发送的操作总是成功并立即返回 从 MPI_PROC_NULL 接收的操作总是成功并立即返回, 且接收缓冲区内容为随机数 status statusmpi_source = MPI_PROC_NULL statusmpi_tag = MPI_ANY_TAG MPI_Get_count(&status,MPI_Datat ype datatype, &count) =>count = 0 2010 年 1 月 77 p0 p1 p2 pi P(n-1) 空进程
空进程应用示意 MPI_Status status; int dest = (rank+1)%p; int source = (rank + p -1)%p; if(source == p-1) source = MPI_PROC_NULL; if(dest == 0) dest = MPI_PROC_NULL; MPI_Sendrecv( &a, 1, MPI_INT, dest, 99, &b 1, MPI_INT, source, 99, MPI_COMM_WORLD, &status); 2010 年 1 月 78
MPI 基础 基本概念点到点通信 (Point to point) MPI 中 API 的主要内容, 为 MPI 最基本, 最重要的内容 MPI 程序的编译和运行 2010 年 1 月 79
MPI 程序的编译 mpicc 编译并连接用 C 语言编写的 MPI 程序 mpicc 编译并连接用 C++ 编写的 MPI 程序 mpif77 编译并连接用 FORTRAN 77 编写的 MPI 程序 mpif90 编译并连接用 Fortran 90 编写的 MPI 程序这些命令可以自动提供 MPI 需要的库, 并提供特定的开关选项 ( 用 -help 查看 ) 2010 年 1 月 80
MPI 程序的编译 用 mpicc 编译时, 就像用一般的 C 编译器一样 还可以使用一般的 C 的编译选项, 含义和原来的编译器相同 例如 : /mpicc -c fooc /mpicc -o foo fooo 2010 年 1 月 81
MPI 程序的运行 MPI 程序的执行步骤一般为 : 编译以得到 MPI 可执行程序 ( 若在同构的系统上, 只需编译一次 ; 若系统异构, 则要在每一个异构系统上都对 MPI 源程序进行编译 ) 将可执行程序拷贝到各个节点机上通过 mpirun 命令并行执行 MPI 程序 2010 年 1 月 82
最简单的 MPI 运行命令 mpirun np N <program> 其中 : N: 同时运行的进程数 <program>: 可执行 MPI 程序名 例如 : mpirun np 6 cpi mpirun np 4 hello 2010 年 1 月 83
一种灵活的执行方式 mpirun p4pg <pgfile> <program> <pgfile> 为配置文件, 其格式为 : < 机器名 > < 进程数 > < 程序名 > < 机器名 > < 进程数 > < 程序名 > < 机器名 > < 进程数 > < 程序名 > 例如 : ( 注 : 第一行的 0 并不表示在 node0 上没有进程, 这里的 0 特指在 node0 上启动 MPI 程序 ) node0 0 /public/dair/mpi/cpi node1 1 /public/dair/mpi/cpi node2 1 /public/dair/mpi/cpi 这种方式允许可执行程序由不同的名字和不同的路径 2010 年 1 月 84
另一种灵活的执行方式 mpirun machinefile <machinefile> -np <N> <program> <machinefile> 为配置文件, 其格式为 : < 机器名 > < 机器名 > < 机器名 > 例如 : node0 node1 node2 node3 2010 年 1 月 85
完整的 MPI 运行方式 MPI 程序的一般启动方式 : mpirun np <number of processor> <program name and argument> 完整的 MPI 运行方式 : mpirun [mpirun_options] <program> [options ] 详细参数信息执行 mpirun -help 2010 年 1 月 86
并行程序设计的一些建议 优化并行算法 大并行粒度 顾及负载平衡 尽量减少通信次数避免大消息 (1M) 避免消息缓冲区的溢出, 且效率较低 避免大消息打包 内存拷贝开销大 2010 年 1 月 87
谢谢! 2010 年 1 月 88
2010 年 1 月 89