为什么每增加一个节点 foreach %dopar% 就会变慢?
Why does foreach %dopar% get slower with each additional node?
我写了一个简单的矩阵乘法来测试我的网络的 multithreading/parallelization 能力,我注意到计算比预期的要慢得多。
测试 很简单:乘以 2 个矩阵 (4096x4096) 和 return 计算时间。既不存储矩阵也不存储结果。计算时间并非微不足道(50-90 秒,具体取决于您的处理器)。
条件:我使用 1 个处理器重复此计算 10 次,将这 10 次计算分成 2 个处理器(每个 5 个),然后是 3 个处理器,...最多 10处理器(对每个处理器进行 1 次计算)。我预计总计算时间会分阶段减少,我预计 10 个处理器完成计算 10 次 的速度与一个处理器完成相同的速度一样快。
结果:相反,我得到的只是计算时间减少了 2 倍,比预期的 SLOWER 减少了 5 倍。
当我计算每个节点的平均计算时间时,我希望每个处理器在相同的时间内(平均)计算测试,而不管分配的处理器数量如何。我很惊讶地发现仅仅将相同的操作发送到多个处理器会减慢每个处理器的平均计算时间。
谁能解释为什么会这样?
注意这是问题不是这些问题的重复:
foreach %dopar% slower than for loop
或
Why is the parallel package slower than just using apply?
因为测试计算不是微不足道的(即 50-90 秒而不是 1-2 秒),并且因为我可以看到处理器之间没有通信(即除了 return 之外没有其他结果被编辑或存储计算时间)。
我附上了下面用于复制的脚本和函数。
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 的矩阵乘法(或尽可能快地执行)。得到与之前非常相似的结果(回复:每个额外的进程都会减慢所有单独的会话)。请注意,我使用 top
和 htop
检查了内存使用情况,并且使用情况从未超过网络容量的 5% (~2.5/64Gb)。
结论:
这个问题似乎是 R 特有的。当我用其他软件运行其他多线程命令时(例如PLINK) i don't run into this problem and parallel process run as expected. I have also tried running the above with Rmpi
and doMPI
with same (slower) results. The problem appears to be related R
sessions/parallelized commands on virtual machine network. What i really need help on is how to pinpoint the problem. Similar problem seems to be pointed out here
我发现 per-node 乘法时间非常有趣,因为计时不包括任何与并行循环相关的开销,而只包括执行矩阵乘法的时间,并且它们表明时间随着在同一台机器上并行执行的矩阵乘法的数量增加。
我能想到可能发生这种情况的两个原因:
- 在 运行 超出核心之前,机器的内存带宽已被矩阵乘法饱和;
- 矩阵乘法为multi-threaded。
您可以通过启动多个 R 会话(我在多个终端中这样做)来测试第一种情况,在每个会话中创建两个矩阵:
> x <- matrix(rnorm(4096*4096), 4096)
> y <- matrix(rnorm(4096*4096), 4096)
然后在每个会话中大约同时执行矩阵乘法:
> system.time(z <- x %*% t(y))
理想情况下,无论您使用多少个 R 会话(最多核心数),这个时间都是相同的,但由于矩阵乘法是一个内存密集型操作,许多机器将 运行在 运行 超出内核之前内存带宽不足,导致时间增加。
如果您的 R 安装是使用 multi-threaded 数学库构建的,例如 MKL 或 ATLAS,那么您可以将所有内核都用于单个矩阵乘法,因此您不能期望更好的性能除非您使用多台计算机,否则使用多个进程。
您可以使用 "top" 等工具查看您是否使用 multi-threaded 数学库。
最后,lscpu
的输出表明您正在使用虚拟机。我从未在 multi-core 虚拟机上做过任何性能测试,但这也可能是问题的根源。
更新
我认为你的并行矩阵乘法 运行 比单个矩阵乘法慢的原因是你的 CPU 无法足够快地读取内存来满足两个以上的核心全速运行,我称之为使内存带宽饱和。如果你的 CPU 有足够大的缓存,你也许可以避免这个问题,但它与你主板上的内存量没有任何关系。
我认为这只是使用单台计算机进行并行计算的局限性。使用集群的优势之一是您的内存带宽以及总聚合内存都会增加。因此,如果您 运行 在 multi-node 并行程序的每个节点上进行一次或两次矩阵乘法运算,您就不会 运行 遇到这个特定问题。
假设您无权访问集群,您可以尝试在计算机上对 multi-threaded 数学库(例如 MKL 或 ATLAS)进行基准测试。 运行宁一个 multi-threaded 矩阵乘法可能比 运行 在多个进程中并行 运行 得到更好的性能。但同时使用 multi-threaded 数学库和并行编程包时要小心。
您也可以尝试使用 GPU。他们显然擅长执行矩阵乘法。
更新 2
要查看问题是否特定于 R,我建议您对 dgemm
函数进行基准测试,该函数是 R 用于实现矩阵乘法的 BLAS 函数。
这是一个简单的 Fort运行 程序,用于基准测试 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 中看到的结果一致,并且我猜测这是一个内存带宽问题。
您还可以 link 针对不同的 BLAS 库执行此操作,以查看哪种实现在您的机器上效果更好。
您可能还会使用 pmbw - Parallel Memory Bandwidth Benchmark 获得有关虚拟机网络内存带宽的一些有用信息,尽管我从未使用过它。
我认为这里显而易见的答案是正确的。矩阵乘法不是令人尴尬的并行。而且您似乎没有修改串行乘法代码以将其并行化。
相反,您将两个矩阵相乘。由于每个矩阵的乘法可能仅由一个内核处理,因此超过两个的每个内核都只是空闲开销。 结果是您只看到速度提高了 2 倍。
您可以通过 运行 2 次以上的矩阵乘法来测试它。但是我不熟悉 foreach
、doParallel
框架(我使用 parallel
框架),也没有看到您的代码中的哪里可以修改它来测试它。
另一种测试是执行矩阵乘法的并行化版本,我直接从 Matloff 那里借来的 Parallel Computing for Data Science. Draft available 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
正如预期的那样,通过并行化矩阵乘法,我们确实看到了我们想要的支出改进,尽管并行开销显然很大。
我写了一个简单的矩阵乘法来测试我的网络的 multithreading/parallelization 能力,我注意到计算比预期的要慢得多。
测试 很简单:乘以 2 个矩阵 (4096x4096) 和 return 计算时间。既不存储矩阵也不存储结果。计算时间并非微不足道(50-90 秒,具体取决于您的处理器)。
条件:我使用 1 个处理器重复此计算 10 次,将这 10 次计算分成 2 个处理器(每个 5 个),然后是 3 个处理器,...最多 10处理器(对每个处理器进行 1 次计算)。我预计总计算时间会分阶段减少,我预计 10 个处理器完成计算 10 次 的速度与一个处理器完成相同的速度一样快。
结果:相反,我得到的只是计算时间减少了 2 倍,比预期的 SLOWER 减少了 5 倍。
当我计算每个节点的平均计算时间时,我希望每个处理器在相同的时间内(平均)计算测试,而不管分配的处理器数量如何。我很惊讶地发现仅仅将相同的操作发送到多个处理器会减慢每个处理器的平均计算时间。
谁能解释为什么会这样?
注意这是问题不是这些问题的重复:
foreach %dopar% slower than for loop
或
Why is the parallel package slower than just using apply?
因为测试计算不是微不足道的(即 50-90 秒而不是 1-2 秒),并且因为我可以看到处理器之间没有通信(即除了 return 之外没有其他结果被编辑或存储计算时间)。
我附上了下面用于复制的脚本和函数。
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 的矩阵乘法(或尽可能快地执行)。得到与之前非常相似的结果(回复:每个额外的进程都会减慢所有单独的会话)。请注意,我使用 top
和 htop
检查了内存使用情况,并且使用情况从未超过网络容量的 5% (~2.5/64Gb)。
结论:
这个问题似乎是 R 特有的。当我用其他软件运行其他多线程命令时(例如PLINK) i don't run into this problem and parallel process run as expected. I have also tried running the above with Rmpi
and doMPI
with same (slower) results. The problem appears to be related R
sessions/parallelized commands on virtual machine network. What i really need help on is how to pinpoint the problem. Similar problem seems to be pointed out here
我发现 per-node 乘法时间非常有趣,因为计时不包括任何与并行循环相关的开销,而只包括执行矩阵乘法的时间,并且它们表明时间随着在同一台机器上并行执行的矩阵乘法的数量增加。
我能想到可能发生这种情况的两个原因:
- 在 运行 超出核心之前,机器的内存带宽已被矩阵乘法饱和;
- 矩阵乘法为multi-threaded。
您可以通过启动多个 R 会话(我在多个终端中这样做)来测试第一种情况,在每个会话中创建两个矩阵:
> x <- matrix(rnorm(4096*4096), 4096)
> y <- matrix(rnorm(4096*4096), 4096)
然后在每个会话中大约同时执行矩阵乘法:
> system.time(z <- x %*% t(y))
理想情况下,无论您使用多少个 R 会话(最多核心数),这个时间都是相同的,但由于矩阵乘法是一个内存密集型操作,许多机器将 运行在 运行 超出内核之前内存带宽不足,导致时间增加。
如果您的 R 安装是使用 multi-threaded 数学库构建的,例如 MKL 或 ATLAS,那么您可以将所有内核都用于单个矩阵乘法,因此您不能期望更好的性能除非您使用多台计算机,否则使用多个进程。
您可以使用 "top" 等工具查看您是否使用 multi-threaded 数学库。
最后,lscpu
的输出表明您正在使用虚拟机。我从未在 multi-core 虚拟机上做过任何性能测试,但这也可能是问题的根源。
更新
我认为你的并行矩阵乘法 运行 比单个矩阵乘法慢的原因是你的 CPU 无法足够快地读取内存来满足两个以上的核心全速运行,我称之为使内存带宽饱和。如果你的 CPU 有足够大的缓存,你也许可以避免这个问题,但它与你主板上的内存量没有任何关系。
我认为这只是使用单台计算机进行并行计算的局限性。使用集群的优势之一是您的内存带宽以及总聚合内存都会增加。因此,如果您 运行 在 multi-node 并行程序的每个节点上进行一次或两次矩阵乘法运算,您就不会 运行 遇到这个特定问题。
假设您无权访问集群,您可以尝试在计算机上对 multi-threaded 数学库(例如 MKL 或 ATLAS)进行基准测试。 运行宁一个 multi-threaded 矩阵乘法可能比 运行 在多个进程中并行 运行 得到更好的性能。但同时使用 multi-threaded 数学库和并行编程包时要小心。
您也可以尝试使用 GPU。他们显然擅长执行矩阵乘法。
更新 2
要查看问题是否特定于 R,我建议您对 dgemm
函数进行基准测试,该函数是 R 用于实现矩阵乘法的 BLAS 函数。
这是一个简单的 Fort运行 程序,用于基准测试 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 中看到的结果一致,并且我猜测这是一个内存带宽问题。
您还可以 link 针对不同的 BLAS 库执行此操作,以查看哪种实现在您的机器上效果更好。
您可能还会使用 pmbw - Parallel Memory Bandwidth Benchmark 获得有关虚拟机网络内存带宽的一些有用信息,尽管我从未使用过它。
我认为这里显而易见的答案是正确的。矩阵乘法不是令人尴尬的并行。而且您似乎没有修改串行乘法代码以将其并行化。
相反,您将两个矩阵相乘。由于每个矩阵的乘法可能仅由一个内核处理,因此超过两个的每个内核都只是空闲开销。 结果是您只看到速度提高了 2 倍。
您可以通过 运行 2 次以上的矩阵乘法来测试它。但是我不熟悉 foreach
、doParallel
框架(我使用 parallel
框架),也没有看到您的代码中的哪里可以修改它来测试它。
另一种测试是执行矩阵乘法的并行化版本,我直接从 Matloff 那里借来的 Parallel Computing for Data Science. Draft available 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
正如预期的那样,通过并行化矩阵乘法,我们确实看到了我们想要的支出改进,尽管并行开销显然很大。