MPI并行编程基础

MPI并行编程基础

简介

分布式内存系统

image-20240525132146035

  • 每个处理器有私有内存,处理器只能访问自己的内存运行
  • 在处理器-内存对上的程序称为进程
  • 进程间采用显式的消息传递进行通信:一个进程调用消息发送函数,另一个进程调用消息接收函数

MPI

  • 使用消息传递的实现称为消息传递接口(Message-Passing Interface,MPI)
  • MPI是一个消息传递接口标准,而不是编程语言
  • 具有可移植性

Pack与UnPack

原理

打包(Pack)和解包(Unpack)操作是为了发送不连续的数据, 在发送前显示地把数据包装大一个连续的缓冲区, 在接收之后从连续的缓冲区中解包。

MPI_PACK

MPI_PACK把由inbuf,incount, datatype指定的发送缓冲区中的incount个datatype类型的消息放到起始为 outbuf 的连续空间,该空间共有 outcount 个字节。 输入缓冲区可以是 MPI_SEND 允许的任何通信缓冲区。 入口参数 position 的值是输出缓冲区中用于打包的起始地址,打包后它的值根据打包消息的大小来增加, 出口参数 position 的值是被打包的消息占用的输出缓冲区后面的第一个地址。 通过连续几次对不同位置的消息调用打包操作, 就将不连续的消息放到了一个连续的空间。 comm参数是将在后面用于发送打包的消息时用的通信域。 (NOTE:这里要连续多次调用打包操作)

int MPI_Pack(void* inbuf, int incount, MPI_Datatype datatype, void* outbuf, int outcount, int *position , MPI_Comm comm)

inbuf, 输入缓冲区起始地址(可选数据类型)

incount, 输入数据项个数(整型)

datatype, 每个输入数据项的类型(句柄)

outbuf, 输出缓冲区开始地址(可选数据类型)

outcount, 输出缓冲区大小(整型)

position, 缓冲区当前位置(整型)

comm,通信域

MPI_UNPACK

MPI_UNPACK 和 MPI_PACK 对应, 它从 inbuf 和 insize 指定的缓冲区空间将不连续的消息解开,放到 outbuf, outcount, datatype 指定的缓冲区中。 输出缓冲区可以是 MPI_RECV 允许的任何通信缓冲区。 输入缓冲区是一个连续的存储空间,大小为insize字节, 开始地址为 inbuf。入口参数 position的初始值是输出缓冲区中被打包消息占用的起始地址, 解包后它的值根据打包消息的大小来增加,因此出口参数 position的值是输出缓冲区中被解包的消息占用空间后面的第一个地址。 通过连续几次对已打包的消息调用与打包时相应的解包操作,就可以将连续的消息解开放到一个不连续的空间。comm参数是用于接收消息的通信域。

int MPI_Unpack(void* inbuf, int insize, int *position, void *outbuf, int outcount, MPI_Datatype datatype, MPI_Comm comm)

inbuf, 输入缓冲区起始(可选数据类型)

insize, 输入数据项数目(整型)

position, 缓冲区当前位置,字节(整型)

outbuf, 输出缓冲区开始(可选数据类型)

outcount, 输出缓冲区大小,字节(整型)

datatype, 每个输入数据项的类型(句柄)

comm,打包的消息的通信域(句柄)

例子
int position , i;	//定义两个整型变量 position 用于记录打包和解包的位置,i 用于指示数组 a 的元素数量。
float a[1000];
char buff[1000];	//定义一个字符数组 buff,长度为 1000,作为 MPI 数据传输的缓冲区。
....
MPI_Comm_rank(MPI_Comm_world,&myrank);	//获取当前进程的 rank(唯一标识符),并存储在变量 myrank 中。
if (myrank ==0) {
  /* SENDER CODE */
  int len[2];
  MPI_Aint disp[2];	//定义一个 MPI_Aint 类型的数组 disp,用于存储每个数据块相对于起始地址的偏移量。
  MPI_Datatype type[2], newtype;
  /* build datatype for i followed by a[0]...a[i-1] */
  len[0]=1;	//i的长度为1
  len[1]=i;	//a的长度为i
  MPI_Address( &i,disp);	//disp[0]=&i
  MPI_Address( a,disp+1);	//disp[1]=a
  type[0]=MPI_INT;
  type[1]=MPI_FLOAT;
  MPI_Type_struct(2,len,disp,type,&newtype);	//构造函数
  MPI_Type_commit(&newtype);
  /*Pack i followed by a[0]...a[i-1] */
  position =0;
  MPI_Pack(MPI_BOTTOM, 1,newtype, buff, 1000,&position,MPI_COMM_WORLD);
  /* Send */
  MPI_Send(buff,postion, PI_PACKED,1,0, MPI_COMM_WORLD)
    /* ***** One can replace the last three lines with^M MPI_Send( MPI_BOTTOM,1,newtype, 1, 0,MPI_COMM_WORLD); ***** */
}

