10.7 Paralelizace výpočtu

Funkce map() a spol. pracují s každým prvkem vektoru samostatně a izolovaně, takže nezáleží na pořadí, v jakém jsou tyto prvky zpracovány. To, mimo jiné, umožňuje paralelizaci výpočtu, tj. zpracování každého prvku na jiném jádře počítače nebo jiném prvku počítačového klastru. Pokud jsou data tak velká, že se to vyplatí, je možné použít balík furrr. Balík furrr implementuje paralelizované ekvivalenty funkcí map(), map2(), pmap(), modify() a všech jejich variant, které zjednodušují výsledek na atomický vektor nebo tabulku jako je map_dbl() nebo transformují jen vybrané prvky jako map_at(). Tyto alternativní funkce mají konzistentní pojmenování: před jméno dané funkce připojují future_, takže místo map() máme future_map() apod. Všechny tyto funkce také vracejí stejné výsledky a berou stejné parametry jako odpovídající funkce z balíku purrr (plus parametr .progress, který umožňuje sledovat průběh výpočtu pomocí progress baru, a parametr .options, který umožňuje předávat dodatečné informace paralelizačnímu stroji).

Před použitím těchto funkcí je třeba nejen načíst balík furrr, ale i nastavit paralelizaci. K tomu slouží funkce plan(). Její hlavní parametr určuje, jak se bude paralelizovat. Implicitní hodnota je sequential, tj. normální výpočet na jednom jádře bez paralelizace. Typické nastavení je

library(furrr)
plan(multiprocess)

které nastaví nejlepší dostupnou paralelizaci na daném stroji. Vlastní iterace jsou pak stejné jako s balíkem purrr, stačí jen přidat future_ ke jménu funkce, tj. např. volat:

future_map_dbl(1:5, ~ . ^ 2)
## [1]  1  4  9 16 25

Na velmi stylizovaném příkladu si ukážeme, jak paralelizace zrychluje výpočet. Pomocí funkce map() a future_map() spustíme třikrát výraz Sys.sleep(1), který na 1 sekundu pozastaví výpočet. Pomocí funkce system.time() změříme, jak dlouho tento “výpočet” trval:

system.time(map(1:3, ~ Sys.sleep(1)))
##    user  system elapsed 
##   0.001   0.000   3.004
system.time(future_map(1:3, ~ Sys.sleep(1)))
##    user  system elapsed 
##   0.031   0.000   1.175

Zatímco s pomocí map() trval, jak bychom očekávali, zhruba tři sekundy, s pomocí future_map() zabral jen o málo víc než 1 sekundu, protože každé jednotlivé volání Sys.sleep(1) proběhlo na mém osmijádrovém počítači v jiném sezení R. Výsledek by byl ještě rychlejší, kdybych na svém Linuxovém stroji nekompiloval tento text v RStudiu, viz dále.

Implicitně se používají všechna jádra počítače. To je možné změnit tak, že nejprve pomocí funkce availableCores() zjistíme počet dostupných jader, a pak nastavíme počet jader použitých výpočtu ve funkci plan() pomocí parametru workers. Pokud tedy chceme např. jedno jádro ušetřit pro ostatní procesy běžící na počítači, můžeme paralelizaci naplánovat takto:

n_cores <- availableCores()
plan(multiprocess, workers = n_cores - 1)

Popis podporovaných způsobů paralelizace najdete v dokumentaci k funkci plan() a v základní vinětě k balíku future, který se stará o backend. Základní způsoby paralelizace jsou čtyři: sequential provádí jednotlivé výpočty normálně bez paralelizace na jednom jádře, multisession spouští jednotlivé výpočty v nových kopiích R, multicore vytváří forky procesů R a cluster používá počítačový klastr. Ve většině případů budeme pracovat na jednom počítači, takže volíme mezi multisession a multicore. mulitcore je při tom efektivnější, protože nemusí kopírovat pracovní prostředí (globální proměnné, balíky apod.) do nového procesu, zatímco multisession musí překopírovat potřebné objekty do nového sezení R. multicore však není dostupný ve Windows, které fork vůbec nepodporují, ani při spuštění výpočtu v rámci RStudia. Nejrozumnější je proto použít multiprocess, který buď o zavolá multicore (na Linuxu nebo macOS při spuštění R mimo RStudio) nebo multisession (jinak).

Při spuštění multisession musí future_map() a spol. překopírovat do nového sezení potřebné proměnné. Většinou to funguje bezproblémově automaticky. Pokud však něco selže, přečtěte si viněty k balíku future, které vysvětlují, jak potřebné proměnné do nového sezení nakopírovat ručně a jak vyřešit i další případné problémy. Ani jeden z autorů tohoto textu však při použití furrr zatím na takový problém nenarazil.

Potřeba přesouvat data do a z nového procesu také znamená, že paralelní výpočet na čtyřech jádrech nebude typicky čtyřikrát rychlejší než na jednom. Pokud byste spouštěli jen malý počet velmi rychlých výpočtů na velkých datech, mohlo by se dokonce stát, že výpočet bude pomalejší než na jednom jádře. Paralelizovat se tak vyplatí jen výpočty, kde každé spuštění funkce nad prvkem seznamu trvá poměrně dlouho nebo se zpracovává poměrně hodně prvků.