MPI Reduce and Allreduce

以下內容翻譯自:MPI Reduce and Allreduce

上一課中,我們介紹了使用MPI_ScatterMPI_Gather執行MPI並行排序計算的應用示例。我們將通過MPI_ReduceMPI_Allreduce進一步擴展集合通信例程。

注——本網站的所有代碼均位於GitHub上。本教程的代碼位於tutorials/mpi-reduce-and-allreduce/code下。

規約簡介

Reduce是函數式編程的經典概念。數據規約涉及通過函數將一組數字縮減爲一個較小的集合。例如,假設我們有一個數字列表[1,2,3,4,5]。用sum函數縮減這個數字列表將產生sum([1,2,3,4,5])= 15。同樣,乘法規約將產生乘法([1, 2, 3, 4, 5]) = 120

正如您可能想象的那樣,將規約函數應用於一組分佈式數字可能非常麻煩。除此之外,很難編制非交換規約,即必須按照設定的順序進行規約。幸運的是,MPI有一個方便的MPI_Reduce函數,它可以處理程序員在並行應用程序中需要做的幾乎所有常見的規約。

MPI_Reduce

MPI_Gather類似,MPI_Reduce在每個進程上接收一組輸入元素,並將一組輸出元素返回到根進程。輸出元素包含規約的結果。 MPI_Reduce的原型如下所示:

MPI_Reduce(
    void* send_data,
    void* recv_data,
    int count,
    MPI_Datatype datatype,
    MPI_Op op,
    int root,
    MPI_Comm communicator)

send_data參數是一組每個進程準備規約的數據,其類型爲datatyperecv_data只與具有根rank的進程相關。recv_data數組包含規約的結果並且其大小爲sizeof(datatype) * countop參數是對數據應用的操作。MPI包含一組可以使用的通用約簡操作。雖然可以定義自定義規約操作,但這超出了本課的範圍。MPI定義的規約操作包括:

  • MPI_MAX ——返回最大元素。
  • MPI_MIN ——返回最小元素。
  • MPI_SUM ——元素求和。
  • MPI_PROD——將所有元素相乘。
  • MPI_LAND——執行元素邏輯與。
  • MPI_LOR ——執行元素邏輯或。
  • MPI_BAND ——對元素的位進行按位與。
  • MPI_BOR ——對元素的位進行按位或。
  • MPI_MAXLOC ——返回最大值及對應的進程rank。
  • MPI_MINLOC ——返回最小值及對應的進程rank。

下面是MPI_Reduce通信模式的圖示。
mpi_reduce_1
在上面,每個進程都包含一個整數。根進程0調用MPI_Reduce以並使用MPI_SUM作爲縮減操作。將這四個數字加起來並存儲在根進程中。

查看當進程包含多個元素時會發生什麼對我們的理解非常有幫助。下圖展示了每個進程中多個數字的規約。
mpi_reduce_2
上面的插圖中每個進程都有兩個元素。由此產生的求和發生在每個元素的基礎上。 換句話說,不是將所有數組中的所有元素合併到一個元素中,而是將來自每個數組的第i個元素合併到進程0的結果數組中的第i個元素。

現在您已經瞭解了MPI_Reduce的概念,我們可以進入一些代碼示例。

MPI_Reduce計算數字的平均值

上一課中,我向您展示瞭如何使用MPI_ScatterMPI_Gather來計算平均值。 使用MPI_Reduce可以簡化上一課的代碼。以下是本課示例代碼中reduce_avg.c的摘錄。

float *rand_nums = NULL;
rand_nums = create_rand_nums(num_elements_per_proc);

// Sum the numbers locally
float local_sum = 0;
int i;
for (i = 0; i < num_elements_per_proc; i++) {
  local_sum += rand_nums[i];
}

// Print the random numbers on each process
printf("Local sum for process %d - %f, avg = %f\n",
       world_rank, local_sum, local_sum / num_elements_per_proc);

// Reduce all of the local sums into the global sum
float global_sum;
MPI_Reduce(&local_sum, &global_sum, 1, MPI_FLOAT, MPI_SUM, 0,
           MPI_COMM_WORLD);

// Print the result
if (world_rank == 0) {
  printf("Total sum = %f, avg = %f\n", global_sum,
         global_sum / (world_size * num_elements_per_proc));
}

在上面的代碼中,每個進程創建隨機數並進行local_sum計算。然後使用MPI_SUMlocal_sum規約到根進程。全局平均值是global_sum / (world_size * num_elements_per_proc)。如果您從repo的tutorials目錄運行reduce_avg程序,則輸出應與此類似。

