doParallel関数に直接ワーカープロセス数を指定するとゾンビプロセスが残る件

次から次へと迫り来る原稿の嵐に追われている休みの昼下がり、何気なく目を向けたTLにこんなつぶやきが。


というわけで調べてみました。

状況の再現(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)

Windowsでもこの方法でワーカープロセスは無事消えた。

どうしてこうなるのか

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  }

Windowsではワーカープロセス数が指定された場合は、

  • 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先生が原因を突き止めていらっしゃっていて、


さすがだな〜。

なお,以下の本の中でregisterDoParallel関数を使用している箇所は、すべてmakeCluster関数と合わせて使用しているため、上記の問題が顕在化しない.