株式会社ホクソエムのブログ

R, Python, データ分析, 機械学習

スパコン上でRを並列実行する方法

アカデミアの大規模超並列クラスタ型スーパーコンピュータ上で、Rを並列実行する際の備忘録です。あまりスパコンに詳しくないので、用語を間違っていたら教えてください。僕が使っているスパコンは富士通のサーバでXeon Scalableプロセッサを積んでて、Intelのコンパイラがインストールされてて、pjsubコマンドでjobをsubmitします。

並列実行するために、試した方法は次の3種類です。順に説明します。

  1. バルクジョブを使う方法
  2. Rのパッケージを利用してMPIで実行する方法
  3. インテルのMPIライブラリのコマンドラインを使う方法

バルクジョブを使う方法

まず実行したいRファイル(calc.R)は以下のようなものとします。

.libPaths(c('/path/to/my_R_lib', .libPaths()))
library(rstan)

args <- commandArgs(trailingOnly=TRUE)
paraID <- as.integer(args[1])
f_in <- sprintf('input/param.%d.csv', paraID)
f_out <- sprintf('output/result.%d.RData', paraID)
# ... resultの計算 ...
save(result, file=f_out)
  • 1行目: Rのパッケージをユーザ権限で/home/以下などにインストールした場合には、このようにそのRパッケージへのパスを.libPaths()関数で加えておく必要があります。
  • 4行目: コマンドラインの引数をcommandArgs関数で受け取ります。
  • 5~7, 9行目: 引数の使用例です。数値の文字列(10とか)を渡して、その数値に対応するファイルを読み込み、計算結果を格納しています。

次にRコードをキックするためのバッチファイルを作ります。

#!/bin/sh

#PJM -g my_proj_name
#PJM -L rscgrp=my_resource
#PJM -L node=1
#PJM -L elapse=01:00:00
#PJM -j
#PJM -X

module load R/3.6.0
Rscript --vanilla /my_dir/calc.R $PJM_BULKNUM
  • 3~8, 10行目: この部分はsubmitするときに指定するオプションとRのPATHの読み込みで、必要かどうかはスパコンの環境に依存します。説明しません。
  • 11行目: ここで/my_dir/calc.Rを実行します。$PJM_BULKNUMpjsubで実行するときに渡されます。この値が先ほどのRコードのargs[1]になります。

最後にjobをsubmitするときのコマンドです。バルクジョブで実行します。

pjsub --bulk --sparam 1-50 run.sh

実行すると150の数値が一つずつ$PJM_BULKNUMに渡されて、サブジョブとしてRが実行されます。

しかしこの方法では、サブジョブ1つ1つが1ノードに割り振られるという扱いになるため、スパコンの使用チケットがノード×(サブ)ジョブ×計算時間の合計で定められている場合には、非常に速いスピードでチケットを消費してしまいます。この状況を打破するためには、MPIを使って1ジョブでマルチプロセスを利用できると良いです。

Rのパッケージを利用してMPIで実行する方法

手っ取り早いのはMPIを利用するRパッケージを利用することです。{foreach}, {doParallel}, {snow}, {Rmpi}パッケージをインストールする必要があります。その後、以下のようなRコードを準備します。

.libPaths(c('/path/to/my_R_lib', .libPaths()))
library(foreach)
library(doParallel)
library(snow)
library(Rmpi)

args <- commandArgs(trailingOnly=TRUE)
paraID_vec <- seq(from=as.integer(args[1]), to=as.integer(args[2]))

cl <- makeCluster(50, type='MPI')
registerDoParallel(cl)

memo <- foreach(paraID=paraID_vec, .packages='rstan', .export=ls(envir = globalenv())) %dopar% {
  f_in <- sprintf('input/param.%d.csv', paraID)
  f_out <- sprintf('output_mpi/result.%d.RData', paraID)
  # ... resultの計算 ...
  save(result, file=f_out)
}

stopImplicitCluster()
  • 7~8行目:ここでは並列実行したい数値のはじまりと終わりをargs変数に渡して、vectorを作っています。
  • 10~11行目:ここでMPIで実行するための準備をしています。50プロセスを使う指定をしています。{Rmpi}パッケージがないとmakeCluster関数でtype='MPI'引数が使えません。
  • 13行目:あとはいつものforeach関数と%dopar%演算子で並列実行します。

次にRコードをキックするためのバッチファイルを作ります。

#!/bin/sh

#PJM -g my_proj_name
#PJM -L rscgrp=my_resource
#PJM -L node=1
#PJM -L elapse=01:00:00
#PJM --mpi proc=50
#PJM -j
#PJM -X

module load R/3.6.0
Rscript --vanilla /my_dir/calc-with-Rmpi.R 1 50
  • 7行目: ここでmpi50プロセス使う指定をしています。Rコード内のプロセス数と合わせる必要があります。
  • 12行目: 上のRコードを実行します。

しかし、この方法はRパッケージ、特に{Rmpi}のインストールが環境によっては難しいという欠点があります。僕も失敗しました。install.packages関数のconfigure.argsオプションを使って、用意されているMPIのincludeディレクトリやlibpathを指定したのですが、エラーを完全に取り除くことができませんでした。だから上のコードも未テストでうまくいくか分かりません。ごめんなさい。

MPIライブラリのコマンドラインを使う方法

triadsouさんに教えてもらいました(URL)。ありがとうございました!

僕のスパコンの環境ではmpirunに相当するコマンドラインがIntelのmpiexec.hydraでした。そのため、以下のようなバッチファイルになります。

#!/bin/sh

#PJM -g my_proj_name
#PJM -L rscgrp=my_resource
#PJM -L node=1
#PJM -L elapse=01:00:00
#PJM --mpi proc=50
#PJM -j
#PJM -X

module load R/3.6.0
mpiexec.hydra \
-n 1 /path/to/bin/Rscript --vanilla /my_dir/calc.R 1 : \
-n 1 /path/to/bin/Rscript --vanilla /my_dir/calc.R 2 : \
-n 1 /path/to/bin/Rscript --vanilla /my_dir/calc.R 3 : \
# ... 50まで ... コマンド書くのが面倒なので別のプログラムで自動生成するとよい
-n 1 /path/to/bin/Rscript --vanilla /my_dir/calc.R 49 : \
-n 1 /path/to/bin/Rscript --vanilla /my_dir/calc.R 50 :
  • 12行目:mpiexec.hydraコマンドを使います。
  • 13~18行目: -n 1は1プロセス分使うという意味です。これを50プロセス分だけコロンでつなげて書きます。なお僕の環境ではRscriptを絶対パスにしないと動きませんでした。

この方法で、無事MPIで50プロセスを使って並列実行することができました。

Enjoy!