Rをプログラムとして見たときに注意・検討すべきところを学んでおきたい、ということで
を読んでいく。
前回
16. 並列R
並列処理は魔法のように高速化をもたらすことを期待するものの、実際には逐次バージョンよりも実行速度が遅い場合が多い。並列処理を上手くいかせるには、並列処理ハードウェアやソフトウェアの特性を理解することが必要となる。
16.1 snowの使い方
前提として、以下のような内容のRファイルSnowMutLinks.Rが存在するものとする。
# 相互リンク問題 mtl <- function(ichunk,m){ n <- ncol(m) matches <- 0 for(i in ichunk){ if(i < n){ rowi <- m[i,] matches <- matches + sum(m[(i+1):n,] %*% rowi) } } matches } mutlinks <- function(cls,m){ n <- nrow(m) nc <- length(cls) # いずれのワーカーがiのどのチャンクに対処するかを決める options(warn = -1) ichunks <- split(1:n,1:nc) options(warn = 0) counts <- clusterApply(cls,ichunks,mtl,m) do.call(sum,counts) / (n * (n-1)/2) }
次に以下のようにRで実行する。
######################## ### snowをトライアル ### ######################## library("snow") source("...\\SnowMutLinks.R") cl <- makeCluster(type = "SOCK", c("localhost","localhost")) testm <- matrix(sample(0:1,16,replace = T), nrow = 4) mutlinks(cl, testm)
これはマシン上で2つの新しいRプロセスを開始するようにsnowに指示している(makeCluster)。ここではこの新しいプロセスをワーカと呼び、元々の[tex:Rプロセスをマネージャと呼ぶ。上記を実行した際には、合計で3つのプロセスが動いていることになる。
ワーカはsnow内部で言うクラスタを作成し、ここではclと名付けている。snowパッケージは並列処理の世界でスキャッタ/ギャザパラダイムと呼ばれる仕組みを用いる:
- マネージャはデータをチャンクに分解し、ワーカに分配する。
- ワーカはそれぞれのチャンクを処理する。
- マネージャはワーカからの結果を集め、アプリケーションに適するように結合する。
16.1.1 snowコードの分析
上記のコードでは、clusterApply()を呼び出すと、指定された同じ関数(ここではmtl())を各ワーカに特化した引数とすべてに共通するオプション引数で呼び出す。
- ワーカ1が関数mtl()を引数ichunks[[1]]とmで呼び出す。
- ワーカ2がmtl()を引数ichunks[[2]]とmで呼び出し、すべてのワーカにも同様に指示する。
- それぞれのワーカが割り当てられたタスクを実行し、その結果をマネージャに返す。
- マネージャはワーカから受けたすべての結果をRのリストに集め、それをcountsに割り当てる。
このとき集計した値がリストであるから、do.call()を用いてcountsからベクトルを抽出してsum()を呼び出す。
16.1.2 どの程度高速化できるか
ワーカクラスタを増やしても反比例的に計算時間を短縮できるわけではない。
ほとんどの並列処理アプリケーションでは、オーバーヘッド(計算活動以外に費やされる無駄な時間)が発生する。今回では、行列をマネージャからワーカに送る時間などがそれに該当する。
16.2 Cの活用
並列Rを用いるとRコードが大幅に高速化する。とはいえ、並列RもまたRであるため、過去に触れたR特有のパフォーマンス問題が存在する。そこで並列Cを呼び出すという方法もある。
16.2.1 マルチコアマシンの仕様
マルチコアシステムでは並列計算ができる。この並列計算はスレッドで行なわれ、オーバーヘッドが生じる。
マシンがマルチコアの場合、共有メモリシステムとして構築されている。全てのコアは同じRAMにアクセスする。
16.2.2 GPUプログラミング
別の種類の共有メモリ並列ハードウェアとしては、GPUで構成されるものがある。
並列Cへのインターフェースを持ったRコードを記述することを想定する
GPUは共有メモリ/スレッドモデルに従う。GPUは何十ないし何百のコアを持っていることが多い。1つのブロック内で複数のスレッドを一緒に実行でき、一定の効率化が図れる。
GPUにアクセスするプログラムは、ホストと呼ばれるマシンのCPU上で実行を始める。そしてGPU上でコードを実行し始める。すなわちデータをホストからデバイスに転送する必要があり、デバイスが計算を終了したら、結果をホストに転送し戻さなければならない。
gputoolsパッケージを用いるとGPUによる並列化が図れる。
16.3 パフォーマンスに関する一般的な考察
16.3.1 オーバーヘッドの要因
適切な並列プログラミングを行なうには、オーバーヘッドの物理的な要因について大まかな知識を持つことが不可欠である。
- 共有メモリマシン マルチコアマシンでメモリ共有していると、2つのコアが同時にメモリにアクセスしようとすると互いに衝突し合った場合に共有はオーバーヘッドも生み出す。 それぞれのコアがキャッシュを持っていることもあり、共有メモリの一部のローカルコピーをキャッシュに保持する。これはコア間でのメモリ衝突を減らすことを目的にしているが、お互いにキャッシュの一貫性を保つために時間を費やす必要があるため、これ自体がオーバーヘッドになる。 GPUは特殊な種類のマルチコアマシンで、遅延(メモリ読込要求の後にメモリからGPUに最初のビットが到着するまでの遅延時間)が非常に大きくなる。またホストとデバイス間のデータ転送でもオーバーヘッドが生じる。 GPUはある特定の種類のアプリケーションでは優れた性能を持つ一方で、オーバーヘッドが大きな問題になる場合もある。
- ネットワークコンピュータシステム ネットワークコンピュータシステムであっても並列計算が可能である。各CPUは各コンピュータ内に存在し、それぞれが独自のメモリを持つ。 ネットワークデータ転送はオーバーヘッドを引き起こす。 またsnowは、たとえばマネージャからワーカに送信する前にベクトルや行列などの数値オブジェクトを文字形式に変更するため、更にオーバーヘッドが生じる。 しかし共有メモリシステムはネットワーク化することができる。
16.3.2 驚異的並列アプリケーションと非驚異的並列アプリケーション
並列化がたやすくできるプログラムを驚異的並列*1という。
驚異的並列問題は経験的にやり取りのオーバーヘッドが少ない傾向がある。これに対して非驚異的並列問題は、そもそもRで扱うには適していない。なぜならばRの関数型プログラミング特性が上手く合わないからである。たとえば
x[3] <- 8
はベクトルx全体を書き直すことができるため一見単純そうに見える。しかし実はやり取りのトラフィック問題が生じる。そのためアプリケーションが驚異的並列でない場合、Cで記述するのが最善策である。
また驚異的並列であってもアルゴリズムが効率的になるとは限らない。やり取りのトラフィックがかなり多いアルゴリズムもあるため、パフォーマンスが犠牲になる。
16.3.3 静的なタスク割り当てと動的なタスク割り当ての対比
実質的に書くスレッドが対処するタスクをあらかじめ割り当てる方法を静的割り当てという。これに対して事前には割り当てられていないものを動的割り当てという。
動的タスクの方が、予め割り当てたスレッドが無駄になることで遅くなり負荷分散問題を起こしにくいため、一見パフォーマンスが良いように思える。しかしオーバーヘッド問題があるため、むしろ動的なコードの方がずっと遅くなる可能性もある。このように大抵の場合は、静的タスク割り当ての場合の方が速くなる場合が多い。
16.3.4 ソフトウェアの魔術:一般的な問題を驚異的並列問題に変える
非驚異的並列アルゴリズムから優れたパフォーマンスを得るのは困難である。しかし統計アプリケーションには非驚異的並列問題を驚異的並列問題に変換する方法がある。ポイントは統計的特性を活用することである。
*1:「驚異的に」簡単という意味である。