R-Code parallelisieren bei unterschiedlichen Laufzeiten: clusterApplyLB()

Pexels: Chait Goli

In einem früheren Beitrag / Video nutzten wir die clusterApply()-Funktion, um R-Code zu parallelisieren. Wie sieht es aus, wenn sich die Laufzeiten der einzelnen Aufgaben deutlich unterscheiden?

Zu Demonstrationszwecken stellen wir eine simple Aufgabe: Sys.sleep, das heißt „Pause machen“. In realen Anwendungen stehen hier dann Berechnungen / Datenoperationen, die unterschiedlich lange dauern.

Vorbereitung der Parallelisierung

library(parallel)
library(snow)
library(bench)

set.seed(2020)
tasktime <- runif(30, min = 0, max = 0.1)

cl <- makeCluster(detectCores() - 1)

Das snow-Paket wird normalerweise nicht mehr für Parallelisierung benötigt, da die wichtigsten Funktionen in das Base-R-Paket parallel aufgenommen wurden. Hier nutzen wir jedoch eine Funktion daraus zur Visualisierung der Auslastung der Arbeiter.

tasktime enthält 30 Zufallszahlen zwischen 0 und 0,1, die als unterschiedliche Laufzeiten dienen. makeCluster bereitet die Arbeiter vor. Auf Server-Umgebungen sollte detectCores() nicht verwendet werden, um den Server nicht (zu stark) zu blockieren. Eine Alternative ist parallelly::availableWorkers(), das auch Umgebungsvariablen berücksichtigen kann.

Benchmarks: clusterApply vs. clusterApplyLB und lapply

Für die Benchmarks nutzen wir wieder das bench-Paket:

bench::mark(
   lapply(tasktime, Sys.sleep),
   clusterApply(cl, tasktime, Sys.sleep),
   clusterApplyLB(cl, tasktime, Sys.sleep),
   iterations = 1
 )

Die genauen Laufzeiten können je nach Hardware variieren. In meinem Fall läuft lapply() 1,68 Sekunden, clusterApply() rund 300 Millisekunden, und clusterApplyLB() ist fast doppelt so schnell mit 185 Millisekunden. Wie kommt der Geschwindigkeitsgewinn zustande?

Auslastung der Arbeiter: snow.time()-Visualisierung von lapply()

lapply() läuft sequentiell – ein Arbeiter war während der Aufgabe permanent ausgelastet.

Auslastung der Arbeiter: snow.time()-Visualisierung von clusterApply()
In meinem Fall (6 physische Kerne, 12 logische Kerne dank Hyperthreading; einer reserviert fürs Betriebssystem) sind 11 Arbeiter mit den Sys.sleep()-Aufrufen beschäftigt. Deutlich sind Lücken zwischen den grünen Balken erkennbar: Schnellere Arbeiter müssen auf langsamere warten, bevor sie eine neue Aufgabe erhalten.

clusterApply() schafft es nicht, die Arbeiter durchgängig auszulasten – es bleiben Lücken zwischen den grünen Balken, weil schnellere Arbeiter auf langsamere warten müssen.

Auslastung der Arbeiter: snow.time()-Visualisierung von clusterApplyLB()

Load Balancing: Lastenausgleich zwischen den Arbeitern

clusterApplyLB() kann die Arbeiter besser auslasten: Wer mit seiner Aufgabe fertig ist, erhält in aller Regel sofort eine neue. So gelingt hier eine deutlich kürzere Gesamtlaufzeit. Das LB steht für Load Balancing: Lastenausgleich zwischen den Arbeitern.

Fazit: R-Code beschleunigen durch Parallelisierung

clusterApplyLB() ist also ein starkes Werkzeug speziell dann, wenn deutlich unterschiedliche Laufzeiten bei den Aufgaben der einzelnen Arbeiter zu erwarten sind. Insgesamt sind wesentliche Geschwindigkeitsgewinne erreichbar durch Parallellisierung.

Viel Erfolg bei Euren R-Projekten! Welche Erfahrungen habt Ihr mit der Optimierung von R-Skripten gemacht?