MPI-note-2

MPI-note-2

Charles Lv7

MPI编程笔记

image

C语言中的头文件 #include "mpi.h"

4个基本函数

MPI_Init(int *argc,char ***argv)
完成MPI程序初始化工作,通过获取main函数的参数,让每一个MPI程序都能获取到main函数

MPI_Comm_rank(MPI_Comm comm,int *rank)
用于获取调用进程在给定的通信域中的进程标识号。默认一个最大通信域word。
通信域中的序号是有序的
假设一个通信域中有p个进程,编号为0到p-1,利用进程的序号来决定负责计算数据集的哪一个部分。

MPI_Comm_size(MPI_Comm comm,int *size)
返回给定的通信域中所包含的进程总数

MPI_Finalize(void)
MPI程序的最后一个调用,清除全部MPI环境

MPI点对点通信函数

MPI的通信机制是在一对进程之间传递数据,称为点对点通信

MPI提供的点对点通信数据传输有两种机制

  • 阻塞:等消息从本地发出之后,才进行执行后续的语句
  • 非阻塞:不需要等待,实现通信与计算的重叠

非阻塞MPI_Send/MPI_Recv

MPI_Send用于发送方

MPI_Send(void*buf,int count,MPI_Datatype datatype,int dest,int tag,MPI_Comm comm)

  • buf 发送的数据缓存区的起始地址
  • count 需要发送数据的个数
  • datatype 需要发送的数据类型
  • dest 目的进程的标识号
  • tag 消息标志为tag
  • comm 进程所在的域

MPI_Recv用于接收方

MPI_Recv(void*buf,int count,MPI_Datatype datatype,int source,int tag,MPI_Comm comm,MPI_Status *status)

从comm通信域中标识号为source的进程,接受消息标记为tag,消息数据类型为datatype,个数为count的消息并存储在buf缓冲区中,并将该过程的状态信息写入status中

在C语言中,status是一个结构体,包含了MPI_SOURCE(数据来源进程标识号),MPI_TAG(消息标记),MPI_ERROR
在Fortran语言中,status是一个整型的数组

阻塞MPI_Isend

Isend
int MPI_Isend(void*buf,int count,MPI_Datatype datatype,int dest,int tag,MPI_Comm comm,MPI_Request *request)

Irecv
int MPI_Irecv(void*buf,int count,MPI_Datatype datatype,int dest,int tag,MPI_Comm comm,MPI_Request *request)

MPI集合通信函数

image

1-n/n-1

集合通信还包括一个同步操作Barrier,所有进程都到达后才继续执行

MPI_Bcast(void *buffer,int count,MPI_Datatype datatype,int root,MPI_Comm comm)

从指定的一个根进程中把相同的数据广播发送给组中的所有其他进程

image

MPI_Scatter(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)

