1. 程式人生 > >【平行計算】用MPI進行分散式記憶體程式設計(二)

【平行計算】用MPI進行分散式記憶體程式設計(二)

 通過上一篇中,知道了基本的MPI編寫並行程式,最後的例子中,讓使用0號程序做全域性的求和的所有工作,而其他的程序卻都不工作,這種方式也許是某種特定情況下的方案,但明顯不是最好的方案。舉個例子,如果我們讓偶數號的程序負責收集求和的工作,情況會怎麼樣?如下圖:

image

    對比之前的圖發現,總的工作量與之前的一樣,但是發現新方案中0號程序只做了3次接收和3次加法(之前的7次接收和7次加法),如果程序都是同時啟動的,那麼全域性求和時間將是0號程序的接收時間和求和時間,即需要的總時間比原來方案的總時間減少了50%多。如果是程序數=1024的話,則原方案需要0號程序執行1023次接收和求和,而新方案只要0號程序10次接收和求和操作。這樣的話就能將原方案的效能提高100倍!!既然改變程序之間的接收和傳送方式能提高效能,這就涉及程序集合之間的集合通訊了,而這些程序集合之間的通訊,MPI都已經苦逼的程式設計師都封裝好了,使得程式設計師能擺脫有無之境的程式優化,而將精力集中解決程式業務上面。首先還是將之前的求積分函式的例子改造一下:

複製程式碼
int main(int argc, char* argv[])
{
    int my_rank = 0, comm_sz = 0, n = 1024, local_n = 0;
    double a = 0.0, b = 3.0, h = 0, local_a = 0, local_b = 0;
    double local_double = 0, total_int = 0;
    int source;

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
    MPI_Comm_size(MPI_COMM_WORLD, 
&comm_sz); h = (b - a) / n; /* h is the same for all processes */ local_n = n / comm_sz; /* So is the number of trapezoids */ local_a = a + my_rank*local_n*h; local_b = local_a + local_n*h; local_double = Trap(local_a, local_b, local_n, h); MPI_Reduce(&local_double, &total_int, 1
, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); if (my_rank == 0) { printf("With n = %d trapezoids, our estimate\n", n); printf("of the integral from %f to %f = %.15e\n", a, b, total_int); } MPI_Finalize(); return 0; }
複製程式碼

注意在這段程式碼中,我們不再使用MPI_Send和MPI_Recv這樣的通訊函式,而是使用了一個MPI_Reduce函式,通過編譯執行

image

同樣能得到結果。各位看官不僅要問,程式碼中的MPI_Reduce函式是個什麼東西呢?如何使用?要回答這些問題,就需要繼續往下深入的學習集合通訊的概念。

1.集合通訊

    在MPI中,涉及所有的程序的通訊函式我們稱之為集合通訊(collective communication)。而單個程序對單個程序的通訊,類似於MPI_Send和MPI_Recv這樣的通訊函式,我們稱之為點對點通訊(point-to-point communication)。程序間的通訊關係可以用如下圖的關係來表示:

image

(1)1對1;

(2)1對部分

(3)1對全部

(4)部分對1

(5)部分對部分

(6)部分對全部

(7)全部對1

(8)全部對部分

(9)全部對全部

那既然區分了集合通訊與點對點通訊,它們之間的各自有什麼不同呢?集合通訊具有以下特點:

(1)、在通訊子中的所有程序都必須呼叫相同的集合通訊函式。

(2)、每個程序傳遞給MPI集合通訊函式的引數必須是“相容的”。

(3)、點對點通訊函式是通過標籤和通訊子來匹配的。而通訊函式不實用標籤,只是通過通訊子和呼叫的順序來進行匹配。

下表彙總了MPI中的集合通訊函式:

image

1.1 歸約

    資料歸約的基本功能是從每個程序收集資料,把這些資料歸約成單個值,把歸約成的值儲存到根程序中。具體例子類似於單科老師(數學老師)收試卷,每個學生都把考試完的數學試卷交給老師,由老師來進行操作(求最大值、求總和等)。如圖所示:

image

MPI_Reduce函式:

int MPI_Reduce (void *sendbuf, void *recvbuf, int count,MPI_Datatype datatype, MPI_Op op, int root,MPI_Comm comm)

在這個函式中,最關鍵的引數是第5個引數MPI_Op op,它表示MPI歸於中的操作符,我們上面的例子就是用的求累加和的歸約操作符。具體的歸約操作符如下表:

運算操作符

描述

運算操作符

描述

MPI_MAX

最大值

MPI_LOR

邏輯或

MPI_MIN

最小值

MPI_BOR

位或

MPI_SUM

