文|Seraph
01 | 模型并行与数据并行
- 模型并行:分布式系统中的不同 GPU 负责网络模型的不同部分。例如,神经网络模型的不同网络层被分配到不同的 GPU,或者同一层内部的不同参数被分配到不同 GPU;
数据并行:不同的 GPU 有同一个模型的多个副本,每个 GPU 分配到不同的数据,然后将所有 GPU 的计算结果按照某种方式合并。 - 模型并行各部分存在一定的依赖性,规模伸缩性差,也就是不能随机增加GPU的数量
- 数据并行则各部分独立,规模伸缩性好,实际训练中更常用,提速效果也更好
01 | 消息传递并行程序设计
- 消息传递并行程序设计值用户必须通过显示地发送和接受消息来实现处理机间的数据交换。
这种编程方式是大规模并行处理机(MPP)和机群(Cluster)采用的主要变成方式。 - 消息传递程序设计要求用户很好地
分解问题
,组织进程间的数据交换。 - 消息传递程序设计是当前并行计算领域的一个非常重要的
并行程序设计方式
。
02 | MPI
- MPI(Message Passing Interface)是消息传递函数库的标准规范,不是特指某一个具体实现。
- MPI具有高可移植性。
03 | 安装OpenMPI
见Horovod使用文中OpenMPI安装步骤。
注:Horovod的数据传递是基于MPI。
04 | Ring Allreduce算法
- Ring Allreduce算法是高性能计算(HPC)领域内一个众所周知的算法。
- Horovod的梯度同步和权值同步就采用了ring-allreduce算法。
具体见Bringing HPC Techniques to Deep Learning
05 | 使用OpenMPI
一、函数介绍
函数原型 | 含义 |
---|---|
int MPI_Init(int *argc, char **argv) | MPI_Init是MPI程序的第一个调用,完成MPI程序的初始化操作,参数接受main函数的参数,所以我们的main函数一定要带参数,以接收当我们运行程序时附加的参数。 |
int MPI_Finalize(void) | MPI_Finalize是MPI程序的最后一个调用,标志并行代码的结束,结束除主进程的其他进程 |
int MPI_Comm_size(MPI_Comm comm, int *size) | 获取进程个数 |
int MPI_Comm_rank(MPI_Comm comm, int *rank) | 获取当前进程的rank值,假如进程总数为p,则rank值为0~p-1 |
int MPI_Send(void *buf, int count, MPI_Datatype datatye, int dest, int tag, MPI_Coom comm) | Blocking Send阻塞发送函数,其中参数含义如下:IN buf 发送缓冲区的起始地址,IN count 发送信息的元素个数,IN datatype 发送信息的数据类型,IN dest 目标进程的rank值,必须指定唯一的接收者,IN tag 消息标签,IN comm 通信域。 |
int MPI_Recv(void *buf, int count, MPI_Datatype, int source, int tag, MPI_Comm comm, MPI_Status *status) | Blocking Receive阻塞接收函数,其中参数含义如下:OUT buf 接收缓冲区的起始地址,IN count 接收缓冲区的大小,IN datatype 接收信息的数据类型,IN dest 目标进程的rank值,IN tag 消息标签,用于标识不同的消息, IN comm 通信域, OUT status 对象status包含实际接收到的消息的相关信息 |
int MPI_Get_cout(MPI_Status status, MPI_Datatype datatype, int *count) | 返回实际接收的数据长度,其中参数含义如下:IN status 接受操作的返回值,IN datatype 接收缓冲区中的元素的数据类型,OUT count 接收消息中的元素个数 |
double MPI_Wtime( void ) | 返回调用的处理器已经过的时间 |
int MPI_Bcast(void *buf, int count, MPI_Datatype datatype, int root, MPI_Comm comm) | rank号为root 进程广播一条消息到通信域组内的所有进程 |
int MPI_Get_processor_name(char *name, int *resultlen) | 获取本进程的机器名,其参数含义如下:OUT name 机器名,OUT resultlen 结果长度。 |
int MPI_Reduce(const void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm); | 将通信域内各进程的同一个变量参与规约计算,并向指定的进程输出计算结果。其中各参数含义如下:IN sendbuf 发送缓冲区的起始地址,OUT recvbuf 接收缓冲区的起始地址, IN datatye 发送元素的类型,IN op 规约操作, IN root 主进程rank |
三、MPI基础数据类型
MPI数据类型 | C 语言数据类型 |
---|---|
MPI_CHAR | signed char |
MPI_SHORT | signed short int |
MPI_INT | signed int |
MPI_LONG | signed long |
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 | 8 binary digits |
MPI_PACKED | data packed or unpacked with MPI_Pack()/MPI_Unpack |
二、运行指令
指令 | 含义 |
---|---|
mpicc | c编译代码,示例:mpicc -o hello hello.c |
mpirun | 运行MPI并行程序,eg.mpirun -np 4 hello ,其中-np 表示进程数 |
三、常量
常量名 | 含义 |
---|---|
MPI_COMM_WORLD | 默认的缺省通信域。在MPI_Init执行后,MPI程序的所有进程形成一个组,这个组的通信域即为MPI_COMM_WORLD。指定通信域是MPI通信操作函数必不可少的参数,用于限定参加通信的进程的范围。 |
MPI_ANY_SOURCE | 用于MPI_Recv函数的source参数赋值,表示接收任意处理器的数据 |
MPI_ANY_TAG | 匹配任意tag值的消息 |
MPI_SUCCESS | 函数返回成功值 |
三、C语言示例代码
- Hello World
#include <stdio.h>
#include <mpi.h>
int main (int argc, char **argv)
{
int myid, numprocs;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
MPI_Finalize();
return 0;
}
- MPI并行通信
#include <stdio.h>
#include <mpi.h>
int main (int argc, char **argv)
{
int myid, numprocs,source;
MPI_Status status;
char message[100];
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
if(myid!=0)
{
sprintf(message,"Greetings from process %d!", myid);
MPI_Send(message, strlen(message)+1, MPI_CHAR, 0, 99, MPI_COMM_WORLD);
}
else
{
for(source = 1; source<numprocs; source++)
{
MPI_Recv(message, 100, MPI_CHAR, source, 99, MPI_COMM_WORLD, &status);
printf("%s\n", message);
}
}
MPI_Finalize();
return 0;
}
- 求PI
题目:
令函数
则有
#include <stdio.h>
#include <mpi.h>
#include <math.h>
double f(double);
double f(double a)
{
return (4.0/(1.0+a*a));
}
int main(int argc, char **argv)
{
int done = 0, n, myid, numprocs, i;
double PI25DT = 3.141592653589793238462643;
double mypi, pi, h, sum, x;
double startwtime = 0.0, endwtime;
int namelen;
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Get_processor_name(processor_name, &namelen);
fprintf(stderr, "Process %d on %s\n", myid, processor_name);
n=0;
while(!done)
{
if(myid==0)
{
if(n==0)
{
n=100;
}
else
{
n=0;
}
startwtime = MPI_Wtime();
}
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
if(n==0)
{
done = 1;
}
else
{
h = 1.0/(double)n;
sum = 0.0;
for(i = myid + 1; i<=n; i+=numprocs)
{
x = h*((double)i - 0.5);
sum += f(x);
}
mypi = h*sum;
MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if(myid == 0)
{
printf("pi is approximately %.16f, Error is %.16f\n", pi, fabs(pi-PI25DT));
endwtime = MPI_Wtime();
printf("wall clock time = %f\n", endwtime-startwtime);
}
}
}
MPI_Finalize();
return 0;
}
06 | MPI与Hadoop的对比
- MPI是计算与存储分离,Hadoop是计算向存储迁移。
- 在MPI中数据存储的节点和数据处理的节点往往是不同的,一般在每次计算开始时MPI需要从数据存储节点读取需要处理的数据分配给各个计算节点,然后进行数据处理,即MPI的数据存储和数据处理是分离的。对于计算密集型的应用MPI能表现出良好的性能,但对于处理TB级数据的数据密集型应用,大量的数据在节点间进行交换,网络通信时间将成为影响系统性能的重要因素,性能会大大降低。
- 在Hadoop中有HDFS文件系统的支持,数据是分布式存储在各个节点的,计算时各节点读取存储在自己节点的数据进行处理,从而避免了大量数据在网络上的传输,实现“计算向存储的迁移”。
- MPI无法应对节点的失效。如果MPI在运行过程出现节点失效及网络通信中断,则只有返回并退出,MPI没有提供一套机制处理节点失效后的备份处理方案问题,所以如果中途出现问题,所有的计算将重新开始,这是非常耗时的。Hadoop为应对服务器的失效,在数据备份上下了很大的功夫,数据块会形成多个副本存储在不同的地方,一般会有3个副本,采用简单化的跨机架数据块存储,最大限度避免了数据丢失,数据的安全性得到了保证。