doParallel関数に直接ワーカープロセス数を指定するとゾンビプロセスが残る件
次から次へと迫り来る原稿の嵐に追われている休みの昼下がり、何気なく目を向けたTLにこんなつぶやきが。
並列化した残骸のRScrpt.exeどうやって処分したらいいの…#メモリを圧迫し続けています
— Hadleyに憑依されてるテラモナギ (@teramonagi) 2014, 9月 22
というわけで調べてみました。
状況の再現(Ubuntu)
まずは、Ubuntu-14.04での再現。
> library(foreach) # doParallelを読みこめばforeachも読み込まれるが念のため > library(doParallel) > registerDoParallel(4) > foreach (i=1:32) %dopar% sqrt(i) > system("ps") PID TTY TIME CMD 19531 pts/14 00:00:00 bash 19766 pts/14 00:00:00 R 19771 pts/14 00:00:00 R <defunct> 19772 pts/14 00:00:00 R <defunct> 19773 pts/14 00:00:00 R <defunct> 19774 pts/14 00:00:00 sh 19775 pts/14 00:00:00 ps > stopImplicitCluster() # クラスタを終了する > system("ps") PID TTY TIME CMD 19531 pts/14 00:00:00 bash 19766 pts/14 00:00:00 R 19771 pts/14 00:00:00 R <defunct> 19772 pts/14 00:00:00 R <defunct> 19773 pts/14 00:00:00 R <defunct> 19776 pts/14 00:00:00 sh 19777 pts/14 00:00:00 ps
ゾンビプロセスが残っていることが確認できる。このRのマスタープロセスを終了しない限り、ゾンビプロセスが残ることを確認。
続いて、
> library(foreach) # doParallelを読みこめばforeachも読み込まれるが念のため > library(doParallel) > cl <- makeCluster(4) > registerDoParallel(cl) > foreach (i=1:32) %dopar% sqrt(i) > stopCluster(cl) > system("ps") PID TTY TIME CMD 19531 pts/14 00:00:00 bash 19782 pts/14 00:00:00 R 19785 pts/14 00:00:00 R 19794 pts/14 00:00:00 R 19803 pts/14 00:00:00 R 19812 pts/14 00:00:00 R 19820 pts/14 00:00:00 sh 19821 pts/14 00:00:00 ps > stopCluster(cl) > system("ps") PID TTY TIME CMD 19531 pts/14 00:00:00 bash 19782 pts/14 00:00:00 R 19830 pts/14 00:00:00 sh 19831 pts/14 00:00:00 ps
無事にワーカープロセスを終了できていることが確認される。
状況の再現(Windows)
続いて、Windows。
> library(foreach) # doParallelを読みこめばforeachも読み込まれるが念のため > library(doParallel) > registerDoParallel(4) > foreach(i=1:32) %dopar% sqrt(i) > stopImplicitCluster()
ワーカープロセスは消えなかった。タスクマネージャ等は省略。
> library(foreach) # doParallelを読みこめばforeachも読み込まれるが念のため > library(doParallel) > cl <- makeCluster(4) > registerDoParallel(cl) > foreach (i=1:32) %dopar% sqrt(i) > stopCluster(cl)
どうしてこうなるのか
doParallelパッケージのregisterDoParallel関数で、上記の現象が関連する部分は23-45行目の以下の箇所。
23 if (missing(cl) || is.numeric(cl)) { 24 if (.Platform$OS.type == "windows") { 25 if (!missing(cl) && is.numeric(cl)) { 26 cl <- makeCluster(cl) 27 } 28 else { 29 if (!missing(cores) && is.numeric(cores)) { 30 cl <- makeCluster(cores) 31 } 32 else { 33 cl <- makeCluster(3) 34 } 35 } 36 assign(".revoDoParCluster", cl, pos = .options) 37 setDoPar(doParallelSNOW, cl, snowinfo) 38 } 39 else { 40 if (!missing(cl) && is.numeric(cl)) { 41 cores <- cl 42 } 43 setDoPar(doParallelMC, cores, mcinfo) 44 } 45 }
- 26行目でmakeCluster関数によりワーカープロセスを生成
- 36行目で、doParallelパッケージの.optionsオブジェクトが確保しているメモリアドレスに、revoDoParClusterオブジェクトにワーカープロセスの情報を保持したオブジェクトclを付値する。
という流れでワーカープロセスが生成され、情報が保持されていることが分かる。
一方で、Windows以外のOSだと、43行目でdoParallelMC関数を呼び出しているが、その中で同じような処理を行っている。
さて、こうして生成されたワーカープロセスは、並列計算が終了したら停止しなければならない。これは、doParallelパッケージのstopImplicitCluster関数を用いて行えるはずである。しかし、上記の再現結果を見れば分かるようにそうにはなっていない。そこで、stopImplicitCluster関数の実装を見てみると、次のようになっている。
1 if (exists(".revoDoParCluster", where = .options) && !is.null(.revoDoParCluster)) { 2 stopCluster(.revoDoParCluster) 3 remove(".revoDoParCluster", where = .options) 4 }
1行目が問題。if文の判定で.revoDoParClusterオブジェクトがNULLかどうかについて確かめているが、それが間違い。これは、本当は次のように.optionsのメモリアドレスに格納された.revoDoParClusterオブジェクトを取得して、そのオブジェクトがNULLかどうかを調べないといけない。さらに、それに呼応して、.optionsのメモリアドレスに格納されたオブジェクトを取得して、ワーカープロセスを停止する処理になっていなければならない。
1 if (exists(".revoDoParCluster", where = .options) && !is.null(get(".revoDoParCluster", envir = .options))) { 2 stopCluster(get(".revoDoParCluster", envir = .options)) 3 remov(".revoDoParCluster", where = .options) 4 }
私がregisterDoParallel関数とstopImplicitCluster関数をウロウロしている間に
@hoxo_m先生が原因を突き止めていらっしゃっていて、
@teramonagi @sfchaos @kos59125 これで止まるとこまではつきとめました。
stopCluster(get(".revoDoParCluster", pos = doParallel:::.options))
— hoxo_m (@hoxo_m) 2014, 9月 23
さすがだな〜。
なお,以下の本の中でregisterDoParallel関数を使用している箇所は、すべてmakeCluster関数と合わせて使用しているため、上記の問題が顕在化しない.