這裡介紹如何藉由 R 的 parallel 套件,使用多個 CPU 核心進行平行運算,提高計算速度。

現在電腦的 CPU 都有好幾個核心,在 R 中處理大計算量的問題時,如果感覺計算速度不夠快,就可以考慮將計算工作平行化,藉著 parallel 平行計算套件,將工作分散至多個 CPU 核心來計算,讓計算速度大幅提昇。

lapply 函數

由於 parallel 套件的實做與使用方式非常類似 R 內建的 lapply 這個隱式迴圈,所以在學習使用 parallel 套件之前,要先熟悉一下 lapply 用法。

假設我們有三組常態分佈的抽樣資料:

# 產生三組常態分佈的抽樣資料
x.list <- list(
  a = rnorm(10),
  b = rnorm(10),
  c = rnorm(10)
)
x.list
$a
 [1]  1.89632434 -0.90004876  0.67664475  0.49483551  0.05591163  0.04862564
 [7] -0.46838166 -0.36932839 -0.03425141  0.76694261

$b
 [1] -1.65421202 -1.39584410 -0.65933271  0.01404326 -0.67322945 -0.54062796
 [7] -1.03605538 -0.68641190 -0.35319759 -1.58982312

$c
 [1]  1.74472367 -0.75457928  0.72840523 -0.89119458  0.52035511 -1.55435785
 [7] -0.40364765  0.07631279 -0.65365062 -1.13598909

如果想要計算這三組資料的平均值與標準差,就可以使用 lapply 來處理:

# 計算平均值與標準差
lapply(x.list, function(x) c(mean(x), sd(x)))
$a
[1] 0.2167274 0.7880909

$b
[1] -0.8574691  0.5486911

$c
[1] -0.2323622  0.9984217

在執行時,lapply 會從第一個參數所指定的列表(或是向量)中,逐一取出所有元素,放進第二個參數所指定的函數中處理,處理完之後,將所有的結果串成一個列表再傳回。

有關於更多 lapply 函數的教學,請參考 R 進階迴圈

使用 parallel 套件

R 在 2.14.0 版之後就已經將 parallel 納入內建的套件,所以使用時只要直接載入即可:

# 載入 parallel 套件
library(parallel)

在實際進行計算之前,要先建立 cluster,指定需要使用的 CPU 核心數,若要發揮電腦的最大效能的話,可以透過 detectCores 自動偵測 CPU 的核心數,使用所有的核心:

# 取得 CPU 核心數
cpu.cores <- detectCores()

然後建立一個 cluster:

# 建立 Cluster
cl <- makeCluster(cpu.cores)

在 cluster 建立之後,就可以利用它來進行平行化的計算了。

parallel 套件提供了一個 parLapply 平行計算函數,其用法跟 lapply 幾乎相同(只多了一個指定 cluster 的參數),它可將不同的工作分配至不同的節點上運算,以平行的方式加速計算,若將上面 lapply 的範例以 parLapply 改寫,就會像這樣:

# 平行化計算
parLapply(cl, x.list, function(x) c(mean(x), sd(x)))

執行之後的計算結果跟普通的 lapply 計算結果相同,只不過計算速度會比較快:

$a
[1] 0.2167274 0.7880909

$b
[1] -0.8574691  0.5486911

$c
[1] -0.2323622  0.9984217

計算完成之後,再將 cluster 關閉:

# 關閉 Cluster
stopCluster(cl)

以上就是 parallel 套件典型的使用模式。

Cluster 類型

parallel 支援的 cluster 有好多種,以下是幾種常用的類型:

PSOCK
預設的 cluster 類型,其以 system("Rscript") 這類的方式建立 cluster 節點,透過 parallel socket 交換資料,支援所有平台。
FORK
以 fork 的方式建立 cluster 節點,不同節點之間可以共用唯讀資料的記憶體,所以執行效能非常好,但是 Windows 平台不支援此種 cluster 節點。
其他
其他的類型則是靠 snow 套件來支援(例如 MPI),請參考 snow 套件的說明