求和

MPI_LXOR

邏輯異或

MPI_PROD

求積

MPI_BXOR

位異或

MPI_LAND

邏輯與

MPI_MINLOC

計算一個全域性最小值和附到這個最小值上的索引--可以用來決定包含最小值的程序的秩

MPI_BAND

位與

MPI_MAXLOC

計算一個全域性最大值和附到這個最大值上的索引--可以用來決定包含最小值的程序的秩

除MPI_Reduce函式之外,資料歸約還有如下一些變種函式:

MPI_Allreduce函式

int MPI_Allreduce (void *sendbuf, void *recvbuf, int count,MPI_Datatype datatype, MPI_Op op,MPI_Comm comm)

此函式在得到歸約結果值之後,將結果值分發給每一個程序,這樣的話,並行中的所有程序值都能知道結果值了。類似的求和計算結果的釋出圖如下:

繪圖1

MPI_Reduce_scatter函式

int MPI_Reduce_scatter (void *sendbuf, void *recvbuf,int *recvcnts,MPI_Datatype datatype, MPI_Op op,MPI_Comm comm)

歸約散發。該函式的作用相當於首先進行一次歸約操作,然後再對歸約結果進行散發操作。

MPI_Scan函式

int MPI_Scan (void *sendbuf, void *recvbuf, int count,MPI_Datatype datatype, MPI_Op op,MPI_Comm comm)

字首歸約(或掃描歸約)。與普通全歸約MPI_Allreduce類似,但各程序依次得到部分歸約的結果。

1.2 資料移動-廣播

在一個集合通訊中,如果屬於一個程序的資料被髮送到通訊子中的所有程序,這樣的集合通訊就叫做廣播。如圖所示:

image     image

MPI_Bcast函式:

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

通訊器comm中程序號為root的程序(稱為根程序) 將自己buffer中的內容傳送給通訊器中所有其他程序。引數buffer、count和datatype的含義與點對點通訊函式(如MPI_Send和MPI_Recv)相同。

下面我們編寫一個具體的例子:

複製程式碼
void blog3::TestForMPI_Bcast(int argc, char* argv[])
{
    int rankID, totalNumTasks;

    MPI_Init(&argc, &argv);
    MPI_Barrier(MPI_COMM_WORLD);
    double elapsed_time = -MPI_Wtime();

    MPI_Comm_rank(MPI_COMM_WORLD, &rankID);
    MPI_Comm_size(MPI_COMM_WORLD, &totalNumTasks);

    int sendRecvBuf[3] = { 0, 0, 0 };

    if (!rankID) {
        sendRecvBuf[0] = 3;
        sendRecvBuf[1] = 6;
        sendRecvBuf[2] = 9;
    }

    int count = 3;
    int root = 0;
    MPI_Bcast(sendRecvBuf, count, MPI_INT, root, MPI_COMM_WORLD); //MPI_Bcast can be seen from all processes  

    printf("my rankID = %d, sendRecvBuf = {%d, %d, %d}\n", rankID, sendRecvBuf[0], sendRecvBuf[1], sendRecvBuf[2]);

    elapsed_time += MPI_Wtime();
    if (!rankID) {
        printf("total elapsed time = %10.6f\n", elapsed_time);
    }

    MPI_Finalize();
}

int main(int argc, char* argv[])
{
    blog3 test;
    test.TestForMPI_Bcast(argc, argv);
}
複製程式碼

結果為:

image

1.3 資料移動-散射

    在進行數值計算軟體開發的過程中,經常碰到兩個向量的加法運算,例如每個向量有1萬個分量,如果有10個程序,那麼就可以簡單的將local_n個向量分量所構成的塊分配到每個程序中去,至於怎麼分塊,這裡有一些方法(塊劃分法、迴圈劃分法、塊-迴圈劃分法),這種將資料分塊傳送給各個程序進行平行計算的方法稱之為散射。

image    image

MPI_Scatter函式:

int MPI_Scatter (void *sendbuf, int sendcnt,MPI_Datatype sendtype, void *recvbuf,int recvcnt, MPI_Datatype recvtype,int root, MPI_Comm comm)

散發相同長度資料塊。根程序root將自己的sendbuf中的np個連續存放的資料塊按程序號的順序依次分發到comm的各個程序(包括根程序自己) 的recvbuf中,這裡np代表comm中的程序數。sendcnt和sendtype 給出sendbuf中每個資料塊的大小和型別,recvcnt和recvtype給出recvbuf的大小和型別,其中引數sendbuf、sendcnt 和sendtype僅對根程序有意義。需要特別注意的是,在根程序中,引數sendcnt指分別傳送給每個程序的資料長度,而不是傳送給所有程序的資料長度之和。因此,當recvtype等於sendtype時,recvcnt應該等於sendcnt。

