「大人の教養・知識・気付き」を伸ばすブログ

一流の大人(ビジネスマン、政治家、リーダー…)として知っておきたい、教養・社会動向を意外なところから取り上げ学ぶことで“気付く力”を伸ばすブログです。データ分析・語学に力点を置いています。 →現在、コンサルタントの雛になるべく、少しずつ勉強中です(※2024年1月21日改訂)。

MENU

プログラムとしてのRを学ぶ(その16/16)

 \mathrm{R}をプログラムとして見たときに注意・検討すべきところを学んでおきたい、ということで

を読んでいく。

16. 並列R

 並列処理は魔法のように高速化をもたらすことを期待するものの、実際には逐次バージョンよりも実行速度が遅い場合が多い。並列処理を上手くいかせるには、並列処理ハードウェアやソフトウェアの特性を理解することが必要となる。

16.1 snowの使い方

 前提として、以下のような内容の\mathrm{R}ファイル\mathrm{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)
}

 次に以下のように\mathrm{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つの新しい\mathrm{R}プロセスを開始するように\mathrm{snow}に指示している(\mathrm{makeCluster})。ここではこの新しいプロセスをワーカと呼び、元々の[tex:\mathrm{R}プロセスをマネージャと呼ぶ。上記を実行した際には、合計で3つのプロセスが動いていることになる。
 ワーカは\mathrm{snow}内部で言うクラスタを作成し、ここでは\mathrm{cl}と名付けている。\mathrm{snow}パッケージは並列処理の世界でスキャッタ/ギャザパラダイムと呼ばれる仕組みを用いる:

  • マネージャはデータをチャンクに分解し、ワーカに分配する。
  • ワーカはそれぞれのチャンクを処理する。
  • マネージャはワーカからの結果を集め、アプリケーションに適するように結合する。
16.1.1 snowコードの分析

 上記のコードでは、\mathrm{clusterApply}()を呼び出すと、指定された同じ関数(ここでは\mathrm{mtl}())を各ワーカに特化した引数とすべてに共通するオプション引数で呼び出す。

  • ワーカ1が関数\mathrm{mtl}()を引数\mathrm{ichunks[[1]]}\mathrm{m}で呼び出す。
  • ワーカ2が\mathrm{mtl}()を引数\mathrm{ichunks[[2]]}\mathrm{m}で呼び出し、すべてのワーカにも同様に指示する。
  • それぞれのワーカが割り当てられたタスクを実行し、その結果をマネージャに返す。
  • マネージャはワーカから受けたすべての結果を\mathrm{R}のリストに集め、それを\mathrm{counts}に割り当てる。

このとき集計した値がリストであるから、\mathrm{do.call}()を用いて\mathrm{counts}からベクトルを抽出して\mathrm{sum}()を呼び出す。

16.1.2 どの程度高速化できるか

 ワーカクラスタを増やしても反比例的に計算時間を短縮できるわけではない。
 ほとんどの並列処理アプリケーションでは、オーバーヘッド(計算活動以外に費やされる無駄な時間)が発生する。今回では、行列をマネージャからワーカに送る時間などがそれに該当する。

16.2 Cの活用

 並列\mathrm{R}を用いると\mathrm{R}コードが大幅に高速化する。とはいえ、並列\mathrm{R}もまた\mathrm{R}であるため、過去に触れた\mathrm{R}特有のパフォーマンス問題が存在する。そこで並列\mathrm{C}を呼び出すという方法もある。

16.2.1 マルチコアマシンの仕様

 マルチコアシステムでは並列計算ができる。この並列計算はスレッドで行なわれ、オーバーヘッドが生じる。
 マシンがマルチコアの場合、共有メモリシステムとして構築されている。全てのコアは同じ\mathrm{RAM}にアクセスする。

16.2.2 GPUプログラミング

 別の種類の共有メモリ並列ハードウェアとしては、\mathrm{GPU}で構成されるものがある。
 並列\mathrm{C}へのインターフェースを持った\mathrm{R}コードを記述することを想定する
 \mathrm{GPU}は共有メモリ/スレッドモデルに従う。\mathrm{GPU}は何十ないし何百のコアを持っていることが多い。1つのブロック内で複数のスレッドを一緒に実行でき、一定の効率化が図れる。
 \mathrm{GPU}にアクセスするプログラムは、ホストと呼ばれるマシンの\mathrm{CPU}上で実行を始める。そして\mathrm{GPU}上でコードを実行し始める。すなわちデータをホストからデバイスに転送する必要があり、デバイスが計算を終了したら、結果をホストに転送し戻さなければならない。
 \mathrm{gputools}パッケージを用いると\mathrm{GPU}による並列化が図れる。

16.3 パフォーマンスに関する一般的な考察

 

16.3.1 オーバーヘッドの要因

 適切な並列プログラミングを行なうには、オーバーヘッドの物理的な要因について大まかな知識を持つことが不可欠である。

  • 共有メモリマシン

     マルチコアマシンでメモリ共有していると、2つのコアが同時にメモリにアクセスしようとすると互いに衝突し合った場合に共有はオーバーヘッドも生み出す。

     それぞれのコアがキャッシュを持っていることもあり、共有メモリの一部のローカルコピーをキャッシュに保持する。これはコア間でのメモリ衝突を減らすことを目的にしているが、お互いにキャッシュの一貫性を保つために時間を費やす必要があるため、これ自体がオーバーヘッドになる。

     \mathrm{GPU}は特殊な種類のマルチコアマシンで、遅延(メモリ読込要求の後にメモリから\mathrm{GPU}に最初のビットが到着するまでの遅延時間)が非常に大きくなる。またホストとデバイス間のデータ転送でもオーバーヘッドが生じる。

     \mathrm{GPU}はある特定の種類のアプリケーションでは優れた性能を持つ一方で、オーバーヘッドが大きな問題になる場合もある。
  • ネットワークコンピュータシステム

     ネットワークコンピュータシステムであっても並列計算が可能である。各\mathrm{CPU}は各コンピュータ内に存在し、それぞれが独自のメモリを持つ。

     ネットワークデータ転送はオーバーヘッドを引き起こす。

     また\mathrm{snow}は、たとえばマネージャからワーカに送信する前にベクトルや行列などの数値オブジェクトを文字形式に変更するため、更にオーバーヘッドが生じる。

     しかし共有メモリシステムはネットワーク化することができる。
16.3.2 驚異的並列アプリケーションと非驚異的並列アプリケーション

 並列化がたやすくできるプログラムを驚異的並列*1という。
 驚異的並列問題は経験的にやり取りのオーバーヘッドが少ない傾向がある。これに対して非驚異的並列問題は、そもそも\mathrm{R}で扱うには適していない。なぜならば\mathrm{R}関数型プログラミング特性が上手く合わないからである。たとえば

x[3] <- 8

はベクトル\mathrm{x}全体を書き直すことができるため一見単純そうに見える。しかし実はやり取りのトラフィック問題が生じる。そのためアプリケーションが驚異的並列でない場合、\mathrm{C}で記述するのが最善策である。
 また驚異的並列であってもアルゴリズムが効率的になるとは限らない。やり取りのトラフィックがかなり多いアルゴリズムもあるため、パフォーマンスが犠牲になる。

16.3.3 静的なタスク割り当てと動的なタスク割り当ての対比

 実質的に書くスレッドが対処するタスクをあらかじめ割り当てる方法を静的割り当てという。これに対して事前には割り当てられていないものを動的割り当てという。

 動的タスクの方が、予め割り当てたスレッドが無駄になることで遅くなり負荷分散問題を起こしにくいため、一見パフォーマンスが良いように思える。しかしオーバーヘッド問題があるため、むしろ動的なコードの方がずっと遅くなる可能性もある。このように大抵の場合は、静的タスク割り当ての場合の方が速くなる場合が多い。

16.3.4 ソフトウェアの魔術:一般的な問題を驚異的並列問題に変える

 非驚異的並列アルゴリズムから優れたパフォーマンスを得るのは困難である。しかし統計アプリケーションには非驚異的並列問題を驚異的並列問題に変換する方法がある。ポイントは統計的特性を活用することである。

16.4 並列Rコードのデバッグ

 並列\mathrm{R}パッケージは各プロセスに対してターミナルウィンドウを開かない。そこでデバッグする際には、単一ワーカ関数をデバッグすることや、\mathrm{cat}()を用いてファイルに書き出すといった手段を取ることが求められる。

*1:「驚異的に」簡単という意味である。

プライバシーポリシー お問い合わせ