并行程序设计(4):MPI编程

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_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_CHARsigned charMPI_UNSIGNEDunsigned int
    MPI_SHORTsigned short intMPI_UNSIGNED_LONGunsigned long int
    MPI_INTsigned intMPI_FLOATfloat
    MPI_LONGsigned long intMPI_DOUBLEdouble
    MPI_LONG_LONGsigned long long intMPI_LONG_DOUBLElong double
    MPI_UNSIGNED_CHARunsigned charMPI_BYTE
    MPI_UNSIGNED_SHORTunsigned short intMPY_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不一致,数据将无法接收
      commMPI 通信器,用于指定通信域
  • 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,则表示接收任何标记的消息
      commMPI 通信器,用于指定通信域。
      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_SHORTshort int
    MPI_INTint
    MPI_LONGlong int
    MPI_LONG_LONGlong long int
    MPI_UNSIGNED_CHARunsigned char
    MPI_UNSIGNED_SHORTunsigned short int
    MPI_UNSIGNEDunsigned int
    MPI_UNSIGNED_LONGunsigned long int
    MPI_UNSIGNED_LONG_LONGunsigned long long int
    MPI_FLOATfloat
    MPI_DOUBLEdouble
    MPI_LONG_DOUBLElong double
    MPI_BYTEchar
  • 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原始数据元素的数量
      datatypeMPI 原始数据类型描述符
      outbuf指向目标缓冲区的指针
      outsize目标缓冲区的大小
      position指向目标缓冲区中下一个可用位置的指针
      commMPI 通信域
    • 例子

      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目标数据元素的数量
      datatypeMPI 原始数据类型描述符
      commMPI 通信域
    • 例子

      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>⋅⋅⋅<基类型n1,偏移n1>}
    在这里插入图片描述

  • 在派生数据类型中,基类型可以是任何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_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_SendMPI_Isend
同步通信MPI_ SsendMPI_ Issend
缓冲通信MPI_ BsendMPI_ Ibsend
就绪通信MPI_ RsendMPI_ Irsend
接收函数MPI_RecvMPI_Irecv
完成检测MPI_WaitMPI_Test
消息到达检测MPI_ProbeMPI_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_GathervMPI_Gather的一般化
    MPI_Allgather全局收集
    MPI_AllgathervMPI_Allgather的一般化
    MPI_Scatter一对多散播不同的消息
    MPI_ScattervMPI_Scatter的一般化
    MPI_Alltoall多对多全局交换消息
    MPI_AlltoallvMPI_Alltoall的一般化
  • 聚集函数

    函数名含义
    MPI_Reduce多对一归约
    MPI_AllreduceMPI_Reduce的一般化
    MPI_Reduce_scatterMPI_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_datatypeMPI_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_AllreduceMPI_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+1xi
    • 两条垂直线段的长度分别为 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=nba

  • 如果称最左边的端点为 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=ax1=a+hx2=a+2hxn1=a+(n1)hxn=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(xn1)+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进程会测试是否有一个标志为终止的消息。
  • 27
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值