web-dev-qa-db-ja.com

ノードを追加するたびにforeach%dopar%が遅くなるのはなぜですか?

ネットワークのマルチスレッド/並列化機能をテストするために単純な行列乗算を作成しましたが、計算が予想よりもはるかに遅いことに気付きました。

テストは単純です:2つの行列(4096x4096)を乗算し、計算時間を返します。行列も結果も保存されません。計算時間は簡単ではありません(プロセッサによっては50〜90秒)。

条件:1つのプロセッサを使用してこの計算を10回繰り返し、これらの10の計算を2つのプロセッサ(各5)に分割し、次に3つのプロセッサに分割しました...最大10個のプロセッサ(各プロセッサに1回の計算)。合計計算時間は段階的に減少すると予想し、10個のプロセッサが1個のプロセッサが実行するのと同じ速さで計算を完了すると予想しました10倍同じ。

結果:代わりに、計算時間が2分の1に短縮され、5倍になりました[〜# 〜]予想より遅い[〜#〜]

enter image description here

ノードごとの平均計算時間を計算したとき、割り当てられたプロセッサの数に関係なく、各プロセッサが同じ時間(平均)でテストを計算することを期待していました。同じ操作を複数のプロセッサに送信するだけで、各プロセッサの平均計算時間が遅くなっていることに驚きました。

enter image description here

なぜこれが起こっているのか誰かが説明できますか?

これは質問であることに注意してください[〜#〜]ではありません[〜#〜]これらの質問の複製です:

foreach%dopar%はforループよりも遅い

または

並列パッケージが単にapplyを使用するよりも遅いのはなぜですか?

テストの計算は簡単ではなく(つまり、1〜2秒ではなく50〜90秒)、プロセッサ間で確認できる通信がないため(つまり、計算時間以外に結果が返されたり保存されたりしないため)。

レプリケーション用に以下のスクリプトと関数を添付しました。

library(foreach); library(doParallel);library(data.table)
# functions adapted from
# http://www.bios.unc.edu/research/genomic_software/Matrix_eQTL/BLAS_Testing.html

Matrix.Multiplier <- function(Dimensions=2^12){
  # Creates a matrix of dim=Dimensions and runs multiplication
  #Dimensions=2^12
  m1 <- Dimensions; m2 <- Dimensions; n <- Dimensions;
  z1 <- runif(m1*n); dim(z1) = c(m1,n)
  z2 <- runif(m2*n); dim(z2) = c(m2,n)
  a <- proc.time()[3]
  z3 <- z1 %*% t(z2)
  b <- proc.time()[3]
  c <- b-a
  names(c) <- NULL
  rm(z1,z2,z3,m1,m2,n,a,b);gc()
  return(c)
}

Nodes <- 10
Results <- NULL
for(i in 1:Nodes){
  cl <- makeCluster(i)
  registerDoParallel(cl)
  ptm <- proc.time()[3]
  i.Node.times <- foreach(z=1:Nodes,.combine="c",.multicombine=TRUE, 
                          .inorder=FALSE) %dopar% {
                            t <- Matrix.Multiplier(Dimensions=2^12)
                          }
  etm <- proc.time()[3]
  i.TotalTime <- etm-ptm
  i.Times <- cbind(Operations=Nodes,Node.No=i,Avr.Node.Time=mean(i.Node.times),
                   sd.Node.Time=sd(i.Node.times),
                   Total.Time=i.TotalTime)
  Results <- rbind(Results,i.Times)
  rm(ptm,etm,i.Node.times,i.TotalTime,i.Times)
  stopCluster(cl)
}
library(data.table)
Results <- data.table(Results)
Results[,lower:=Avr.Node.Time-1.96*sd.Node.Time]
Results[,upper:=Avr.Node.Time+1.96*sd.Node.Time]
Exp.Total <- c(Results[Node.No==1][,Avr.Node.Time]*10,
               Results[Node.No==1][,Avr.Node.Time]*5,
               Results[Node.No==1][,Avr.Node.Time]*4,
               Results[Node.No==1][,Avr.Node.Time]*3,
               Results[Node.No==1][,Avr.Node.Time]*2,
               Results[Node.No==1][,Avr.Node.Time]*2,
               Results[Node.No==1][,Avr.Node.Time]*2,
               Results[Node.No==1][,Avr.Node.Time]*2,
               Results[Node.No==1][,Avr.Node.Time]*2,
               Results[Node.No==1][,Avr.Node.Time]*1)
Results[,Exp.Total.Time:=Exp.Total]

jpeg("Multithread_Test_TotalTime_Results.jpeg")
par(oma=c(0,0,0,0)) # set outer margin to zero
par(mar=c(3.5,3.5,2.5,1.5)) # number of lines per margin (bottom,left,top,right)
plot(x=Results[,Node.No],y=Results[,Total.Time],  type="o", xlab="", ylab="",ylim=c(80,900),
     col="blue",xaxt="n", yaxt="n", bty="l")
title(main="Time to Complete 10 Multiplications", line=0,cex.lab=3)
title(xlab="Nodes",line=2,cex.lab=1.2,
      ylab="Total Computation Time (secs)")
axis(2, at=seq(80, 900, by=100), tick=TRUE, labels=FALSE)
axis(2, at=seq(80, 900, by=100), tick=FALSE, labels=TRUE, line=-0.5)
axis(1, at=Results[,Node.No], tick=TRUE, labels=FALSE)
axis(1, at=Results[,Node.No], tick=FALSE, labels=TRUE, line=-0.5)
lines(x=Results[,Node.No],y=Results[,Exp.Total.Time], type="o",col="red")
legend('topright','groups',
       legend=c("Measured", "Expected"), bty="n",lty=c(1,1),
       col=c("blue","red"))
dev.off()

jpeg("Multithread_Test_PerNode_Results.jpeg")
par(oma=c(0,0,0,0)) # set outer margin to zero
par(mar=c(3.5,3.5,2.5,1.5)) # number of lines per margin (bottom,left,top,right)
plot(x=Results[,Node.No],y=Results[,Avr.Node.Time],  type="o", xlab="", ylab="",
     ylim=c(50,500),col="blue",xaxt="n", yaxt="n", bty="l")
title(main="Per Node Multiplication Time", line=0,cex.lab=3)
title(xlab="Nodes",line=2,cex.lab=1.2,
      ylab="Computation Time (secs) per Node")
axis(2, at=seq(50,500, by=50), tick=TRUE, labels=FALSE)
axis(2, at=seq(50,500, by=50), tick=FALSE, labels=TRUE, line=-0.5)
axis(1, at=Results[,Node.No], tick=TRUE, labels=FALSE)
axis(1, at=Results[,Node.No], tick=FALSE, labels=TRUE, line=-0.5)
abline(h=Results[Node.No==1][,Avr.Node.Time], col="red")
epsilon = 0.2
segments(Results[,Node.No],Results[,lower],Results[,Node.No],Results[,upper])
segments(Results[,Node.No]-epsilon,Results[,upper],
         Results[,Node.No]+epsilon,Results[,upper])
segments(Results[,Node.No]-epsilon, Results[,lower],
         Results[,Node.No]+epsilon,Results[,lower])
legend('topleft','groups',
       legend=c("Measured", "Expected"), bty="n",lty=c(1,1),
       col=c("blue","red"))
dev.off()

編集:応答@HongOoiのコメント

UNIXではlscpuを使用して取得しました。

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                30
On-line CPU(s) list:   0-29
Thread(s) per core:    1
Core(s) per socket:    1
Socket(s):             30
NUMA node(s):          4
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 63
Model name:            Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz
Stepping:              2
CPU MHz:               2394.455
BogoMIPS:              4788.91
Hypervisor vendor:     VMware
Virtualization type:   full
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              20480K
NUMA node0 CPU(s):     0-7
NUMA node1 CPU(s):     8-15
NUMA node2 CPU(s):     16-23
NUMA node3 CPU(s):     24-29

編集:@SteveWestonのコメントへの返信。

最大30個のクラスターにアクセスできる仮想マシンネットワーク(ただし、私は管理者ではありません)を使用しています。私はあなたが提案したテストを実行しました。 5つのRセッションを開き、1,2 ... 5で同時に(またはタブオーバーして実行できる限り迅速に)行列乗算を実行しました。以前と非常によく似た結果が得られました(再:プロセスを追加するたびに、個々のセッションがすべて遅くなります)。 tophtopを使用してメモリ使用量を確認しましたが、使用量がネットワーク容量の5%(〜2.5/64Gb)を超えることはありませんでした。

enter image description here

結論:

問題はR固有のようです。他のソフトウェアで他のマルチスレッドコマンドを実行すると(例: [〜#〜] plink [〜#〜] )、この問題は発生せず、並列プロセスは期待どおりに実行されます。上記をRmpidoMPIで実行してみましたが、同じ(遅い)結果になりました。この問題は、仮想マシンネットワーク上のRセッション/並列化されたコマンドに関連しているようです。私が本当に助けを必要としているのは、問題を特定する方法です。同様の問題が指摘されているようです ここ

20

タイミングには並列ループに関連するオーバーヘッドは含まれず、行列乗算を実行する時間のみが含まれ、行列乗算の数とともに時間が増加することを示しているため、ノードごとの乗算時間は非常に興味深いと思います。同じマシン上で並行して実行します。

私はそれが起こるかもしれない2つの理由を考えることができます:

  1. マシンのメモリ帯域幅は、コアが不足する前に行列の乗算によって飽和します。
  2. 行列の乗算はマルチスレッドです。

複数のRセッションを開始して(私はこれを複数の端末で実行しました)、各セッションで2つのマトリックスを作成することにより、最初の状況をテストできます。

> x <- matrix(rnorm(4096*4096), 4096)
> y <- matrix(rnorm(4096*4096), 4096)

次に、これらの各セッションでほぼ同時に行列乗算を実行します。

> system.time(z <- x %*% t(y))

理想的には、この時間は使用するRセッションの数(コアの数まで)に関係なく同じですが、行列の乗算はかなりメモリを消費する操作であるため、多くのマシンはメモリ帯域幅が不足する前に不足しますコア、時間が増加します。

RインストールがMKLやATLASなどのマルチスレッド数学ライブラリで構築されている場合は、すべてのコアを単一の行列乗算で使用できるため、使用しない限り、複数のプロセスを使用してもパフォーマンスの向上は期待できません。複数のコンピューター。

「top」などのツールを使用して、マルチスレッドの数学ライブラリを使用しているかどうかを確認できます。

最後に、lscpuからの出力は、仮想マシンを使用していることを示しています。マルチコア仮想マシンでパフォーマンステストを行ったことがありませんが、それも問題の原因となる可能性があります。


更新

並列行列の乗算が単一の行列の乗算よりも実行速度が遅い理由は、CPUがメモリを読み取るのに十分な速度で、フルスピードで約2コア以上を供給することができないためだと思います。これをメモリ帯域幅の飽和と呼びます。 。 CPUに十分な大きさのキャッシュがある場合、この問題を回避できる可能性がありますが、マザーボードにあるメモリの量とは実際には何の関係もありません。

これは、並列計算に1台のコンピューターを使用する場合の制限にすぎないと思います。クラスターを使用する利点の1つは、合計メモリだけでなく、メモリ帯域幅も増加することです。したがって、マルチノード並列プログラムの各ノードで1つまたは2つの行列乗算を実行した場合、この特定の問題は発生しません。

クラスターにアクセスできない場合は、コンピューターでMKLやATLASなどのマルチスレッド数学ライブラリーのベンチマークを試すことができます。複数のプロセスで並列に実行するよりも、1つのマルチスレッド行列乗算を実行する方がパフォーマンスが向上する可能性があります。ただし、マルチスレッドの数学ライブラリと並列プログラミングパッケージの両方を使用する場合は注意が必要です。

GPUを使用してみることもできます。それらは明らかに行列乗算の実行に優れています。


アップデート2

問題がR固有であるかどうかを確認するには、dgemm関数をベンチマークすることをお勧めします。これは、行列の乗算を実装するためにRが使用するBLAS関数です。

dgemmをベンチマークするための簡単なFortranプログラムを次に示します。ベンチマークで説明したのと同じ方法で、複数の端末から実行することをお勧めします%*% in R:

      program main
      implicit none
      integer n, i, j
      integer*8 stime, etime
      parameter (n=4096)
      double precision a(n,n), b(n,n), c(n,n)
      do i = 1, n
        do j = 1, n
          a(i,j) = (i-1) * n + j
          b(i,j) = -((i-1) * n + j)
          c(i,j) = 0.0d0
        end do
      end do
      stime = time8()
      call dgemm('N','N',n,n,n,1.0d0,a,n,b,n,0.0d0,c,n)
      etime = time8()
      print *, etime - stime
      end

私のLinuxマシンでは、1つのインスタンスが82秒で実行され、4つのインスタンスが116秒で実行されます。これは、Rで見た結果と一致しており、これはメモリ帯域幅の問題であると推測しています。

これをさまざまなBLASライブラリとリンクして、マシンでどの実装がより適切に機能するかを確認することもできます。

pmbw --Parallel Memory Bandwidth Benchmark を使用して、仮想マシンネットワークのメモリ帯域幅に関する有用な情報を取得することもできますが、これは使用したことがありません。

12
Steve Weston

ここでの明白な答えは正しいものだと思います。行列の乗算は驚異的並列ではありません。そして、あなたはそれを並列化するためにシリアル乗算コードを変更したようには見えません。

代わりに、2つの行列を乗算しています。各行列の乗算は単一のコアによってのみ処理される可能性が高いため、2を超えるすべてのコアは単にアイドルオーバーヘッドです。 結果として、2倍の速度向上しか見られません。

これは、3つ以上の行列乗算を実行することでテストできます。しかし、私はforeachdoParallelフレームワーク(私はparallelフレームワークを使用しています)に精通しておらず、テストのためにこれを変更するコードの場所もわかりません。

別のテストは、行列乗算の並列化バージョンを実行することです。これは、マトロフの データサイエンスの並列コンピューティング から直接借用しています。ドラフトが利用可能 ここ 、27ページを参照

mmulthread <- function(u, v, w) {
  require(parallel)
  # determine which rows for this thread
  myidxs <- splitIndices(nrow(u), myinfo$nwrkrs ) [[ myinfo$id ]]
  # compute this thread's portion of the result
  w[myidxs, ] <- u [myidxs, ] %*% v [ , ]
  0 # dont return result -- expensive
}
# t e s t on snow c l u s t e r c l s
test <- function (cls,  n = 2^5) {
  # i n i t Rdsm
  mgrinit(cls)
  # shared variables
  mgrmakevar(cls, "a", n, n)
  mgrmakevar(cls, "b", n, n)
  mgrmakevar(cls, "c", n, n)
  # f i l l i n some t e s t data
  a [ , ] <- 1:n
  b [ , ] <- rep (1 ,n)

  # export function
  clusterExport(cls , "mmulthread" )
  # run function
  clusterEvalQ(cls , mmulthread (a ,b ,c ))
  #print ( c[ , ] ) # not p ri n t ( c ) !
}


library(parallel)
library(Rdsm)

c1 <- makeCluster(1)
c2 <- makeCluster (2)
c4 <- makeCluster(4)
c8 <- makeCluster(8)

library(microbenchmark)

microbenchmark(node1= test(c1, n= 2^10),
           node2= test(c2, n= 2^10),
           node4= test(c4, n= 2^10),
           node8= test(c8, n= 2^10))



 Unit: milliseconds
  expr      min       lq     mean   median       uq      max neval  cld
 node1 715.8722 780.9861 818.0487 817.6826 847.5353 922.9746   100    d
 node2 404.9928 422.9330 450.9016 437.5942 458.9213 589.1708   100   c 
 node4 255.3105 285.8409 309.5924 303.6403 320.8424 481.6833   100 a   
 node8 304.6386 328.6318 365.5114 343.0939 373.8573 836.2771   100  b  

予想どおり、行列の乗算を並列化することで、並列オーバーヘッドが明らかに広範囲に及ぶものの、必要な支出の改善が見られます。

3
Alex W

私はそれがすでにここで答えられたと思いますか? foreach%dopar%はforループよりも遅い

同じ応答を少し外挿したことを意味します。したがって、基本的に概念は一定のままです。

これは、プロセスが並行して(順次または順不同で)発生する別の例です。答えは組み合わされることを期待していません。同じ変数を破棄することにより、同じ結果を単純に再利用します。 (単純なforループとまったく同じです)。しかし、それでもdoParallelではループが遅くなります foreach()%do%がforよりも遅いことがあるのはなぜですか?

0
Mandar