基於MPI並行方法實現矩陣乘法
目錄
1.
實驗目的
① 掌握集羣的使用方法
② 掌握怎樣以並行/分佈式的方式來設計程序
③ 掌握怎樣用並行的思想來分析一個特定問題
④ 掌握怎樣對一個現有方案進行多方位優化
2. 實驗環境
① 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的環境,可進行直接進行編程。
3. 實驗內容
3.1. 實驗題目
本實驗要求使用並行的方法完成矩陣乘法的計算,之後分析串並行條件下,矩陣乘法運行時間的加速比,優化所設計的並行程序。
整個實驗有以下三點具體要求:
① 編程生成所需矩陣,作爲負載輸入。矩陣維數的最小要求是1000.
② 用並行的方法設計程序,比如MR,OPENMP,MPI。
③ 串並行加速比要求越高越好
3.2. 實驗過程
3.2.1. 集羣使用
① 下載並安裝xmanager企業版。
② 打開xftp,建立新的連接,主機地址:*************.在home/pppusr的文件夾下以學號爲名建立文件夾,在自己的文件夾下進行上傳和修改操作。
③ 打開xshell,點擊新建,寫入主機ip同上,再輸入賬號和密碼。
④ $sudo ssh blade13
#cd /home/pppusr/學號,開始編程。
⑤ 編程完成,使用命令行:mpicc -o 程序名 ×××.c 來編譯源代碼,生成可執行程序。
⑥ 使用命令行:mpiexec -machine /home/pppusr/mpi.host -np 4 程序名 來運行程序。
3.2.2. 源碼及解析
本實驗中我使用的並行方法是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.3. 執行時間截圖
3.3.1. 基準程序參數設計
在完成代碼部分的編譯,生成可執行程序之後,我開始對實驗中各種參數進行設置,得到矩陣乘法的運行時間。如表 3-1所示,該表反映的是我爲全面進行測試設計的參數指標,根據表中的設計執行程序,得到各種情況下矩陣乘法運行時間的截圖。
場景 |
進程數 |
矩陣維數 |
矩陣類型 |
稀疏程度 |
運行次數 |
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 |
3.3.2. 運行結果截圖
① 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)
3.3.3. 稀疏/稠密矩陣參數設計
我使用mpi_sparse程序,在以串\並,以及並行程度爲測試參數,對稀疏和密集矩陣的乘法執行性能做了測試。結果截圖如下:
圖 3-1 串行1000維稀疏矩陣執行時間
圖 3-2 串行1000維稠密矩陣執行時間
圖 3-3 並行1000維稀疏矩陣執行時間
圖 3-4 並行1000維稠密矩陣執行時間
4. 實驗結論
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,之後趨於平穩趨勢。這是由於程序的性能並不是由進程數唯一決定的,還有程序的並行度以及通訊開銷緊密相連的,當並行進程越多的時候,就意味着進程之間的通信開銷也就越大,所以會導致執行時間的增加。
通過上圖,我們可以直觀看出隨着並行程度增加,執行時間的變化趨勢,但是還是不能將串並行程序進行更加精確的對比,得到性能最優的情況,所以我們引出加速比的概念。
在我們的程序中,串行執行的平均時間=11.294s。而加速比=使用一個處理器的執行時間/使用具有n個處理器的並行處理時間。我們根據所得平均數據,通過公式,計算得到各進程數下的加速比,做得下表,爲了直觀表現加速比的變化趨勢,根據表 4-2做得圖 4-2,由圖我們可以看出,並行度爲16的時候,程序的執行性能達到頂峯,之前是在一個直線增長的階段,而在之後,則是處在一個平緩下降,最終趨於穩定的過程,與我們之前根據執行時間直接分析所得的結果是一樣的。
進程數 |
2 |
4 |
16 |
24 |
32 |
加速比 |
1.011 |
1.976 |
6.098 |
3.916 |
3.913 |
在實驗中,我們還測試了不同矩陣維度與執行時間之間的關係,最小的矩陣維度是1000,最大的矩陣維度是2000。而程序的並行度,我們選取的是之前結果中,執行時間最短的進程數:16。根據統計數據,做得下圖。從圖中,我們可以清楚地看到,程序串行執行的時間隨着矩陣維度的增加以一定斜率趨於直線上升,而並行執行的時間相較之下,上升地及其平緩,這表現出,隨着矩陣維度地增加,並行程序的性能很好。
圖 4-3 矩陣維度與執行時間圖
同樣地,我們也計算了不同矩陣維度下的加速比圖,不難看出,在矩陣維數爲1700的時候,加速比達到最大,之後隨着矩陣維數的增加,呈現緩慢下降的趨勢。
圖 4-4 不同矩陣維度下的加速比圖
4.2. 稀疏/稠密矩陣結果分析
從上一章節中,我們的實驗結果(並行度爲4):1000維稀疏矩陣串行執行的時間是13.93s,而1000維稠密矩陣串行執行的時間爲17.69s,1000維稀疏矩陣並行執行時間是4.41s,1000維稠密矩陣串行執行的時間爲9.62s。從這組結果可以看出,無論是串行還是並行,執行時間都受到了矩陣稀疏還是稠密的影響。但是並行的結果還是優於串行的結果的。1000維稀疏矩陣的串並行加速比是3.16,而1000維稠密矩陣的串並行加速比是1.839。總體看來,稀疏矩陣的程序執行性能是高於稠密矩陣的。