>>> cd tutorials
>>> ./run.py reduce_avg
mpirun -n 4  ./reduce_avg 100
Local sum for process 0 - 51.385098, avg = 0.513851
Local sum for process 1 - 51.842468, avg = 0.518425
Local sum for process 2 - 49.684948, avg = 0.496849
Local sum for process 3 - 47.527420, avg = 0.475274
Total sum = 200.439941, avg = 0.501100

現在是時候繼續討論`MPI_Reduce的兄弟——MPI_Allreduce了。

MPI_Allreduce

許多並行應用程序需要所有進程訪問規約的結果,而不僅是根進程。與MPI_AllgatherMPI_Gather的補充類似,MPI_Allreduce進行規約並將結果分發給所有進程。函數原型如下:

MPI_Allreduce(
    void* send_data,
    void* recv_data,
    int count,
    MPI_Datatype datatype,
    MPI_Op op,
    MPI_Comm communicator)

正如您可能已經注意到的,MPI_AllreduceMPI_Reduce相同,不同之處在於它不需要根進程ID(因爲結果分發到所有進程)。以下說明了MPI_Allreduce的通信模式:

MPI_Allreduce
MPI_Allreduce相當於MPI_Reduce後跟MPI_Bcast。很簡單,對吧?

MPI_Allreduce計算標準差

許多計算問題需要多次規約才能解決。該類問題的一個例子是找到分佈的一組數字的標準差。也許有人已經忘記了,標準差是衡量數字與他們平均數之間的分散程度。較低的標準偏差意味着數字間更接近,反之亦然。

要找到標準偏差,首先必須計算所有數字的平均值。在計算平均值之後,計算與平均值的平方差的和。平方差的和的平方根是最終結果。鑑於問題描述,我們知道至少會有兩個所有數的和,轉化爲兩個規約。課程代碼中的reduce_stddev.c節選顯示了MPI中的實現方法。

rand_nums = create_rand_nums(num_elements_per_proc);

// Sum the numbers locally
float local_sum = 0;
int i;
for (i = 0; i < num_elements_per_proc; i++) {
  local_sum += rand_nums[i];
}

// Reduce all of the local sums into the global sum in order to
// calculate the mean
float global_sum;
MPI_Allreduce(&local_sum, &global_sum, 1, MPI_FLOAT, MPI_SUM,
              MPI_COMM_WORLD);
float mean = global_sum / (num_elements_per_proc * world_size);

// Compute the local sum of the squared differences from the mean
float local_sq_diff = 0;
for (i = 0; i < num_elements_per_proc; i++) {
  local_sq_diff += (rand_nums[i] - mean) * (rand_nums[i] - mean);
}

// Reduce the global sum of the squared differences to the root
// process and print off the answer
float global_sq_diff;
MPI_Reduce(&local_sq_diff, &global_sq_diff, 1, MPI_FLOAT, MPI_SUM, 0,
           MPI_COMM_WORLD);

// The standard deviation is the square root of the mean of the
// squared differences.
if (world_rank == 0) {
  float stddev = sqrt(global_sq_diff /
                      (num_elements_per_proc * world_size));
  printf("Mean - %f, Standard deviation = %f\n", mean, stddev);
}

在上面的代碼中,每個進程計算元素的local_sum並使用MPI_Allreduce對它們進行求和。每個進程都獲得全局和之後,計算平均值以便計算local_sq_diff。一旦計算出所有局部平方差,就可以使用MPI_Reduce找到global_sq_diff。然後,根進程可以通過取全局平方差的平均值的平方根來計算標準差。

使用運行腳本運行示例代碼會生成如下所示的輸出:

>>> ./run.py reduce_stddev
mpirun -n 4  ./reduce_stddev 100
Mean - 0.501100, Standard deviation = 0.301126

接下來

現在您已經熟悉了所有常見集合——MPI_BcastMPI_ScatterMPI_GatherMPI_Reduce,我們可以利用它們構建複雜的並行應用程序。在下一課中,我們將開始MPI組和通信者

對於所有課程,請參閱MPI教程章節

想貢獻?

這個網站完全託管在 GitHub上。本網站不再由原作者(Wes Kendall)積極貢獻,但它放在GitHub上,希望其他人可以編寫高質量的MPI教程。點擊這裏獲取更多關於如何貢獻的信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章