上一篇博文:R語言中的代碼運算性能提升
R語言運行在CPU單核單線程上,使用並行計算原因是程序運行時間太長。大部分程序都可以進行並行化改造以提高運算性能
1.lapply
只需要一個參數(list\vector\array\matrix\data.frame),和一個以該參數爲輸入的函數,函數返回列表list
lapply(1:3/3, round, digits=3);
[[1]]
[1]
0.333
[[2]]
[1]
0.667
[[3]]
[1]
1
2.parallel
1.基本概述
在同一個CPU上利用多個核同時運算相同函數,parallel首先初始化一個集羣,集羣數量最好是CPU核數-1。如果一臺8核建立數量=8集羣,那CPU就幹不了其他事情
由於parallel包函數使用Rscript調用方式,對象被複制多份(多核),因此內存佔用較多,在大數據條件就要謹慎使用
library(parallel)
#Calculate the number of cores檢查電腦當前可用核數
no_cores<-detectCores(logical=F) #F-物理CPU核心數/T-邏輯CPU核心數
#Initiate cluster發起集羣,同時創建數個R進行並行計算
#只是創建待用的核,而不是並行運算環境
cl<-makeCluster(no_cores)
#現只需要使用並行化版本的lapply,parLapply就可以
parLapply(cl, 1:10000,function(exponent) 2^exponent)
#當結束後要關閉集羣,否則電腦內存會始終被R佔用
stopCluster(cl)
2.Parallel變量作用域
在Mac/Linux系統中使用 makeCluster(no_core, type="FORK")選項從而當並行運行時可包含所有環境變量
在Windows中由於使用的是Parallel Socket Cluster (PSOCK),每個集羣只加載base包,所以運行時要指定加載特定的包或變量
cl<-makeCluster(no_cores)
base<-2 #特定變量
clusterExport(cl, "base") #將base變量加載到集羣中,導入多個c("a","b","c")
parLapply(cl, 2:4, function(exponent) base^exponent)
stopCluster(cl)
##############################
clusterExport(cl=NULL,varlist,envir=.GlobalEnv) #varlist-要導入的對象名稱(字符向量)
clusterEvalQ(cl,expr)利用創建的cl核執行expr命令語句(若命令太長可寫到文件中,<-)
clusterEvalQ(cl,source(file="code.r"))
在函數中使用一些其他包就要使用clusterEvalQ加載,比如使用rms,要用clusterEvalQ(cl, library(rms))。要注意的是在clusterExport加載進某些變量後,這些變量的任何變化都會被忽略
cl<-makeCluster(no_cores)
base=2
clusterExport(cl, "base") #加載base變量
base <- 4 #變量值發生變化
parLapply(cl, 2:4, function(exponent) base^exponent)
# Finish
stopCluster(cl)
[[1]]
[1]
4
[[2]]
[1]
8
[[3]]
[1]
16
3.parSapply
讓程序返回向量|矩陣而不是列表,那麼就應該使用sapply,同樣也有並行版本parSapply
par開頭族函數與apply函數族用法基本一樣,還有parApply/parRapply/parCapply等
parSapply(cl = NULL, X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
parSapply(cl, 2:4, function(exponent) base^exponent)
[1] 4 8 16
#輸出矩陣並顯示行名和列名(因此才需要使用as.character,如果不做轉化,將是矩陣默認列名)
parSapply(cl, as.character(2:4), function(exponent){
x <- as.numeric(exponent)
c(base = base^x, self = x^x)
})
2 3 4
base 4 8 16
self 4 27 256
3.foreach
支持並行運算的擴展包,發揮多核計算優勢
1.基本概述
設計foreach思想可能是要創建一個lapply和for循環的標準,初始化過程有些不同,需要register註冊集羣
後面的表達式儘量用{}括起來
%do%-執行單進程任務,即便啓動多進程環境也是徒勞
%dopar%-多進程任務
library(foreach)
library(doParallel) #doParallel適合Windows/Linux/Mac
no_cores<-detectCores(logical=F)
cl<-makeCluster(no_cores) #先發起集羣
registerDoParallel(cl) #再進行登記註冊
#最後結束集羣
stopImplicitCluster() #停止隱式集羣
stopCluster()
#foreach函數可使用參數.combine控制結果彙總方法
base=2 #不需要將base變量加載到集羣中
foreach(exponent = 2:4, .combine = c) %dopar% base^exponent
[1] 4 8 16
#數據框
foreach(exponent = 2:4, .combine = rbind) %dopar% base^exponent
[,1]
result.1 4
result.2 8
result.3 16
foreach(exponent = 2:4, .combine = list, .multicombine = TRUE) %dopar% base^exponent
[[1]]
[1] 4
[[2]]
[1] 8
[[3]]
[1] 16
#最後list-combine方法是默認的.這個例子中用到.multicombine參數可避免未知的嵌套列表foreach(exponent = 2:4, .combine = list) %dopar% base^exponent
[[1]]
[[1]][[1]]
[1] 4
[[1]][[2]]
[1] 8
[[2]]
[1] 16
2.foreach中變量作用域
foreach中變量作用域有些不同,它會自動加載本地環境(不能直接調用上層環境)到函數中
base<-2
cl<-makeCluster(2)
registerDoParallel(cl)
foreach(exponent = 2:4, .combine = c) %dopar% base^exponent
stopCluster(cl)
[1] 4 8 16
#但對於父環境變量則不會加載
test<-function(exponent) {
foreach(exponent = 2:4, .combine = c) %dopar% base^exponent
}
test()
Error in base^exponent : task 1 failed - "object 'base' not found"
#爲解決error可使用.export參數而不要使用clusterExport.它可以加載最終版本變量,在函數運行前變量都是可以改變
base<-2
cl<-makeCluster(2)
registerDoParallel(cl)
base<-4
test<-function (exponent) {
foreach(exponent = 2:4, .combine = c, .export = "base") %dopar%
base^exponent
}
test()
stopCluster(cl)
[1] 16 64 256
#類似可使用.packages參數來加載包(非系統安裝包),比如.packages = c("rms", "mice")
3.使用Fork|Sork
若主要在windows上做分析,也習慣使用PSOCK。對使用其他系統的人要意識到兩者區別
FORK:"to divide in branches and go separate ways"
系統:Unix/Mac (not Windows)
環境:所有
PSOCK:並行socket集羣
系統:All (including Windows)
環境:空
4.內存控制
#如果不打算使用windows系統,建議嘗試FORK模式,可實現內存共享從而節省內存
PSOCK:
library(pryr)
no_cores<-detectCores(logical=F)
cl<-makeCluster(no_cores)
clusterExport(cl,"a")
clusterEvalQ(cl,library(pryr))
#address檢查R對象的內部屬性
parSapply(cl, X = 1:10, function(x) address(a)) == address(a)
#輸出結果爲FLASE說明沒有實現內存共享
[1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
FORK:
cl<-makeCluster(no_cores, type="FORK")
parSapply(cl, X = 1:10, function(x) address(a)) == address(a)
#輸出結果爲TRUE說明實現內存共享
[1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
b<-0
clusterExport(cl,"b")
parSapply(cl, X = 1:10, function(x) {b<-b + 1; b})
[1]
1 1 1 1 1 1 1 1 1 1
parSapply(cl, X = 1:10, function(x) {b <<- b + 1; b}) #兩個核心集羣
[1] 1 2 3 4 5 1 2 3 4 5
b
[1] 0
5.程序調試
並行環境中debug很困難,不能使用browser/cat/print函數來發現問題
tryCatch-list:stop函數不是好方法,因爲當程序運行1-2天后突然彈出錯誤,就只因爲這一個錯誤程序終止,並把之前做的計算全部扔掉,這是不合適的。爲此可使用tryCatch捕捉錯誤,從而使出現錯誤後程序還能繼續執行
foreach(x=list(1,2,"a"),.combine=list) %dopar%
{
tryCatch({
c(1/x, x, 2^x)
}, error = function(e) return(paste0("The variable '", x, "'",
" caused the error: '", e, "'")))
}
[[1]]
[1] 1 1 2
[[2]]
[1] 0.5 2.0 4.0
[[3]]
[1] "The variable 'a' caused the error: 'Error in 1/x: non-numeric argument to binary operator\n'"
創建文件輸出:當無法在控制檯觀測每個工作時,可設置共享文件,讓結果輸出到文件中
cl<-makeCluster(no_cores, outfile = "debug.txt")
registerDoParallel(cl)
foreach(x=list(1, 2, "a")) %dopar%
{
print(x)
}
stopCluster(cl)
#debug文件輸出,當代碼出現錯誤時不會出現以下信息
starting worker pid=7392 on localhost:11411 at 00:11:21.077
starting worker pid=7276 on localhost:11411 at 00:11:21.319
starting worker pid=7576 on localhost:11411 at 00:11:21.762
創建結點專用文件:若數據集存在一些問題時,可以方便觀測
cl<-makeCluster(no_cores, outfile = "debug.txt")
registerDoParallel(cl)
foreach(x=list(1, 2, "a")) %dopar%
{
cat(dput(x), file = paste0("debug_file_", x, ".txt"))
}
stopCluster(cl)
DEBUG日誌輸出文件
library(foreach)
library(doParallel)
no_cores<-detectCores(logical=F)
cl<-makeCluster(no_cores,outfile="debug.txt")
registerDoParallel(cl)
ceshi<-function(x){
a<-tryCatch(
{
c(1/x, x, 2^x)
},error=function(e)
return(paste0("The variable '", x, "'", " caused the error: '", e, "'"))
)
cat(x,'-----',a,file=paste0("debug_file_", x, ".txt")) #不同節點必須寫入不同文件
return(a)
}
foreach(x=list(1,2,"a",3,0,'b',4),.combine=c) %dopar% ceshi(x)
stopImplicitCluster()
stopCluster(cl)
6.任務載入、載入平衡
無論parLapply還是foreach都是包裝(wrapper)函數,意味着它們不是直接執行並行計算代碼,而是依賴其他函數實現的。在parLapply中的定義如下:
parLapply <- function (cl = NULL, X, fun, ...)
{
cl <- defaultCluster(cl)
do.call(c, clusterApply(cl, x = splitList(X, length(cl)),
fun = lapply, fun, ...), quote = TRUE)
}
splitList(X,length(cl)) 將任務分割成多個部分,然後將其發送到不同集羣中。如果有很多cache或者存在一個任務比其他worker中任務都大,那麼在這個任務結束前,其他提前結束的worker都會處於空閒狀態。爲避免這一情況需要將任務儘量平均分配給每個worker。舉個例子,若需要計算優化神經網絡的參數,這一過程可並行地以不同參數來訓練神經網絡
# From the nnet example
parLapply(cl, c(10, 20, 30, 40, 50), function(neurons)
nnet(ir[samp,], targets[samp,], size = neurons))
改爲:順序調整,分配更加合理
# From the nnet example
parLapply(cl, c(10, 50, 30, 40, 20), function(neurons)
nnet(ir[samp,], targets[samp,], size = neurons))
7.內存載入
在大數據情況下使用並行計算會很快出現問題。因爲使用並行計算會極大消耗內存,必須注意不要讓R運行內存到達內存上限,否則將會導致崩潰或非常緩慢。使用Forks是控制內存上限的重要方法,Fork通過內存共享實現,而不需要額外的內存空間,這對性能的影響是很顯著的