把指定的根进程中的数据分散发送给组中的所有进程(包括自己

image

MPI_Gather(void *sendbuf,int sent_count,MPI_Datatype send_datatype,void *recv_data,int recv_count,MPI_Datatype recv_datatype,int root,MPI_Comm communicator)

在组中指定一个进程收集组中进程发来的消息,这个函数操作与MPI_Scatter函数操作相反
所有进程调用该函数,把指定位置的数据发送给根进程的指定位置

image

MPI_Reduce(void *send_data,void *recv_data,int count,MPI_Datatype datatype,MPI_Op op, int root,MPI_Comm communicator)

在组内所有的进程中,执行一个规约操作(算术等),并把结果存放在指定的一个进程中

image

举例

image

n-n

image

image

常用函数

计时函数
double MPI_Wtime(void)
功能:返回某一时刻到调用时刻经历的时间(s)

案例

1
2
3
4
5
6
double start_time,end_time,total_time; //初始化
start_time = MPI_Wtime();
//需要计时的部分
end_time = MPI_Wtime();
total_time = end_time - start_time
Printf("It looks %f seconds\n",total_time);

获得本进程的机器名函数
int MPI_Get_processor_name(char name,intresultlen)
name为返回的机器名字符串,resultlen为返回的机器名长度

1
2
3
4
5
//MPI_MAX_PROCESSOR_NAME代表MPI中允许机器名字的最大长度
int resultlen;
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Get_processor_name(processor_name,&resultlen);//resultlen存放长度
cout"当前运行的机器" <<processor_name<<endl;

测试案例

send

home/mpi/example/send_test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <iostream>
#include "mpi"

using namespace std;
/*从0号进程发送信息,其他进程接受信息*/
int main(int argc,char ** argv)
{
int rank;//记录进程标识号
int size;//记录通讯域中的进程个数
int senddata,recvdata;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
MPI_Comm_size(MPI_COMM_WORLD,&size);
MPI_Status status;
recvdata=0;
if(rank==0){ //发送数据的进程
for(int i=1;i<size;i++){
senddata=i+1000;
MPI_Send(&senddata,1,MPI_INT,i,i,MPI_COMM_WORLD);
}
}
if(rank!=0){ //接受数据的进程
MPI_Recv(&recvdata,1,MPI_INT,0,rank,MPI_COMM_WORLD,&status);
cout << "进程 " << rank <<"从 "<< status.MPI_SOURCE <<"接收信息,tag="<<status.MPI_TAG <<" and data=" <<recvdata<<endl;
}
MPI_Finalize();
return 0;
}

编译运行

1
2
3
[ranan@c106 examples]$ mpicxx -o testmpi send_test.cpp
[ranan@c106 examples]$ xsync testmpi //把执行程序分发给c105 c107
[ranan@c106 examples]$ mpirun -np 6 -f nodes ./testmpi

image

MPI_Scatter 与 MPI_Gather

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include "stdio.h"
#include "mpi.h"
#include "stdlib.h"
#include <iostream>
#include <vector>

using namespace std;

const int N=2; //每个进程接受两个数据


int main(int argc,char ** argv){
int size,rank; //size记录总进程数,rank记录当前进程的标识号
int *send; //每个进程的发送缓存区
int *recv; //每个进程的接受缓存区
int *result; //接受其他进程发送过来的数据
int send_data[N]; //其他进程发送的数据;
int i=0;
int j=0;
int resultlen; //记录机器名的长度
char processor_name[MPI_MAX_PROCESSOR_NAME]; //运行的机器名

MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&rank); //获得当前进程号
MPI_Comm_size(MPI_COMM_WORLD,&size); //获得总进程数,假设4个
MPI_Get_processor_name(processor_name,&resultlen);
recv = (int*) malloc(N*sizeof(int));
for(;i<N;i++){
recv[i] = 0; //原始的数据
}

//开始准备发送的数据
if(rank==0){
send = (int*) malloc(size*N*sizeof(int));
result = (int*) malloc(size*N*sizeof(int));
for(;j<N*size;j++){
send[j]=j+6; //6,7,8,9,10,11,12,13 ,大于等于10返回1,小于10返回0
}
}

//开始进行任务分发
MPI_Scatter(send,N,MPI_INT,recv,N,MPI_INT,0,MPI_COMM_WORLD); //0号进程分发

//输出分发结果
cout << "-----------------------"<<endl;
cout << "进程号"<<rank<<"当前运行的机器" <<processor_name<<endl;

for(i=0;i<N;i++){
cout << "传输后的recv["<<i<<"] = " <<recv[i]<<endl;
//数据处理
send_data[i] = (recv[i]>=10)?1:0;
cout << "当前值:" <<recv[i]<<",对应的send_data = "<< send_data[i] <<endl;

}
cout << "-----------------------"<<endl;

MPI_Gather(send_data,N,MPI_INT,result,N,MPI_INT,0,MPI_COMM_WORLD); //0号进程接受

//MPI_Barrier调用函数时进程将处于等待状态,直到通信子中所有进程 都调用了该函数后才继续执行。
MPI_Barrier(MPI_COMM_WORLD); //等待所有进程传送数据结束。
//输出汇聚的结果
if(rank==0){
for(j=0;j<N*size;j++){
cout << "result["<<j<<"]="<<result[j]<<endl;
}
}
MPI_Finalize();
return 0;
}

