在Ubuntu下,R中的多核和内存使用情况

27
我在一台拥有8个虚拟核心和8 GB RAM的Ubuntu工作站上运行R。我希望经常使用multicore包以并行地利用这8个核心; 但我发现整个R进程会被复制8次。 由于R似乎使用的内存比gc报告的多得多(即使在gc()之后,也是5倍),这意味着即使是相对较小的内存使用(一个200MB对象)在重复8次后也变得难以处理。 我考虑使用bigmemory让子进程共享相同的内存空间; 但它需要对我的代码进行一些重大改写,因为它不能处理数据框架。
有没有办法在分叉之前尽可能地精简R,即使操作系统重新获取尽可能多的内存?
编辑: 我现在认为我理解了发生了什么。问题不在我原以为的地方 - 存在于父线程中且未被操作的对象不会复制八次。相反,我认为我的问题来自于我让每个子进程执行的操作的性质。每个子进程都必须操作一个具有数十万级别的大要素,并且我认为这是占用内存最多的部分。因此,总内存负载确实与核心数量成比例; 但不像我想象的那样激烈。 我学到的另一个教训是,在拥有4个物理核心+可能的超线程的情况下,超线程实际上通常不适用于R。收益微乎其微,而内存成本可能是非常重要的。因此,从现在开始我将使用4个核心。
对于那些想要实验的人,这是我正在运行的代码类型:
# Create data
sampdata <- data.frame(id = 1:1000000)
for (letter in letters) {
sampdata[, letter] <- rnorm(1000000)
}
sampdata$groupid = ceiling(sampdata$id/2)

# Enable multicore
library(multicore)
options(cores=4) # number of cores to distribute the job to

# Actual job
system.time(do.call("cbind", 
    mclapply(subset(sampdata, select = c(a:z)), function(x) tapply(x, sampdata$groupid, sum))
))

5
有没有让R变得更轻便的方法?没有。这似乎是它最大的弱点。 - David Heffernan
2
你能再详细说明一下你的工作流吗?当我使用多核时,我发现生成的线程并不是很大。但你可能在使用我不熟悉的过程/方法/工作流。你能创建一些虚拟数据并进行演示吗? - JD Long
1
@roman 非常感谢提供的链接,非常有帮助,并且证实了我的猜想(对于R来说,超线程并不有用),尽管我没有观察到这些奇怪的模式。核心似乎始终以100%的效率工作,只是与4个核心相比,并没有任何收益。4个核心 > 3个核心 > 2个核心 > 1个核心;但是在大约4个核心时,我达到了一个上限。 - crayola
2
与线程去的地方不同,但是在tapply(x, f, sum)中,f被强制转换为因子,并且每个迭代大约需要1/2的时间。因此,在循环外将其作为因子可以加快计算速度并避免重复,从而减少内存使用。另外,tapply的一个显著成本是简化结果,我们可以使用unlist(lapply(split(x, f), sum), use.names=FALSE)自己完成这项工作(以更脆弱的代码为代价)。这些操作可以导致5-6倍的加速,至少节省3年的时间! - Martin Morgan
2
愚蠢的问题 - 但你确定你所陈述的内存使用情况是正确的吗?我怀疑可能存在共享情况 - 因此你不能仅通过加总所有内容来得出总内存使用量。 - cbz
显示剩余10条评论
2个回答

17

你尝试过data.table吗?

> system.time(ans1 <- do.call("cbind",
lapply(subset(sampdata,select=c(a:z)),function(x)tapply(x,sampdata$groupid,sum))
))
   user  system elapsed 
906.157  13.965 928.645 

> require(data.table)
> DT = as.data.table(sampdata)
> setkey(DT,groupid)
> system.time(ans2 <- DT[,lapply(.SD,sum),by=groupid])
   user  system elapsed 
186.920   1.056 191.582                # 4.8 times faster

> # massage minor diffs in results...
> ans2$groupid=NULL
> ans2=as.matrix(ans2)
> colnames(ans2)=letters
> rownames(ans1)=NULL

> identical(ans1,ans2)
[1] TRUE

