Cloud2016 yuanfan
哈哈,实操中有好多问题,我也不是都能解答。
system.time
的结果来自于proc.time
,具体的含义可以在文档里看看,但是我其实也不完全明白。对于纯粹的单线程运行而言,基本应该是user + system = elapsed的关系,但是并行的程序就不一定了。不过总归elapsed应该就是对应着电脑前的用户等了多久的时间。
问题本身确实复杂度不够,以glmnet处理的回归问题而言,变量数是比较主导复杂度的,也就是这里x
的列数,目前只有200,其实复杂不高。
cv.glmnet
的内部应该是对完整数据跑一次glmnet
,然后根据用户设定的cv折数(默认应该是nfolds = 10
折),跑nfolds
个小的glmnet,而它的并行就是在这个位置,是用foreach
框架的。那么理想状态,并行版本应该最好情况下只需要2次glmnet的时间,单线程版本需要nfolds + 1次glmnet的时间。不过实际情况看起来显然不是这样的……
并行计算是有额外开销的,比方说这里你可以通过增加数据的规模来增大单次计算消耗的时间,但增大的数据规模也意味着后面并行的时候每次分配任务也需要把一个更大的数据发送给子线程。或者还有一些其它的开销。这些都会抵消并行计算带来的速度提升。
关于该怎么设置并行数量,大方向一定是边际效用递减的,但具体的我其实没有什么特别好的方法,只是凭经验调一调。比方说我会先跑一次单线程版本的,观察一下CPU、内存的最大消耗情况。然后以此为基准,按照每份并行都需要同样的消耗,就能得到一个可以设置的上限,之后再这个基础上做一些不同并行数量的测试,尽量找一个甜点。比方说我曾经有一段代码,测试下来并行8线程的消耗是4线程的双倍,但速度一样,那就没有必要把并行线程的数量设置到8了。
我个人没有太多slurm集群的经验,曾经用服务器时候也不是非常在意物理核心和逻辑核心的区别。
跑benchmark的话,其实有很多地方要控制。比方说虽然你的R是单线程,但是你的底层BLAS是openblas,或者你的C代码用了OpenMP,这些都会导致看似单线程的R实际是多线程运行,盲目开并行可能并不会有那么多收益。我这次用的
RhpcBLASctl::omp_set_num_threads(1)
RhpcBLASctl::blas_set_num_threads(1)
和
parallel::clusterEvalQ(cl, RhpcBLASctl::omp_set_num_threads(1))
parallel::clusterEvalQ(cl, RhpcBLASctl::blas_set_num_threads(1))
控制主线程和子线程的底层运算都是单线程的。
- 以下开始一些我不能理解的内容:
# 生成数据并传递给子线程
x <- matrix(rnorm(3e5 * 800), 3e5, 800)
y <- rnorm(3e5)
cl <- parallel::makeCluster(2)
parallel::clusterExport(cl, c("x", "y"))
# 单线程的glmnet
system.time(res <- glmnet(x, y, nlambda = 100, type.gaussian = "covariance"))
# user system elapsed
# 79.618 1.495 81.052
# 每个并行核心上跑一次glmnet
system.time({
parallel::clusterEvalQ(cl, res <- glmnet(x, y, nlambda = 100, type.gaussian = "covariance"))
})
# user system elapsed
# 0.299 0.203 100.450
这里我已经刻意增加了x的列数、采用高维时更慢的type.gaussian = "covariance"
参数,并提前传递了x
和y
,但是纯粹的并行重复还是比单线程单次运算多出了20%的时间,这个瓶颈的原因我其实并不是完全理解。
而后面更不能理解的是在线性回归上的发现:
system.time({
res <- parallel::parLapply(cl, 1 : 2, function(id){
tmp <- lm(y ~ x)
return(0)
})
})
# user system elapsed
# 0.681 0.454 195.330
system.time({
res <- lapply(1 : 2, function(id){
tmp <- lm(y ~ x)
return(0)
})
})
# user system elapsed
# 283.117 5.736 288.891
显然在lm
上这个瓶颈也非常明显。2个lm,并行同时运行一次190s,但是单线程重复2次也才288s。
system.time({
res <- parallel::parLapply(cl, 1 : 2, function(id){
tmp <- solve(t(x) %*% x) %*% t(x) %*% y
return(0)
})
})
# user system elapsed
# 0.082 0.052 30.227
system.time({
res <- lapply(1 : 2, function(id){
tmp <- solve(t(x) %*% x) %*% t(x) %*% y
return(0)
})
})
# user system elapsed
# 55.217 4.427 59.624
但是如果只看线性回归里最核心的运算,这个并行效率的提升又非常完美了。所以我也不明白lm
和glmnet
里到底是哪些内容的瓶颈这么大。