这有点复杂,所以我认为分享我正在使用的确切代码并不值得,但我应该能够使用伪代码很好地理解这一点:
背景知识: 本质上,我正在尝试在嵌套的操作循环上进行并行计算。 我有两个大型函数,第一个需要运行并返回TRUE才能运行第二个函数,如果第二个函数运行,则需要循环进行多次迭代。 现在这是一个嵌套循环,因为在各种情况下,我需要多次运行上述整个操作。 我尝试使用的伪代码如下:
Output <- foreach(1 to “I”, .packages=packages, .combine=rbind) %:%
Run the first function
If the first function is false:
Print and record
Else:
Foreach(1 to J, .packages=packages, .combine=rbind) %dopar%{
Run the second function
Create df summarizing each loop of second function
}
这是我要执行的操作以及遇到的错误的简化版本:
library(doParallel)
library(foreach)
func1 <- function(int1){
results <- list(int1,TRUE)
return(results)
}
func2 <- function(int2){
return(int1/int2)
}
int1list <- seq(1,10)
int2list <- seq(1,15)
out <- foreach(i=1:length(int1list),.combine=rbind) %:%
out1 <- func1(i)
if(out1[[2]]==FALSE){
print("fail")
next
} else{
foreach(j=1:length(int2),.combine=rbind) %dopar% {
int3 <- func2(j)
data.frame("Scenario"=i,"Result"=int3)
}
}
错误:func1(i)错误:未找到对象'i'
当我执行上述操作时,它实际上告诉我甚至找不到对象“ I”,我认为发生这种情况是因为我正在最内层循环之外运行调用“ I”的事物。我以前可以嵌套并行化的循环工作,但是我没有最里面的循环之外需要运行的任何东西,所以我假设这是软件包的问题,不知道在其中执行操作的顺序。 / p>
我有一个解决方法,我可以只并行运行第一个函数,然后根据第一个循环(实际上是两个单独的循环而不是嵌套循环)的结果并行运行第二个函数,但是我想知道是否有一种方法可以使类似嵌套循环的东西正常工作,因为我认为这样会更有效。在生产中运行时,此代码可能需要花费几个小时才能运行,因此节省一些时间是值得的。
答案 0 :(得分:0)
我不是foreach
的专业人士,但是有一些突出的表现:
func2
引用了int1
和int2
,但只给了后者。这可能只是您的简化示例的产物,也许不是吗?您的代码需要用大括号括起来,即,您需要更改为
out <- foreach(i=1:length(int1list),.combine=rbind) %:%
out1 <- func1(i)
if(out1[[2]]==FALSE) ...
到
out <- foreach(i=1:length(int1list),.combine=rbind) %:% {
out1 <- func1(i)
if(out1[[2]]==FALSE) ...
}
foreach
的文档建议二进制运算符%:%
是一个嵌套运算符,可在两个foreach
调用之间使用,但您没有这样做。我认为我可以使其与%do%
(或%dopar%
)一起正常工作print
在并行foreach
循环内无法正常工作……可能在主节点上却无法在所有其他节点上找到,参考:How can I print when using %dopar% int1list
的内容(只是其长度),在此示例中我将进行补救next
在“普通” R循环中工作,而不是在这些专门的foreach
循环中工作;不过,这不是问题,因为您的if
/ else
结构提供了相同的效果这是您的示例,略作修改以说明上述所有情况。我添加UsedJ
来表示
library(doParallel)
library(foreach)
func1 <- function(int1){
results <- list(int1,int1>2)
return(results)
}
func2 <- function(int1,int2){
return(int1/int2)
}
int1list <- seq(1,3)
int2list <- seq(1,5)
out <- foreach(i=1:length(int1list),.combine=rbind) %do% {
out1 <- func1(int1list[i])
if(!out1[[2]]){
data.frame("Scenario"=i, "Result"=out1[[1]], UsedJ=FALSE)
# next
} else{
foreach(j=1:length(int2list),.combine=rbind) %dopar% {
int3 <- func2(out1[[1]], int2list[j])
data.frame("Scenario"=i,"Result"=int3, UsedJ=TRUE)
}
}
}
out
# Scenario Result UsedJ
# 1 1 1.00 FALSE
# 2 2 2.00 FALSE
# 3 3 3.00 TRUE
# 4 3 1.50 TRUE
# 5 3 1.00 TRUE
# 6 3 0.75 TRUE
# 7 3 0.60 TRUE
修改
如果您没有看到并行化,则可能是因为您尚未设置“集群”。基于foreach
使用%:%
运算符的嵌套循环方法,工作流程还进行了一些其他更改,以使其很好地并行化。
为了“证明”这是并行进行的,我添加了一些基于How can I print when using %dopar%的日志记录(因为并行进程并不像人们希望的那样print
)。
library(doParallel)
library(foreach)
Log <- function(text, ..., .port = 4000, .sock = make.socket(port=.port)) {
msg <- sprintf(paste0(as.character(Sys.time()), ": ", text, "\n"), ...)
write.socket(.sock, msg)
close.socket(.sock)
}
func1 <- function(int1) {
Log(paste("func1", int1))
Sys.sleep(5)
results <- list(int1, int1 > 2)
return(results)
}
func2 <- function(int1, int2) {
Log(paste("func2", int1, int2))
Sys.sleep(1)
return(int1 / int2)
}
使用记录代码需要从外部读取该套接字的方法。我在这里将netcat(nc
或Nmap的ncat
与ncat -k -l 4000
一起使用。这项工作当然不是必需的,但是在这里方便地了解事情的进展。 (注意:在您尝试使用Log
之前,此监听器/服务器需要先运行 。)
我无法获取嵌套的“ foreach
-> func1
-> foreach
-> func2
”以正确地并行化func2
。根据睡眠情况,对func1
的三个调用将花费5秒,对func2
的五个调用将花费2秒(两个批次,每个批次三个),但是需要10秒(三个并行)调用func1
,然后依次调用五个func2
):
system.time(
out <- foreach(i=1:length(int1list), .combine=rbind, .packages="foreach") %dopar% {
out1 <- func1(int1list[i])
if (!out1[[2]]) {
data.frame(Scenario=i, Result=out1[[1]], UsedJ=FALSE)
} else {
foreach(j=1:length(int2list), .combine=rbind) %dopar% {
int3 <- func2(out1[[1]], int2list[j])
data.frame(Scenario=i, Result=int3, UsedJ=TRUE)
}
}
}
)
# user system elapsed
# 0.02 0.00 10.09
以及相应的控制台输出:
2018-11-12 11:51:17: func1 2
2018-11-12 11:51:17: func1 1
2018-11-12 11:51:17: func1 3
2018-11-12 11:51:23: func2 3 1
2018-11-12 11:51:24: func2 3 2
2018-11-12 11:51:25: func2 3 3
2018-11-12 11:51:26: func2 3 4
2018-11-12 11:51:27: func2 3 5
(请注意,不能保证顺序。)
因此我们可以先将其分解为计算func1
的内容:
system.time(
out1 <- foreach(i = seq_along(int1list)) %dopar% {
func1(int1list[i])
}
)
# user system elapsed
# 0.02 0.01 5.03
str(out1)
# List of 3
# $ :List of 2
# ..$ : int 1
# ..$ : logi FALSE
# $ :List of 2
# ..$ : int 2
# ..$ : logi FALSE
# $ :List of 2
# ..$ : int 3
# ..$ : logi TRUE
控制台:
2018-11-12 11:53:21: func1 2
2018-11-12 11:53:21: func1 1
2018-11-12 11:53:21: func1 3
然后处理func2
的内容:
system.time(
out2 <- foreach(i = seq_along(int1list), .combine="rbind") %:%
foreach(j = seq_along(int2list), .combine="rbind") %dopar% {
Log(paste("preparing", i, j))
if (out1[[i]][[2]]) {
int3 <- func2(out1[[i]][[1]], j)
data.frame(i=i, j=j, Result=int3, UsedJ=FALSE)
} else if (j == 1L) {
data.frame(i=i, j=NA_integer_, Result=out1[[i]][[1]], UsedJ=FALSE)
}
}
)
# user system elapsed
# 0.03 0.00 2.05
out2
# i j Result UsedJ
# 1 1 NA 1.00 FALSE
# 2 2 NA 2.00 FALSE
# 3 3 1 3.00 FALSE
# 4 3 2 1.50 FALSE
# 5 3 3 1.00 FALSE
# 6 3 4 0.75 FALSE
# 7 3 5 0.60 FALSE
两秒钟(第一批三是1秒,第二批两是1秒)是我所期望的。控制台:
2018-11-12 11:54:01: preparing 1 2
2018-11-12 11:54:01: preparing 1 3
2018-11-12 11:54:01: preparing 1 1
2018-11-12 11:54:01: preparing 1 4
2018-11-12 11:54:01: preparing 1 5
2018-11-12 11:54:01: preparing 2 1
2018-11-12 11:54:01: preparing 2 2
2018-11-12 11:54:01: preparing 2 3
2018-11-12 11:54:01: preparing 2 4
2018-11-12 11:54:01: preparing 2 5
2018-11-12 11:54:01: preparing 3 1
2018-11-12 11:54:01: preparing 3 2
2018-11-12 11:54:01: func2 3 1
2018-11-12 11:54:01: preparing 3 3
2018-11-12 11:54:01: func2 3 2
2018-11-12 11:54:01: func2 3 3
2018-11-12 11:54:02: preparing 3 4
2018-11-12 11:54:02: preparing 3 5
2018-11-12 11:54:02: func2 3 4
2018-11-12 11:54:02: func2 3 5
您可以看到func2
被正确调用了五次。不幸的是,您看到循环内部存在许多“旋转”。当然,它实际上是一个无操作(如2.05秒运行时所证明),因此节点上的负载可以忽略不计。
如果有人有办法避免这种不必要的旋转,我欢迎您提出评论或“竞争”答案。
答案 1 :(得分:0)
我感谢r2evans所提供的帮助,尽管由于我的经验不足和无法弄清楚如何使ncat在我的计算机上工作,我实际上无法复制他的工作,但他帮助我意识到我的原始方法不会既可以工作,又可以分成两个单独的foreach并行化循环,在这一点上,我已经将其转换为工作版本。
这是最初提出的解决方案:
library(doParallel)
library(foreach)
cl <- makeCluster(detectCores())
registerDoParallel(cl)
func1 <- function(int1){
results <- list(int1,int1>2)
return(results)
}
func2 <- function(int1,int2){
return(int1/int2)
}
int1list <- seq(1,3)
int2list <- seq(1,5)
out <- foreach(i=1:length(int1list),.combine=rbind) %do% {
out1 <- func1(int1list[i])
if(!out1[[2]]){
data.frame("Scenario"=i, "Result"=out1[[1]], UsedJ=FALSE)
# next
} else{
foreach(j=1:length(int2list),.combine=rbind) %dopar% {
int3 <- func2(out1[[1]], int2list[j])
data.frame("Scenario"=i,"Result"=int3, UsedJ=TRUE)
}
}
}
stopCluster(cl)
registerDoSEQ()
out
但是,这将导致一个循环,该循环等待func1的func2迭代的第一次迭代完成,然后再开始func1的第二次迭代。我选择将其分为两个单独的循环,如下所示:
library(doParallel)
library(foreach)
cl <- makeCluster(detectCores())
registerDoParallel(cl)
func1 <- function(int1){
results <- list(int1,int1>2)
return(results)
}
func2 <- function(int1,int2){
return(int1/int2)
}
int1list <- seq(1,3)
int2list <- seq(1,5)
out1 <- foreach(i=1:length(int1list)) %dopar%{
func1(i)
}
finalOut <- data.frame("Scenario"=integer(),"UsedJ"=logical(),"Result"=double())
for (i in 1:length(int1list)){
if(out1[[2]]==FALSE){
tempOut <- data.frame("Scenario"=i,"UsedJ"=FALSE,"Result"=NA)
} else{
tempOutput <- foreach(j=1:length(int2list),.combine=rbind) %dopar% {
Result <- func2(i,j)
data.frame("Scenario"=i,"UsedJ"=TRUE,"Result"=Result)
}
}
}
stopCluster(cl)
registerDoSEQ()
finalOut
这个算法似乎很符合我的目的。它虽然效率不高,但是应该可以完成工作并且不要太浪费。