以下內容翻譯自:MPI Reduce and Allreduce
在上一課中,我們介紹了使用MPI_Scatter
和MPI_Gather
執行MPI並行排序計算的應用示例。我們將通過MPI_Reduce
和MPI_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
參數是一組每個進程準備規約的數據,其類型爲datatype
。 recv_data
只與具有根rank的進程相關。recv_data
數組包含規約的結果並且其大小爲sizeof(datatype) * count
。op
參數是對數據應用的操作。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
通信模式的圖示。
在上面,每個進程都包含一個整數。根進程0調用MPI_Reduce
以並使用MPI_SUM
作爲縮減操作。將這四個數字加起來並存儲在根進程中。
查看當進程包含多個元素時會發生什麼對我們的理解非常有幫助。下圖展示了每個進程中多個數字的規約。
上面的插圖中每個進程都有兩個元素。由此產生的求和發生在每個元素的基礎上。 換句話說,不是將所有數組中的所有元素合併到一個元素中,而是將來自每個數組的第i個元素合併到進程0的結果數組中的第i個元素。
現在您已經瞭解了MPI_Reduce
的概念,我們可以進入一些代碼示例。
用MPI_Reduce
計算數字的平均值
在上一課中,我向您展示瞭如何使用MPI_Scatter
和MPI_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_SUM
將local_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_Allgather
對MPI_Gather
的補充類似,MPI_Allreduce
進行規約並將結果分發給所有進程。函數原型如下:
MPI_Allreduce(
void* send_data,
void* recv_data,
int count,
MPI_Datatype datatype,
MPI_Op op,
MPI_Comm communicator)
正如您可能已經注意到的,MPI_Allreduce
與MPI_Reduce
相同,不同之處在於它不需要根進程ID(因爲結果分發到所有進程)。以下說明了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_Bcast
、MPI_Scatter
、MPI_Gather
和MPI_Reduce
,我們可以利用它們構建複雜的並行應用程序。在下一課中,我們將開始MPI組和通信者。
對於所有課程,請參閱MPI教程章節。
想貢獻?
這個網站完全託管在 GitHub上。本網站不再由原作者(Wes Kendall)積極貢獻,但它放在GitHub上,希望其他人可以編寫高質量的MPI教程。點擊這裏獲取更多關於如何貢獻的信息。