else /* myrank ==0 */ {
  /* RECIEVER CODE */
  MPI_Status status; /* Receive */
  MPI_Recv(buff, 1000,MPI_PACKED,0,0,&status); /* Unpack i */
  position =0;
  MPI_Unpack(buff,1000,&position,&i,1,MPI_INT,MPI_COMM_WORLD);
  /* Unpack a[0]...a[i-1] */
  MPI_Unpack(buff,1000,&position,a,i,MPI_FLOAT,MPI_COMM_WORLD);
}

MPI_Type_vector

MPI_Type_vector 允许复制的数据之间有空隙,下面是函数原型:

int MPI_Type_vector(
    int count,              // 块的数量
    int blocklength,        // 每个块中所含元素的个数
    int stride,             // 各块第一个元素之间相隔的元素数
    MPI_Datatype oldtype,   // 旧数据类型
    MPI_Datatype *newtype   // 新数据类型
)

为了更加直观的理解,我们给出 count=2, blocklength=2, stride=3 时的示例图。上面的是原始数据,下面的新数据类型所包含的数据。
在这里插入图片描述
MPI_Type_hvectorMPI_Type_vector 功能类似,只不过 MPI_Type_hvector 针对的是字节,下面是函数原型:

int MPI_Type_hvector(
    int count,              // 块的数量
    int blocklength,        // 每个块中所含元素的个数
    int stride,             // 各块第一个元素之间相隔的字节数
    MPI_Datatype oldtype,   // 旧数据类型
    MPI_Datatype *newtype   // 新数据类型
)

下面是使用 MPI_Type_vector 的一个使用示例:

void vector_type() {
    int rank;
    int n = 10;
    int buffer[10];
    int i;
    MPI_Datatype newtype;
    MPI_Status status;
    MPI_Init(NULL, NULL);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Type_vector(2, 2, 3, MPI_INT, &newtype);
    MPI_Type_commit(&newtype);
    if(rank == 0) {
        for(i = 0; i < n; i++) {
            buffer[i] = i + 1;
        }
        MPI_Send(&buffer, 1, newtype, 1, 99, MPI_COMM_WORLD);
    }
    if(rank == 1) {

        for(int i = 0; i < n; i++) {
            buffer[i] = 100;
        }
        MPI_Recv(&buffer, 1, newtype, 0, 99, MPI_COMM_WORLD, &status);
        for(i = 0; i < n; i++) {
            printf("buffer[%d] is %d\n", i, buffer[i]);
        }
    }
    MPI_Finalize();
}

MPI_Type_vector(2, 2, 3, MPI_INT, &newtype); 创建一个向量数据类型。这里表示创建一个向量,包含 2 个块,每个块有 2 个整数,块之间的跨度是 3 个整数的空间。因此,如果我们有一个数组,这个向量数据类型将选择数组中的第 0,1,3,4 四个整数。

rank == 1 的进程接收数据并打印数组内容时,它将显示 buffer 数组的元素。由于发送的数据类型 newtype 是向量类型,选择了数组中的特定元素(0,1,3,4)发送,接收端将在这些位置接收到值,其余位置的值保持初始化值 100。

发送端 (rank == 0) 的初始化是这样的:

  • buffer = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

接收端 (rank == 1) 在接收前后的数组值是:

  • 接收前:buffer = {100, 100, 100, 100, 100, 100, 100, 100, 100, 100}

  • 接收后,由于接收的数据类型选择了元素 0, 1, 3, 4,因此预期更新后的数组是:

    buffer = {1, 2, 100, 3, 4, 100, 100, 100, 100, 100}

通信域

