并行计算机
并行计算机分类
数据与指令
图1:按指令(程序)数据的个数分类SIMD
单条指令并行计算多条数据,如 A=A+1(备注:其中A为数组)
MISD
多条指令并行计算多条数据,如 A=(B+C)+(D-E)+(FG)(备注: B+C 、D-E、FG并行执行*)
存储方式
图2:按存储方式分类问题并行求解
图3:问题求解过程并行编程模型与并行语言
并行编程模型
表1:数据并行与消息传递并行模型对比数据并行
相同的操作同时作用于不同的数据,适合SIMD、SPMD并行计算机上运行。在向量机上通过数据并行求解问题的实践也说明数据并行是可以高效地解决一大类科学与工程计算问题的。
它给编程者提供一个统一的存储地址空间,由编程语言本身提供并执行语义,如tensorflow的向量计算等就是此类并行计算。
消息传递并行
各个并行执行的部分之间通过传递消息来交换信息 协调步伐 控制执行。一般面向分布式内存,用数据并行很难实现的运算。由开发者就行计算部件之间的信息交换,控制,协调。
并行语言
并行语言实现方式并行算法
并行算法的分类
根据运算的基本对象分类
- 数值计算
- 非数值计算
根据进程之间的依赖关系分类
- 同步并行算法:各进程步调一致,使用全局时钟(不一定物理时钟)协调进程的步调;
- 异步并行算法:各进程步调不一致,根据计算的不同阶段决定等待、继续或者终止;
- 纯并行算法:各进程步调没有关系
根据并行计算任务的粒度分类
- 粗粒度并行计算:一个并行任务包含较长的程序段和较大的计算量;
- 细粒度并行计算:一个并行任务包含较短的程序段和较小的计算量;
一般而言 并行的粒度越小就有可能开发更多的并行性提高并行度 这是有利的方面但是另一个不利的方面就是并行的粒度越小通信次数和通信量就相对增多这样就增加了额外的开销 因此合适的并行粒度需要根据计算量通信量计算速度通信速度进行综合平衡这样才能够取得高效率。
并行算法的设计
- SIMD适合同步并行计算
- MIMD适合异步并行算法
-
机群计算:加大计算时间相对于通信时间的比例,减少通信次数,甚至以计算换通信。计算粒度不能太小,
MPI简介
什么是MPI
- MPI是一个库 而不是一门语言
- MPI是一种标准或规范的代表 而不特指某一个对它的具体实现
- MPI是一种消息传递编程模型,并成为这种编程模型的代表和事实上的标准
MPI的目的
- 较高的通信性能
- 较好的程序可移植性
- 强大的功能
目前MPI的主要实现
- MPICH:http://www-unix.mcs.anl.gov/mpi/mpich
- CHIMP: Edinburgh开发,ftp://ftp.epcc.ed.ac.uk/pub/packages/chimp/release/
- LAM: 由Ohio State University 开发,http://www.mpi.nd.edu/lam/download/
第一个MPI程序
#include "mpi.h"
#include <stdio.h>
void main(int argc,char* argv[])
{
int myid, numprocs;
int namelen;
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Init(&argc,&argv); //初始化MPI框架
MPI_Comm_rank(MPI_COMM_WORLD,&myid); //获取本进行的id
MPI_Comm_size(MPI_COMM_WORLD,&numprocs); //获取总的进程数量
MPI_Get_processor_name(processor_name,&namelen); //获取节点名称
fprintf(stderr,"Hello World! Process %d of %d on %s\n", myid, numprocs, processor_name);
MPI_Finalize();
}
编译
gcc -o mpipro mpipro.c -std=c99 -I /usr/local/openmpi4/include/ -L/usr/local/openmpi4/lib -L/usr/lib64/openmpi3/ -lopen-rte -lopen-pal -lmpi
运行
如果root用户执行,需要加--allow-run-as-root选项。
mpirun -n 2 mpipro
执行结果
6个接口构成的MPI子集
子集的介绍
/**1
* 描述:完成MPI框架的初始化工作
*/
MPI_Init(int argc, char* argv[])
/**2
* 描述:完成MPI框架的结束工作
*/
MPI_Finalize(void)
/**3
* 描述:获取当前进程的标识
* @comm:IN, 该进程所在的通信域(句柄)
* @rank:OUT, 调用进程在comm中的标识号
* @return:
*/
int MPI_Comm_rank(MPI_Comm comm, int *rank)
/**4
* 描述:获取当前进程的标识
* @comm:IN, 该进程所在的通信域(句柄)
* @size:OUT, 通信域comm内包括的进程数
* @return:
*/
int MPI_Comm_size(MPI_Comm comm, int *size)
/**5
* 描述:将发送缓冲区中的count个datatype数据类型的数据发送到目的进程,目的进程在通信域中的标识号
是dest,本次发送的消息标志是tag,使用这一标志就可以把本次发送的消息和本进程向同一目的进程发送的
其它消息区别开来
* @buf:IN, 发送缓冲区的起始地址
* @count:IN, 将发送的数据的个数
* @datatype: IN, 发送数据的数据类型(句柄)
* @dest: IN, 目的进程标识号(整型)
* @tag: IN, 消息标志(整数)
* @comm IN, 通信域(句柄)
*/
int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
/**6
* 描述:从指定的进程source接收消息,并且该消息的数据类型和消息标识和本接收进程指定的datatype和
tag相一致,接收到的消息所包含的数据元素的个数最多不能超过count
* @buf:IN, 接收缓冲区的起始地址
* @count:IN, 将发送的数据的个数
* @datatype: IN, 发送数据的数据类型(句柄)
* @source: IN, 目的进程标识号(整型)
* @tag: IN, 消息标志(整数)
* @comm IN, 通信域(句柄)
* @status OUT,返回状态,需要调用者分配空间,包含status.MPI_SOURCE、status.MPI_TAG 和status.MPI_ERROR三个域
*/
int MPI_Recv(void* buf, int count,
MPI_Datatype datatype, int source, int tag,
MPI_Comm comm, MPI_Status *status)
简单的消息收发的例子
#include <mpi.h>
#include <stdio.h>
#include <string.h>
void main(int argc,char* argv[])
{
char *message = "Hello, MPI";
char send_buf[20];
char recv_buf[20];
int myid, numprocs;
int namelen;
MPI_Status status;
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
if(myid == 0)
{
strcpy(send_buf, message);
MPI_Send(message, strlen(message), MPI_CHAR, 1, 99, MPI_COMM_WORLD);
}
if (myid == 1)
{
MPI_Recv(recv_buf, strlen(message), MPI_CHAR, 0, 99, MPI_COMM_WORLD, &status);
printf("receive message:%s\n", recv_buf);
printf("source:%d, tag:%d, error:%d\n", status.MPI_SOURCE, status.MPI_TAG, status.MPI_ERROR);
}
MPI_Finalize();
}
执行结果
执行结果MPI预定义的数据类型
预定义数据类型与c语音的关系附加的数据类型
- MPI_BYTE 由一个字节组成
MPI数据类型匹配和数据转换
数据类型匹配规则
MPI消息传递过程需要数据类型匹配的步骤
- 消息装配:发送缓存区的数据类型和消息装配指定的类型匹配
- 消息传递:发送操作指定的类型和接收操作指定的类型匹配
- 消息拆卸:接收缓存区的类型接收操作指定的类型匹配
匹配的意思
- 宿主语言的类型和通信指定的类型相匹配
- 发送方和接收方的类型相匹配
- MPI_BYTE/MPI_PACKED可以和任意以字节为单位的存储相匹配,MPI_PACKED用于数据的打包和解包
匹配规则
- 有类型数据的通信:发送方和接收方均使用相同的数据类型
- 无类型数据的通信:发送方和接收方均以MPI_BYTE作为数据类型
- 打包数据的通信:发送方和接收方均使用MPI_PACKED
数据转换
数据类型的转换
改变一个值的数据类型。MPI严格类型匹配,不存在类型转换。
数据表示的转换
不同异构环境数据的表示不一致,MPI需要在异构系统之间进行数据表示转换。转换原则为:整形、逻辑、字符型保持值不变,浮点型转换为目标平台上能表示的最接近的值。
如果在发送的过程中,转换操作失败,会导致发送或接收,或者两者都会失败。
MPI消息
消息格式
image.png- 消息:信封(目的/源、tag、通信域)+数据(起始地址、个数、数据类型)
tag的作用
进程0
MPI_SEND( x,1,整型,1,tag1,comm) 发送消息1
MPI_SEND( y,1,整型,1,tag2,comm) 发送消息2
进程1
MPI_RECV(x,1,整型,0,tag1,comm,status)
MPI_RECV(x,1,整型,0,tag2,comm,status)
- 如果tag2消息先达到,进程1第一条接收语句并不会把它当成tag1,而是继续阻塞,等待tag2消息到达。
任意源与任意标识
接收者可以指定以下两个标识,接收任意源或者任意tag的消息。发送者必须指定目标和tag。
- MPI_ANY_SOURCE:
- MPI_ANY_TAG
MPI通信域
MPI通信域包含两部分
- 进程组:所有参加通信的进程集合
- 上下文:提供相对独立的通信域,不同的消息在不同的上下文传递,互不干涉。将不同的通信区分开。
MPI_COMM_WORLD为预定义的通信域,其包括了初始化时全部的进程。
不同的通信域可以有自己的进程计数方案。
简单的MPI程序示例
用MPI实现计时功能
- double MPI_Wtime() 返回过去某时刻到调用时刻的秒数。
- double MPI_Wtick() 返回MPI_Wtime()的时间精度,一个滴答所占用的时间。
获取机器的名字和MPI版本号
int MPI_Get_processor_name ( char *name, int *resultlen)
int MPI_Get_version(int * version, int * subversion)
是否初始化及错误退出
int MPI_Initialized(int *flag) 判断MPI_Init()是否已经执行
int MPI_Abort(MPI_Comm comm, int errorcode) 使通信域comm中的所有进程退出
数据接力传送
任意进程间相互问候
任意源和任意标识的使用
#include "mpi.h"
#include <stdio.h>
int main(int argc, char* argv[])
{
int rank, size, i, buf[1];
MPI_Status status;
MPI_Init( &argc, &argv );
MPI_Comm_rank( MPI_COMM_WORLD, &rank );
MPI_Comm_size( MPI_COMM_WORLD, &size );
if (rank == 0) {
for (i=0; i<100*(size-1); i++) {
MPI_Recv( buf, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status );
printf( "Msg=%d from %d with tag %d\n", buf[0], status.MPI_SOURCE, status.MPI_TAG );
}
}
else {
for (i=0; i<100; i++){
buf[0]=rank+i;
MPI_Send( buf, 1, MPI_INT, 0, i, MPI_COMM_WORLD );
}
}
printf("process %d exit\n", rank);
MPI_Finalize();
}
编写安全的MPI程序
- 通信调用的顺序不当,会造成死锁
- 发送和接收必须匹配
网友评论