【笔记】OpenMPI基本使用1

【笔记】OpenMPI基本使用1

介绍

在 90 年代之前,对不同的计算架构写并发程序是一件困难而且复杂的事情。当时,虽然很多软件库可以帮助写并发程序,但是并没有一个大家都接受的标准。

在当时,大多数的并发程序出现在科学研究领域。其中最广为接受的模型就是消息传递模型。那么,什么是消息传递模型呢?它其实是指程序通过在进程间传递消息(消息可以理解成带有一些信息和数据的一个数据结构)来完成某些任务。在实践中,将并发程序用这个模型实现十分容易。举例来说,主进程(manager process)可以通过对从进程(worker process)发送一个描述工作的消息来把这个工作分配给它。实际上,几乎所有的并行程序都可以使用消息传递模型来描述。

由于当时很多软件库都用到了消息传递模型,但是在定义上却有一些差异,这些库的作者以及其他一些人为了解决这个问题就在1992 Supercomputing 大会上定义了一个消息传递接口的标准,也就是 MPI。这个标准接口使得程序员写的并发程序可以在所有主流的并发框架中运行,并且允许使用一些流行库的特性和模型。

到 1994 年的时候,一个完整的接口标准已经被定义好了(MPI-1)。MPI 其实就是一个接口的定义,然后需要程序员去根据不同的架构去实现这个接口。但很幸运的是仅仅一年之后,一个完整的 MPI 实现就出现了,之后 MPI 就被大量地使用在消息传递应用程序中,并且一直是写这类程序的标准(de-facto)。

设计

通讯器定义了一组能够互相发消息的进程。在这组进程中,每个进程会被分配一个序号,称作秩(rank),进程间显性地通过指定 rank 来进行通信。

通信的基础建立在不同进程间发送和接收操作。一个进程可以通过指定另一个进程的 rank 以及一个独一无二的消息标签(tag)来发送消息给另一个进程。接受者可以只接收特定标签标记的消息(或者也可以完全不管标签来接收任何消息),然后依次处理接收到的数据。类似这样的涉及一个发送者以及一个接受者的通信被称作点对点(point-to-point)通信。

当然在很多情况下,某个进程可能需要跟所有其他进程通信。比如主进程想发一个广播给所有的从进程。在这种情况下,手动去写一个个进程点对点的信息传递就显得很笨拙,并且这样也会导致网络利用率低下。MPI 有专门的接口来帮我们处理这类所有进程间的集体性(collective)通信。

安装

安装过程详见openmpi入门1-安装与测试,这里只简单介绍一下

openmpi官网下载openmpi,也可以使用wget下载:

wget https://download.open-mpi.org/release/open-mpi/v4.0/openmpi-4.0.4.tar.gz

下载完成之后进行解压:

tar -zxvf openmpi-4.0.4.tar.gz

进入openmpi-4.0.4文件夹检查配置文件

cd openmpi-4.0.4
./configure

编译安装

sudo make all install

配置openmpi环境变量

vim /etc/profile

# 在这个文件末尾添加如下两行
export PATH=/usr/local/path:$PATH
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
source /etc/profile

进入到examples文件夹中,执行make编译一下测试代码,进行测试

mpirun -np 4 hello_c

正常输出结果则说明安装完成

如果出现 enough slots available in the system 报错可传送至最后一节查看解决方法

Hello World

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

int main(int argc, char** argv) {
    
    
    // Initialize the MPI environment
    MPI_Init(NULL, NULL);

    // Get the number of processes
    int world_size;
    MPI_Comm_size(MPI_COMM_WORLD, &world_size);

    // Get the rank of the process
    int world_rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);

    // Get the name of the processor
    char processor_name[MPI_MAX_PROCESSOR_NAME];
    int name_len;
    MPI_Get_processor_name(processor_name, &name_len);

    // Print off a hello world message
    printf("Hello world from processor %s, rank %d out of %d processors\n",
           processor_name, world_rank, world_size);

    // Finalize the MPI environment.
    MPI_Finalize();
}
EXECS=mpi_hello_world
MPICC?=mpicc

all: ${EXECS}