MPI_Scatterv函式:

int MPI_Scatterv (void *sendbuf, int *sendcnts,int *displs, MPI_Datatype sendtype,void *recvbuf, int recvcnt,MPI_Datatype recvtype, int root,MPI_Comm comm)

散發不同長度的資料塊。與MPI_Scatter類似,但允許sendbuf中每個資料塊的長度不同並且可以按任意的順序排放。sendbuf、sendtype、sendcnts和displs僅對根程序有意義。陣列sendcnts和displs的元素個數等於comm中的程序數,它們分別給出傳送給每個程序的資料長度和位移,均以sendtype為單位。

下面我們來看一個例子:

複製程式碼
void blog3::TestForMPI_Scatter(int argc, char* argv[])
{
    int totalNumTasks, rankID;

    float sendBuf[SIZE][SIZE] = {
        { 1.0,   2.0,    3.0,    4.0 },
        { 5.0,   6.0,    7.0,    8.0 },
        { 9.0,   10.0,   11.0,   12.0 },
        { 13.0,  14.0,   15.0,   16.0 }
    };

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &rankID);
    MPI_Comm_size(MPI_COMM_WORLD, &totalNumTasks);

    if (totalNumTasks == SIZE) {
        int source = 0;
        int sendCount = SIZE;
        int recvCount = SIZE;
        float recvBuf[SIZE];
        //scatter data from source process to all processes in MPI_COMM_WORLD  
        MPI_Scatter(sendBuf, sendCount, MPI_FLOAT,
            recvBuf, recvCount, MPI_FLOAT, source, MPI_COMM_WORLD);

        printf("my rankID = %d, receive Results: %f %f %f %f, total = %f\n",
            rankID, recvBuf[0], recvBuf[1], recvBuf[2], recvBuf[3],
            recvBuf[0] + recvBuf[1] + recvBuf[2] + recvBuf[3]);
    }
    else if (totalNumTasks == 8) {
        int source = 0;
        int sendCount = 2;
        int recvCount = 2;
        float recvBuf[2];

        MPI_Scatter(sendBuf, sendCount, MPI_FLOAT,
            recvBuf, recvCount, MPI_FLOAT, source, MPI_COMM_WORLD);

        printf("my rankID = %d, receive result: %f %f, total = %f\n",
            rankID, recvBuf[0], recvBuf[1], recvBuf[0] + recvBuf[1]);
    }
    else {
        printf("Please specify -n %d or -n %d\n", SIZE, 2 * SIZE);
    }

    MPI_Finalize();
}

int main(int argc, char* argv[])
{
    blog3 test;

    test.TestForMPI_Scatter(argc, argv);

    return 0;
}
複製程式碼

其結果為:

image

1.4 資料移動-聚集

image    image    image

MPI_Gather函式:

int MPI_Gather (void *sendbuf, int sendcnt,MPI_Datatype sendtype, void *recvbuf,int recvcnt, MPI_Datatype recvtype,int root, MPI_Comm comm)

收集相同長度的資料塊。以root為根程序,所有程序(包括根程序自己) 將sendbuf中的資料塊傳送給根程序,根程序將這些資料塊按程序號的順序依次放到recvbuf中。傳送和接收的資料型別與長度必須相配,即傳送和接收使用的資料型別必須具有相同的型別序列。引數recvbuf,recvcnt 和recvtype僅對根程序有意義。需要特別注意的是,在根程序中,引數recvcnt指分別從每個程序接收的資料長度,而不是從所有程序接收的資料長度之和。因此,當sendtype等於recvtype時,sendcnt應該等於recvcnt。

MPI_Allgather函式:

int MPI_Allgather (void *sendbuf, int sendcnt,MPI_Datatype sendtype, void *recvbuf,int recvcnt, MPI_Datatype recvtype,MPI_Comm comm)

MPI_Allgather與MPI_Gather類似,區別是所有程序同時將資料收集到recvbuf中,因此稱為資料全收集。MPI_Allgather相當於依次以comm中的每個程序為根程序呼叫普通資料收集函式MPI_Gather,或者以任一程序為根程序呼叫一次普通收集,緊接著再對收集到的資料進行一次廣播

MPI_Gatherv函式:

int MPI_Gatherv (void *sendbuf, int sendcnt,MPI_Datatype sendtype, void *recvbuf,int *recvcnts, int *displs,MPI_Datatype recvtype, int root,MPI_Comm comm)

