web-dev-qa-db-ja.com

data.tableと並列計算

この投稿に続いて: Rのマルチコアおよびdata.table で、data.tableを使用するときにすべてのコアを使用する方法があるかどうか疑問に思っていました。通常、グループによる計算は並列化できます。 plyrはそのような操作を設計上許可しているようです。

39
statquant

最初に確認することは、_data.table_ FAQ 3.1ポイント2が沈んでいることです。

最大のグループに対してのみ1つのメモリ割り当てが行われ、そのメモリは他のグループに再利用されます。収集するごみはほとんどありません。

それがdata.tableのグループ化が速い理由の1つです。しかし、このアプローチは並列化には向いていません。並列化とは、データを他のスレッドにコピーすることを意味し、代わりに時間がかかります。しかし、私の理解では、_data.table_のグループ化は通常、_.parallel_を使用したplyrよりも高速です。これは、各グループのタスクの計算時間、およびその計算時間を簡単に削減できるかどうかに依存します。多くの場合、データの移動が支配的です(大規模なデータタスクの1回または3回の実行をベンチマークする場合)。

より多くの場合、これまでのところ、実際には_[.data.table_のj式に食い込んでいるいくつかの落とし穴があります。たとえば、最近_data.table_のグループ化ではパフォーマンスが低下しましたが、原因はmin(POSIXct)であることが判明しました( Rで80Kを超える一意のIDで集計 )。その落とし穴を回避すると、50倍以上のスピードアップが実現しました。

したがって、マントラは次のとおりです:RprofRprofRprof

さらに、同じFAQからのポイント1は重要かもしれません:

その列のみがグループ化され、他の19は無視されます。data.tableはj式を検査し、他の列を使用していないことを認識しているためです。

したがって、_data.table_は、split-apply-combineパラダイムにまったく従いません。動作は異なります。 split-apply-combineは並列化に適していますが、実際には大きなデータには対応していません。

Data.tableイントロビネットの脚注3も参照してください。

ベクタースキャンであるコードに並列技術を展開している人は何人いるのでしょうか。

それは、「確かに、並列処理はかなり高速ですが、効率的なアルゴリズムでは実際にどれくらいの時間がかかるのでしょうか?」と言っているのです。

しかし、(Rprofを使用して)プロファイルを作成し、グループごとのタスクreally is計算集約型の場合、Word "multicore"を含むdatatable-helpへの3つの投稿が役立つ可能性があります。

datatable-helpへのマルチコア投稿

もちろん、並列化がdata.tableで適切である多くのタスクがあり、それを行う方法があります。しかし、通常は他の要因がかみ合っているため、まだ行われていません。そのため、優先度は低くなっています。ベンチマークとRprof結果を含む再現可能なダミーデータを投稿できれば、優先度の向上に役立ちます。

48
Matt Dowle

@matt dowleの以前のマントラRprof、Rprof、Rprofに基づいていくつかのテストを行いました

私が見つけたのは、並列化の決定はコンテキストに依存するということです。しかし、おそらく重要です。テスト操作(たとえば、以下のfoo、カスタマイズ可能)および使用するコアの数(8と24の両方を試してみます)に応じて、異なる結果が得られます。

以下の結果:

  1. 8コアを使用して、並列化のこの例では21%の改善が見られます
  2. 24コアを使用すると、14%向上します

また、24コアで並列化した(33%または25%、2つの異なるテスト)より大きな改善を示す実際の(共有できない)データ/操作もいくつか見ます。 2018年5月を編集新しい実際の例のケースは、1000グループの並列操作から85%近く改善されています。

R> sessionInfo() # 24 core machine:
R version 3.3.2 (2016-10-31)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: CentOS Linux 7 (Core)

attached base packages:
[1] parallel  stats     graphics  grDevices utils     datasets  methods
[8] base

other attached packages:
[1] microbenchmark_1.4-2.1 stringi_1.1.2          data.table_1.10.4

R> sessionInfo() # 8 core machine:
R version 3.3.2 (2016-10-31)
Platform: x86_64-Apple-darwin13.4.0 (64-bit)
Running under: macOS Sierra 10.12.4

attached base packages:
[1] parallel  stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] microbenchmark_1.4-2.1 stringi_1.1.5          data.table_1.10.4     

