👩‍👩‍👧‍👦

JuliaHub を使って複数ノードのDistributed Computingを試す

2022/05/01に公開1

本日は

JuliaHub を用いて分散コンピューティング(Distributed computing) を試すお話です.

JuliaHubの入門はここにも書きました

この記事ではみんな大好き「乱数を用いた円周率の計算」を複数のコンピュータを用いて並列計算させます. 大学や研究機関など特別な機関に所属していないような普通の人間でも(お金さえ払えば)できてしまうのです.

この記事を書いている中の人は「自腹で4台のラズパイを買って, LANケーブル繋いでMPIによって並列計算をPythonでおこなった」程度の経験しかないよくある一般人です. それよりも多いノードを使ったスパコンシステムを使うのは JuliaHub が初めてになります.

この記事を読んだ方で普段スパコンを触っている方がいれば是非 JuliaHub を使って既存のシステムとの比較・所感を書いていただけると幸せになる人がいると思います.

前提

  • JuliaHub に登録しクレジットカード認証が通った状態
  • 手元で簡単なJulia のコードの記述・実行ができる
  • Distributed.jl を使いこなせれば尚可.

扱うトピック

JuliaHub のチュートリアル をベースに話を進めていきます. みんな大好き乱数によって円周率を計算します.

[-1, 1]\times [-1, 1] \subset \mathbb{R}^2 の領域でランダムに点(x, y)\in\mathbb{R}^2を生成します. この生成を n 回繰り返しましょう.各々の試行で生成された点が半径が1の円の中に所属するかどうかを観測します. その様子を描いたのが下記の図になります.

緑: 円の中に入った点, 赤色: 円の外側に所属した点

画像は下記のリンクから抜粋
https://help.juliahub.com/juliahub_tutorials/vscode_extension/#executing_a_script

円の面積を領域の面積で割った値は \pi/4 になります. ということで

4(円の中に入った点の数)/n

を円周率の近似値として得ることができます.

用意するコード

読者が JuliaHub のチュートリアルを真似できるようにコアなロジックは

https://help.juliahub.com/juliahub_tutorials/vscode_extension/#distributed_job

そのまま用いることにします. 方針は下記の計算をひたすら実行させるだけ

x, y = rand() * 2 - 1, rand() * 2 - 1
Int(x^2 + y^2 <= 1)

このロジックは複数台のマシンで独立に計算させることができるので分散コンピューティングの練習にはちょうど良い素材です.

下記のコードを calcpi.jl という名前でひとまず保存します.

calcpi.jl
using Distributed

function estimate_pi_distributed(n)
    n > 0 || throw(ArgumentError("number of iterations must be > 0, got $n"))
    incircle = @distributed (+) for i in 1:n
        x, y = rand() * 2 - 1, rand() * 2 - 1
        Int(x^2 + y^2 <= 1)
    end
    return 4 * incircle / n
end

function main()
    @show nprocs()
    @show estimate_pi_distributed(1024)

    for k in 1:13
        n = Int(exp10(k))
        ret = @timed estimate_pi_distributed(n)
        @info "progress logging:" k=k n=n elapsed_time=ret.time log10_elapsed_time=log10(ret.time) pi_approx=ret.value
    end
end

main()

手元のPCで実行

動作確認のため手元のマシンで実行してみます.

	    _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.6.6 (2022-03-28)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> versioninfo()
Julia Version 1.6.6
Commit b8708f954a (2022-03-28 07:17 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin18.7.0)
  CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-11.0.1 (ORCJIT, skylake)
julia -p auto calcpi.jl

を実行してみましょう. -p 数 で指定した数の複数プロセスを立ち上げて計算を並列実行させます.
auto だと論理コア分だけ発生させます.

手元だと下記のような出力が得られました(終わらないので途中で止めた):

julia -p auto calcpi.jl
nprocs() = 17
estimate_pi_distributed(1024) = 3.2265625
┌ Info: progress logging:
│   k = 1
│   n = 10
│   elapsed_time = 0.00547458
│   log10_elapsed_time = -2.261649193506851
└   pi_approx = 3.6
┌ Info: progress logging:
│   k = 2
│   n = 100
│   elapsed_time = 0.003604553
│   log10_elapsed_time = -2.4431485843897405
└   pi_approx = 3.2
┌ Info: progress logging:
│   k = 3
│   n = 1000
│   elapsed_time = 0.001483053
│   log10_elapsed_time = -2.828843328272869
└   pi_approx = 3.172
┌ Info: progress logging:
│   k = 4
│   n = 10000
│   elapsed_time = 0.001327724
│   log10_elapsed_time = -2.8768921943464734
└   pi_approx = 3.138
┌ Info: progress logging:
│   k = 5
│   n = 100000
│   elapsed_time = 0.001279914
│   log10_elapsed_time = -2.892819210492916
└   pi_approx = 3.14728
┌ Info: progress logging:
│   k = 6
│   n = 1000000
│   elapsed_time = 0.001757383
│   log10_elapsed_time = -2.7551335790511238
└   pi_approx = 3.140364
┌ Info: progress logging:
│   k = 7
│   n = 10000000
│   elapsed_time = 0.012431846
│   log10_elapsed_time = -1.9054643783504694
└   pi_approx = 3.1405128
┌ Info: progress logging:
│   k = 8
│   n = 100000000
│   elapsed_time = 0.105849097
│   log10_elapsed_time = -0.9753128426005442
└   pi_approx = 3.14166212
┌ Info: progress logging:
│   k = 9
│   n = 1000000000
│   elapsed_time = 1.036959515
│   log10_elapsed_time = 0.01576180098371608
└   pi_approx = 3.141531148
┌ Info: progress logging:
│   k = 10
│   n = 10000000000
│   elapsed_time = 10.796728385
│   log10_elapsed_time = 1.0332921758951752
└   pi_approx = 3.1416026752
┌ Info: progress logging:
│   k = 11
│   n = 100000000000
│   elapsed_time = 132.697088801
│   log10_elapsed_time = 2.1228613951211797
└   pi_approx = 3.1415873828

