MPI——基础知识

基础知识

编译与执行

编译和运行程序的细节主要取决于系统,很多系统都成为mpicc的命令来编译程序

mpicc -g -Wall -o  mpi_hello mpi_hello.c

mpicc是C语言编译器的包装脚本(wrapper script)。包装脚本的主要目的是运行某个程序。指定使用c99

mpiexec -n <number of processes> ./mpi_hello

很多系统还支持用mpiexec命令来启动程序

例如:使用1个进程运行程序

mpiexec -n 1 ./mpi_hello

使用4个进程运行程序

mpiexec -n 4 ./mpi_hello

对于程序

#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, "Greeting from process %d of %d!",my_rank,comm_sz);
        MPI_Send(greeting, strlen(greeting)+1, MPI_CHAR, 0, 0, MPI_COMM_WORLD);
    }
    else {
        printf("Greeting 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_Finalize();
    return 0;
}

由于里面有C语言的新特性,可以指定

mpicc -g -Wall -std=c11  -o hello hello.c

运行4个进程,得到的输出

➜  mpiexec -n 4 ./hello
Greeting from process 0 of 4!
Greeting from process 1 of 4!
Greeting from process 2 of 4!
Greeting from process 3 of 4!

MPI程序

所有的MPI定义的标识符都是由字符串MPI_开始,下划线后的第一个字符大写,表示函数名和MPI定义的类型。MPI定义的宏和常量的所有字母都是大写的。

MPI_Init和MPI_Finalize

调用MPI_Init是为了告知系统进行所有必要的初始化设置。

int MPI_Init(
	int * argc_p /*in/out*/,
    char ** argv_p /*in/out*/
);

参数argc_pargv_p是指向参数argcargv的指针。当不需要的时候设置为NULL。

调用MPI_Finalize是为了告知MPI系统MPI已经使用完毕。为MPI分配的任何资源都可以释放了。

MPI程序的基础构架

#include <mpi.h>

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

通信子

通信子(communicator)指的是一组可以互相发送信息的进程集合。MPI_Init的其中一个目的,是在用户启动程序时,定义有用户启动的所有进程组成的通信子。称为MPI_COMM__WORLD

int MPI_Comm_size(
	MPI_Comm comm ,
    int * comm_sz_p
);
int MPI_Comm_rank(
	MPI_Comm comm,
    int * my_rank_p
);

第一个参数是一个通信子,它所属的类型是MPI的通信子定义的特殊类型:MPI_Comm

MPI_Comm_size 函数在它的第二个参数返回通信子的进程数。MPI_Comm_rank函数在它的第二个参数返回正在调用进程的通信子中的进程号。

MPI_Send

1,2,.comm_sz11,2,\cdots.comm\_sz-11,2,⋯.comm_sz−1号进程执行的发送其实很复杂的。

int MPI_Send(
	void * 	msg_buf_p,
    int		msg_size,
    MPI_Datatype 	msg_type,
    int 	dest,
    int 	tag,
    MPI_Comm	commmunicator
);

第一个参数msg_buf_p是一个指向消息内容的内存块的指针。

第二个参数和第三个参数:msg_size和msg_type 指定了要发送的数据量

MPI数据类型 C语言数据类型
MPI_CHAR signed char
MPI_SHORT signed short int
MPI_INT signed int
MPI_LONG signed long int
MPI_LONG_LONG signed 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_FLOAT float
MPI_DOUBLE double
MPI_LONG_DOUBLE long double
MPI_BYTE
MPI_PACKED

第四个参数:dest指定了要接收消息的进程的进程号

第五个参数:tag是个非负int型,用于区分看上去完全一样的消息。

最后一个参数:是一个通信子,用于指定通信范围。通信子指的是一组互相发送消息的进程的集合。一个通信子的进程所发送的消息不能被另一个通信子的进程所接收。

MPI_Recv

int MPI_Recv(
    void * msg_buf_p,
    int 	buf_size,
    MPI_Datatype	buf_type,
    int 	source,
    int 	tag, 
    MPI_Comm	communicator,
    MPI_Status*	status_p
);

参数source用来指定了接收消息应该从哪个进程发送来的,参数tag要与发送消息的参数tag相匹配。参数communicator必须与发送进程所用的通信子匹配。

消息匹配

假定qqq号进程调用MPI_Send()函数

MPI_Send(send_buf_p, send_buf_sz, send_type, dest, send_tag, send_comm)

假定rrr号进程调用了MPI_Recv()函数

MPI_Recv(recv_buf_p, recv_buf_sz, recv_type, src, recv_tag, recv_comm, &status)

则q号进程调用MPI_Send函数所发送的消息可以被r号进程调用MPI_Recv函数接收,如果

  • recv_comm = send_comm
  • recv_tag = send_tag
  • dest=r & src =q

在多数的情况下,满足下面的规则就可以了

  • 如果recv_type = send_type 同时recv_buf_sz \ge≥ send_buf_sz 那么由q号进程发送的消息就可以被r号进程成功的接收。

一个进程可以接收多个进程发送的消息,接收进程并不知道其他进程执行发送消息的顺序。MPI提供了一个特殊的常量MPI_ANY_SOURCE,就可以传递给MPI_Recv。

for(i=1; i<comm_sz;i++) {
    MPI_Recv(result, result_sz, result_type, MPI_ANY_SOURCE, result_tag, comm, MPI_STATUS_IGNORE);
    Process_result(result);
}

类似的,一个进程有可能接收多条来自另一个进程的有着不同的标签的消息,并且接收进程不知道消息发送的顺序。

使用通配符(wildcard)参数时,需要注意的几点:

  • 只要接收者可以调用通配符参数,发送者必须指定一个进程号与另一个非负整数标签。此外,MPI使用的是所谓的推(push)通信机制,而不是拉(pull)通信机制。
  • 通信子参数没有通配符,发送者和接收者必须指定通信子。

status_p

MPI类型MPI_Status是一个有至少三个成员的结构,MPI_SOURCE,MPI_TAG和MPI_ERROR。假定程序有如下的定义:

MPI_Status status;

&status作为最后一个参数传递给MPI_Recv函数并调用它后,可以通过检查以下两个成员来确定发送者和标签。

status.MPI_SOURCE
status.MPI_TAG

接收量不是存储在应用程序可以直接访问到的域内,但是用户可以调用MPI_Get_count函数找到这个值。

int MPI_Get_count(
	MPI_Status * 	status_p,
    MPI_Datatype 	type,
    int *			count_p
);

MPI_Send和MPI_Recv

发送进程可以缓冲消息,也可以阻塞。

如果是缓冲消息,则MPI系统将会把消息放置在自己内部存储器里,并返回MPI_Send的调用。

如果是系统发生阻塞,那么它将一直等待,知道可以开始发送消息,并不立即返回MPI_Send的调用。

MPI_Send 的精确行为可以由MPI实现所决定的,但是,典型的实现方法有一个默认的消息截止大小,如果一条消息的大小小于截止大小,,就会被缓冲,如果大于截止大小,就会被阻塞。

MPI_Recv函数总是阻塞是,直到接收到一条匹配消息,当MPI_Recv函数调用返回时,就知道一条消息已经存储在接收缓冲区中,接收消息函数同样可以替代,系统检查是否有一条匹配的消息并返回。

MPI要求消息是不可超越的(nonvertaking),如果q号进程发送两条消息给r号进程,那么q号发送的第一条消息必须在第二条消息之前可用,但是如果消息来自不同的进程的消息的到达顺序是没有限制的。

上一篇:三阶段遇到的问题(二)


下一篇:MPI创建的进程的共享内存访问控制机制