你的示例非常有趣。它相当大(200MB),有许多组(1/2百万),每个组非常小(2行)。191秒的处理时间可能可以大大提高,但至少这是一个开端。[2011年3月]


现在,这个习惯用法(即lapply(.SD,...))已经得到了很大的改进。使用v1.8.2以及比上面的测试更快的计算机和最新的R版本等进行更新比较:

sampdata <- data.frame(id = 1:1000000)
for (letter in letters) sampdata[, letter] <- rnorm(1000000)
sampdata$groupid = ceiling(sampdata$id/2)
dim(sampdata)
# [1] 1000000      28
system.time(ans1 <- do.call("cbind",
  lapply(subset(sampdata,select=c(a:z)),function(x)
    tapply(x,sampdata$groupid,sum))
))
#   user  system elapsed
# 224.57    3.62  228.54
DT = as.data.table(sampdata)
setkey(DT,groupid)
system.time(ans2 <- DT[,lapply(.SD,sum),by=groupid])
#   user  system elapsed
#  11.23    0.01   11.24                # 20 times faster

# massage minor diffs in results...
ans2[,groupid:=NULL]
ans2[,id:=NULL]
ans2=as.matrix(ans2)
rownames(ans1)=NULL

identical(ans1,ans2)
# [1] TRUE


sessionInfo()
R version 2.15.1 (2012-06-22)
Platform: x86_64-pc-mingw32/x64 (64-bit)

locale:
[1] LC_COLLATE=English_United Kingdom.1252   LC_CTYPE=English_United Kingdom.1252
[3] LC_MONETARY=English_United Kingdom.1252  LC_NUMERIC=C
[5] LC_TIME=English_United Kingdom.1252

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
[1] data.table_1.8.2 RODBC_1.3-6     

当我尝试使用ans3 <- DT[,mclapply(.SD,sum),by=groupid]时,出现了一大堆问题。有什么猜测吗? - Ryogi
@Ryogi 快速猜测是尝试拆分每个.SD的_column_。这是你想要的吗?出了什么问题? - Matt Dowle
抱歉没有及时跟进。使用mclapply确实返回了正确的答案。让我困惑的是(1)它需要很长时间,(2)R控制台会打印出几条消息"select: Interrupted system call"(红色但不是错误)。仔细想想,“需要”很长时间应该是可以预料到的:对于每个单独的groupid(半百万),都要调用mclapply,而开销超过了并行化带来的收益。直观地说,为了提高性能,DT的列应该在“by”之前进行拆分。 - Ryogi

2

我在64位Ubuntu上尝试过的R语言解决方案,按照成功程度排序:

  • 使用较少的核心,就像你正在做的那样。

  • 将mclapply任务分成几部分,并使用DBI将部分结果保存到数据库中,append=TRUE。

  • 经常使用rmgc()函数。

我尝试了所有这些方法,但mclapply仍会在运行时创建越来越大的进程,这让我怀疑每个进程都在保留一些它实际上不需要的残余内存。

P.S. 我在使用data.table,似乎每个子进程都会拷贝这个数据表。


如果它是一个data.frame,那么每个子进程也会复制它吗?你使用的data.table是我在答案中使用的方式,还是以不同的方式使用?sessionInfo()的结果也很好,可以确认您正在使用各种软件的最新版本。 - Matt Dowle
好的,问题是关于 multicore 包的,所以我没有像那样使用 data.table。我希望最终 data.table 能够使用多个处理器。 - Sasha Goodman
顺便说一下,Data.table通常是大多数问题的绝佳解决方案。问题是关于multicore包的。在我的情况下,我认为系统认为data.table已被修改,这就是为什么内存增加的原因?我怀疑data.frames也会做同样的事情。我的代码非常复杂,我正在处理数万亿行之间的数百万次比较,以产生“模糊连接”。还有一件事我应该说,如果能够使用任意函数来执行模糊连接,那将是很棒的。 - Sasha Goodman
在这个问题的背景下,关键点实际上是data.table intro vigentte(第5页)的脚注3。如果您有适当使用multicore的情况,那么这将成为一个很好的新问题。请直接将这些新功能请求添加到data.table tracker,听起来不错。这样,当它们被讨论或进展时,您将获得自动更新。 - Matt Dowle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接