r - 为什么foreach%dopar%随每个其他节点变慢?

标签 r multithreading foreach parallel-processing doparallel

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

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

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

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

enter image description here

当我计算每个节点的平均计算时间时,无论分配多少处理器,我都希望每个处理器在相同的时间(平均)内计算测试。我惊讶地发现,仅向多个处理器发送相同的操作就会减慢每个处理器的平均计算时间。

enter image description here

谁能解释为什么会这样?

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

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 session ,并同时在1,2 ... 5上运行矩阵乘法(或尽可能快地切换并执行)。得到了与之前非常相似的结果(例如:每个附加过程都会减慢所有单独的 session 的速度)。注意我使用tophtop检查了内存使用情况,使用情况从未超过网络容量的5%(〜2.5/64Gb)。

enter image description here

结论:

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

最佳答案

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

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

  • 在内核用完之前,计算机的内存带宽被矩阵乘法饱和。
  • 矩阵乘法是多线程的。

  • 您可以通过启动多个R session 来测试第一种情况(我在多个终端中进行了此操作),并在每个 session 中创建两个矩阵:
    > x <- matrix(rnorm(4096*4096), 4096)
    > y <- matrix(rnorm(4096*4096), 4096)
    

    然后大约在同一时间在每个 session 中执行矩阵乘法:
    > system.time(z <- x %*% t(y))
    

    理想情况下,无论您使用多少R session (最多内核数),此时间都应该相同,但是由于矩阵乘法是相当耗费内存的操作,因此许多计算机在耗尽内存之前将耗尽内存带宽。核心,导致时间增加。

    如果您的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获得有关虚拟机网络的内存带宽的一些有用信息,尽管我从未使用过。

    关于r - 为什么foreach%dopar%随每个其他节点变慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41925706/

    相关文章:

    PHP Mailer - 邮件正文中的 foreach

    javascript - 将 toJson R 对象转换为适合 d3.js 树布局的格式

    r - 错误: package 'lsei' is not installed for 'arch=x64'

    r - 绘制两个不同的数据框,按一个或多个变量分组,在图例中使用不同的标签

    r - fwrite.data.table 和 `yyyy-mm-dd hh:mm:ss` 格式优化,具有固定的 UTC 偏移量

    java - 适用于 Android 的多线程 Web 服务器

    C#线程池限制线程

    excel - 每个循环的 VBA Excel

    c - OSX 命令行 C 应用程序中的线程管理

    powershell - 防止在 Powershell 脚本中多次写入输出