前回、全ワーカーの計算結果をマスターに集めてから、全データの平均値を計算したのだった。こういう計算は、しばしば行われるのではないだろうか。
ということは、もっと簡単な方法、関数を1つ実行するだけで全データをマスターに掻き集めることはできないだろうか。
これを行うのに、send()とrecv() を使い、forルーブを回したのだった。
でも、そんなことをしないでも、以上のことを一発で行う関数が用意されていた。
gather(集める)である。
values = comm.gather(pival,root=0)
この関数を、全ノードで実行するだけ。
最初の引数のpivalは、各ノードから送るデータ(円周率)である。
root= でどのノードに集めるかを指定する。すると、そのノード(マスター)では、全ノードからのデータを集めたリストが、この関数から帰ってくる。
そのノード以外では、何も帰ってこないので、valuesはNoneになってしまうが、その後使わないので問題ない。
ということで、プログラムを以下のように変更した。
# -*- coding: utf-8 -*-
from mpi4py import MPI
import socket
import random
name = socket.gethostname()
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
def near_pi(n):
inside = 0
for i in range(n):
x = random.random()
y = random.random()
if x*x+y*y < 1.0:
inside += 1
return 4.0 * inside / n
def print_pi_from( r, pv ):
print( "rank {:2d} 円周率:{:.10f}".format(r,pv) )
if __name__ == '__main__':
n = 100000000
pival = near_pi(n)
values = comm.gather(pival,root=0)
if rank==0:
for i in range(size):
print_pi_from(i,values[i])
pifinal = sum(values)/size
print( "\nsize {:2d} 円周率:{:.10f}".format(size,pifinal) )
MPI.Finalize()
$ time mpiexec -H RP0,RP1,RP2,RP3 python3 near_pi_gather.py
rank 0 円周率:3.1415037200
rank 1 円周率:3.1415540800
rank 2 円周率:3.1415069200
rank 3 円周率:3.1415748000
size 4 円周率:3.1415348800
real 1m31.775s
user 1m29.560s
sys 0m1.834s
正しく動いているようだが、送る前のデータ、comm.gather()の出力を全ノードについてprintするように少し追加した。
if __name__ == '__main__':
n = 100000000
pival = near_pi(n)
print_pi_from(rank,pival)
values = comm.gather(pival,root=0)
print( "rank {:2d} values {}".format(rank,values) )
if rank==0:
print()
for i in range(size):
print_pi_from(i,values[i])
pifinal = sum(values)/size
print( "\nsize {:2d} 円周率:{:.10f}".format(size,pifinal) )
MPI.Finalize()
$ time mpiexec -H RP0,RP1,RP2,RP3 python3 near_pi_gather2.py
rank 3 円周率:3.1417947200
rank 2 円周率:3.1416591200
rank 0 円周率:3.1416906800
rank 3 values None
rank 2 values None
rank 1 円周率:3.1412770400
rank 1 values None
rank 0 values [3.14169068, 3.14127704, 3.14165912, 3.14179472]
rank 0 円周率:3.1416906800
rank 1 円周率:3.1412770400
rank 2 円周率:3.1416591200
rank 3 円周率:3.1417947200
size 4 円周率:3.1416053900
real 1m34.910s
user 1m31.029s
sys 0m3.471s
各ノード(rank)での、円周率の計算が終了する順序はばらばらである。
gather() からの戻り地は、rootで指定したノードでは全データがリストになったものが返ってくるが、その他のノード(ワーカー)ではNoneが返ってきている。
また、リストの中身は、rankの順番に並んでいる。
目的は達成されたのだが、gather関係をもう少し追及してみよう。
gatherで集めるノードを指定したが、集めた結果を全ノードで共有するための関数として allgather()が用意されている。特定のノードに集めないので、root= の指定はない。
if __name__ == '__main__':
n = 100000000
pival = near_pi(n)
print_pi_from(rank,pival)
values = comm.allgather(pival)
print( "rank {:2d} values {}".format(rank,values) )
if rank==0:
print()
for i in range(size):
print_pi_from(i,values[i])
pifinal = sum(values)/size
print( "\nsize {:2d} 円周率:{:.10f}".format(size,pifinal) )
MPI.Finalize()
$ time mpiexec -H RP0,RP1,RP2,RP3 python3 near_pi_gather3.py
rank 3 円周率:3.1413848400
rank 2 円周率:3.1416482400
rank 0 円周率:3.1416437600
rank 1 円周率:3.1418317600
rank 0 values [3.14164376, 3.14183176, 3.14164824, 3.14138484]
rank 0 円周率:3.1416437600
rank 1 円周率:3.1418317600
rank 2 円周率:3.1416482400
rank 3 円周率:3.1413848400
size 4 円周率:3.1416271500
rank 2 values [3.14164376, 3.14183176, 3.14164824, 3.14138484]
rank 3 values [3.14164376, 3.14183176, 3.14164824, 3.14138484]
rank 1 values [3.14164376, 3.14183176, 3.14164824, 3.14138484]
real 1m29.612s
user 1m28.916s
sys 0m0.274s
複数のノード間で一気にデータを転送するのをCollective Communication(集団通信)と呼び、まだいくつかあるが、出てきた時に説明する。
MPIの説明に円周率を求めるという課題を利用したが、実際に円周率を何万桁、何億桁と求める時に、乱数を利用して行うことはない。ここで紹介した円周率の求め方は、極めて遅い求め方である。
実際には、非常に収束の速い級数を用いる。有名なところではラマヌジャンの公式などを用いるが、円周率の利用はこれで終わりにし、もっとMPIで並列処理を行うにふさわしい題材を次回から始める。