CPUの使用率を観測するとどのコアも一生懸命働いている様子がわかると思います.

残念ながら一台のマシンではk=12, 13 の結果は残念ながらすぐには求まりませんでした.

JuliaHub の出番です

x, y = rand() * 2 - 1, rand() * 2 - 1
Int(x^2 + y^2 <= 1)

を複数台のPCで計算させてそれを集約すればいいんでしょ?
はい.そうです. でも複数台のPCの用意を誰がするんでしょう? やりたくないですよね?

JuliaHub を使うとボタン一つであたかも複数台のPCを用意した環境を作成してくれます.

Job List に行く

JuliaHubにログイン後,画面右上にあるメニューの COMPUTE から Job List にいきます.

Start job のボタンを押します.

Submit Job の調整

ノード(計算機の台数)をいくつ繋げて計算するかというパラメータを指定します.ここでは

Start a distributed CPU job with 2 vCPUs per node and 4 GB of memory per vCPU with one Julia process for each vCPU.

となるように1台あたりの計算機に要請する性能を指定します. つまり

1 台のマシンには2つ仮想CPUが積んでいる. メモリは 2x4=8GB.
1つの仮想CPUに対してJuliaの1プロセスが割り当てられる

Number of Nodes をここでは 64 にします. そうすると resulting in 127 worker process と小さく注意書きが表示されます. ここで, 127 = 2 x 64 - 1 であることに注意.

これによって 127 の計算する妖精さんを操れるようになります. 加えて妖精さんを操るボスを用意しますので nprocs() の値は 127 + 1 = 128 になるはずです.

Limit by の設定

JuliaHub は使った分だけお金が取られます. ここでは 1 hour 分だけ使うようにします. 私はチキンなので途中で止めてしまいますが, JuliaHub の機能た正しく動作すれば高々 $10.88 だけしかかからないはずです.

Code の設定

# Write code or drop a file here. のところに上記で実行したコードをそのまま貼り付けます.

今回は標準ライブラリだけで済むコードなので Project TOML file などの指定は不要です. 外部ライブラリを使う場合は Project.toml を手元で作成しそれをアップロードする作業が必要です.

ここまでの状態

ここまでの説明に従うとみなさんの手元の画面は下記のような画面になるはずです.

覚悟はいいですか? Start ボタンを押しましょう.

ぽちぃ!!

Start ボタンを押すとジョブが実行されます.

しばらくすると Job List のページに戻ります.下図のように砂時計マークがついているリストが出てきます.

Logs の画面

Logs のリンクを押すとジョブが実行している最中に発生する出力を監視することができます.

64個のノードを生成するので時間がかかります. ^^;

@info は便利よ?

先ほどのコードに @info macro が挿入されていること思い出しましょう.

@info "progress logging:" k=k n=n elapsed_time=ret.time log10_elapsed_time=log10(ret.time) pi_approx=ret.value

これをコードに書いておくとログとして値が出力されるだけでなく JuliaHub がそれらの値を収集しグラフを作ってくれます.

下記の図の左下にあるアイコンをクリックするとグラフを描画する空間が出てきます.

ここで X: の部分のドロップダウンの値として n を Y: の方を elapsed_time としましょう.

そうすると下記のような線形に伸びるグラフが見えるでしょう.

これは実行時間がループの数 n = 10^k 分の試行に比例していることを意味しています.

手元の環境と比較

さて, X 軸を n としておくと値が大きいのでイメージしづらいです. そこで X 軸の値を k でとるように変更します.

マウスオーバした箇所から k = 11 の場合だと 6 秒で終了したことがわかりました.

ちなみに手元の環境のログを見返すと下記の通り elapsed_time = 132.697088801 だったので
たくさんの妖精さんが働く JuliaHub の性能はすごいことがわかりますね:

┌ Info: progress logging:
│   k = 11
│   n = 100000000000
│   elapsed_time = 132.697088801
│   log10_elapsed_time = 2.1228613951211797
└   pi_approx = 3.1415873828

さて, 手元のマシンで計算できなかった k = 12, 13 のケースはJuliaHubだと各々60秒と600秒程度で済んでいます.

for k in 1:13 のタスク全てを回しても大体13分ほどですみました. クラウド破産せずにすみそうです.

まとめ

  • 並列計算のスクリプトを記述しそれを JuliaHub で実行することができました.
  • 手元ですぐに確認が難しい規模のものでもノード数を爆上げさせて現実的な時間とお金で計算を回すことができました.
  • JuliaHub の無料枠は 25 ドルありますので一回程度なら動かしてもいいんじゃないかなと思います.
  • 冒頭にも書きましたけれど普段スパコンを使っている方がいましたらこの機会にお試し使った感想をシェアしていただけると嬉しいです.
GitHubで編集を提案

Discussion