在源脚本中使用parLapply会导致内存泄漏

时间:2019-01-29 22:17:49

标签: r parallel-processing

这可能是一个适合哲学家的...(或@Steve Weston或@Martin Morgan)

在使用parLapply时,我一直遇到内存泄漏的问题,并且在研究了足够多的线程之后,我认为这个问题很合理。我花了一些时间来尝试解决这个问题,尽管我对所观察到的行为为何会发生什么有了一个线索,但我却不知道如何解决它。

将以下内容视为源脚本,另存为:parallel_question.R

rf.parallel<-function(n=10){
  library(parallel)
  library(randomForest)

  rf.form<- as.formula(paste("Final", paste(c('x','y','z'), collapse = "+"), sep = " ~ "))

  rf.df<-data.frame(Final=runif(10000),y=runif(10000),x=runif(10000),z=runif(10000))

  rf.df.list<-split(rf.df,rep(1:n,nrow(rf.df))[1:nrow(rf.df)])

  cl<-makeCluster(n)
  rf.list<-parLapply(cl,rf.df.list,function(x,rf.form,n){
    randomForest::randomForest(rf.form,x,ntree=100,nodesize=10, norm.votes=FALSE)},rf.form,n)
  stopCluster(cl)

  return(rf.list)
  }

我们通过以下方式获取并运行脚本:

scrip.loc<-"G:\\Scripts_Library\\R\\Stack_Answers\\parallel_question.R"

source(scrip.loc)

rf.parallel(n=10)

公平直截了当地...我们并行运行了几个随机森林。似乎可以提高内存效率。我们可以稍后合并它们,或者做其他事情。便利。真好表现得很好。

现在考虑以下脚本,另存为parallel_question_2.R

rf.parallel_2<-function(n=10){
  library(parallel)
  library(magrittr)
  library(randomForest)

  rf.form<- as.formula(paste("Final", paste(c('x','y','z'), collapse = "+"), sep = " ~ "))

  rf.df<-data.frame(Final=runif(10000),y=runif(10000),x=runif(10000),z=runif(10000))

  large.list<-rep(rf.df,10000)

  rf.df.list<-split(rf.df,rep(1:n,nrow(rf.df))[1:nrow(rf.df)])

  cl<-makeCluster(n)
  rf.list<-parLapply(cl,rf.df.list,function(x,rf.form,n){

    randomForest::randomForest(rf.form,x,ntree=100,nodesize=10, norm.votes=FALSE)},rf.form,n)

  stopCluster(cl)

  return(rf.list)
}

在第二个脚本中,我们在源环境中有大量清单。我们不是在调用列表或将其引入并行函数。我已将列表的大小设置为至少在32GB的计算机上可能是个问题。

scrip.loc<-"G:\\Scripts_Library\\R\\Stack_Answers\\parallel_question_2.R"

source(scrip.loc)

rf.parallel_2(n=10)

当我们运行第二个脚本时,最终会携带大约3gb(大列表的大小)*设置为集群的工作线程数,以及其他内容。如果我们在非源环境中运行第二个脚本的内容,则不是这种情况;相反,我们得到了一个〜3gb的列表,并行化函数运行没有问题,这就是结尾。

因此..工作环境如何/为什么从父环境中获取不必要的变量元素?为什么它仅在源脚本中发生?当我有一个源代码,大而复杂的脚本时,该如何缓解这一问题呢?该脚本的各个子节都是并行的(但可能携带3-10gb的中间数据)?

相关或相似的主题:

Using parLapply and clusterExport inside a function

clusterExport, environment and variable scoping

2 个答案:

答案 0 :(得分:2)

parLapply(cl, X, FUN, ...)的签名将FUN应用于X的每个元素。工作人员需要知道FUN,因此它将被序列化并发送给工作人员。什么是R函数?它是定义函数的代码,定义函数的环境。为什么要环境?因为在 R 中,引用在FUN之外定义的变量是合法的,例如,

f = function(y) x + y
x = 1; f(1)
## [1] 2

第二个复杂度是 R 允许函数更新函数外部的变量

f = function(y) { x <<- x + 1; x + y }
x = 1; f(1)
## [1] 3

在上面,我们可以想象我们可以找出f()环境的哪些部分(仅变量x),但是通常这种分析不是可能不需要实际评估函数,例如f = function(y, name) get(name) + y; x = 1; f(1, "x")

因此,要在工作程序上评估FUN,工作程序需要知道FUN的定义和环境FUN的内容已在其中定义。 R 通过使用FUN使工作人员了解serialize()。结果很容易看到

f = function(n) { x = sample(n); length(serialize(function() {}, NULL)) }
f(1)
## [1] 754
f(10)
## [1] 1064
f(100)
## [1] 1424

环境中的较大对象会导致向工作人员发送/使用的更多信息。

如果考虑到这一点,那么到目前为止的描述将意味着整个R会话应该序列化到工作程序(或磁盘,如果使用serialize()来保存对象) )-f()中隐式函数的环境包括f()的主体,还包括f()的环境(即全局环境)和全局环境,这是搜索路径...(检出environment(f)parent.env(.GlobalEnv))。 R 有一个任意规则,即它停止在全局环境中。因此,不要使用隐式的function() {},而应在.GlobalEnv

中定义它
g = function() {}
f = function(n) { x = sample(n); length(serialize(g, NULL)) }
f(1)
## [1] 592
f(1000)
## [1] 592

还请注意,这会对可以序列化哪些功能产生影响。例如,如果g()在下面的代码中序列化,它将“知道” x

f = function(y) { x = 1; g = function(y) x + y; g() }
f(1)
## [1] 2

但是这里并没有-它知道在其定义的环境中的符号,但是不知道从其调用环境中的符号。

rm(x)
g = function(y) x + y
f = function(y) { x = 1; g() }
f()
## Error in g() : object 'x' not found

您可以在脚本中进行比较

cl = makeCluster(2)
f = function(n) {
    x = sample(n)
    parLapply(
        cl, 1,
        function(...)
            length(serialize(environment(), NULL))
    )
}
f(1)[[1]]
## [1] 256
f(1000)[[1]]
## [1] 4252

使用

g = function(...) length(serialize(environment(), NULL))
f = function(n) {
    x = sample(n)
    parLapply(cl, 1, g)
}
f(1)[[1]]
## [1] 150
f(1000)[[1]]
## [1] 150

答案 1 :(得分:0)

在处理快要结束时,我正在将近50 GB的数据传递回parLapply,这是不理想的。

我最终创建了一个名为parLapply的新函数。我将其放在嵌套循环中,在其中创建了一个新环境,将父环境设置为.GlobalEnv,仅将所需的变量传递给新环境,然后将该环境传递给clusterExport

有关环境的详细信息,我建议this博客文章。此外,我发现Ethan McCallum和Stephen Weston撰写的Parallel R书也很有帮助。在第15-17页上,“雪”包中有关于此问题的讨论。