MPIクラスタとRstudioを併用した中規模データの分析レポート作成の実際

2012.12.1

Rユーザー会2012

岡田 昌史

本日の内容

  • 動機
  • マルチコアでの実験
  • 小規模クラスタでの実験
  • スーパーコンピューターでの実行

スライドは slidify ( http://www.slidify.org/ )で作成しました。 http://rpubs.com/mokjpn/mpirstudio にあります。

動機

  • Rstudio+knitRでさくさく解析レポートをつくる日々
  • でも、データベースの構造が解析向きでなく、前処理が重い
  • とはいっても、中間テーブルを作るのは作業ミスのもと。
  • データベース→knitR→解析レポートという美しい流れを乱したくない
  • knitRにはコードチャンク単位で実行結果をキャッシュしておく機能もあるから、重い処理だって(ちょっと待てれば)平気だけど...
  • Rstudioに慣れてしまうと、ちょっとの待ちでもがまんできない体に。
  • というわけで、重い処理を並列化して速くしましょう。

どんなデータの時に並列化したくなったか

  • 2500名弱の患者さんを半年に1度、3年半追跡したデータ
  • 病院への受診日を記録したテーブルから、個人別の受診間隔の最大値を出す
  • 血液検査の結果と年齢と性別から、ある指標値を計算してその経年変化を示すテーブルを作る
  • ...のように、1患者ごとに*独立して*処理が可能なので、並列化により効果が期待できる

マルチコアCPUの性能を活かそう

  • 最近のCPUはたいてい4以上のコアを持つが、通常Rは1個のコアしか使用しない。
sapply(abs(floor(rnorm(10000, mean = 1000, sd = 200))), function(x) {
    mean(rep(iris$Sepal.Length, x))
})

重い処理を実行中のアクティビティモニタ表示 CPU使用率表示

並列化した場合

library(Rmpi)
library(snow)
cl <- makeCluster(11, type = "MPI")
parSapply(cl, abs(floor(rnorm(10000, mean = 1000, sd = 200))), function(x) {
    mean(rep(iris$Sepal.Length, x))
})

重い処理を実行中のアクティビティモニタ表示 CPU使用率表示

並列化の方法

  • snow
    さまざまな並列化手法に対応している。OSへのライブラリの導入や、各ノードへのデータコピーなどを記述する必要がある
  • multicore
    プロセスをforkすることで並列処理。初期設定がほぼ不要なので、すぐに使い始めることができるが、マシンをまたぐ並列化はできない
  • parallel
    R 2.14.0 以降は標準パッケージ化。snowとmultcoreをあわせたような関数群を提供しており、これらのパッケージを使用していたコードはほぼそのまま動かせる。

Rmpi+snowによる並列化

  • Message Passing Interface(MPI)を使用して並列化
  • snowのおかげで、sapply()をparSapply()に変えるだけでメインの処理はほぼ変える必要はない
  • 1台だけのマルチコア環境から、自前で数台のLinuxマシンをつなげて作る小規模クラスタ、統数研スパコンまで、ほぼ同じコードで走らせることができ、スケーラブル
  • 「MPIの導入」と、「ファイルの同期」を解決する必要がある。

マルチコアでの実験

  • ファイルの同期は必要ないが、MPIの導入が必要
  • MPIは仕様なので、さまざまな実装がある
  • Windowsでは...試していません
  • Mac/Linuxでは、Open MPI ( http://www.open-mpi.org/ )を使用できる。
  • Macでは、Snow LeopardまではOpen MPIが同梱されていたが、Lion以降はなくなったので、自分でインストールする必要がある。
    • Open MPI 1.6 に configure --disable-dlopen でmake成功しました
  • Rmpi パッケージと snowパッケージをインストール

MPIクラスターの初期化

library(Rmpi)
library(snow)
cl <- makeCluster(11, type = "MPI")  # 12コア機
data(iris)
tf <- function(x) {
    mean(rep(iris$Sepal.Length, x))
}
# 並列処理で利用する変数および関数を各ノードにコピー
clusterExport(cl, c("iris", "tf"))

並列処理の実行

parSapply(cl, abs(floor(rnorm(10000, mean = 1000, sd = 200))), tf)
  • 時間がかかる処理なので、R markdownでレポートを作る場合は cache=TRUEにしておくと便利

どのくらい高速化?

system.time(parSapply(cl, abs(floor(rnorm(10000, mean = 1000, sd = 200))), 
    function(x) {
        mean(rep(iris$Sepal.Length, x))
    }))
system.time(sapply(abs(floor(rnorm(10000, mean = 1000, sd = 200))), 
    function(x) {
        mean(rep(iris$Sepal.Length, x))
    }))
並列化    ユーザ   システム   経過
あり 0.591 3.991 4.562
なし 26.772 10.947 37.637

小規模クラスタでの実験

  • 4-8コア程度のLinuxサーバが数台あったので、せっかくだからつないでもっと並列化数を多くしてみよう
  • snow+MPIの場合、並列化コードはほぼ変わらないが、事前準備が必要
    • すべてのサーバへのOpen MPIのインストール
    • サーバ間でsshの上でMPI通信をするため、sshで相互にパスワード入力なしでログインできるよう設定
    • サーバ間のファイル共有の設定
    • ファイル共有はNFS等でもいいが、DropboxのLinuxクライアントが便利。

Rコードの変更

library(Rmpi)
library(snow)
cl <- makeCluster(11, type = "MPI")  # 12コア機

を、

library(Rmpi)
library(snow)
cl <- getMPIcluster()

に変更。

実行

mokada@axis:~$ cat hosts
axis.example.com slots=3
texas.example.com slots=4
mokada@axis:~$ mpirun -n 7 --hostfile /home/mokada/hosts /home/mokada/bin/RMPISNOW  CMD BATCH /home/mokada/Dropbox/MPItest.R
  • hostfile を作成し、使用するサーバのホスト名と、slots=使用コア数 を書いておく
  • Open MPIに含まれる mpirunコマンドでRMPISNOWを実行。-n 引数で並列化数を指定する。
  • snowパッケージに含まれる RMPISNOW シェルスクリプトを、全てのサーバで共通のパスに入れておく。Rスクリプトも同様

結果

並列化    ユーザ   システム   経過
4コア+3コア 5.448 17.093 22.541
12コア 0.591 3.991 4.562
なし 26.772 10.947 37.637

Rstudioとの連携

  • 1つのサーバ上でRstudioのサーバ版を動かせば、Tools→Shell..やsystem()で並列化実行コマンド(mpirun)を打てるので、使い勝手は良い
  • knitRを使う場合
    • parSapply()やclusterExport()はRmdファイルに記述しておき、knitだけをする下記のようなスクリプトを作成してmpirunで実行すればよい。
library(knitr)
library(markdown)
knit("AnalysisReport.Rmd")
markdownToHTML("AnalysisReport.md", "AnalysisReport.html")

スーパーコンピューターで実験

  • 統数研のスパコンなど、Portable Batch System (PBS)を使用する場合
  • 128並列、512並列など可能
  • MPIがサポートされているスパコンであれば、Rコードは小規模クラスタと*同一*でOk
  • PBSにジョブを投入するために、Job Control Language(JCL)としてシェルスクリプトを作成する必要がある

Rstudioとの連携

  • Rstudioサーバ版を動かすというわけにはいかないので...
  • Rstudioとのファイルのやりとりは、rsync, ssh等を使用。たとえば以下の3種のシェルスクリプトを作ってsystem()やTools→Shell.. から起動する。
    • 使用データとRmdファイルをスパコンに差分コピーする
    • ジョブを投入する
    • 生成されるHTMLファイルと実行ログをスパコンからコピーする
  • 統数研のスパコンはR バージョン2.15系になったので、knitrも使えます

JCLの例 - 128ノードでの実行

#!/bin/sh
#@$-q r128
#@$-r MPItest
#@$-lP 128
#@$-lp 1
#@$-o MPItest.out
#@$-e MPItest.err
#@$-oi
cd ${QSUB_WORKDIR}
cd ~
GOTO_NUM_THREADS=1
export GOTO_NUM_THREADS
mpiexec -n 128 RMPISNOW CMD BATCH -q --no-save MPItest.R

結果

並列化    ユーザ   システム   経過
128ノード 0.222 0.002 0.225
4コア+3コア 5.448 17.093 22.541
12コア 0.591 3.991 4.562
なし 26.772 10.947 37.637

まとめ

  • 処理単位が小さく独立して計算できることは、並列化することで性能向上
  • 1つのマシン内でマルチコアを活かすのであれば、multicore もしくは parallel パッケージを導入してlapplyをmclapplyに変えるのがお手軽
  • 複数マシン、スパコンなどへスケールするつもりがあれば、snowもしくはparallel+MPIがおすすめ
  • 統数研のスパコンならknitrも使える
  • Rstudio+R markdownはやはり気持ちいい