f <- function(x) 2*x
y <- f(1)
z <- f(3)
@HenrikBengtsson
Parallel & distributed processing can be used to:
We may choose to parallelize on:
Mathematics:
\[ f(x) = 2 \cdot x \\ y = f(1) \\ z = f(3) \]
R:
f <- function(x) 2*x
y <- f(1)
z <- f(3)
f <- function(x) {
2*x
}
A function has:
2*x
x
When we call the function;
y <- f(4)
R evaluates the code with the value of argument x
(= 4), returns the value (= 8), and assigns the value to variable y
.
f <- function(x, b) {
b*x
}
A function has:
b*x
x
and b
When we call the function;
y <- f(4, 2)
R evaluates the code with the values of arguments x
(= 4) and b
(= 2), returns the value, and assigns the value to variable y
.
f <- function(x, b = 2) {
b*x
}
When we call the function;
y <- f(4)
R evaluates the code with the value of argument x
(= 4), the default value of b
(= 2), returns the value, and assigns the value to variable y
.
\[ h(\mathbf{x}) = \sum_{i=1}^{n} x_i ; \mathbf{x} = (x_1, x_2, \ldots, x_n) \\ \]
h <- function(x) {
sum <- 0
n <- length(x)
for (i in 1:n) sum <- sum + x[i]
sum
}
\[ h(\mathbf{x}) = \sum_{i=1}^{n} x_i ; \mathbf{x} = (x_1, x_2, \ldots, x_n) \\ \]
\[ h(\mathbf{x}) = \sum_{i=1}^{n} x_i \]
\[ = \sum_{i=1}^{k} x_i + \sum_{i=k+1}^{n} x_i \\ \]
\[ = h(\mathbf{x}_{1:k}) + h(\mathbf{x}_{(k+1):n}) \]
\[ h(\mathbf{x}) = \sum_{i=1}^{n} x_i \\ \mathbf{x} = (1, 2, \ldots, 10) \]
\[ y = h(\mathbf{x}) \]
\[ a = h(\mathbf{x}_{1:5}) \\ b = h(\mathbf{x}_{6:10}) \\ y = a + b \]
\[ y = 55 \]
\[ ~\\ ~\\ y = h(\mathbf{x}) \]
y <- h(x)
\[ a = h(\mathbf{x}_{1:5}) \\ b = h(\mathbf{x}_{6:10}) \\ y = a + b \]
a <- h(x[1:5])
b <- h(x[6:10])
y <- a + b
\[ b = h(\mathbf{x}_{6:10}) \\ a = h(\mathbf{x}_{1:5}) \\ y = a + b \]
b <- h(x[6:10])
a <- h(x[1:5])
y <- a + b
\[ y = 55 \]
\[ \text{concurrently} \left\{\begin{matrix} a = h(\mathbf{x}_{1:5}) \\ b = h(\mathbf{x}_{6:10}) \\ \end{matrix}\right. \\ \hspace{10ex} y = a + b \]
How can we calculate \(a\) and \(b\) concurrently in R?
TL;DR
a %<-% h(x[1:5])
b %<-% h(x[6:10])
y <- a + b
a
and b
takes 5 seconds each => everything takes ~5 seconds
%<-%
is called the future-assignment operator
install.packages("futureverse", dependencies = TRUE)
tic()
and toc()
Create functions tic()
and toc()
to measure time by copy’n’pasting:
These functions can be used as a timer, e.g.
slow_sum()
slow_sum <- function(x) {
sum <- 0
for (value in x) {
Sys.sleep(1.0) # one-second slowdown per value
sum <- sum + value
}
sum
}
slow_sum()
Create \(x = (1, 2, ..., 10)\) in R
Call y <- slow_sum(x)
and confirm it takes 10 seconds
Call a <- slow_sum(x[1:5])
and confirm it takes 5 seconds
Same for b <- slow_sum(x[6:10])
Confirm correct result
Confirm that it finishes in 5 seconds - use tic()
and toc()
How long does each line take? - use tic()
and toc()
An R assignment:
v <- expr
y <- slow_sum(x)
Friedman & Wise (1976, 1977), Hibbard (1976), Baker & Hewitt (1977)
Verify result
Verify total run time
Verify run time per line
\[ a = \text{slow_sum}(\mathbf{x}_{1:5}) \\ b = \text{slow_sum}(\mathbf{x}_{6:10}) \\ y = a + b \]
a <- slow_sum(x[1:5])
b <- slow_sum(x[6:10])
y <- a + b
Low friction:
Static-code inspection by walking the abstract syntax tree (AST):
x <- rnorm(n = 100) lobstr::ast( { slow_sum(x) } )
f <- future({ slow_sum(x) }) | █─`{`
└─────────────| └─█─ slow_sum
| └─ x
=> globals identified and exported to the worker:
slow_sum()
- a function (also searched recursively)x
- a numeric vector of length 100For-loop:
xs <- list(1:25, 26:50, 51:75, 76:100)
ys <- list()
for (i in seq_along(xs)) {
ys[[i]] <- slow_sum(xs[[i]])
}
for (x in xs) {
ys <- c(ys, slow_sum(x))
}
Base R:
Plyr:
Foreach:
Base R:
ys <- lapply(xs, slow_sum)
is identical to
ys <- xs |> lapply(slow_sum)
R can’t tell the difference! (e.g. try with lobstr::ast(...)
)
Tidyverse:
ys <- xs |> map(slow_sum)
Plyr:
ys <- xs |> llply(xs, slow_sum)
ys <- xs |> map(slow_sum)
ys <- xs |> future_map(slow_sum)
Combine the idea of:
with:
to make a “concurrent” for-loop.
Hint: You need two for-loops - one for future()
and one for value()
ys <- future_map(xs, slow_sum)
future()
, value()
, and resolved()
are easy to understand, powerful constructsThese building blocks lay the foundation for higher-level functions:
future_lapply()
and future_replicate()
future_map()
and future_map_dbl()
foreach() %dofuture% { ... }
Parallel alternatives to traditional, sequential functions:
Yes, we can of course use base-R or magrittr pipes where we want to, e.g.
ys <- xs |> future_map(slow_sum)
ys <- xs %>% future_map(slow_sum)
Our ys <- future_map(xs, slow_sum)
can be resolved on lots of different backends - it doesn’t matter where!
On the current machine
plan(sequential)
sequentially (default)
plan(multisession)
plan(multisession, workers=4)
parallelly via background R workers
plan(mirai_multisession)
plan(mirai_multisession, workers=4)
parallelly via future.mirai; low latency
plan(callr)
plan(callr, workers=4)
parallelly via future.callr; all memory is returned when as each future is resolved
On other machines
plan(cluster, workers=c("n1", "m2.uni.edu", "vm.cloud.org"))
distributed on other local or remote computer
plan(mirai)
distributed via future.mirai daemons queue
On high-performance compute (HPC) cluster
plan(batchtools_lsf)
plan(batchtools_openlava)
plan(batchtools_sge)
plan(batchtools_slurm)
plan(batchtools_torque)
parallelly on HPC job schedulers (LSF, OpenLava, TORQUE/PBS, Grid Engine SGE/AGE, Slurm); for long-running tasks; high latency
Sequential:
library(future)
plan(sequential) # default
xs <- list(1:5, 6:10, 11:15, 16:20)
ys <- future_map(xs, slow_sum)
Built-in multisession:
plan(multisession)
ys <- future_map(xs, slow_sum)
Add-on mirai-based multisession:
plan(future.mirai::mirai_multisession)
ys <- future_map(xs, slow_sum)
What type of results may this function produce?
xs <- c(1, 2, -3, 4)
bs <- c(4, 3, 2, 1)
ys <- furrr::future_map2(xs, bs, g)
xs <- c(1, 2, 3, 4)
bs <- c(4, 3, -2, 1)
ys <- furrr::future_map2(xs, bs, g)
The name foreach() tricks us to believe it’s a for-loop
The name foreach() tricks us to believe it’s a for-loop
library(foreach)
fq <- fs::dir_ls(glob = "*.fq")
bam <- list()
foreach(ii = seq_along(fq)) %dopar% {
bam[[ii]] <- align(fq[[ii]])
}
This does not work because … foreach() is not a for-loop!
fq <- fs::dir_ls(glob = "*.fq")
bam <- list()
lapply(seq_along(fq), function(ii) {
bam[[ii]] <- align(fq[[ii]])
})
This does not work, because the <-
assignment is done inside a function
fq <- fs::dir_ls(glob = "*.fq")
bam <- list()
purrr::map(seq_along(fq), function(ii) {
bam[[ii]] <- align(fq[[ii]])
})
This does not work, because the <-
assignment is done inside a function
fq <- fs::dir_ls(glob = "*.fq")
bam <- list()
foreach(seq_along(fq), function(ii) {
bam[[ii]] <- align(fq[[ii]])
})
… it would be clear that the <-
assignment is done inside a function.
Sequential
processing
Parallelization
with 4 workers
Nested parallelization
with 5 workers each
running 3 parallel tasks
plan(future.batchtools::batchtools_slurm)
fq <- fs::dir_ls(glob = "*.fq") # 80 files; 200 GB each
bam <- furrr::future_map(fq, align) # 1 hour each
Series of exciting feature announcements at:
https://www.futureverse.org/blog.html