MPI编程
-
每个处理器有私有内存,处理器只能访问自己的内存
-
在消息传递程序中,运行在一个核-内存对上的程序通常称为一个进程。
-
进程间采用显式的消息传递进行通信
- 一个进程调用发送函数,另一个调用接收函数。
-
使用消息传递的实现称为消息传递接口(Message-Passing Interface,MPI)
- MPI是一个消息传递接口标准,而不是编程语言
- MPI标准定义了一组具有可移植性的编程接口
- MPI以语言独立的形式存在,可运行在不同的操作系统和硬件平台上
-
涉及多于两个进程的“全局”通信函数。这些函数称为集合通信。
MPI程序初接触
- 在并行编程中,将进程按照非负整数来进行标注是非常常见的。如果有p个进程,则这些进程将被编号为0,1,2,…,p-1。
- 对于我们的并行“hello,world”程序,我们指派0号进程为输出进程,其余进程向它发送消息。
编译与执行
#include <stdio.h>
#include <string.h> /* For strlen */
#include <mpi.h> /* For MPI functions.etc */
const int MAX_STRING=100;
int maint(void)
{
char greeting[MAX_STRING];
int comm_sz; /* Number of processes */
int my_rank; /* My process rank */
MPI_init(NULL,NULL);
MPI_Cumm_size(MPI_COMM_WORLD,&comm_sz):
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank):
if(my_rank!=0)
{
sprintf(greeting,"Greetings from process %d of %d!", my_rank,comm_sz);
MPI_Send(greeting,strlen(greeting)+1,MPI.CHAR,0,0,MPI_COMM_WORLD);
}
else
{
printtf("Greetings fron process %d of %d!\n",my_rank,comm_sz);
for(1nt q=1:q<comm_sz;q++)
{
MPI_Recv(greeting,MAM_STRING,MPI_CHAR,q,0,MPI_COMM_WORLD,
MPI_STATUS_IGMDRE);
printf("%s\n",greeting):
}
}
MPi_Finalize();
return 0:
} /* main */
-
mpicc 是C语言编译器的包装脚本(wrapper scripx)。
- 用 mpicc 的命令来编译程序
$ mpicc -g -Wall -o mpi_hello mpi_hello.c
- 用 mpiexec 命令来启动程序
$ mpiexec -n <number of processes> ./mpi_hello
-
用1个进程运行程序
- 输入
$ mpiexec -n 1 ./mpi_hello
- 输出
Greetings from process 0 of 1!
-
用4个进程运行程序
- 输入
$ mpiexec -n 4 ./mpi_hello
- 输出
Greetings from process 0 of 4! Greetings from process 1 of 4! Greetings from process 2 of 4! Greetings from process 3 of 4!
MPI_Init和MPI_Finalize
-
调用 MPI_Init 是为了告知 MPI 系统进行所有必要的初始化设置。
- 语法结构
int MPI_Init( int* argc_p /* 向参数 argc 的指针。 */, char** argv_p /* 向参数 argv 的指针。 */ ); // 程序不使用这些参数时,可以只是将它们设置为 NULL。
- MPI_Init返回一个int 型错误码
-
调用 MPI_Finalize 是为了告知 MPI 系统 MPI 已经使用完毕,为 MPI 而分配的任何资源都可以释放了。
- 语法结构
int MPI_Finalize(void)
- 在调用 MPI_Finalize 后,就不应该调用 MPI 函数了。
-
MPI 程序的基本框架
- 基本框架
. . . #include<mpi.h> . . . int main(int argc,char* argv[]) { . . . /* Mo NPI calls before this */ MPI_Init(&argc,&argv); . . . MPI_Finalize(); /* Mo NPI calls after this */ . . . return 0; }
- 注意点:
- 不一定要向 MPI_Init 传递 argc和argv 的指针
- 不一定要在 main 函数中调用 MPI_Init 和 MPI_Finalize。
通信子、MPI_Comm_size 和MPI_Comm_rank
-
通信子 (communicator) 指的是一组可以互相发送消息的进程集合。
- MPI_Init 的其中一个目的,是在用户启动程序时,定义由用户启动的所有进程所组成的通信子。
- 这个通信子称为 MPI_COMM_WORLD 。
- MPI_Init 的其中一个目的,是在用户启动程序时,定义由用户启动的所有进程所组成的通信子。
-
MPI_Comm_size
- 语法结构
int MPI_Comm_size( MPI_Comm comm /* in */. int* comm_sz_p /* out */ );
- 第一个参数是一个通信子,它所属的类型是MPI为通信子定义的特殊类型:MPI_Comm。
- MPI_Comm_size 函数在它的第二个参数里返回通信子的进程数
-
MPI_Comm_rank
- 语法结构
int MPI_Comm_rank( MPI_Comm comm /* in */. int* my_rank_p /* out */ );
- 第一个参数是一个通信子,它所属的类型是MPI为通信子定义的特殊类型:MPI_Comm。
- MPI_Comm_rank 函数在它的第二个参数里返回正在调用进程在通信子中的进程号。
-
在 MPI_COMM_WORLD 中经常用参数 comm_sz 表示进程的数量,用参数 my_rank 来表示进程号。
MPI_Send
-
每个发送都是由调用 MPI_Send 来实现的,其语法结构为:
int MPI_Send( void* msg_buf_p /* in */, int msg_size /* in */, MPI_Datatype msg_type /* in */, int dest /* in */, int tag /* in */, MPI_Comm communicator /* in */ );
- msg_buf_p是一个指向包含消息内容的内存块的指针
- msg_size 和 msg_type ,指定了要发送的数据量。
- dest 指定了要接收消息的进程的进程号。
- tag 是个非负int型,用于区分看上去完全一样的消息。
- communicator 是个通信子,用于指定通信范围
-
C语言中的类型(int、char等)不能作为参数传递给函数
- MPI定义了一个特殊类型:MPI_Datatype,用于参数msg_type。
MPI 数据类型 C 语育数据类型 MPI 数据类型 C 语育数据类型 MPI_CHAR signed char MPI_UNSIGNED unsigned int MPI_SHORT signed short int MPI_UNSIGNED_LONG unsigned long int MPI_INT signed int MPI_FLOAT float MPI_LONG signed long int MPI_DOUBLE double MPI_LONG_LONG signed long long int MPI_LONG_DOUBLE long double MPI_UNSIGNED_CHAR unsigned char MPI_BYTE MPI_UNSIGNED_SHORT unsigned short int MPY_PACKED
MPI_Recv
-
MPI_Recv语法结构
int MPI_Send( void* msg_buf_p /* in */, int buf_size /* in */, MPI_Datatype buf_type /* in */, int source /* in */, int tag /* in */, MPI_Comm communicator /* in */, MPI_Status* status_p /* out */ );
-
MPI_Recv 的前六个参数对应了MPI_Send 的前六个参数:
- msg_buf_p指向内存块
- buf_size 指定了内存块中要存储对象的数量
- buf_type 说明了对象的类型
- 参数 source 指定了接收的消息应该从哪个进程发送而来
- 参数 tag要与发送消息的参数 tag 相匹配
- 参数 comnunicator 必须与发送进程所用的通信子相匹配
-
参数 status_p
- 不使用这个参数时,赋予其特殊的MPI 常量 MPI_STATUS_IGNORE 就行了。
MPI消息
定义
-
一个消息好比一封信
-
消息的内容即信的内容,在MPI中称为消息缓冲(Message Buffer)
- 消息缓冲由三元组<起始地址,数据个数,数据类型>标识
-
消息的接收/发送者及信的地址,在MPI中称为消息信封(Message Envelop)
-
消息信封由三元组<源/目标进程,消息标签,通信域>标识
-
-
三元组的方式使得MPI可以表达更为丰富的信息,功能更强大
消息匹配
-
当发送者连续发送两个相同类型消息给同一个接收者,如果没有消息标签,接收者将无法区分这两个消息
-
消息标签使得服务进程可以对两个不同的用户进程分别处理,提高灵活性
-
MPI_Send 函数
-
函数格式 :
int MPI_Send( void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm )
-
参数说明
参数 具体描述 buf 指向发送缓冲区的指针 count 发出的消息的数量 datatype 发出消息的数据类型 dest 目标进程的标识符(MPI_Comm 值),用于指定消息的目标进程 tag (1)消息标记,用于识别消息
(2)如果数据发送时指定的tag和数据接受时指定的tag不一致,数据将无法接收comm MPI 通信器,用于指定通信域
-
-
MPI_Recv 函数
-
函数格式 :
int MPI_Recv( void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status )
-
参数说明
参数 具体描述 buf 接收缓冲区的指针,用于接收从另一个进程中到来的数据 count 接收数据的数量 datatype 接收数据的数据类型。 source (1)发送消息的源进程的标识符,该参数用于指定发送消息的源进程
(2)如果该值等于 MPI_ANY_SOURCE,则表示从任何进程接收消息tag (1)消息标记,用于识别消息
(2)如果该值等于 MPI_ANY_TAG,则表示接收任何标记的消息comm MPI 通信器,用于指定通信域。 status (1)包含接收到的消息的状态信息,包括消息的源、标记和错误信息
(2)如果不需要这些状态信息,可以传递 MPI_STATUS_IGNORE。
-
-
q 号进程调用 MPI_Send 函数所发送的消息可以被 r 号进程调用 MPI_Recv 函数接收,需满足
条件 说明 send_comm = recv_comm
两个进程处于同一个通信域 send_tag = recv_tag
两个进程发送的消息标记相同 dest = r 且 src = q
指定了源和目的 send_type = recv_type
发送信息和接收信息的数据类型相同 recv_buf_p
≥ \ge ≥send_buf_p
接收缓冲区更大 -
通配符
通配符 说明 MPI_ANY_SOURCE 传递给 MPI_Recv 的参数src,可以按照进程完成工作的顺序来接收结果 MPI_ANY_TAG (1)传递给 MPI_Recv 的参数 tag
(2)一个进程也有可能接收多条来自另一个进程的有着不同标签的消息,并且接收进程并不知道消息发送的顺序- 只有接收者可以使用通配符参数。发送者必须指定一个进程号与一个非负整数标签。
- 通信子参数没有通配符。发送者和接收者都必须指定通信子。
-
例子
//0号进程给1号进程发送数据 #include "mpi.h" #include <unistd.h> #include <iostream> int main(int argc, char *argv[]) { int err = MPI_Init(&argc,&argv); int rank,size; MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &size); if(0 == rank) { int sendNum = 99; //这里指定消息的标签为0 目标进程的标识符为1 int tag = 0; int dest = 1; MPI_Send(&sendNum, 1 ,MPI_INT ,dest , tag , MPI_COMM_WORLD); } else { int recvNum = 0; //这里指定接收的消息的标签为0 消息源的进程的标识符为1 //如果这里tag 不等于0,将无法接收到发送进程来的消息 //如果指定tag为MPI_ANY_TAG,将可以接受源进程发送的任意标签的消息 // int tag = 0; int source = 1; MPI_Status status; std::cout << "before recv , recv num = " << recvNum << std::endl; MPI_Recv(&recvNum, 1 ,MPI_INT , source, tag, MPI_COMM_WORLD, &status); std::cout << "before recv , recv num = " << recvNum << std::endl; } err = MPI_Finalize(); return 0; }
消息状态
-
MPI 类型 MPI_Status 定义
- 消息的源进程标识:MPI_SOURCE
- 消息标签:MPI_TAG
- MPI_ERROR
-
MPI_Status 使用
-
若程序含有如下的定义:
MPI_Status status;
-
将 &status 作为最后一个参数传递给 MPI_Recv 函数并调用它后,可以通过检查以下两个成员来确定发送者和标签:
- status.MPI_SOURCE
- status.MPI_TAG
-
但用户可以调用 MPI_Get_Count 函数接收到的数据量
- MPI_Get_Count 函数
int MPI_Get_Count ( MPI_Status* status_p /* in */, MPI.Datatype type /* in */, int* count_p /* out */ );
-
MPI_Send 和 MPI_Recv 的语义
-
发送进程可以缓冲消息,也可以阻塞(block)
- 如果系统缓沖消息,则 MPI 系统将会把消息(包括数据和信封)放量在它自己的内部存储器里,并返回 MPI_Send 的调用。
- 如果系统发生阻塞,则 MPI 系统将一直等待,直到可以开始发送消息,並不立即返回对 MPI_Send 的调用。
-
MPI 要求消息是不可超越的((nonovertaking)
- 对于同一进程发来的多条信息,第 i i i 条信息必须在第 i + 1 i+1 i+1 条信息到来前可以
- 如果消息是来自不同进程的,消息的到达顺序是没有限制的
-
MPI_Send 和 MPI_Recv 的区别
-
MPI_Send
-
如果使用MPI_Send,当函数返回时,实际上并不知道消息是否已经发送出去。
-
MPI_Send 的精确行为是由 MPI 实现所决定的。
- 如果消息的大小小于 “截止” 大小,它将被缓冲;
- 如果消息的大小大于 截止 大小,那么 MPI_Send 函数将被阻塞。
-
-
MPI_Recv
- MPI_Recv 函数总是阻塞的,直到接收到一条匹配的消息
- 当 MPI_Recv 函数调用返回时,就知道一条消息已经存储在接收缓沖区中了(除非产生了错误)。
-
可能的错误
- 如果调用 MPI_Send 发生了阻塞,并且没有相匹配的接收,那么发送进程就悬挂起来
- 如果调用 MPI_Send 被缓冲,但没有相匹配的接收,那么消息将被丢失。
MPI数据类型
- MPI的消息类型分为两种:预定义类型和派生数据类型(Derived Data Type)
- 预定义数据类型:MPI支持异构计算(Heterogeneous Computing)
- 异构计算指在不同计算机系统上运行程序,每台计算可能有不同生产厂商,不同操作系统。
- MPI通过提供预定义数据类型来解决异构计算中的互操作性问题,建立它与具体语言的对应关系
- 派生数据类型:MPI引入派生数据类型来定义由数据类型不同且地址空间不连续的数据项组成的消息
预定义数据类型
预定义数据类型
-
MPI 数据结构以及它们在 C 语言里对应的结构
MPI 数据结构 C 语言结构 MPI_SHORT short int MPI_INT int MPI_LONG long int MPI_LONG_LONG long long int MPI_UNSIGNED_CHAR unsigned char MPI_UNSIGNED_SHORT unsigned short int MPI_UNSIGNED unsigned int MPI_UNSIGNED_LONG unsigned long int MPI_UNSIGNED_LONG_LONG unsigned long long int MPI_FLOAT float MPI_DOUBLE double MPI_LONG_DOUBLE long double MPI_BYTE char -
MPI_BYTE:表示一个字节,所有的计算系统中一个字节都代表8个二进制位
-
MPI_PACKED:预定义数据类型被用来实现传输地址空间不连续的数据项
MPI_PACKED
-
消息打包,然后发送
-
函数格式
int MPI_Pack( const void *inbuf, int incount, MPI_Datatype datatype, void *outbuf, int outsize, int *position, MPI_Comm comm );
-
参数说明
参数 具体说明 inbuf 指向原始数据的指针 incount 原始数据元素的数量 datatype MPI 原始数据类型描述符 outbuf 指向目标缓冲区的指针 outsize 目标缓冲区的大小 position 指向目标缓冲区中下一个可用位置的指针 comm MPI 通信域 -
例子
int n = 5; double v[5] = {1.0,2.0,3.0,4.0,5.0}; // 打包 int position = 0; int buffer_size = n*sizeof(double) + 100; void *buffer = malloc(buffer_size); MPI_Pack(&n,1,MPI_INT,buffer,buffer_size,&position, MPI_COMM_WORLD); MPI_Pack(v,n,MPI_DOUBLE,buffer,buffer_size,&position, MPI_COMM_WORLD); // 发送缓冲区 MPI_Send(buffer, position, MPI_PACKED, 1, 0, MPI_COMM_WORLD); // 释放内存 free(buffer); return 0;
-
-
消息接收,然后拆包
-
函数格式
int MPI_Unpack( const void *inbuf, int insize, int *position, void *outbuf, int outcount, MPI_Datatype datatype, MPI_Comm comm );
-
参数说明
参数 具体说明 inbuf 指向目标缓冲区的指针 incount 目标缓冲区的大小 position 指向目标缓冲区中下一个可用位置的指针 outbuf 存储目标数据的指针 outcount 目标数据元素的数量 datatype MPI 原始数据类型描述符 comm MPI 通信域 -
例子
int count; double *data; // 接收打包后的数据 MPI_Status status; MPI_Probe(0,0,MPI_COMM_WORLD,&status); int size; MPI_Get_count(&status,MPI_PACKED,&size); void *buffer = malloc(size); MPI_Recv(buffer,size,MPI_PACKED,0,0,MPI_COMM_WORLD,&status); // 解包 int position = 0; MPI_Unpack(buffer, size, &position, &count, 1, MPI_INT, MPI_COMM_WORLD); data = (double*)malloc(count*sizeof(double)); MPI_Unpack(buffer, size, &position, data, count, MPI_DOUBLE, MPI_COMM_WORLD); // 打印解包后的数据 printf("count = %d\n", count); for(int i=0; i<count; i++) printf("%f ", data[i]); printf("\n"); // 释放内存 free(buffer); free(data);
-
派生数据类型
-
派生数据类型可以用类型图来描述,这是一种通用的类型描述方法,它是一系列二元组<基类型,偏移>的集合
-
<基类型,偏移>表示方式
{ < 基类型 0 , 偏移 0 > , ⋅ ⋅ ⋅ , < 基类型 n − 1 , 偏移 n − 1 > } \Large \{<基类型0,偏移0>,···,<基类型n-1,偏移n-1>\} {<基类型0,偏移0>,⋅⋅⋅,<基类型n−1,偏移n−1>}
-
在派生数据类型中,基类型可以是任何MPI预定义数据类型,也可以是其它的派生数据类型,即支持数据类型的嵌套定义
-
MPI提供了全面而强大的构造函数(Constructor Function)来定义派生数据类型
函数名 含义 MPI_Type_contiguous 定义由相同数据类型的元素组成的类型 MPI_Type_vector 定义由成块的元素组成的类型,块之间具有相同间隔 MPI_Type_indexed 定义由成块的元素组成的类型,块长度和偏移由参数指定 MPI_Type_struct 定义由不同数据类型的元素组成的类型 MPI_Type_commit 提交一个派生数据类型 MPI_Type_free 释放一个派生数据类型 -
例子
double A[100]; MPI_Data_type EvenElements; ··· MPI_Type_vector(50, 1, 2, MPI_DOUBLE, &EvenElements); MPI_Type_commit(&EvenElements); MPI_Send(A, 1, EvenElements, destination, ···);
- 首先声明一个类型为MPI_Data_type的变量EvenElements
- 调用构造函数MPI_Type_vector(count, blocklength, stride, oldtype, &newtype)来定义派生数据类型
- 新的派生数据类型必须先调用函数MPI_Type_commit获得MPI系统的确认后才能调用MPI_Send进行消息发送
MPI_Type_vector
-
函数格式
int MPI_Type_vector( int count, int blocklength, int stride, MPI_Datatype oldtype, MPI_Datatype *newtype )
-
参数说明
参数 描述 count 派生数据类型newtype由count个数据块构成 blocklength、oldtype 每个数据块由blocklength个oldtype类型的连续数据项组成 stride 两个连续数据块的起始位置之间的oldtype类型元素的个数
因此,两个块之间的间隔可以由 (stride-blocklength) 来表示 -
例子
-
MPI_Type_vector(50,1,2,MPI_DOUBLE,&EvenElements)
- 产生了派生数据类型EvenElements,它由50个块组成
- 每个块包含一个MPI_DOUBLE ,后跟一个(2-1) MPI_DOUBLE (8字节) 的间隔。
-
构造10×10整数矩阵的所有偶序号的行
MPI_Type_vector( 5, // count 10, // blocklength 20, // stride MPI_INT, // oldtype &newtype )
-
MPI_Type_struct
-
函数格式
MPI_Type_struct( count, //成员数 array_of_blocklengths, //成员块长度数组 array_of_displacements, //成员偏移数组 array_of_types, //成员类型数组 newtype // 新类型 )
MPI通信域
- 通信域(Communicator)包括进程组(Process Group)和通信上下文(Communication Context)等内容,用于描述通信进程间的通信关系。
- 通信域分为组内通信域和组间通信域,分别用来实现MPI的组内通信(Intra-communication)和组间通信(Inter-communication)。
通信域
-
进程组是进程的有限、有序集。
- 有限:在一个进程组中,进程的个数n是有限的,这里的n称为进程组大小(Group Size)。
- 有序:进程的编号是按0,1,…,n-1排列的
-
一个进程用它在一个通信域(组)中的编号进行标识。
- 组的大小和进程编号可以通过调用以下的MPI函数获得:
- MPI_Comm_size(communicator, &group_size)
- MPI_Comm_rank(communicator, &my_rank)
- 组的大小和进程编号可以通过调用以下的MPI函数获得:
-
通信上下文:安全的区别不同的通信以免相互干扰
- 通信上下文不是显式的对象,只是作为通信域的一部分出现
-
进程组和通信上下文结合形成了通信域
- MPI_COMM_WORLD是所有进程的集合
-
MPI提供丰富的函数用于管理通信域
函数名 含义 MPI_Comm_size 获取指定通信域中进程的个数 MPI_Comm_rank 获取当前进程在指定通信域中的编号 MPI_Comm_compare 对给定的两个通信域进行比较 MPI_Comm_dup 复制一个已有的通信域生成一个新的通信域,两者除通信上下文不同外,其它都一样。 MPI_Comm_create 根据给定的进程组创建一个新的通信域 MPI_Comm_split 从一个指定通信域分裂出多个子通信域,每个子通信域中的进程都是原通信域中的进程。 MPI_Comm_free 释放一个通信域
MPI_Comm_dup函数
-
函数格式
MPI_Comm_dup(MPI_COMM_WORLD,&MyWorld)
-
函数说明:创建了一个新的通信域MyWorld,它包含了与原通信域MPI_COMM_WORLD相同的进程组,但具有不同的通信上下文。
MPI_Comm_split函数
-
函数格式
MPI_Comm_split( MPI_Comm comm, int color, int key, MPI_Comm* newcomm )
-
函数说明:
- 在通信域MyWorld的基础上产生了几个分割的子通信域。
- 原通信域MyWorld中的进程按照不同的Color值处在不同的分割通信域中,每个进程在不同分割通信域中的进程编号则由Key值来标识。
-
例子
// 获取原始通讯器的秩和大小 int world_rank, world_size; MPI_Comm_rank(MPI_COMM_WORLD, &world_rank); MPI_Comm_size(MPI_COMM_WORLD, &world_size); int color = world_rank / 4; // 根据行确定颜色 // 根据颜色拆分通讯器,然后调用 // 利用原始秩 MPI_Comm row_comm; MPI_Comm_split(MPI_COMM_WORLD, color, world_rank, &row_comm); int row_rank, row_size; MPI_Comm_rank(row_comm, &row_rank); MPI_Comm_size(row_comm, &row_size); printf("WORLD RANK/SIZE: %d/%d \t ROW RANK/SIZE: %d/%d\n", world_rank, world_size, row_rank, row_size); MPI_Comm_free(&row_comm);
组间通信域
-
组间通信域是一种特殊的通信域,该通信域包括了两个进程组,分属于两个进程组的进程之间通过组间通信域实现通信。
-
一般把调用进程所在的进程组称为本地进程组,而把另外一个称为远程进程组。
-
组间通信域函数
函数名 含义 MPI_Comm_test_inter 判断给定的通信域是否为组间通信域 MPI_Comm_remote_size 获取指定组间通信域中远程进程组的大小 MPI_Comm_remote_group 返回给定组间通信域的远程进程组 MPI_Intercomm_creat 根据给定的两个组内通信域生成一个组间通信域。 MPI_Intercomm_merge 将给定组间通信域包含的两个进程组合并,形成一个新的组内通信域
点对点通信
- MPI的点对点通信(Point-to-Point Communication )同时支持多种通信模式
- 通信模式(Communication Mode):缓冲管理,以及发送方和接收方之间的同步方式
- 共有下面四种通信模式:
- 同步(synchronous)通信模式
- 缓冲(buffered)通信模式
- 标准(standard)通信模式
- 就绪(ready)通信模式
- 两种通信机制
- 阻塞
- 非阻塞。
- 不同通信模式和不同通信机制的结合,便产生了非常丰富的点对点通信函数。
通信模式
同步通信模式
-
同步通信模式:只有相应的接收过程已经启动,发送过程才正确返回。
-
同步发送返回后,表示发送缓冲区中的数据已经全部被系统缓冲区缓存,并且已经开始发送。
-
同步发送返回后,发送缓冲区可以被释放或者重新使用
缓冲通信模式
-
缓冲通信模式:缓冲通信模式的发送不管接收操作是否已经启动都可以执行。
-
需要用户程序事先申请一块足够大的缓冲区,通过MPI_Buffer_attch实现,通过MPI_Buffer_detach来回收申请的缓冲区。
标准通信模式
-
标准通信模式:是否对发送的数据进行缓冲由MPI的实现来决定,而不是由用户程序来控制。
-
发送可以是同步的或缓冲的,取决于实现。
就绪通信模式
-
就绪通信模式:发送操作只有在接收进程相应的接收操作已经开始才进行发送。
-
当发送操作启动而相应的接收还没有启动,发送操作将出错。就绪通信模式的特殊之处就是接收操作必须先于发送操作启动。
通信机制
- 阻塞和非阻塞通信的主要区别在于返回后的资源可用性
- 阻塞通信返回的条件:
- 通信操作已经完成,即消息已经发送或接收。
- 调用的缓冲区可用
- 若是发送操作,则该缓冲区可以被其它的操作更新
- 若是接收操作,该缓冲区的数据已经完整,可以被正确引用。
- 非阻塞通信返回后并不意味着通信操作的完成
- MPI还提供了对非阻塞通信完成的检测,主要的有两种MPI_Wait函数和MPI_Test函数
- MPI的发送操作支持四种通信模式,它们与阻塞属性一起产生了MPI中的8种发送操作。
- 两种MPI的接收操作
- 阻塞接收
- 非阻塞接收。
点对点通信函数
MPI 原语 | 阻塞 | 非阻塞 |
---|---|---|
标准通信 | MPI_Send | MPI_Isend |
同步通信 | MPI_ Ssend | MPI_ Issend |
缓冲通信 | MPI_ Bsend | MPI_ Ibsend |
就绪通信 | MPI_ Rsend | MPI_ Irsend |
接收函数 | MPI_Recv | MPI_Irecv |
完成检测 | MPI_Wait | MPI_Test |
消息到达检测 | MPI_Probe | MPI_Iprobe |
重叠计算与通信
-
阻塞通信
-
在阻塞通信的情况下,通信还没有结束的时候,处理器只能等待,浪费了计算资源。
-
一种常见的技术就是设法使计算与通信重叠,非阻塞通信可以用来实现这一目的。
-
例如:一条三进程的流水线,中间的进程连续地从左边的进程接收输入数据流,计算出结果,然后将结果发送给右边的进程。
while (Not_Done) { MPI_Irevc(NextX, … ); MPI_Isend(PreviousY, … ); CurrentY=Q(CurrentX); }
-
-
非阻塞通信
-
双缓冲是一种常用的方法。需要为X和Y各自准备两个单独的缓冲,当接收进程向缓冲中放下一个X时,计算进程可能从另一个缓冲中读当前的X。
-
需要确信缓冲中的数据在缓冲被更新之前使用 。
while (Not_Done) { if (X==Xbuf0) { X=Xbuf1; Y=Ybuf1; Xin=Xbuf0; Yout=Ybuf0; } else { X=Xbuf0; Y=Ybuf0; Xin=Xbuf1; Yout=Ybuf1; } MPI_Irevc(Xin, …, recv_handle); MPI_Isend(Yout, …, send_handle); Y=Q(X); /* 重叠计算*/ MPI_Wait(recv_handle,recv_status); MPI_Wait(send_handle,send_status); }
-
-
send_handle和revc_handle分别用于检查发送接收是否完成
-
通过调用MPI_Wait(Handle, Status)来实现,该函数直到Handle指示的发送或接收操作已经完成才返回
-
MPI_Test(Handle, Flag, Status)只测试由Handle指示的发送或接收操作是否完成,如果完成,就对Flag赋值True,这个函数不像MPI_Wait,它不会被阻塞。
Send-Recv函数
-
函数用途
- 给一个进程发送消息,从另一个进程接收消息;
- 适用于在进程链(环)中进行“移位”操作,而避免在通讯为阻塞方式时出现死锁。
-
函数格式
int MPI_Sendrecv( const 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 );
-
参数说明
参数 具体说明 sendbuf 发送缓冲区的起始地址,指向要发送的数据 sendcount 发送缓冲区中发送数据的个数 sendtype 发送数据的类型 dest 接收消息的进程号,必须在通信域中 sendtag 发送消息的标签 recvbuf 接收缓冲区的起始地址,指向接收到的数据 recvcount 接收缓冲区中接收数据的个数 recvtype 接收数据的类型 source 发送消息的进程号,必须在通信域中 recvtag 接收消息的标签 comm 通信域 status 表示接收到消息的状态 -
MPI_Sendrecv
函数是阻塞式函数,即函数会一直阻塞,直到发送和接收两个操作都完成。 -
如果发送和接收两个操作不能同时进行,该函数会产生死锁。
集合通信
定义和说明
-
集合通信(Collective Communications):一个进程组中的所有进程都参加的全局通信操作。
-
集合通信一般实现三个功能:
功能 作用 通信功能 完成组内数据的传输 聚集功能 在通信的基础上对给定的数据完成一定的操作 同步功能 实现组内所有进程在执行进度上取得一致 -
按照通信方向的不同,又可以分为三种
通信方式 描述 一对多通信 一个进程向其它所有的进程发送消息,这个负责发送消息的进程叫做Root进程 多对一通信 一个进程从其它所有的进程接收消息,这个接收的进程也叫做Root进程 多对多通信 每个进程都向其它所有的进程发送或者接收消息
集合通信函数
-
通信函数
函数名 含义 MPI_Bcast 一对多广播同样的消息 MPI_Gather 多对一收集各个进程的消息 MPI_Gatherv MPI_Gather的一般化 MPI_Allgather 全局收集 MPI_Allgatherv MPI_Allgather的一般化 MPI_Scatter 一对多散播不同的消息 MPI_Scatterv MPI_Scatter的一般化 MPI_Alltoall 多对多全局交换消息 MPI_Alltoallv MPI_Alltoall的一般化 -
聚集函数
函数名 含义 MPI_Reduce 多对一归约 MPI_Allreduce MPI_Reduce的一般化 MPI_Reduce_scatter MPI_Reduce的一般化 MPI_Scan 扫描 -
同步函数
函数名 含义 MPI_Barrier 路障同步
广播MPI_Bcast
-
广播是一对多通信的典型例子
-
调用格式
MPI_Bcast( Address, //发送/接收buf/ Count, //元素个数 Datatype, Root, //指定根进程 Comm )
-
Root进程发送相同的消息给通信域Comm中的所有进程。
-
消息的内容如同点对点通信一样由三元组<Address, Count, Datatype>标识。
- 对Root进程来说,这个三元组既定义了发送缓冲也定义了接收缓冲。
- 对其它进程来说,这个三元组只定义了接收缓冲
-
例子
int p, myrank; float buf; MPI_Comm comm = MPI_COMM_WORLD; MPI_Init(&argc, &argv); /*得进程编号*/ MPI_Comm_rank(comm, &my_rank); /* 得进程总数 */ MPI_Comm_size(comm, &p); if(myrank==0) buf = 1.0; MPI_Bcast(&buf,1,MPI_FLOAT,0, comm);
散播MPI_Scatter
-
散播也是一个一对多操作
-
调用格式
MPI_Scatter( void* send_data, //在根进程上的一个数据数组 int send_count, //发送给每个进程的数据数量 MPI_Datatype send_datatype, //数据类型 void* recv_data, //缓存,存有recv_count个recv_datatype数据类型的元素 int recv_count, MPI_Datatype recv_datatype, int root, //开始分发数组的根进程 MPI_Comm communicator //对应的communicator )
-
Scatter执行与Gather相反的操作。
-
Root进程给所有进程(包括它自已)发送不同的消息,这n (n为进程域comm包括的进程个数)个消息在Root进程的发送缓冲区中按进程标识的顺序有序地存放。
-
缓存定义
-
接收缓冲由三元组
<recv_data, recv_count, recv_datatype>
标识。 -
Root进程的发送缓冲由三元组
<send_data, send_count, send_datatype>
标识。 -
非Root进程忽略发送缓冲
-
-
send_count 用处
- 如果
send_count
是1,send_datatype
是MPI_INT
的话,进程0会得到数据里的第一个整数,以此类推。 - 如果
send_count
是2的话,进程0会得到前两个整数,进程1会得到第三个和第四个整数,以此类推。
- 如果
收集MPI_Gather
-
收集是多对一通信的典型例子
-
调用格式
MPI_Gather( void* send_data, int send_count, MPI_Datatype send_datatype, void* recv_data, int recv_count, MPI_Datatype recv_datatype, int root, MPI_Comm communicator )
-
Root进程从进程域Comm的所有进程(包括它自已)接收消息。
-
消息按照进程的标识rank排序并进行拼接,然后存放在Root进程的接收缓冲中。
-
缓冲定义
- 接收缓冲由三元组
<recv_data, recv_count, recv_datatype>
标识 - 发送缓冲由三元组
<send_data, send_count, send_datatype>
标识
- 接收缓冲由三元组
-
所有非Root进程忽略接收缓冲。
-
例子
int p, myrank; float data[10]; /*分布变量*/ float* buf; MPI_Comm comm = MPI_COMM_WORLD; MPI_Init(&argc, &argv); /*得进程编号*/ MPI_Comm_rank(comm,&my_rank); /* 得进程总数 */ MPI_Comm_size(comm, &p); if(myrank==0) buf=(float*)malloc(p*10*sizeof(float);/*开辟接收缓冲区*/ MPI_Gather(data,10,MPI_FLOAT, buf,10,MPI_FLOAT,0,comm);
全局收集MPI_Allgather
-
全局收集多对多通信的典型例子
-
调用格式
MPI_Allgather( void* send_data, int send_count, MPI_Datatype send_datatype, void* recv_data, int recv_count, MPI_Datatype recv_datatype, MPI_Comm communicator )
-
Allgather操作相当于每个进程都作为Root进程执行了一次Gather调用,即每一个进程都按照Gather的方式收集来自所有进程(包括自己)的数据。
全局交换MPI_Alltoall
-
全局交换也是一个多对多操作
-
调用格式
MPI_Alltoall( SendAddress, SendCount, SendDatatype, RecvAddress, RecvCount, RecvDatatype, Comm )
-
每个进程发送一个消息给所有进程(包括它自已)。
-
n (n为进程域comm包括的进程个数)个消息在发送缓冲中以进程标识的顺序有序地存放。从接收角度看,每个进程都从所有进程接收一个消息,这n个消息以标号的顺序被连接起来,存放在接收缓冲中。
-
全局交换等价于每个进程作为Root进程执行了一次散播操作。
路障MPI_Barrier
-
同步功能用来协调各个进程之间的进度和步伐 。
- 目前MPI的实现中支持一个同步操作,即路障同步(Barrier)。
-
路障同步的调用格式如下
MPI_Barrier(Comm)
-
在路障同步操作MPI_Barrier(Comm)中
- 通信域Comm中的所有进程相互同步。
- 在该操作调用返回后,可以保证组内所有的进程都已经执行完了调用之前的所有操作,可以开始该调用后的操作。
聚合操作: 归约和扫描
-
集合通信的聚合功能使得MPI进行通信的同时完成一定的计算。
-
MPI聚合的功能分三步实现
- 通信的功能,即消息根据要求发送到目标进程,目标进程也已经收到了各自需要的消息;
- 对消息的处理,即执行计算功能;
- 把处理结果放入指定的接收缓冲区。
-
MPI提供了两种类型的聚合操作: 归约和扫描。
归约MPI_Reduce
-
调用格式
MPI_Reduce( void* send_data, //每个进程都希望归约的 datatype 类型元素的数组 void* recv_data, //具有 root 秩的进程相关 int count, //recv_data 包含归约的结果,大小为sizeof(datatype)* count MPI_Datatype datatype, MPI_Op op, //应用于数据的操作 int root, MPI_Comm communicator )
-
归约操作对每个进程的发送缓冲区(SendAddress)中的数据按给定的操作进行运算,并将最终结果存放在Root进程的接收缓冲区(RecvAddress)中。
-
参与计算操作的数据项的数据类型在Datatype中定义,归约操作由Op定义。
-
归约操作可以是MPI预定义的,也可以是用户自定义的。
-
归约操作允许每个进程提供向量值,而不只是标量值,向量的长度由Count定义。
-
例子
-
MPI_Reduce: root=0,Op=MPI_SUM
-
MPI_Allreduce: Op=MPI_SUM
-
-
预定义的归约操作
操作 含义 操作 含义 MPI_MAX 最大值 MPI_LOR 逻辑或 MPI_MIN 最小值 MPI_BOR 按位或 MPI_SUM 求和 MPI_LXOR 逻辑异或 MPI_PROD 求积 MPI_BXOR 按位异或 MPI_LAND 逻辑与 MPI_MAXLOC 最大值且相应位置 MPI_BAND 按位与 MPI_MINLOC 最小值且相应位置 -
自定义的归约操作
-
基本格式
int MPI_Op_create( //用户自定义归约函数 MPI_User_function *function, // if (commute==true) Op是可交换且可结合 // else 按进程号升序进行Op操作 int commute, MPI_Op *op )
-
归约操作函数
typedef void MPI_User_function( void *invec, void *inoutvec, int *len, //从MPI_Reduce调用中传入的count MPI_Datatype *datatype );
-
函数语义
for(i=0;i<*len;i++) { *inoutvec = *invec USER_OP *inouvec; inoutvec++; invec++; }
-
例子
typedef struct { double real,imag; } Complex; /* the user-defined function */ void myProd( Complex *in, Complex *inout, int *len, MPI_Datatype *dptr ) { int i; Complex c; for (i=0; i< *len; ++i) { c.real = inout->real * in->real - inout->imag * in->imag; c.imag = inout->real * in->imag + inout->imag * in->real; *inout = c; in++; inout++; } } /* explain to MPI how type Complex is defined */ MPI_Type_contiguous( 2, MPI_DOUBLE, &ctype ); MPI_Type_commit( &ctype ); /* create the complex-product user-op */ MPI_Op_create( myProd,1, &myOp ); MPI_Reduce( a, answer, LEN, ctype, myOp, 0, MPI_COMM_WORLD );
-
-
MPI_Allreduce
MPI_Allreduce( void* send_data, void* recv_data, int count, MPI_Datatype datatype, MPI_Op op, MPI_Comm communicator )
MPI_Allreduce
与MPI_Reduce
相同,不同之处在于它不需要根进程 ID(因为结果分配给所有进程)MPI_Allreduce
等效于先执行MPI_Reduce
,然后执行MPI_Bcast
扫描MPI_scan
-
调用格式
MPI_scan(SendAddress, RecvAddress, Count, Datatype, Op, Comm)
-
扫描的特点
- 可以把扫描操作看作是一种特殊的归约,即每一个进程都对排在它前面的进程进行归约操作。
- MPI_SCAN调用的结果是:对于每一个进程i,它对进程0,1,…,i的发送缓冲区的数据进行了指定的归约操作。
- 扫描操作也允许每个进程贡献向量值,而不只是标量值。向量的长度由Count定义。
-
例子:MPI_scan:Op=MPI_SUM
集合通信特点
- 通信域中的所有进程必须调用群集通信函数。如果只有通信域中的一部分成员调用了群集通信函数而其它没有调用,则是错误的。
- 除MPI_Barrier以外,每个群集通信函数使用类似于点对点通信中的标准、阻塞的通信模式。也就是说,一个进程一旦结束了它所参与的群集操作就从群集函数中返回,但是并不保证其它进程执行该群集函数已经完成。
- 一个群集通信操作是不是同步操作取决于实现。MPI要求用户负责保证他的代码无论实现是否同步都必须是正确的。
- 所有参与群集操作的进程中,Count和Datatype必须是兼容的。
- 群集通信中的消息没有消息标签参数,消息信封由通信域和源/目标定义。例如在MPI_Bcast中,消息的源是Root进程,而目标是所有进程(包括Root)。
实现梯形积分法
梯形积分法
基本思想
-
将 x x x 轴上的区间划分为 n n n 个等长的子区间。然后估计介于函数图像及每个子区间内的梯形区域的面积。
-
梯形的底边是 x x x 轴上的子区间,两条垂直边是经过子区间端点的垂直线,第四条边是两条垂直边与函数图像所相交的两个交点之间的连线。
- 设子区间的端点为 x i x_i xi 和 x i + 1 x_{i+1} xi+1 ,那么子区间的长度 h = x i + 1 − x i h=x_{i+1}-x_i h=xi+1−xi 。
- 两条垂直线段的长度分别为 f ( x i ) f(x_i) f(xi) 和 f ( x i + 1 ) f(x_{i+1}) f(xi+1) ,那么该梯形区城的面积就为:
S 梯形 = h 2 [ f ( x i ) − f ( x i + 1 ) ] S_{梯形}=\frac{h}{2}[f(x_i)-f(x_{i+1})] S梯形=2h[f(xi)−f(xi+1)]
-
由于 n n n 个子区间是等分的,因此如果两条垂直线包围区域的边界分别为 x = a x=a x=a 和 x = b x=b x=b ,那么 h = b − a n h=\frac{b-a}{n} h=nb−a,
-
如果称最左边的端点为 x 0 x_0 x0 ,最右边的端点为 x n x_n xn ,则有:
x 0 = a , x 1 = a + h , x 2 = a + 2 h , ⋯ , x n − 1 = a + ( n − 1 ) h , x n = b x_0=a,x_1=a+h,x_2=a+2h,\cdots,x_{n-1}=a+(n-1)h,x_n=b x0=a,x1=a+h,x2=a+2h,⋯,xn−1=a+(n−1)h,xn=b -
这片区域的所有梯形的面积和为
∑ S = h [ f ( x 0 ) 2 + f ( x 1 ) + ⋯ + f ( x n − 1 ) + ( x n ) 2 ] \sum S=h\Big[\frac{f(x_0)}{2}+f(x_1)+\cdots+f(x_{n-1})+\frac{(x_n)}{2}\Big] ∑S=h[2f(x0)+f(x1)+⋯+f(xn−1)+2(xn)]
-
一个串行程序的伪代码
/* Input:a,b,n */ h=(b-a)/n; approx = (f(a)+f(b))/2.0; for(i=1; i<n; i++) { x_i=a+i*h; approx +=f(x_i): } approx = h* approx;
并行化梯形积分法
- 对于梯形积分法的两种任务:
- 一种是获取单个矩形区域的面积,
- 一种是计算这些区域的面积和。
- 利用通信信道将每个第一种任务与一个第二种任务相连接,
-
利用 MPI 进行解决
- 将区间 [ a , b ] [a,b] [a,b] 分成 comm_sz 个子区间。
- 如果 comm_sz 可以整除 n n n,即梯形数目,那么我们可以简单地在 n/comm_sz 个梯形和所有 comm_sz个子空间上应用梯形积分法。
- 最后可以利用进程中的某一个,如0号进程,将这些梯形面积的估计值累加起来,完成整个计算过程。
-
假设 comm_sz 可以整除 n n n ,得到伪代码
Get a,b,n; h = (b-a)/n; 1ocal_n = n/comm-sz; local_a = a+ my_rank * local_n * h; 1ocal_b = local_a + local_n * h; local_integral = Trap(local_a, 1ocal_b, 1ocal_n, h); if(my_rank != 0) Send local_integral to process 0; else { total_integral = local_integral; for(proc =1;proc<comm_sz;proc++) { Receive local_integral from proc; total_integral += local_integral; } } if(my_rank == 0) { print result; }
-
完整代码
#include <stdio.h> #include <string.h> #include <mpi.h> double Trap(double left_endpt,double right_endpt,int trap_count,double base_len); double func(double x); // f(x) = 2x +10 int main(void) { int my_rank,comm_sz,n = 1024, local_n; double a = 0.0, b = 3.0, h, local_a, local_b, local_int, total_int; int source; MPI_Init(NULL,NULL); MPI_Comm_rank(MPI_COMM_WORLD, &my_rank); MPI_Comm_size(MPI_COMM_WORLD, &comm_sz); h = (b - a) / n; local_n = n / comm_sz; local_a = a + my_rank * local_n * h; local_b = local_a + local_n * h; local_int = Trap(local_a,local_b,local_n,h); if(my_rank != 0){ MPI_Send(&local_int,1,MPI_DOUBLE,0,0,MPI_COMM_WORLD); } else { total_int = local_int; for(source = 1;source < comm_sz;source++) { MPI_Reduce(&local_int,&total_int,1,MPI_DOUBLE,MPI_SUM,0,MPI_COMM_WORLD); total_int += local_int; } } if(my_rank == 0) { printf("With n = %d trapezoids, our extimate\n",n); printf("of the integral from %f to %f = %.15e\n",a,b,total_int); } MPI_Finalize(); return 0; }
I/O
输出
-
MPI 标准没有指定哪些进程可以访问哪些 I/O 设备,但是几乎所有的 MPI 实现都允许 MPI_COMM_WORLD 里的所有进程都能访问标准输出 stdout 和标准错误输出 stderr 。
-
梯形积分法 MPI 程序的第一个版本
#include <stdio.h> #include <mpi.h> double Trap(double left_endpt, double right_endpt, int trap_count,double base_len) { double estimate, x; int i; estimate = (f(left_endpt) + f(right_endpt)/2.0); for(i=1; i<=trap_count-1;i++) { x = left_endpt + i * base_len; estimate +=f(x); } estimate = estimate * base_len; return estimate; } int main(void) { int my_rank, comm_sz, n=1024, local_n; double a = 0.0, b = 3.0, h, local_a, local_b; double local_int, total_int; int source; MPI_Init(NULL, NULL); MPI_Comm_rank(MPI_COMM_WORLD, &my_rank); MPI_Comm_size(MPI_COMM_WORLD, &comm_sz); h = (b-a)/n; 1ocal_n = n/comm-sz; local_a = a+ my_rank * local_n * h; 1ocal_b = local_a + local_n * h; local_integral = Trap(local_a, 1ocal_b, 1ocal_n, h); if(my_rank != 0) MPI_Send(&local_int, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); else { total_integral = local_integral; for(source =1; source < comm_sz; source++) { MPI_Recv(&local_int, 1, MPI_DOUBLE, source, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE) total_int += local_int; } } if(my_rank == 0) { printf(" With n = %d trapezoids, our estimate\n",n); printf("of the Integral from %f to %f =%.15e\n", a, b, total_int); } MPI_Finalize(); return 0; }
-
大部分的 MPI 实现并不提供对这些 I/O 设备访问的自动调度。
- 如果多个进程试图写标准输出 stdout ,那么这些进程的输出顺序是无法预测的,甚至会发生一个进程的输出被另一个进程的输出打断的情况。
#include <stdio.h> #include <mpi.h> int main(void) { int myrank, comm_sz; MPI_Init(NULL,NULL); MPI_Comm_rank(MPI_COMM_WORLD, &my_rank); MPI_Comm_size(MPI_COMM_WORLD, &comm_sz); printf("Proc %d of %d > Does anyone have a toothpick?\n", my_rank,comm_sz); MPI_Finalize() return 0; }/* main */
- MPI 进程都在相互“竞争”,以取得对共享输出设备、标准输出 stdout 的访问。
- 不可能预测进程的输出是以怎样的顺序排列。
- 这种竞争会导致不确定性,即每次运行的实际输出可能会变化。
输入
-
大部分的 MPI实现只允许 MPI_COMM_NORLD 中的0号进程访问标准输入 stdin 。
-
为了编写能够使用 scanf 的MPI 程序,我们根据进程号来选取转移分支。
- 0 号进程负责读取数据,并将数据发送给其他进程
void Get_input(int my_rank, int comm_sz, double* a_p, double* b_p, int* n_p ) { int dest; if(my_rank==0) { printf("Enter a, b, and n\n"); scanf("&lf &lf &d",a_p, b_p, n_p); for (dest=1:dest < comm_sz; dest++) { MPI_Send(a_p, 1, MPI_DOUBLE, dest, 0, MPI_COMM_WORLD); MPI_Send(b_p, 1, MPI_DOUBLE, dest, 0, MPI_COMM_WORLD); MPI_Send(n_p, 1, MPI_INT, dest, 0, MPI_COMM_WORLD); } } else { MPI_Recv(a_p, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); MPI_Recv(b_p, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); MPI_Recv(n_p, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); } }
- 初始化 my_rank 和comm_sz 后,才能调用该函数;
. . . MPI_Comm_rank(MPI_COMM_WORLD, &my_rank); MPI_Comm_size(MPI_COMM_WORLD, &comm_sz); Get_datat(my_rank, comm_sz, &a, &b, &n); . . .
MPI程序的安全性
-
使用标准通信模式的MPI_Send函数进行通信
- 一般MPI标准实现中,相对较小的消息就交由MPI设置的缓冲区并返回,但对于大型数据直到对应的MPI_Recv出现前都阻塞。
- 每个进程阻塞在MPI_Send上,程序会死锁或挂起
-
使用缓冲通信模式的MPI_Bsend函数
-
多个进程先同时发送消息,再同时接收消息是MPI程序不安全的最大因素:环形传递问题
-
使用MPI_Sendrecv函数
树形搜索问题
树形搜索问题
-
旅行商(Traveling Salesperson Problem, TSP)问题
-
问题描述:旅行商有一个要访问城市的列表和每两个城市之间旅行的开销,他要访问每个城市仅一次,并且回到出发的城市。
-
数据结构:有向图G = <V, E, C>:
-
V是结点的集合,表示城市
-
E是边的集合,表示城市间的交通线路
-
C表示城市间交通的开销
-
-
-
NP 完全( NP-complete) 问题
- 还没有一个已知的算法可以比穷举法更好地解决该问题
- 在n个城市的TSP 问题中添加一个城市,就会增加n-1倍的可行解
-
树形搜索:
-
在搜寻可行解时构造一棵树。
- 树的叶子结点对应于一种回路,树的其他结点对应于部分回路——访问了部分城市,但不是全部城市的路线。
-
采用深度优先搜索算法
-
递归的DFS
void Depth_first_search(tour_t tour) { city_t city; if (City_count(tour) == n) { if (Best_tour(tour)) Update_best_tour(tour); } else { for each neighboring city { if (Feasible(tour, city)) { Add_city (tour, city); Depth_first_search (tour); Remove_last_city(tour, city); } } } }
- n表示城市数量函数
- City_count测试回路tour是否访问了n个城市函数
- Best_tour测试是否是最佳回路函数
- Feasible检测city是否被tour访问过
-
非递归的DFS
Push_copy(stack, tour);/*初始tour只有一个城市*/ while (!Empty(stack)) { curr_tour = Pop(stack); if (City_count(curr_tour) == n) { if (Best_tour(curr_tour)) Update_best_tour(curr_tour); } else { for (nbr = n-1; nbr >= 1; nbr--) if (Feasible(curr_tour, nbr)) { Add_city(curr_tour, nbr); Push_copy(stack, curr_tour); Remove_last_city(curr_tour); } } Free_tour(curr_tour); }
- 基本思想:模拟递归实现,递归的函数调用可以通过把当前递归函数的状态压入运行时栈来实现
- 将部分回路作为栈的记录压入栈
void Push_copy ( my_stack_t stack, tour_t tour) { int loc = stack -> list_sz; tour_t tmp = Alloc_tour(); Copy_tour (tour, tmp); stack -> list[loc] = tmp; stack -> list_sz++; } /* Push */
- 多个线程/进程可以方便地各自存取回
- 回路用结构体表示:存储城市的数组、城市的数量和部分回路的代价
-
MPI实现静态并行化树搜索
-
线程0使用广度优先搜索来搜索树,直至部分回路的数量至少为comm_sz。
-
进程0将回路发送给每个进程
- 采用循环的点对点通信发送
- 采用集合通信发送:MPI_Scatterv
-
每个进程选取应该得到的初始部分回路,并把这些回路压入自己的私有栈中。
-
维持最佳回路
-
让每个进程各自计算最佳回路很可能造成计算上的浪费有些进程的最佳回路可能比其他进程的绝大多数回路代价大
-
当一个进程发现了一个新的最佳回路时,只需要发送该最佳回路的代价给其他进程
-
发现最佳回路的进程调用MPI_Send函数
for (dest=0; dest < comm_sz; dest++) if (dest != my_rank) MPI_Send(&new_best_cost, 1, MPI_INT, dest, NEW_COST_TAG, comm) ;
-
使用MPI_Send可能导致进程阻塞,可以用MPI_Bsend
// 目的进程可以周期性地检查新的最佳回路代价的到达 MPI_Iprobe(MPI_ANY_SOURCE, NEW_COST_TAG, comm, &msg_avail,&status); // 如果msg_avail为true,那么通过调用MPI_Recv接收 while (msg_avail) { MPI_Recv(&received_cost, 1, MPI_INT, status. MPI_SOURCE,NEW_COST_TAG, comm, MPI_STATUS_ IGNORE); if (received_cost < best_tour_cost) best_tour_cost = received_cost; MPI_Iprobe(MPI_ANY_SOURCE, NEW_COST_TAG, comm, &msg_avail,&status); }
-
-
输出最佳回路
-
进程将自己的最佳回路发送给进程0
- 存在多个相同代价的最佳回路
- 可能发送不是最佳回路
-
让每个进程存储自己的局部最佳回路,在所有进程完成搜索后都调用MPI_Allreduce ,拥有全局最佳回路的进程才可以把它发送给进程。
struct { int cost; int rank; }loc_data, global_data; loc_data.cost = Tour_cost( loc_best_tour); loc_data.rank = my_rank; // MPI_MINLOC作用于一对参数值 // 第一个参数值是回路的代价,第二个参数值是拥有最佳回路进程的进程号。 MPI_Allreduce(&loc_data, &global_data, 1, MPI_2INT, MPI_MINLOC,comm); if (global_data.rank == 0) return; if (my_rank == 0) Receive best tour from process global_data.rank; else if (my_rank == global_data.rank) Send best tour to process 0;
-
-
未接收的消息
- 某些消息可能在并行化树搜索的过程中被漏掉而没有接收
- 进程可能在某些进程发现最佳回路前已经搜索完了自己的子树
- 不会导致程序输出错误的结果
- 未接收的消息会对调用MPI_Buffer_detach 或MPI_Finalize 造成一些影响
- 如果一个进程正在存储在缓冲区中但未被接收的消息,该进程可能会在这些调用上挂起
- 关闭MPI 前,可以调用MPI_Iprobe 尝试接收那些未被接收的消息
- 某些消息可能在并行化树搜索的过程中被漏掉而没有接收
MPI实现动态并行化树搜索
-
如果将子树初始化分配给各个进程的工作没有做好
- 那些分配到“小”子树的进程就会很早完成工作,同时那些分配到大子树的进程却会继续工作
- 静态并行化策略也无法对工作进行重新分配
- 造成负载不均衡,影响并行程序性能
-
在计算的过程中动态重新分配
- 当一个进程完成任务后,它进入忙等待,等待接收更多的工作或者接收到程序终止的信号。
- 一个有任务可做的进程可以划分它的栈,把自己的工作分配给一个空闲进程。
-
终止条件不是!Empty(stack)为fails,需要重新改写
-
终止函数Terminated
-
判断进程终止的函数Terminated
-
完成任务的进程应该发送请求任务的消息给其他进程
-
当进程进入Terminated 函数时,它会检查是否有来自其他进程的请求任务消息
- 如果有,并且该进程也没有可做的工作,会返回一个拒绝的消息
-
代码
if (My_avail_tour_count(my_stack) >= 2) { /*至少两条"值得发送"的回路*/ Fulfill_request(my_stack); /*检查进程是否已经接收到请求任务的消息,如果没有接收到请求,就直接返回*/ return false; /* 进程还有工作,不终止*/ } else { /* 最多只剩一个回路需要检测*/ Send_rejects(); /*检查是否有请求任务的消息,并发送一个"没有工作" (no work) 的回复给每个请求任务的进程*/ if (!Empty_stack(my_stack)) { return false; /*进程还有工作,不终止*/ } else { /*自己的栈为空*/ if (comm_sz == 1) return true; Out_of_work(); /*宣布自己没有工作了,是“分布式终止检测算法”的部分实现*/ work_request_sent = false; /*设置变量*/ while (1) { /*进程持续等待直到接收到分配的任务或者搜索工作已经完成的消息*/ Clear_msgs(); /*处理任何未被接收的消息, 最佳回路代价的更新或请求任务的消息*/ if (No_work_left()) { return true; /*没有工作,退出*/ } else if (!work_request_sent) { Send_work_request(); /*请求工作*/ work_request_sent = true; } else { /*有一个未完成任务请求*/ Check_for_work(&work_request_sent, &work_avail); if (work_avail) { /*接收新的工作并返回继续搜索*/ Receive_work(my_stack); return false; } } } /*while */ }/*Empty stack */ }/*At most 1 available tour */
-
-
My_avail_tour_count
- 返回进程栈的大小,使用“截至长度”(split_cutoff)
- 发送回路代价很大,可以尝试只发送边数少于截止长度的回路
-
Fulfill_request
- 如果一个进程有足够的任务可做,即可以有效地分离它的栈时,需要调用该函数
- 使用MPI_Iprobe 测试来自其他进程的任务请求
- 如果存在这样的请求,就接收它,并分离自己的栈,分配自己的工作给发送请求任务消息的进程;如果没有这样的请求,进程就继续往下执行。
-
分离栈函数Split_stack
-
分离栈函数Split_stack由Fulfill_request函数调用
- 将少于split_cutoff 个城市的部分回路,并发送给请求任务的进程
- 新栈的内容打包到连续的存储空间,然后把地址连续的内存块发送出去,并由接收者解包到自己的栈里
-
表示回路的结构体
typedef struct { int* cities; /*部分回路中的城市 */ int count; /* 部分回路中的城市数量*/ int cost; /* 部分回路的代价*/ ) tour_struct; typedef tour_struct* tour_t:
-
发送回路的函数Send_tour
void Send_tour(tour_t tour, int dest) { int position = 0; MPI_Pack(tour->cities, n+1,MPI_INT,contig_buf,LARGE,&position,comm); MPI_Pack(&tour->count, 1, MPI_INT,contig_buf,LARGE,&position, comm); MPI_Pack(&tour->cost,1,MPI_INT, contig_buf, LARGE,&position, comm); MPI_Send(contig_buf, position,MPI_PACKED, dest, 0, comm); }
-
接收回路的函数Receive_tour
void Receive_tour(tour_t tour, int src) { int position = 0; MPI_Recv(contig_buf, LARGE,MPI_PACKED, src, 0, comm, MPI_STATUS_IGNORE); MPI_Unpack(contig_buf,LARGE, &position, tour->cities, n+1,MPI_INT,comm); MPI_Unpack(contig_buf,LARGE, &position, &tour->count, 1, MPI_INT,comm); MPI_Unpack(contig_buf,LARGE, &position, &tour->cost,1,MPI_INT,comm); }
-
Send_rejects
- 使用MPI_Iprobe测试来自其他进程的任务请求消息
- 可以通过用WORK_REQ_TAG标签标识任务请求消息
- 找到这样的消息后就接收该消息,然后把"没有适合分配的工作"的信息回复给请求任务的进程
-
-
分布式终止检测
- Out_of_work 和No_work_left实现了终止检测算法
- 分布式终止检测是一个具有挑战性的问题
- 最简单的一种实现是追踪一个守恒量
- 在程序的起始处,每个进程有1个单位的能量。当一个进程完成了任务,就把自己的能量发送给进程0;当一个进程完成了一次请求任务的操作,就把自己的能量一分为二,自己留一份,另外一份发送给接收任务的进程;因为能量是守恒的,起始的份额是comm_sz个单位,所以程序会在进程0接收到comm_sz个单位的能量后终止执行
- 函数Out_of_work在被除了进程0以外的进程执行时会发送能量给进程0;如果进程0调用,就接收由Out_of_work发送的消息,并调整变量received_energy(记录能量数)。
- 如果received_energy 等于comm_sz ,进程0就发送一个终止消息(具有特殊标志)给每个进程。而非0进程会测试是否有一个标志为终止的消息。