image

补充问题

MPI如何与CUDA一起使用

答案来自文心一言

MPI(Message Passing Interface)和CUDA(Compute Unified Device Architecture)是两种不同的并行计算技术,它们可以结合使用,以在多个GPU和多个节点之间实现高效的并行计算。

MPI是一种基于消息传递的并行计算技术,它可以在多个节点之间分配和同步计算任务。MPI通过发送和接收消息来在节点之间传递数据,并且可以使用各种通信协议和网络接口来连接不同的节点。

CUDA是一种基于GPU的并行计算技术,它可以在单个GPU上实现高效的并行计算。CUDA使用一种称为“核函数”的编程模型,该模型使得开发者可以在GPU上执行大量的并行计算任务。

将MPI与CUDA结合使用,可以充分发挥多个GPU和多个节点的计算能力,从而加速各种高性能计算任务。下面是一些将MPI与CUDA结合使用的方法:

  1. 使用CUDA-aware MPI库:CUDA-aware MPI库是一种特殊的MPI库,它可以识别GPU设备,并在需要时将数据直接传输到GPU设备中。这样可以避免通过CPU进行数据传输的开销,从而提高并行计算的效率。
  2. 将计算任务分配给多个GPU:通过MPI将计算任务分配给多个GPU,可以使用CUDA在每个GPU上执行并行计算任务。这种方法可以加速各种深度学习和高性能计算任务。
  3. 在MPI中使用CUDA核函数:可以将CUDA核函数作为MPI进程的一部分来使用。这样可以在MPI进程之间共享CUDA核函数,并在多个节点上并行执行这些核函数。

总之,将MPI与CUDA结合使用可以加速各种高性能计算任务,但需要开发者熟悉这两种技术,并了解如何在它们之间进行有效的交互和数据传输。

NUMA架构简介

NUMA(Non-Uniform Memory Access)架构是一种计算机内存设计,它用于多处理器系统,其中不同的处理器或处理器组具有其自己的局部内存,而这些局部内存又与系统中的其他处理器共享。

在NUMA架构中,处理器可以访问自己的局部内存(本地内存访问)比访问其他处理器的内存(远程内存访问)更快。因此,内存访问时间不再是均匀的,这与传统的统一内存访问(UMA)模型形成对比,UMA模型中所有处理器访问内存的速度是相同的。

NUMA的主要特点

  1. 本地和远程内存:每个处理器或处理器组有自己的本地内存。当处理器需要访问非本地内存时,它必须通过一个连接到其他处理器的内部互联网络。

  2. 内存访问延迟:访问本地内存的延迟比访问远程内存的延迟小。

  3. 扩展性:NUMA是为了解决多处理器系统的可扩展性问题而设计的,特别是在处理器数量增加时,UMA架构中的内存带宽和延迟成为瓶颈。

  4. 数据局部性:在NUMA系统中,软件优化通常着重于提高数据局部性,这意味着尽量确保处理器从其本地内存中访问数据。

为什么需要NUMA

随着处理器核心数量的增加,UMA架构中的总线和内存访问成为瓶颈,因为所有处理器都试图访问同一个内存控制器和内存块。NUMA通过为每个处理器或处理器组提供自己的内存控制器和内存块,从而缓解了这个瓶颈。

注意事项

在NUMA系统上编写和优化代码需要对NUMA架构有深入的了解。例如,操作系统和应用程序可能需要进行特定的调整,以确保数据和线程尽可能地局部于特定的处理器和内存。

总的来说,NUMA是一种解决高性能、多处理器系统中内存瓶颈问题的架构。随着现代服务器和数据中心中多核和多处理器系统的普及,NUMA的知识和优化技巧变得越来越重要。

  • Title: MPI-note-2
  • Author: Charles
  • Created at : 2023-09-15 08:34:15
  • Updated at : 2023-09-21 11:42:25
  • Link: https://charles2530.github.io/2023/09/15/mpi-note-2/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments