parallel::detectCores()[1] 4
PHS 7045: Advanced Programming
Loosely, from R’s perspective, we can think of HPC in terms of two, maybe three things:
Big data: How to work with data that doesn’t fit your computer
Parallel computing: How to take advantage of multiple core systems
Compiled code: Write your own low-level code (if R doesn’t has it yet…)
(Checkout CRAN Task View on HPC)
High Throughput Computing Cluster (HTC Cluster)
Supercomputer (HPC Cluster)

Embarassingly Parallel
In terms of scale
HTC > HPC > Single node > Socket > Core > Thread | SIMD vectorization
How many cores does your computer has?
parallel::detectCores()[1] 4
f <- function(n) n*2
f(1:4)f <- function(n) n*2
f_pll <- ...magic to parallelize f...
f_pll(1:4)When in doubt, profile your code first! In R, you can use the profvis package. which will give you a visual representation of where your code is spending most of the time.
Good example usage case:
Parallelization is not free: Most cost is in sending+receiving data.
In R (and other flavors), you can mitigate by (i) reducing the amount of data communicated, and (ii) reducing the number of times you communicate.
While there are several alternatives (just take a look at the High-Performance Computing Task View), we’ll focus on the following R-packages for explicit parallelism
Some examples:
- parallel: R package that provides ‘[s]upport for parallel computation, including random-number generation’.
- foreach: R package for ‘general iteration over elements’ in parallel fashion.
- future: ‘[A] lightweight and unified Future API for sequential and parallel processing of R expression via futures.’
- slurmR: R package for working with the Slurm Workload Manager (by yours truly).
Implicit parallelism, on the other hand, are out-of-the-box tools that allow the programmer not to worry about parallelization, e.g. such as gpuR for Matrix manipulation using GPU, tensorflow
And there’s also a more advanced set of options
- A ton of other type of resources, notably the tools for working with batch schedulers such as Slurm, HTCondor, etc.
PSOCK, Fork, MPI, etc.(Usually) We do the following:
PSOCK/FORK (or other) cluster using makePSOCKCluster/makeForkCluster (or makeCluster)Copy/prepare each R session (if you are using a PSOCK cluster):
Copy objects with clusterExport
Pass expressions with clusterEvalQ
Set a seed
parApply, parLapply, etc.clusterStop| Type | Description | Pros | Cons |
|---|---|---|---|
PSOCK |
Multiple machines via socket connection | Works in all OSs | Slowest |
FORK |
Single machine via forking | Avoids memory duplication | Only for Unix-based |
MPI2 |
Multiple machines via Message Passage Interface | Best alternative for HPC clusters | Sometimes hard to setup |
Using PSOCK, the slurmR package creates clusters featuring multiple nodes in HPC environments, think hundreds of cores.
[[1]]
[1] "Hello from process #1224. x = 20"
[[2]]
[1] "Hello from process #1223. x = 20"
[[3]]
[1] "Hello from process #1222. x = 20"
[[4]]
[1] "Hello from process #1225. x = 20"
Problem: Run multiple regressions on a very wide dataset. We need to fit the following model:
\[ y = X_i\beta_i + \varepsilon,\quad \varepsilon\sim N(0, \sigma^2_i),\quad\forall i \]
dim(X)[1] 500 999
X[1:6, 1:5] x001 x002 x003 x004 x005
1 0.61827227 1.72847041 -1.4810695 -0.2471871 1.4776281
2 0.96777456 -0.19358426 -0.8176465 0.6356714 0.7292221
3 -0.04303734 -0.06692844 0.9048826 -1.9277964 2.2947675
4 0.84237608 -1.13685605 -1.8559158 0.4687967 0.9881953
5 -1.91921443 1.83865873 0.5937039 -0.1410556 0.6507415
6 0.59146153 0.81743419 0.3348553 -1.8771819 0.8181764
str(y) num [1:500] -0.8188 -0.5438 1.0209 0.0467 -0.4501 ...
X.
lm) and returns the coefficients (coef).
y, we pass it as an argument.
apply with parApply.
parApply.
Both results should be the same.
Are we going any faster? The microbenchmark package can help us with that:
library(microbenchmark)
microbenchmark(
parallel = parallel::parApply(
cl = cl,
X = X, MARGIN = 2,
FUN = function(x, y) coef(lm(y ~ x)),
y = y
),
serial = apply(
X = X, MARGIN = 2,
FUN = function(x, y) coef(lm(y ~ x)),
y = y
),
times = 10,
unit = "relative"
)Unit: relative
expr min lq mean median uq max neval
parallel 1.00000 1.00000 1.000000 1.000000 1.000000 1.000000 10
serial 1.85252 1.79609 1.743099 1.708131 1.696984 1.733521 10
parallel::stopCluster(cl)Problem: We want to bootstrap a logistic regression model. We need to fit the following model:
\[ P(Y=1) = \text{logit}^{-1}\left(X\beta\right) \]
dim(X)[1] 100 5
head(X) [,1] [,2] [,3] [,4] [,5]
[1,] -0.13592452 1.1921489 -1.04101654 0.26500638 -0.51561099
[2,] -0.04079697 -0.1231379 -0.43190705 1.38694989 0.39568325
[3,] 1.01053901 -0.5741648 -0.77781632 -0.29149014 -0.78301461
[4,] -0.15826244 -1.4903169 0.37368178 -1.83027672 0.88538861
[5,] -2.15663750 2.3638289 0.31256458 -1.62766978 -0.38212891
[6,] 0.49864683 -2.9510362 0.07122864 -0.01630346 0.05333596
y[1:6][1] 1 1 0 0 1 0
my_boot <- function(y, X, B=1000) {
# Generating the indices
n <- length(y)
indices <- sample.int(n = n, size = n * B, replace = TRUE) |>
matrix(nrow = n)
# Fitting the model
apply(indices, 2, function(i) {
glm(y[i] ~ X[i,], family = binomial("logit")) |>
coef()
}) |> t()
}
set.seed(3312)
ans <- my_boot(y, X, B=50)
head(ans) (Intercept) X[i, ]1 X[i, ]2 X[i, ]3 X[i, ]4 X[i, ]5
[1,] 2.943576 -0.46986617 2.292807 1.1069735 2.117947 0.7839228
[2,] 4.265760 -0.01445575 3.881603 2.5052960 4.300462 0.0542386
[3,] 2.702185 -0.40973910 2.315127 1.1693082 3.059388 0.2927383
[4,] 4.827939 -1.52854114 2.692226 1.7977035 4.370736 0.7825011
[5,] 3.229396 -0.56316370 1.980704 1.4054200 3.949632 0.2806117
[6,] 2.933971 0.25911455 2.193838 0.6953409 1.970649 -0.3528708
my_boot_pll <- function(y, X, cl, B=1000) {
# Generating the indices
n <- length(y)
indices <- sample.int(n = n, size = n * B, replace = TRUE) |>
matrix(nrow = n)
# Making sure y and X are available in the cluster
parallel::clusterExport(cl, c("y", "X"))
# Fitting the model
parallel::parApply(cl, indices, 2, function(i) {
glm(y[i] ~ X[i,], family = binomial("logit")) |>
coef()
}) |> t()
}
cl <- parallel::makeForkCluster(4)
set.seed(3312)
ans_pll <- my_boot_pll(y, X, cl, B=50)
head(ans_pll) (Intercept) X[i, ]1 X[i, ]2 X[i, ]3 X[i, ]4 X[i, ]5
[1,] 2.943576 -0.46986617 2.292807 1.1069735 2.117947 0.7839228
[2,] 4.265760 -0.01445575 3.881603 2.5052960 4.300462 0.0542386
[3,] 2.702185 -0.40973910 2.315127 1.1693082 3.059388 0.2927383
[4,] 4.827939 -1.52854114 2.692226 1.7977035 4.370736 0.7825011
[5,] 3.229396 -0.56316370 1.980704 1.4054200 3.949632 0.2806117
[6,] 2.933971 0.25911455 2.193838 0.6953409 1.970649 -0.3528708
How much faster?
microbenchmark::microbenchmark(
parallel = my_boot_pll(y, X, cl, B=1000),
serial = my_boot(y, X, B=1000),
times = 1,
unit = "s"
)Unit: seconds
expr min lq mean median uq max neval
parallel 0.9318644 0.9318644 0.9318644 0.9318644 0.9318644 0.9318644 1
serial 1.2132553 1.2132553 1.2132553 1.2132553 1.2132553 1.2132553 1
parallel::stopCluster(cl)Problem: Revisit of the overhead cost of parallelization. We want to fit the following model \[y = X_k\beta_k + \varepsilon,\quad k = 1, \dots\]
# Simulating some data
n <- 1e4
k <- 3e3
X <- matrix(rnorm(n*k), ncol=k)
y <- rnorm(n)
X[1:4, 1:5] [,1] [,2] [,3] [,4] [,5]
[1,] -1.91835255 -0.2359106 -1.4642601 -0.5320349 -0.4639574
[2,] -0.09017806 -0.1022420 -0.6735899 1.6146947 -2.3792154
[3,] -1.25551672 1.2079800 0.2159515 -0.1323614 0.9867689
[4,] 1.28006769 -0.2806277 -0.2026345 -0.7375033 -0.1067501
y[1:6][1] 0.3570720 0.4850507 1.0281664 -0.7044579 1.1378356 -0.9032009
For this exercise only, we are excluding the time required to setup and stop the cluster. Those times are usually negligible for large computations but are also part of the overhead cost.
Let’s start with the naive approach: fitting the model and returning the full output.
| Elapsed time (s) | |
|---|---|
| Serial | 4.528 |
| Parallel naive | 7.651 |
The problem: we are returning a lot of information that we may not need:
# Approximate size of the output of apply/parApply
format(ncol(X) * object.size(lm(y ~ X[,1])), units="GB")[1] "7.2 Gb"
Instead of capturing the full output, we can just return the coefficients.
| Elapsed time (s) | |
|---|---|
| Serial | 4.528 |
| Parallel naive | 7.651 |
| Parallel coef | 1.705 |
The coefficients are much smaller, significantly reducing the overhead cost to about 0.8 Mb.
Since we only get coefficients, we can use a lighter version of lm called lm.fit.
| Elapsed time (s) | |
|---|---|
| Serial | 4.528 |
| Parallel naive | 7.651 |
| Parallel coef | 1.705 |
| Parallel lite | 0.544 |
Using a Fork cluster instead of a PSOCK cluster can further reduce the overhead cost. Both X and y would have been automatically available in the Fork cluster at 0 cost.
Parallel computing is a powerful tool to speed up your R code.
It’s not always the best solution, you have to think first!
In R the parallel package is a good starting point for explicit parallelism.
When parallelizing, think about the overhead cost and how to “do less”:
gvegayon
ggvy.cl
george.vegayon@utah.edu
For more, checkout the CRAN Task View on HPC
We know that \(\pi = \frac{A}{r^2}\). We approximate it by randomly adding points \(x\) to a square of size 2 centered at the origin.
So, we approximate \(\pi\) as \(\Pr\{\|x\| \leq 1\}\times 2^2\)
set.seed(1231)
p <- matrix(runif(5e3*2, -1, 1), ncol=2)
pcol <- ifelse(
sqrt(rowSums(p^2)) <= 1,
adjustcolor("blue", .7),
adjustcolor("gray", .7)
)
plot(p, col=pcol, pch=18)The R code to do this
pisim <- function(i, nsim) { # Notice we don't use the -i-
# Random points
ans <- matrix(runif(nsim*2), ncol=2)
# Distance to the origin
ans <- sqrt(rowSums(ans^2))
# Estimated pi
(sum(ans <= 1)*4)/nsim
}library(parallel)
# Setup
cl <- makePSOCKcluster(4L)
clusterSetRNGStream(cl, 123)
# Number of simulations we want each time to run
nsim <- 1e5
# We need to make -nsim- and -pisim- available to the
# cluster
clusterExport(cl, c("nsim", "pisim"))
# Benchmarking: parSapply and sapply will run this simulation
# a hundred times each, so at the end we have 1e5*100 points
# to approximate pi
microbenchmark::microbenchmark(
parallel = parSapply(cl, 1:100, pisim, nsim=nsim),
serial = sapply(1:100, pisim, nsim=nsim),
times = 10,
unit = "relative"
)Unit: relative
expr min lq mean median uq max neval
parallel 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 10
serial 1.504559 1.494615 1.410375 1.489041 1.428538 1.091074 10
R version 4.5.3 (2026-03-11)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.4 LTS
Matrix products: default
BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
locale:
[1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
[3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
[5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
[7] LC_PAPER=en_US.UTF-8 LC_NAME=C
[9] LC_ADDRESS=C LC_TELEPHONE=C
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
time zone: Etc/UTC
tzcode source: system (glibc)
attached base packages:
[1] parallel stats graphics grDevices utils datasets methods
[8] base
other attached packages:
[1] microbenchmark_1.5.0
loaded via a namespace (and not attached):
[1] digest_0.6.39 codetools_0.2-20 fastmap_1.2.0 xfun_0.56
[5] knitr_1.51 htmltools_0.5.9 rmarkdown_2.30 cli_3.6.5
[9] compiler_4.5.3 tools_4.5.3 evaluate_1.0.5 yaml_2.3.12
[13] otel_0.2.0 rlang_1.1.7 jsonlite_2.0.0 htmlwidgets_1.6.4