通信域(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_COMM_WORLD是所有进程的集合

MPI_Comm_dup和MPI_Comm_split函数

MPI_Comm_dup(MPI_COMM_WORLD,&MyWorld)创建了一个新的通信域MyWorld,它包含了与原通信域MPI_COMM_WORLD相同的进程组,但具有不同的通信上下文。

MPI_Comm_split(MyWorld,Color,Key,&SplitWorld)函数调用则在通信域MyWorld的基础上产生了几个分割的子通信域。原通信域MyWorld中的进程按照不同的Color值处在不同的分割通信域中,每个进程在不同分割通信域中的进程编号则由Key值来标识。

MPI_Comm MyWorld, SplitWorld;
int my_rank, group_size, Color, Key;
MPI_Init(&argc, &argv);
MPI_Comm_dup(MPI_COMM_WORLD,&MyWorld);
MPI_Comm_rank(MyWorld,&my_rank);
MPI_Comm_size(MyWorld,&group_size);
Color=my_rank%3;
Key=my_rank/3;
MPI_Comm_split(MyWorld,Color,Key,&SplitWorld);
Rank in MyWorld0123456789
Color0120120120
Key0001112223
Rank in SplitWorld(Color=0)0123
Rank in SplitWorld(Color=1)012
Rank in SplitWorld(Color=2)012

组间通信域

组间通信域是一种特殊的通信域,该通信域包括了两个进程组,分属于两个进程组的进程之间通过组间通信域实现通信。

一般把调用进程所在的进程组称为本地进程组,而把另外一个称为远程进程组。

消息状态

消息状态(MPI_Status类型)存放接收消息的状态信息,包括:

  • 消息的源进程标识--MPI_SOURCE
  • 消息标签--MPI_TAG
  • 错误状态--MPI_ERROR
  • 其他--包括数据项个数等,但多为系统保留的。

是消息接收函数MPI_Recv的最后一个参数。当一个接收者从不同进程接收不同大小和不同标签的消息时,消息的状态信息非常有用

while (true){
  MPI_Recv(received_request,100,MPI_BYTE,MPI_Any_source,MPI_Any_tag,comm,&Status);
  switch (Status.MPI_Tag) {
    case tag_0: perform service type0;
    case tag_1: perform service type1;
    case tag_2: perform service type2;
  }
}

点对点通信

通信模式

指的是缓冲管理,以及发送方和接收方之间的同步方式

同步(synchronous)通信模式

只有相应的接收过程已经启动,发送过程才正确返回。

同步发送返回后,表示发送缓冲区中的数据已经全部被系统缓冲区缓存,并且已经开始发送。

同步发送返回后,发送缓冲区可以被释放或者重新使用。

在这里插入图片描述
在这里插入图片描述

缓冲(buffered)通信模式

缓冲通信模式的发送不管接收操作是否已经启动都可以执行。

但是需要用户程序事先申请一块足够大的缓冲区,通过MPI_Buffer_attch实现,通过MPI_Buffer_detach来回收申请的缓冲区。

在这里插入图片描述
在这里插入图片描述

标准(standard)通信模式

是否对发送的数据进行缓冲由MPI的实现来决定,而不是由用户程序来控制。

发送可以是同步的或缓冲的,取决于实现。

image-20240525171044217
在这里插入图片描述

就绪(ready)通信模式

发送操作只有在接收进程相应的接收操作已经开始才进行发送。

当发送操作启动而相应的接收还没有启动,发送操作将出错。就绪通信模式的特殊之处就是接收操作必须先于发送操作启动

在这里插入图片描述
在这里插入图片描述

通信机制

阻塞和非阻塞通信的主要区别在于返回后的资源可用性

阻塞通信返回的条件:

  • 通信操作已经完成,即消息已经发送或接收。
  • 调用的缓冲区可用。若是发送操作,则该缓冲区可以被其它的操作更新;若是接收操作,该缓冲区的数据已经完整,可以被正确引用。

在这里插入图片描述
非阻塞通信返回后并不意味着通信操作的完成,MPI还提供了对非阻塞通信完成的检测,主要的有两种MPI_Wait函数和MPI_Test函数

在这里插入图片描述

MPI的发送操作支持四种通信模式,它们与阻塞属性一起产生了MPI中的8种发送操作。

而MPI的接收操作只有两种:阻塞接收和非阻塞接收。

MPI 原语阻塞非阻塞
标准通信MPI_SendMPI_Isend
同步通信MPI_ SsendMPI_ Issend
缓冲通信MPI_ BsendMPI_ Ibsend
就绪通信MPI_ RsendMPI_ Irsend
接收函数MPI_RecvMPI_Irecv
完成检测MPI_WaitMPI_Test
消息到达检测MPI_ProbeMPI_Iprobe
  • 33
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值