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 + ba 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 <- expry <- 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 eachSeries of exciting feature announcements at:
https://www.futureverse.org/blog.html