以下の例:

library(data.table)
library(stringi)
library(microbenchmark)

set.seed(7623452L)
my_grps <- stringi::stri_Rand_strings(n= 5000, length= 10)

my_mat <- matrix(rnorm(1e5), ncol= 20)
dt <- data.table(grps= rep(my_grps, each= 20), my_mat)

foo <- function(dt) {
  dt2 <- dt ## needed for .SD lock
  nr <- nrow(dt2)

  idx <- sample.int(nr, 1, replace=FALSE)

  dt2[idx,][, `:=` (
    new_var1= V1 / V2,
    new_var2= V4 * V3 / V10,
    new_var3= sum(V12),
    new_var4= ifelse(V10 > 0, V11 / V13, 1),
    new_var5= ifelse(V9 < 0, V8 / V18, 1)
  )]


  return(dt2[idx,])
}

split_df <- function(d, var) {
  base::split(d, get(var, as.environment(d)))
}

foo2 <- function(dt) {
  dt2 <- split_df(dt, "grps")

  require(parallel)
  cl <- parallel::makeCluster(min(nrow(dt), parallel::detectCores()))
  clusterExport(cl, varlist= "foo")
  clusterExport(cl, varlist= "dt2", envir = environment())
  clusterEvalQ(cl, library("data.table"))

  dt2 <- parallel::parLapply(cl, X= dt2, fun= foo)

  parallel::stopCluster(cl)
  return(rbindlist(dt2))
}

print(parallel::detectCores()) # 8

microbenchmark(
  serial= dt[,foo(.SD), by= "grps"],
  parallel= foo2(dt),
  times= 10L
)

Unit: seconds
     expr      min       lq     mean   median       uq      max neval cld
   serial 6.962188 7.312666 8.433159 8.758493 9.287294 9.605387    10   b
 parallel 6.563674 6.648749 6.976669 6.937556 7.102689 7.654257    10  a 

print(parallel::detectCores()) # 24

Unit: seconds
     expr       min        lq     mean   median       uq      max neval cld
   serial  9.014247  9.804112 12.17843 13.17508 13.56914 14.13133    10   a
 parallel 10.732106 10.957608 11.17652 11.06654 11.30386 12.28353    10   a

プロファイリング:

this answer を使用して、プロファイリングに対する@matt dowleの元のコメントへのより直接的な応答を提供できます。

その結果、計算時間の大部分はdata.tableではなくbaseによって処理されることがわかります。 data.table操作自体は、予想どおり、非常に高速です。これはdata.table内に並列処理の必要がないことの証拠であると主張する人もいますが、私はこのワークフロー/操作セットは非定型ではないと推測します。つまり、大規模なdata.table集計の大部分が相当量のdata.table以外のコードに関係しているというのが私の強い疑いです。これは、インタラクティブな使用と開発/実稼働での使用との間に相関関係があること。したがって、大規模な集計の場合、並列処理はdata.table内で価値があると結論付けます。

library(profr)

prof_list <- replicate(100, profr::profr(dt[,foo(.SD), by= "grps"], interval = 0.002),
                       simplify = FALSE)

pkg_timing <- fun_timing <- vector("list", length= 100)
for (i in 1:100) {
  fun_timing[[i]] <- tapply(prof_list[[i]]$time, paste(prof_list[[i]]$source, prof_list[[i]]$f, sep= "::"), sum)
  pkg_timing[[i]] <- tapply(prof_list[[i]]$time, prof_list[[i]]$source, sum)
}

sort(sapply(fun_timing, sum)) #  no large outliers

fun_timing2 <- rbindlist(lapply(fun_timing, function(x) {
  ret <- data.table(fun= names(x), time= x)
  ret[, pct_time := time / sum(time)]
  return(ret)
}))

pkg_timing2 <- rbindlist(lapply(pkg_timing, function(x) {
  ret <- data.table(pkg= names(x), time= x)
  ret[, pct_time := time / sum(time)]
  return(ret)
}))

fun_timing2[, .(total_time= sum(time),
                avg_time= mean(time),
                avg_pct= round(mean(pct_time), 4)), by= "fun"][
  order(avg_time, decreasing = TRUE),][1:10,]