mpi_hello_world: mpi_hello_world.c
	${MPICC} -o mpi_hello_world mpi_hello_world.c

clean:
	rm ${EXECS}

我们可以编辑makefile文件来运行此Demo,注意makefile文件复制过程中的 Tab

make
mpirun --oversubscribe -np 4 mpi_hello_world

image-20230305200508098

OpenMPI程序基本结构

结合上述Hello World,我们不难发现 OpenMPI程序的基本结构

首先添加 MPI 头文件#include <mpi.h>. 然后使用以下命令进行 MPI 初始化:

MPI_Init(int* argc,char*** argv)

MPI_Init 的过程中,所有 MPI 的全局变量或者内部变量都会被创建。举例来说,一个通讯器 communicator 会根据所有可用的进程被创建出来(进程是我们通过 mpi 运行时的参数指定的),然后每个进程会被分配独一无二的秩 rank。当前来说,MPI_Init 接受的两个参数是没有用处的,不过参数的位置保留着,可能以后会需要用到。

MPI_Init 之后,有两个主要的函数被调用,这两个函数是几乎所有 MPI 程序都会用到的。

MPI_Comm_size(MPI_Comm communicator,int* size)

MPI_Comm_size返回通信器的大小。在示例中,MPI_COMM_WORLD(由 MPI 为我们构建)包含其所有进程,因此该调用应返回进程数。

MPI_Comm_rank(MPI_Comm communicator,int* rank)

MPI_Comm_rank返回通信器中进程的rank。通信器内的每个进程都被分配了一个从零开始的递增等级。进程的rank主要用于发送和接收消息时的识别。

程序的最终调用是MPI_Finalize用于清理 MPI 环境。

我们亦可以通过一些配置实现多机的分布式并行计算,这里先不再介绍。

发送与接收

MPI 的发送和接收方法是按以下方式进行的:

开始的时候,A 进程决定要发送一些消息给 B 进程。A进程就会把需要发送给B进程的所有数据打包好,放到一个缓存里面。因为所有数据会被打包到一个大的信息里面,因此缓存常常会被叫做信封(envelopes),就像我们把好多信纸打包到一个信封里,然后再寄去邮局。数据打包进缓存之后,通信设备(通常是网络)就需要负责把信息传递到正确的地方。这个正确的地方也就是根据特定 rank 确定的那个进程。

尽管数据已经被送达到 B 了,但是进程 B 依然需要确认它想要接收 A 的数据。一旦它进行了确定,数据就被传输成功了。进程 A 会接收到数据传递成功的信息,然后去干其他的事情。

有时候 A 需要传递很多不同的消息给 B。为了让 B 能比较方便地区分不同的消息,MPI 发送者和接受者额外指定一些信息ID (即标签 tags)。当 B 只要求接收某种特定标签的信息的时候,其他的不是这个标签的信息会先被缓存起来,等到 B 需要的时候才会给 B。

MPI_Send(
    void* data,
    int count,
    MPI_Datatype datatype,
    int destination,
    int tag,
    MPI_Comm communicator)
MPI_Recv(
    void* data,
    int count,
    MPI_Datatype datatype,
    int source,
    int tag,
    MPI_Comm communicator,
    MPI_Status* status)

第一个参数是数据缓冲区。第二个和第三个参数分别描述了数据的数量和类型。MPI_Send会精确地发送 count 指定的数量个元素,MPI_Recv会最多接受 count 个元素接。第四个和第五个参数指定发送方/接受方进程的rank以及信息的标签。第六个参数指定通信器,MPI_Recv 方法特有的最后一个参数提供了接受到的信息的状态。

相关例子与测试:https://github.com/mpitutorial/mpitutorial/tree/gh-pages/tutorials/mpi-send-and-receive/code

报错:enough slots available in the system

原因:在虚拟机中运行经常会出现报错enough slots available in the system,这是由于Open MPI 会估算我们的CPU承载能力,用一定算法计算出进程的上限。

解决:一般来说,我们加上--oversubscribe超出上限个数运行即可(但要注意可能会有未定义行为)

参考

A Comprehensive MPI Tutorial Resource

猜你喜欢

转载自blog.csdn.net/m0_52739647/article/details/129350739