一般來說若在 Windows 系統上要使用多核心的 CPU 做運算,都是使用預設的 PSOCK,而在 Linux 系統上則可以選擇效能較好的 FORK,若在大型的叢集電腦中使用多台電腦同時做運算的話,才會使用到 snow 所支援的 MPI

變數空間

在使用預設的 PSOCK cluster 時,若想要讓所有的節點可以使用預先設定好的全域變數,必須以 clusterExport 將變數傳遞至所有節點,之後才能在各個節點中使用:

# 建立 PSOCK 的 cluster
cl <- makeCluster(cpu.cores)

# 建立全域變數
y <- 2

# 將變數傳遞至所有節點
clusterExport(cl, "y")

# 在各節點使用全域變數
parLapply(cl, c(1, 2, 3), function(x) x^y)

stopCluster(cl)

在 Linux 系統上若使用 FORK cluster 的話,由於各個節點都是以 forking 的方式產生的,所以可直接共用目前工作空間的資料:

# 建立 FORK 的 cluster
cl <- makeCluster(cpu.cores, type = "FORK")

# 建立全域變數
y <- 2

# 在各節點使用全域變數
parLapply(cl, c(1, 2, 3), function(x) x^y)

stopCluster(cl)

載入套件

若要在各個節點上使用特定的套件,可以直接將載入套件的 library 指令寫在 parLapply 所呼叫的函數中,或是使用 clusterEvalQ 讓所有節點預先載入套件:

cl <- makeCluster(cpu.cores)

# 讓所有節點載入指定套件
clusterEvalQ(cl, library(e1071))

# 產生測試資料的索引
s <- lapply(1:3, function(i) { sample(1:nrow(iris), 50) } )

# 在各節點使用載入的套件
parLapply(cl, s, function(x) { svm(Species ~ ., data = iris[x,]) })

stopCluster(cl)

clusterEvalQ 的作用就是單純讓每個節點執行指定的指令,只不過它不能像 parLapply 一樣傳入參數而已。

亂數種子

在平行化的計算中,如何產生不會重複的亂數是一個比較複雜的問題,而 parallel 套件幫我們處理掉大部分了,在使用上唯一需要注意的大概就是亂數種子不可以使用一般 set.seed 函數,要改用 clusterSetRNGStream

cl <- makeCluster(cpu.cores)

# 初始化 RNG Stream
clusterSetRNGStream(cl, 123)

# 產生亂數
clusterEvalQ(cl, runif(5))

stopCluster(cl)

mclapply 函數

parallel 套件中還有一個 mclapply 函數,它會自動使用多核心的 CPU 進行平行計算,不需要手動建立 cluster,使用起來更方便,不過它不支援 Windows 系統,只能在 Linux 或 Mac OS 中使用:

# 使用多核心 CPU 平行計算(不適用於 Windows 平台)
mclapply(x.list, function(x) c(mean(x), sd(x)))

以上只是基本的 parallel 套件使用方式,更詳細的用法請參它的官方文件。

計算 π 值範例

以下是一個用蒙地卡羅方法計算 π 值的程式,輸入的 n 值為模擬次數,傳回的結果就是 π 的近似值:

# 估計 PI 值
est.pi <- function(n){
  x <- runif(n, 0, 1)
  y <- runif(n, 0, 1)
  r <- sqrt(x^2 + y^2)
  sum(r < 1) / n * 4
}

我們可以使用 system.time 來測量計算時間,比較有無平行化的差異:

# 普通計算
system.time(lapply(rep(1e7, 4), est.pi))

# 平行計算
system.time({
  cl <- makeCluster(cpu.cores)
  parLapply(cl, rep(1e7, 4), est.pi)
  stopCluster(cl)
})

# 平行計算(不適用於 Windows 平台)
system.time(mclapply(rep(1e7, 4), est.pi))

參考資料:Stranity BlogG-FORGER-posts.comParallel Processing in R