为啥 foreach %dopar% 每增加一个节点就会变慢?

Posted

技术标签:

【中文标题】为啥 foreach %dopar% 每增加一个节点就会变慢?【英文标题】:Why does foreach %dopar% get slower with each additional node?为什么 foreach %dopar% 每增加一个节点就会变慢? 【发布时间】:2017-06-15 00:11:48 【问题描述】:

我编写了一个简单的矩阵乘法来测试我的网络的多线程/并行化能力,我注意到计算速度比预期的要慢得多。

测试很简单:将 2 个矩阵 (4096x4096) 相乘并返回计算时间。既不存储矩阵也不存储结果。计算时间并非微不足道(50-90 秒,具体取决于您的处理器)。

条件:我使用 1 个处理器重复此计算 10 次,将这 10 个计算分成 2 个处理器(每个 5 个),然后是 3 个处理器,...最多 10 个处理器(1 个计算每个处理器)。我预计总计算时间会逐步减少,并且我预计 10 个处理器完成计算的速度10 倍是一个处理器完成相同操作所需的速度。

结果:相反,我得到的只是计算时间减少了 2 倍,是预期的 5 倍 SLOWER

当我计算每个节点的平均计算时间时,我希望每个处理器在相同的时间内(平均)计算测试,而不管分配的处理器数量如何。我惊讶地发现,仅仅将相同的操作发送到多个处理器就会减慢每个处理器的平均计算时间。

谁能解释为什么会这样?

请注意,这个问题不是这些问题的重复:

foreach %dopar% slower than for loop

Why is the parallel package slower than just using apply?

因为测试计算不是微不足道的(即 50-90 秒而不是 1-2 秒),并且因为我可以看到处理器之间没有通信(即除了计算时间之外没有返回或存储结果)。

我已附上下面的脚本和函数以供复制。

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()

编辑:回复@Hong Ooi 的评论

我在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

编辑:回复@Steve Weston 的评论。

我使用的虚拟机网络(但我不是管理员)最多可以访问 30 个集群。我进行了您建议的测试。打开 5 个 R 会话并同时在 1,2...5 上运行矩阵乘法(或者尽可能快地切换并执行)。得到与以前非常相似的结果(re:每个额外的过程都会减慢所有单独的会话)。请注意,我使用 tophtop 检查了内存使用情况,使用情况从未超过网络容量的 5% (~2.5/64Gb)。

结论:

这个问题似乎是 R 特有的。当我使用其他软件(例如PLINK)运行其他多线程命令时,我不会遇到这个问题并且并行进程按预期运行。我还尝试使用RmpidoMPI 运行上述程序,结果相同(较慢)。该问题似乎与虚拟机网络上的R 会话/并行命令有关。我真正需要帮助的是如何查明问题。类似问题似乎被指出here

【问题讨论】:

你用的是什么硬件? 在底部添加了lscpu 的结果。这有帮助吗? 实际上,您正在分析 R 环境中 GEMM 的强大可扩展性。我建议你分别从计算效率和通信成本来分析问题。 谢谢@Patric,您能否详细说明 GEMM 是什么,并可能提供一些您喜欢的资源,以便我进一步探索 您似乎在一台 UNIX 机器上。如果您使用fork 集群而不是psock 集群(默认)会发生什么?它应该减少 I/O 开销...您的打印输出也有非常小的 L1 和 L2 缓存。您可能正在经历大量的缓存未命中,这极大地减慢了速度。我很确定缓存未命中会随着并行化而增加(尽管我必须检查一下)。 【参考方案1】:

我发现每个节点的乘法时间非常有趣,因为时间不包括与并行循环相关的任何开销,而只包括执行矩阵乘法的时间,并且它们表明时间随着数量的增加而增加在同一台机器上并行执行的矩阵乘法。

我能想到可能发生这种情况的两个原因:

    在您用完内核之前,机器的内存带宽已被矩阵乘法饱和; 矩阵乘法是多线程的。

您可以通过启动多个 R 会话(我在多个终端中执行此操作)来测试第一种情况,在每个会话中创建两个矩阵:

> 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 无法以足够快的速度读取内存以全速提供超过两个内核,我称之为饱和你的内存带宽。如果你的 CPU 有足够大的缓存,你也许可以避免这个问题,但它与你主板上的内存量没有任何关系。

