Raspberry Pi スパコン (8) 円周率


2023年 03月 09日

全ノードのデータを収集

前回、全ワーカーの計算結果をマスターに集めてから、全データの平均値を計算したのだった。こういう計算は、しばしば行われるのではないだろうか。

ということは、もっと簡単な方法、関数を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で並列処理を行うにふさわしい題材を次回から始める。