1. 实验目的 3
2. 实验环境 4
3. 实验内容 4
3.1. 实验题目 4
3.2. 实验过程 5
3.2.1. 集群使用 5
3.2.2. 源码及解析 5
3.3. 执行时间截图 12
3.3.1. 基准程序参数设计 12
3.3.2. 运行结果截图 14
3.3.3. 稀疏/稠密矩阵参数设计 18
4. 实验结论 18
4.1. 基准矩阵乘法结果分析 18
4.2. 稀疏/稠密矩阵结果分析 21
5. 附加项 21
① 掌握集群的使用方法
② 掌握怎样以并行/分布式的方式来设计程序
③ 掌握怎样用并行的思想来分析一个特定问题
④ 掌握怎样对一个现有方案进行多方位优化
① Wiki:http://grid.hust.edu.cn/hpcc
② 本机实验环境:
Windows 版本 |
windows 7旗舰版 |
处理器 |
Intel(R) Core(TM) i7 CPU Q720 @1.6GHZ |
安装内存(RAM) |
4.00GB(2.99GB可用) |
系统类型 |
32位操作系统 |
③ 集群实验环境:
与服务器建立远程连接,主机地址:*******,用户名:***** 密码:******。
使用ssh命令行登录集群的两个节点(sudo ssh blade13/blade15)。
远程主机上已经搭建好了MPI和Hadoop的环境,可进行直接进行编程。
本实验要求使用并行的方法完成矩阵乘法的计算,之后分析串并行条件下,矩阵乘法运行时间的加速比,优化所设计的并行程序。
整个实验有以下三点具体要求:
① 编程生成所需矩阵,作为负载输入。矩阵维数的最小要求是1000.
② 用并行的方法设计程序,比如MR,OPENMP,MPI。
③ 串并行加速比要求越高越好
① 下载并安装xmanager企业版。
② 打开xftp,建立新的连接,主机地址:*************.在home/pppusr的文件夹下以学号为名建立文件夹,在自己的文件夹下进行上传和修改操作。
③ 打开xshell,点击新建,写入主机ip同上,再输入账号和密码。
④ $sudo ssh blade13
#cd /home/pppusr/学号,开始编程。
⑤ 编程完成,使用命令行:mpicc -o 程序名 ×××.c 来编译源代码,生成可执行程序。
⑥ 使用命令行:mpiexec -machine /home/pppusr/mpi.host -np 4 程序名 来运行程序。
本实验中我使用的并行方法是MPI,主要编写了两个.c文件,分别是基准矩阵乘法程序(mpi_basic.c)和基于稠密/稀疏矩阵的乘法运算程序(mpi_sparse.c)。
① mpi_basic.c
在文件中,我们通过void Init(float *a)函数对矩阵A,B进行初始化,具体是使用c语言自带的srand()和rand()函数,srand()会设置供rand()使用的随机数种子,每个种子对应一组根据算法预先生成的随机数,所以,在相同的平台环境下,不同时间产生的随机数是会不同的,所以在程序中,我使用系统定时器/计数器的值作为随机种子,生成不同的随机矩阵。同时为了避免两次设置随机种子的时间间隔太小,生成相同的两组随机数,所以我在srand(unsigned)time(NULL)后面乘以一个整数,srand((unsigned)time(NULL)*100)。为了方便表示,我把矩阵值为0,1,所以rand()%2。在void main()函数中,首先是启动MPI,获取当前运行进程号,获取进程个数,因为矩阵的维数是可以有用户随机定义的,所以程序中会判断运行程序的命令行是否包含矩阵维数的参数,若不包含,给出错误提示信息。然后根据进程个数将矩阵进行划分。本实验方法使用的是块状的划分方法。0号rank主要完成矩阵初始化,将矩阵发送给其他从进程,最后接收从进程的运行结果,进行最后的整合,得到最终结果。而其余进程则是接收由0进程传递的数据,计算完成后传递给0号进程。值得注意的是,在我的程序设计中,运行程序的命令行参数-np若输入的值为1,则为串行程序,反之则为并行程序。鉴于保证运行代码和运行过程的一致性以最大程度减少外在因素对串并行程序运行结果造成的影响,所以在此我并没有单独写另外的串行程序。而在多次运行中,也没有发现这种方式对程序的运行结果造成了极大误差,所以我采用了这种方式。完整源程序如下:
#include"stdio.h"
#include"stdlib.h"
#include"time.h"
#include"mpi.h"
int dimension;
void Init(float* a)
{
int i,j;
srand(((int)time(0))*100);//设置随机数种子
for (i=0;i<dimension;i++)
{
for (j=0;j<dimension;j++)
{
a[i*dimension+j]=rand()%2;
}
}
}
int main(int argc,char *argv[])
{
float *M,*N,*P,*buffer,*ans;
int my_rank,numprocs;
int m=1,i,line,j,k;
float temp=0.0;
double start_time,end_time,sub_start,sub_end;
MPI_Status status;
MPI_Init(&argc, &argv);//MPI启动
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);//获得当前进程号
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);//获得进程个数
if (argc != 2)
{
if (my_rank == 0)
printf("usage: mpiexec -machinefiel ../mpi.host -np 4 process_name Dimension(such as 1000)\n");
MPI_Finalize();
return 0;
}
dimension = atoi(argv[1]); /* 总矩阵维数 */
line = dimension/numprocs;//将数据分为(进程数)个块,主进程也要处理数据
M = (float*)malloc(sizeof(float)*dimension*dimension);//M矩阵
N = (float*)malloc(sizeof(float)*dimension*dimension);//N矩阵
P = (float*)malloc(sizeof(float)*dimension*dimension);
//缓存大小大于等于要处理的数据大小,大于时只需关注实际数据那部分
buffer = (float*)malloc(sizeof(float)*dimension*line);//数据分组大小
ans = (float*)malloc(sizeof(float)*dimension*line);//保存数据块结算的结果
//主进程对矩阵赋初值,并将矩阵N广播到各进程,将矩阵M分组广播到各进程
if (my_rank==0)
{
printf("The num of process is %d\n",numprocs);//打印进程数
printf("The matrix is %d*%d\n",dimension,dimension);//打印进程数
//矩阵赋初值
Init(M);
Init(N);
start_time=MPI_Wtime();
for (i = 1;i < numprocs;i++)
{
MPI_Send(N,dimension*dimension,MPI_FLOAT,i,0,MPI_COMM_WORLD);
//依次将N的各行发送给各从进程
}
for (m = 1;m < numprocs; m++)
{
MPI_Send(M+(m-1)*line*dimension,dimension*line,MPI_FLOAT,m,1,MPI_COMM_WORLD);
//依次将M的各行发送给各从进程
}
//接收从进程计算的结果
for (k = 1;k < numprocs;k++)
{
MPI_Recv(ans,line*dimension,MPI_FLOAT,k,3,MPI_COMM_WORLD,&status);
//将结果传递给数组P
for (i=0;i<line;i++)
{
for (j=0;j<dimension;j++)
{
P[((k-1)*line+i)*dimension+j]=ans[i*dimension+j];
}
}
}
//计算M剩下的数据
for (i=(numprocs-1)*line;i<dimension;i++)
{
for (j=0;j<dimension;j++)
{
temp=0.0;
for (k=0;k<dimension;k++)
temp += M[i*dimension+k]*N[k*dimension+j];
P[i*dimension+j]=temp;
}
}
//统计时间
double end_time = MPI_Wtime();
printf("my_rank:%dtime:%.2fs\n",my_rank,(end_time-start_time));//结果测试}
//其他进程接收数据,计算结果后,发送给主进程
else
{
sub_start = MPI_Wtime();
//接收广播的数据(矩阵N) MPI_Recv(N,dimension*dimension,MPI_FLOAT,0,0,MPI_COMM_WORLD,&status); MPI_Recv(buffer,dimension*line,MPI_FLOAT,0,1,MPI_COMM_WORLD,&status);
//计算乘积结果,并将结果发送给主进程
for (i=0;i<line;i++)
{
for (j=0;j<dimension;j++)
{
temp=0.0;
for(k=0;k<dimension;k++)
temp += buffer[i*dimension+k]*N[k*dimension+j];
ans[i*dimension+j]=temp;
}
}
//将计算结果传送给主进程 MPI_Send(ans,line*dimension,MPI_FLOAT,0,3,MPI_COMM_WORLD);
sub_end = MPI_Wtime();
printf("my_rank:%d time:%.2fs\n",my_rank,(sub_end - sub_start));//子进程测试时间
}
MPI_Finalize();//结束
return 0;
}
② mpi_sparse.c
mpi_sparse.c文件与mpi_basic.c文件的主要不同点,在于矩阵生成方式的不同,这里我只贴出矩阵生成部分的代码,进行分析。这里有两个生成函数,是通过命令行的一个参数进行区别化调用的。如果该参数设置为1,则调用稀疏矩阵生成函数,如果为0,则调用稠密矩阵生成函数。同时,我们还设置了用户可随意设置的矩阵的稀疏密度参数。使程序可以生成不同稀疏程度的矩阵,进行矩阵稀疏程度对并行矩阵乘法的影响的探究。
void Init(float* a)
{
int i,j;
float k;
srand(((int)time(0))*100);//设置随机数种子
for (i=0;i<dimension;i++)
{
for (j=0;j<dimension;j++)
{
k =(rand()%10) * 0.1;
if(k >=dentity) a[i*dimension+j] = 0.0;
if(k < dentity) a[i*dimension+j] = 1.0;
}
}
}
void Sparse_init(float* a)
{
int i,j;
float k;
srand(((int)time(0))*100);//设置随机数种子
for (i=0;i<dimension;i++)
{
for (j=0;j<dimension;j++)
{
k =(rand()%10) * 0.1;
if(k >=dentity) a[i*dimension+j] = 1.0;
if(k < dentity) a[i*dimension+j] = 0.0;
}
}
}
在完成代码部分的编译,生成可执行程序之后,我开始对实验中各种参数进行设置,得到矩阵乘法的运行时间。如表 3-1所示,该表反映的是我为全面进行测试设计的参数指标,根据表中的设计执行程序,得到各种情况下矩阵乘法运行时间的截图。
表 3-1 mpi_basic参数指标表
场景 |
进程数 |
矩阵维数 |
矩阵类型 |
稀疏程度 |
运行次数 |
1 |
1(串行) |
1000 |
常规 |
无 |
10 |
2 |
2(并行) |
1000 |
常规 |
无 |
10 |
3 |
4(并行) |
1000 |
常规 |
无 |
10 |
4 |
16(并行) |
1000 |
常规 |
无 |
10 |
5 |
24(并行) |
1000 |
常规 |
无 |
10 |
6 |
32(并行) |
1000 |
常规 |
无 |
10 |
7 |
1(串行) |
1200 |
常规 |
无 |
10 |
8 |
1(串行) |
1500 |
常规 |
无 |
10 |
9 |
1(串行) |
1700 |
常规 |
无 |
10 |
10 |
1(串行) |
2000 |
常规 |
无 |
10 |
11 |
16(并行) |
1200 |
常规 |
无 |
10 |
12 |
16(并行) |
1500 |
常规 |
无 |
10 |
13 |
16(并行) |
1700 |
常规 |
无 |
10 |
14 |
16(并行) |
2000 |
常规 |
无 |
10 |
① 1000*1000的矩阵串行执行时间:
② 1000*1000的矩阵并行度为2的执行时间:
③ 1000*1000的矩阵并行度为4的执行时间:
④ 1000*1000的矩阵并行度为16的执行时间:
⑤ 1000*1000的矩阵并行度为24的执行时间:
⑥ 1000*1000的矩阵并行度为32的执行时间:
⑦ 1200*1200矩阵串/并行执行的时间:(并行度16)
⑧ 1500*1500矩阵串/并行执行的时间:(并行度16)
⑨ 1700*1700矩阵串/并行执行的时间:(并行度16)
⑩ 2000*2000矩阵串/并行执行的时间:(并行度16)
我使用mpi_sparse程序,在以串\并,以及并行程度为测试参数,对稀疏和密集矩阵的乘法执行性能做了测试。结果截图如下:
图 3-1 串行1000维稀疏矩阵执行时间
图 3-2 串行1000维稠密矩阵执行时间
图 3-3 并行1000维稀疏矩阵执行时间
图 3-4 并行1000维稠密矩阵执行时间
为了保证实验的准确性,以下的数据都是我们多次测量取平均值计算得出的。在基准矩阵乘法的实验中,我们统计出数据结果,如表 4-1所示。
表 4-1 进程数与执行时间表
进程数 |
1 |
2 |
4 |
16 |
24 |
32 |
次数 |
||||||
第1次 |
11.33 |
11.16 |
5.71 |
1.85 |
2.92 |
3.47 |
第2次 |
11.35 |
11.23 |
5.7 |
1.84 |
2.89 |
2.77 |
第3次 |
11.25 |
11.28 |
5.72 |
1.84 |
3.28 |
2.43 |
第4次 |
11.33 |
11 |
5.73 |
1.85 |
2.55 |
2.7 |
第5次 |
11.21 |
11.21 |
5.72 |
1.88 |
2.78 |
3.06 |
平均值 |
11.294 |
11.176 |
5.716 |
1.852 |
2.884 |
2.886 |
由于表格直观展现数据的能力并不强,所以我把数据转化为折线图,以便能够更加清晰地看出其中的变化规律。如图 4-1所示,我所编写的基准并行程序中,串行的平均时间是11.294s,而2个进程并行的情况下,执行时间是11.176s,性能提升并不是很明显,随后,执行时间就直线下降,到16个进程并行时,执行时间是最短的,为1.952s。与此同时,我们可以发现并不是并行程度越高,执行时间就越短,它是达到一个最小值之后,平稳上升,到24个进程时,时间达到2.884s,32个进程时是2.886s,之后趋于平稳趋势。这是由于程序的性能并不是由进程数唯一决定的,还有程序的并行度以及通讯开销紧密相连的,当并行进程越多的时候,就意味着进程之间的通信开销也就越大,所以会导致执行时间的增加。
图 4-1 并行程度与执行时间图
通过上图,我们可以直观看出随着并行程度增加,执行时间的变化趋势,但是还是不能将串并行程序进行更加精确的对比,得到性能最优的情况,所以我们引出加速比的概念。
在我们的程序中,串行执行的平均时间=11.294s。而加速比=使用一个处理器的执行时间/使用具有n个处理器的并行处理时间。我们根据所得平均数据,通过公式,计算得到各进程数下的加速比,做得下表,为了直观表现加速比的变化趋势,根据表 4-2做得图 4-2,由图我们可以看出,并行度为16的时候,程序的执行性能达到顶峰,之前是在一个直线增长的阶段,而在之后,则是处在一个平缓下降,最终趋于稳定的过程,与我们之前根据执行时间直接分析所得的结果是一样的。
表 4-2 进程数加速比
进程数 |
2 |
4 |
16 |
24 |
32 |
加速比 |
1.011 |
1.976 |
6.098 |
3.916 |
3.913 |
图 4-2 不同并行度下的加速比
在实验中,我们还测试了不同矩阵维度与执行时间之间的关系,最小的矩阵维度是1000,最大的矩阵维度是2000。而程序的并行度,我们选取的是之前结果中,执行时间最短的进程数:16。根据统计数据,做得下图。从图中,我们可以清楚地看到,程序串行执行的时间随着矩阵维度的增加以一定斜率趋于直线上升,而并行执行的时间相较之下,上升地及其平缓,这表现出,随着矩阵维度地增加,并行程序的性能很好。
图 4-3 矩阵维度与执行时间图
同样地,我们也计算了不同矩阵维度下的加速比图,不难看出,在矩阵维数为1700的时候,加速比达到最大,之后随着矩阵维数的增加,呈现缓慢下降的趋势。
图 4-4 不同矩阵维度下的加速比图
从上一章节中,我们的实验结果(并行度为4):1000维稀疏矩阵串行执行的时间是13.93s,而1000维稠密矩阵串行执行的时间为17.69s,1000维稀疏矩阵并行执行时间是4.41s,1000维稠密矩阵串行执行的时间为9.62s。从这组结果可以看出,无论是串行还是并行,执行时间都受到了矩阵稀疏还是稠密的影响。但是并行的结果还是优于串行的结果的。1000维稀疏矩阵的串并行加速比是3.16,而1000维稠密矩阵的串并行加速比是1.839。总体看来,稀疏矩阵的程序执行性能是高于稠密矩阵的。