`purrr::map()` が並列実行できるようになるらしい

追記:並列実行はできるんですけど、ぜんぜん違うデザインになりました。まだまだリリースまで油断できないですね...


今朝この pull request がマージされていました。CRAN リリースがいつになるかはわかりませんが、便利そうな機能なのでちょっとだけ解説します。

変更点

以下の関数に .paralell という引数が付きました。これを TRUE にすると、処理を並列で実行してくれるようになりました。ワクワクしますね。

  • map()map_*()
  • map2()map2_*()
  • pmap()pmap_*()

とりあえずやってみる

とりあえず何も考えずに実行してみましょう。

purrr::map(c(1,1,1,1,1), \(x) Sys.sleep(x), .parallel = TRUE)

もし必要なパッケージがインストールされていないと以下のようなインタラクションが発生しますが、1を選択すると自動でインストールしてくれます。

#> ℹ The package "carrier" is required for parallel map.
#> ✖ Would you like to install it?
#> 
#> 1: Yes
#> 2: No
#> 
#> Selection: 

それが済むと実行されますが...

purrr::map(c(1,1,1,1,1), \(x) Sys.sleep(x), .parallel = TRUE)
#> Error in `purrr::map()`:
#> ! No daemons set - use e.g. `mirai::daemons(6)` to set 6 local daemons.

エラーになってしまいました。やはり何も考えずに実行するだけではダメみたいです。

仕組み

実は、この並列実行の仕組みは mirai というパッケージをバックエンドとして成り立っているものです。 詳細は以下のドキュメントを読んでもらうのがよさそうですが、ここではかいつまんで紹介します。

https://purrr.tidyverse.org/dev/reference/parallelization.html

mirai の特徴は、並列に動くワーカーとメインスレッドが直接やりとりをするのではなく、 メッセージキューを介してタスクがワーカーに割り振られる点です。 これによって、オーバーヘッドが少なく、スケールする分散処理が実現されています(私は技術的な詳細についてまでは理解できていないので、ふわっとした説明ですみません)。

そんな mirai を使うには、並列実行のためのワーカーをあらかじめ立ち上げておく必要があります。 daemons() で 1 以上の数字を指定すれば、その指定した数のワーカーが起動されます。 (リモートのマシンを使う方法とか、より詳細は mirai の公式ドキュメントを読んでください)

daemons(6)

これで 6 個のワーカーが立ち上がりました。 こうしておくと、同じコードを実行しても今度は成功します。 これは1秒スリープするという処理ですが、全部合計しても1秒で終わっているので、並列で実行されていることがわかると思います。

system.time(
  purrr::map(c(1,1,1,1,1), \(x) Sys.sleep(x), .parallel = TRUE)
)
#> ✔ Automatically crated `.f`: 952 B
#>    user  system elapsed 
#>    0.00    0.00    1.02 

立ち上げたワーカーは、セッションが終了するか、daemons()0 を指定すると、シャットダウンされます。

daemons(0)

変数の渡し方

並列実行モードのときは、その関数の実行環境には明示的に渡した変数しか存在しません。 例えばこういうのはエラーになります。y という変数はワーカー側には渡されていないからです。

y <- 1
purrr::map_vec(1:5, \(x) x + y, .parallel = TRUE)

#> Error in `purrr::map_vec()`:
#> ℹ In index: 1.
#> Caused by error:
#> ! object 'y' not found

ではどうすればいいかというと、 carrier::crate() であらかじめ関数をシリアライズしておく必要があるみたいです。

y <- 1
purrr::map_vec(1:5, carrier::crate(\(x) x + y, y = y), .parallel = TRUE)

詳細は以下を読みましょう。

https://purrr.tidyverse.org/dev/reference/parallelization.html#crating-a-function:enbed

furrr は?

詳しい方は、似たようなパッケージとして furrr を思い浮かべるかもしれません。 purrr と同じインターフェースで、future をバックエンドとして並列実行するパッケージです。

端的には、mirai の方が並列実行が速い、ということになるんだと思いますが、そもそも future と mirai は若干レイヤーの違う技術です。

future も、並列実行のためのフレームワークではありますが、これはどちらかというと「様々なバックエンドを統一的に使えるインターフェース」です。 一方で、mirai は並列実行の仕組みそのものです。

実際、future のバックエンド一覧を見てみると、mirai もこの中に入っていることがわかると思います。 ということで、mirai と future は競合する部分もありますが、実際にはそんなバチバチ勝負しているような関係ではないのです。

話を戻して、furrr でよくない?というのは、確かにそうだと思います。 furrr を介して mirai をバックエンドとして使うこともできますし、将来 mirai よりもっといいバックエンドが開発されたらそれを使うことも簡単です。 ただ、バックエンドがたくさんあるということはトラブルも多種多様だということで、tidyverse の中に入れるにはちょっとサポートが大変かも...、というあたりなんでしょうか。しいて不安要素を挙げるとすれば。

shikokuchuo 氏が r-lib のいくつかのパッケージにもコントリビュートしてるのを見ると、もっといろいろ計画があるのかもしれないなと思ったりしますが、まあ考えてもわからないので待ちましょう。