MPI学习记录

先来个总程序

#include<stdio.h>
#include<string.h>
#include<mpi.h>

const int MAX_STRING=100;

int main(){
    char greeting[MAX_STRING];
    int comm_sz; //线程的个数
    int my_rank;

    MPI_Init(NULL,NULL);
    MPI_Comm_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{
        printf("Greetings from process %d of %d!\n",my_rank,comm_sz);
        for(int q=1;q<comm_sz;q++){
            MPI_Recv(greeting,MAX_STRING,MPI_CHAR,q,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
            printf("%s\n",greeting);
        }
    }

    MPI_Finallize();
    return 0;
}

MPI定义的宏和常量都是大写的,但是像初始化、结束回收、Comm_size、Comm_rank就是首字母大写。

初始化和结束MPI_Init和MPI_Finalize

完整的函数应该是这样

int main(int argc,char *argv[]){
	MPI_Init(&argc,&argv);
	MPI_Finalize();
}

MPI_Comm_size和MPI_Comm_rank

传入两个参数,一个是通信子,一个是进程数/进程号。通信子是MPI专门定义的数据类型MPI_Comm,我们使用的时候只要传MPI_COMM_WORLD即可。

MPI_Send

这是最关键的函数。一共6个参数,逐个分析。
①void *msg_buf_p:指向消息内容的内存块的指针。
②int msg_size:传递的消息的长度。如果要传字符串,使用字符数组的形式的话,就要传数组的长度(注意要算上\0)。如果传的是单个数字,就写1即可。
③msg_type:数据类型。全大写,比如MPI_INT,MPI_CHAR。
④int dest:指明了要接收的进程的进程号。
⑤int tag:非负的int。用于区分看上去完全一样的数据。比如同一批数据有的要打印有的要计算,可以设置这个标识位让有的比如0用来打印,1用来输出。需要接受函数与其对应
⑥MPI_Comm communicator:通信子。通信子是规定了一个集合,在这个集合之内的进程才可以互相通信。用来指定范围的。在总程序中,使用MPI_COMM_WORLD作为该通信子。

MPI_Recv

7参数,一个个看:
前三个和Send一样。
④int source:指明了从哪个进程发过来。确定要接收的消息的来源地。
⑤int tag:要和发送方相匹配
⑥通信子:也要匹配
⑦MPI_Status* status_p:大部分时候用不到这个,使用常量MPI_STATUS_IGNORE即可。
所以,对照总程序,for循环遍历从1号到n号进程,将遍历变量q的值赋值给source就是遍历发送进程的进程号。

很自然地,会想到一个问题:我们不是要并行程序么?为什么非得按顺序从1到n接收?
所以,为了解决这个问题,MPI提供一个特殊的常量MPI_ANY_SOURCE,将其用在MPI_Recv中换掉刚才的q,就可以让只要有进程准备给0号发,就可以发,而不用必须等到前面的发送完了再发。
同理,tag也可能有不同的取值,所以MPI也提供了MPI_ANY_TAG,用来当做tag的通配符。

注意点:
①只有接收方可以使用通配符
②通信子没有通配符

MPI_Send和MPI_Recv的语义

MPI_Send可以设置一个截止大小cutoff,若一条消息小于cutoff,则缓冲;大于则阻塞。
MPI_Recv则是一直阻塞的,直到接收到一条匹配的消息。因此,当Recv返回的时候是一定接收了一条消息的。而Send发出了也不懂是在缓冲里还是已经到接受方。

MPI要求的仅仅是同一个进程发送的消息有先后到达顺序,而不同的进程顺序不能限制。

梯形面积计算实例

int main(){
    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;
    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_Recv(&local_int,1,MPI_DOUBLE,source,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
            total_int+=local_int;
        }
    }
	if(my_rank==0){
	printf...;
	}
    MPI_Finalize();
    return 0;
}

double Trap(double left,double right,int basecount,double baselen){
    double estimate,x;
    int i;
    estimate = (f(left)+f(right)) / 2.0;
    for(i =1;i<basecount;i++){
        x = left+i*baselen;
        estimate+=f(x);
    }
    estimate = estimate*baselen;
    return estimate;
}

上述程序在输出的时候存在一个问题,各个进程会抢占输出的资源,这不是我们期望的。我们希望进程到来的顺序不限制,但是输出是由0号进程决定的。

输入问题

我们只希望0号进程从stdin中读取数据,所以我们自己实现读取控制函数get_input,这个函数必须在MPI_Comm_rank和MPI_Comm_size之后使用。

...
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);

Get_input(my_rank,comm_sz,&a,&b,&n);
...

Get_input的实现如下:

void Get_input(int my_rank,int comm_sz,double* ap,double* bp,int *np){
	int dest;//定义0号传送的目的地
	if(my_rank==0){
		printf("Enter a,b and n\n");
		scanf("%lf %lf %d",&ap,&bp,&np);
		for(dest = 1;dest<comm_sz;dest++)({
			MPI_Send(ap,1,MPI_DOUBLE,dest,0,MPI_COMM_WORLD);
			MPI_Send(bp,1,MPI_DOUBLE,dest,0,MPI_COMM_WORLD);
			MPI_Send(np,1,MPI_DOUBLE,dest,0,MPI_COMM_WORLD);
		}
	}else{
		MPI_Recv(ap,1,MPI_DOUBLE,0,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
		MPI_Recv(bp,1,MPI_DOUBLE,0,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
		MPI_Recv(np,1,MPI_DOUBLE,0,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
	}

}

写完明显感觉很扯淡,如果有n个变量,这个程序岂不是得cv n次MPI_Send和MPI_Recv?后面有解决方案。

集合通信

概念:为了避免无限制的程序优化(树形结构通信),MPI的开发人员站出来了!他给我们提供了归约函数MPI_Reduce。由于涉及两个以上的进程通信,像这种涉及通信子中所有进程的通信称为集合通信。区别于MPI_Send和MPI_Recv这种通信方式,我们把MPI_Send和MPI_Recv称为点对点通信。

MPI的工作人员们实现了几个常见的操作供我们归约时使用,这个到时候用到了查表即可。MPI_Reduce函数一共7个参数,逐一解释:
①输入变量的指针
②输出变量的指针
③int count;数的个数
④datatype;预定义的类型
⑤MPI_Op operator;查表的操作
⑥int dest_process;归约到谁身上
⑦通信子

e.g.

MPI_Reduce(&local_int, &total_int, 1, MPI_DOUBLE, MPI_SUM, 0,MPI_COMM_WORLD);
//这里0,直接写归约到的最终位置,不用像之前那样通过循环变量确定

N维向量就得传N
注意点:
①通信子中所有进程必须使用同个集合通信函数。比如MPI_Reduce与MPI_Resc匹配就会出错。
②每个进程传递给MPI集合通信函数的参数必须是相容的。就是都传一样的
③虽然输出变量只作用在dest上,但其他的进程也得传。
④点对点是通过tag和通信子匹配的,然而集合通信函数不看标签,只通过通信子调用的顺序来匹配。

感觉0号进程是大爹,全看0的指挥。e.g.
0~2号进程都是a=1;c=2;
0: Reduce(&a,&b); 1:Reduce(&c,&d); 2:Reduce(&a,&b); //b最后是1+2+1=4
1: Reduce(&c,&d); 1:Reduce(&a,&b); 2:Reduce(&c,&d); //d最后是2+1+2=5

别作死把这两个参数重名。

MPI_Allreduce

这种结构被称为蝶形结构。就是两棵树相接。不仅计算出结果,也要将结果分发给每个进程。
他的参数和MPI_Reduce是一样的,除了没有dest_process这个参数。

MPI_Bcast

广播:把一个进程的数据分发给一个通信子里的其他进程。一共有5个参数:
①void* data_p;数据的起始地址
这里补充一下:前面的MPI_Reduce之所以那些变量要加&,是因为定义的时候定义成double类型的,而这里直接传名字是因为定义的类型就是指针类型
②int count:数据量
③datatype:数据类型
④source:哪个人发的
⑤通信子
新的Get_input实现为:

void Get_input(int my_rank,int comm_sz,double* ap,double* bp,int *np){
	int dest;//定义0号传送的目的地
	if(my_rank==0){
		printf("Enter a,b and n\n");
		scanf("%lf %lf %d",&ap,&bp,&np);
	}
    MPI_Bcast(ap,1,MPI_DOUBLE,0,MPI_COMM_WORLD);
    MPI_Bcast(bp,1,MPI_DOUBLE,0,MPI_COMM_WORLD);
    MPI_Bcast(np,1,MPI_DOUBLE,0,MPI_COMM_WORLD);
}

数据分发的思想

有块划分、循环划分、块-循环划分三种方法。

散射

假设由0号进程读取了一个向量,要让所有进程都获得向量的某个部分。如果直接像之前的方法广播的话,有些不必要的存储开销和通信开销就浪费了。我们假设在使用数据分发(块划分等)之后,只需要向0~9号进程发送需要的分量。MPI也提供了一个函数;

MPI_Scatter

由于这个函数的实现是将第一个块分给了0号,第二个块给了1号依次类推,所以数据分发的时候只能用块划分方法。并且向量的分量个数必须可以整除comm_size。
8个参数,逐个解释:
①void* send_buff_p;指向要被发送的变量
②int send_count;指定要被分成comm_size份
③发送数据的MPI数据类型
④void* recv_buf_p;每个进程应该分得的本地变量指针
⑤int recv_count;每个进程分到的数据数量
⑥接收数据的MPI数据类型
⑦int src_proc:分发数据的进程号
⑧通信子

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值