收集不同長度的資料塊。與MPI_Gather類似,但允許每個程序傳送的資料塊長度不同,並且根程序可以任意排放資料塊在recvbuf中的位置。recvbuf,recvtype,recvcnts和displs僅對根程序有意義。陣列recvcnts和displs的元素個數等於程序數,用於指定從每個程序接收的資料塊長度和它們在recvbuf中的位移,均以recvtype為單位。

MPI_Allgatherv函式:

int MPI_Allgatherv (void *sendbuf, int sendcnt,MPI_Datatype sendtype, void *recvbuf,int *recvcnts, int *displs,MPI_Datatype recvtype, MPI_Comm comm)

不同長度資料塊的全收集。引數與MPI_Gatherv類似。它等價於依次以comm中的每個程序為根程序呼叫MPI_Gatherv,或是以任一程序為根程序呼叫一次普通收集,緊接著再對收集到的資料進行一次廣播。

例子:

複製程式碼
void blog3::TestForMPI_Gather(int argc, char* argv[])
{
    int rankID, totalNumTasks;

    MPI_Init(&argc, &argv);
    MPI_Barrier(MPI_COMM_WORLD);
    double elapsed_time = -MPI_Wtime();

    MPI_Comm_rank(MPI_COMM_WORLD, &rankID);
    MPI_Comm_size(MPI_COMM_WORLD, &totalNumTasks);

    int* gatherBuf = (int *)malloc(sizeof(int) * totalNumTasks);
    if (gatherBuf == NULL) {
        printf("malloc error!");
        exit(-1);
        MPI_Finalize();
    }

    int sendBuf = rankID; //for each process, its rankID will be sent out  

    int sendCount = 1;
    int recvCount = 1;
    int root = 0;
    MPI_Gather(&sendBuf, sendCount, MPI_INT, gatherBuf, recvCount, MPI_INT, root, MPI_COMM_WORLD);

    elapsed_time += MPI_Wtime();
    if (!rankID) {
        int i;
        for (i = 0; i < totalNumTasks; i++) {
            printf("gatherBuf[%d] = %d, ", i, gatherBuf[i]);
        }
        putchar('\n');
        printf("total elapsed time = %10.6f\n", elapsed_time);
    }

    MPI_Finalize();
}

int main(int argc, char* argv[])
{
    blog3 test;

    test.TestForMPI_Gather(argc, argv);

    return 0;
}
複製程式碼

結果為:

image

1.5 資料移動-其它

image   

MPI_Alltoall函式:

int MPI_Alltoall (void *sendbuf, int sendcnt,MPI_Datatype sendtype, void *recvbuf,int recvcnt, MPI_Datatype recvtype,MPI_Comm comm)

相同長度資料塊的全收集散發:程序i將sendbuf中的第j塊資料傳送到程序j的recvbuf中的第i個位置,i, j =0, . . . , np-1 (np代表comm 中的程序數)。sendbuf 和recvbuf 均由np個連續的資料塊構成,每個資料塊的長度/型別分別為sendcnt/sendtype和recvcnt/recvtype。該操作相當於將資料在程序間進行一次轉置。例如,假設一個二維陣列按行分塊儲存在各程序中,則呼叫該函式可很容易地將它變成按列分塊儲存在各程序中。

MPI_Alltoallv函式:

int MPI_Alltoallv (void *sendbuf, int *sendcnts,int *sdispls, MPI_Datatype sendtype,void *recvbuf, int *recvcnts,int *rdispls, MPI_Datatype recvtype,MPI_Comm comm)
不同長度資料塊的全收集散發。與MPI_Alltoall類似,但每個資料塊的長度可以不等,並且不要求連續存放。各個引數的含義可參考函式MPI_Alltoall,MPI_Scatterv和MPI_Gatherv。

2.MPI程式的效能評估

我們使得程式並行化,就是希望解決相同問題的時候,並行程式比序列程式執行的快一些,那如何去評判這個“快”呢?

假如有如下面這樣一個矩陣-向量乘法程式

image

分別用不同的comm_sz執行,其計時結果如下:

image

從上表中可以看出,對於值很大的n來說,程序數加倍大約能減少一半的執行時間。然而,對於值很小的n,增大comm_sz獲得的效果就不是很明顯,例如:n=1024的時候,程序數從8增加到16後,執行時間沒有出現變化。這種現象的原因是:並行程式還有程序之間通訊會有額外的開銷。一般定義並行程式的時間為:

公式7

當n值較小,p值較大時,公式中的T開銷就起主導作用了。這裡的T開銷一般來之通訊。

加速比:用來衡量序列運算和並行運算時間之間的關係,表示序列時間與並行時間的比值。

公式8