pkg_timing2[, .(total_time= sum(time),
                avg_time= mean(time),
                avg_pct= round(mean(pct_time), 4)), by= "pkg"][
  order(avg_time, decreasing = TRUE),]

結果:

                      fun total_time avg_time avg_pct
 1:               base::[    670.362  6.70362  0.2694
 2:      NA::[.data.table    667.350  6.67350  0.2682
 3:       .GlobalEnv::foo    335.784  3.35784  0.1349
 4:              base::[[    163.044  1.63044  0.0655
 5:   base::[[.data.frame    133.790  1.33790  0.0537
 6:            base::%in%    120.512  1.20512  0.0484
 7:        base::sys.call     86.846  0.86846  0.0348
 8: NA::replace_dot_alias     27.824  0.27824  0.0112
 9:           base::which     23.536  0.23536  0.0095
10:          base::sapply     22.080  0.22080  0.0089

          pkg total_time avg_time avg_pct
1:       base   1397.770 13.97770  0.7938
2: .GlobalEnv    335.784  3.35784  0.1908
3: data.table     27.262  0.27262  0.0155

github/data.table にクロスポスト

6
Alex W

はい(ただし、それだけの価値はないかもしれませんが、@ Alex Wによって指摘されています)。

以下は、そのための簡単なパターンを示しています。説明を簡単にするために、価値のない例(mean関数を使用)を使用しますが、パターンを示しています。

例:

アイリスデータセットの種ごとの平均Petal.Lengthを計算するとします。

あなたはdata.tableを次のように使用してかなり直接行うことができます:

as.data.table(iris)[by=Species,,.(MPL=mean(Petal.Length))]
      Species   MPL
1:     setosa 1.462
2: versicolor 4.260
3:  virginica 5.552

ただし、代わりにmeanが十分に長時間かかる高価な計算である場合(おそらく「明白」な場合もありますが、プロファイリングによって決定される場合)、parallel::mclapplyを使用することをお勧めします。 mclapplyが生成するすべてのサブプロセスとの通信を最小限にすると、data.tableから各サブプロセスに選択を渡す代わりに、全体的な計算を大幅に削減できるため、選択のインデックスのみを渡します。さらに、最初にdata.tableをソートすることにより、これらのインデックスの範囲(最大および最小)のみを渡すことができます。このような:

> o.dt<-as.data.table(iris)[order(Species)] # note: iris happens already to be ordered
> i.dt<-o.dt[,by=Species,.(irange=.(range(.I)))]
> i.dt
      Species  irange
1:     setosa    1,50
2: versicolor  51,100
3:  virginica 101,150


> result<-mclapply(seq(nrow(i.dt)),function(r) o.dt[do.call(seq,as.list(i.dt[r,irange][[1]])),.(MPL=mean(Petal.Length))])
> result
[[1]]
     MPL
1: 1.462

[[2]]
    MPL
1: 4.26

[[3]]
     MPL
1: 5.552

> result.dt<-cbind(i.dt,rbindlist(result))[,-2]
> result.dt
      Species   MPL
1:     setosa 1.462
2: versicolor 4.260
3:  virginica 5.552

パターンを確認する:

  • 入力を注文します。
  • 各グループのインデックス範囲を計算します。
  • 匿名functionを定義して、グループメンバーを構成する行を抽出し、必要な計算(この場合は平均)を実行します。
  • インデックス範囲の行インデックスに対してmclapplyを使用して、各グループに関数を適用します。
  • 結果をdata.tableとして取得するにはrbindlistを使用し、入力にはcbindを使用し、インデックス列をドロップします(他の理由で保持する必要がない限り)。

ノート:

  • 最後のrbindlistは一般的に高価であり、アプリケーションによってはスキップされる場合があります)。

ToDo:

  • data.tableチームに、このパターンは十分に一般的であり、追加のdata.tableインデックスオプションが呼び出す必要があるほど有用であることを納得させます。 mc = TRUEを渡すとこのパターンが呼び出され、追加の並列オプションをサポートすることを想像してみてください...
iris.dt[by=Species,,.(MPL=mean(Petal.Length)), mc=TRUE, mc.preschedule=FALSE, mc.set.seed=TRUE,...]
1
malcook