R語言中的並行計算彙總

上一篇博文: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通過內存共享實現,而不需要額外的內存空間,這對性能的影響是很顯著的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章