我认为这只是使用单台计算机进行并行计算的限制。使用集群的优点之一是您的内存带宽以及您的总聚合内存都会增加。因此,如果您在多节点并行程序的每个节点上运行一个或两个矩阵乘法,就不会遇到这个特殊问题。

假设您无权访问集群,您可以尝试在您的计算机上对多线程数学库(例如 MKL 或 ATLAS)进行基准测试。与在多个进程中并行运行它们相比,运行一个多线程矩阵乘法很有可能获得更好的性能。但同时使用多线程数学库和并行编程包时要小心。

您也可以尝试使用 GPU。他们显然擅长执行矩阵乘法。


更新 2

要查看问题是否特定于 R,我建议您对 dgemm 函数进行基准测试,这是 R 用于实现矩阵乘法的 BLAS 函数。

这是一个简单的 Fortran 程序,用于对 dgemm 进行基准测试。我建议以与我在 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 机器上,一个实例在 82 秒内运行,而四个实例在 116 秒内运行。这与我在 R 中看到的结果一致,并且我猜测这是内存带宽问题。

您还可以将其链接到不同的 BLAS 库,以查看哪种实现在您的计算机上效果更好。

您还可以使用pmbw - Parallel Memory Bandwidth Benchmark 获得有关虚拟机网络内存带宽的一些有用信息,尽管我从未使用过。

【讨论】:

所以我已经编辑了上面的问题,我认为你的测试建议达到了目标。运行多个(最多 5 个)R 会话导致每次乘法速度减慢与单个会话 %dopar% 相同。我也相信这不是内存问题,因为我离内存上限还差得很远(使用率为 5%)。 我使用的 R 版本是 R version 3.3.2 (2016-10-31) -- "Sincere Pumpkin Patch",我在检查 R.Version() 时没有看到任何特殊的数学库。您会建议我使用多线程版本重新安装吗? 另外我相信你已经回答了我的问题,但我的问题仍然存在,你会建议我结束这个问题并开始一个新的问题吗? 从计算效率的角度来看,这是一个很好的分析,我推荐 CPU 上的 Intel MKL,它为可扩展性提供了出色的性能。 here 中的一个例子 问题是我在集群上运行,这是我只在使用 R 时遇到的问题。当我使用其他软件包运行多线程命令时,我不会遇到这种大幅减速(例如 PLINK cog-genomics.org/plink2 )。【参考方案2】:

我认为这里显而易见的答案是正确的。矩阵乘法并不是令人尴尬的并行。而且您似乎没有修改串行乘法代码以使其并行化。

相反,您将两个矩阵相乘。由于每个矩阵的乘法可能仅由单个内核处理,因此每个超过两个的内核都只是空闲开销。 结果是您只看到了 2 倍的速度提升。

您可以通过运行 2 个以上的矩阵乘法来测试这一点。但我不熟悉foreachdoParallel 框架(我使用parallel 框架),我也看不到你的代码在哪里修改它来测试它。

另一种测试是执行矩阵乘法的并行化版本,我直接从 Matloff 的 Parallel Computing for Data Science 中借用它。草稿可用here,见第 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  

正如预期的那样,通过并行化矩阵乘法,我们确实看到了我们想要的支出改进,尽管并行开销显然很大。

【讨论】:

谢谢,我会尽快测试这个脚本。作为对您答案的上半部分的回应,当我打开单独的 R 会话并在每个会话中运行非并行矩阵乘法时,问题是如何持续存在的(另外,请参见我帖子中的最后一个图,这是对 @Patric 好的,我已经更新了我的帖子。基本上,我是对的……我认为 @JustGettinStarted LMK 如果这是您要找的...我认为您对多个连续会话的评论证实了我的观点,而不是反驳它。 会的,集群目前已被预订,但我会尽快测试(如果在此之前到期,则更新赏金)

以上是关于为啥 foreach %dopar% 每增加一个节点就会变慢?的主要内容,如果未能解决你的问题,请参考以下文章

foreach %dopar% 使用带有 PSock 集群的顺序工作程序设置?

如何以编程方式在 foreach 中的 %do% 和 %dopar% 之间切换?

如何在 R 中将“foreach”和“%dopar%”与“R6”类一起使用?

R:如何在 foreach %dopar% 中拆分数据帧

R:在 foreach %dopar% 中显示错误和警告消息

使用 foreach 进行并行处理时出错:“找不到函数“%dopar%””