Parallelization in r: Difference between revisions
Created page with " There are a few ways to parallelize within <code>R</code>, these generally broken up into 3 different approaches the capture different “for-loop” frameworks in <code>R</c..." |
Updated and cleaned up the language on this page. |
||
| Line 1: | Line 1: | ||
There are a few ways to parallelize within <code>R</code>, | There are a few ways to parallelize within <code>R</code>, generally broken up into 3 different approaches the capture different “for-loop” frameworks in <code>R</code>. They include: | ||
# | #Parallelization in straight <code>for</code>-loop-style with <code>foeach</code> | ||
# | #Parallelization in <code>apply</code>-style loops with <code>mclapply</code> | ||
# | #Parallelization with <code>tidyverse</code> nesting-style loops with <code>multidplyr</code> | ||
The final option the above list | The final option in the above list is still too experimental and limited to truly encourage. | ||
== General | == General Starting Point == | ||
To start parallelizing within R we need to have some understanding of how many cores we have. | To start parallelizing within <code>R</code> , we need to have some understanding of how many cores we have. <syntaxhighlight lang="r">library(parallel) | ||
<syntaxhighlight lang="r">library(parallel) | |||
numCores <- detectCores() | numCores <- detectCores() | ||
numCores</syntaxhighlight> | numCores</syntaxhighlight> | ||
This will tell you the total number of cores on your computer. We often recommend using | This will tell you the total number of cores on your computer. We often recommend using less than the total number of cores when working on your personal computer or on the department servers to leave room for other jobs. You can always determine the current server system use by checking the hydra usage page. | ||
= for-loop style: <code>foreach</code> and <code>doParallel</code> library (<code>foreach () %dopar%</code>) = | = for-loop style: <code>foreach</code> and <code>doParallel</code> library (<code>foreach () %dopar%</code>) = | ||
The combination of <code>foreach</code> and <code>doParallel</code> can convert for-loop style coding into parallel processing and provide a lot of additional parameters that can help reduce unnecessary memory usage, provide progress reports and more. | The combination of <code>foreach</code> and <code>doParallel</code> can convert for-loop style coding into parallel processing and provide a lot of additional parameters that can help reduce unnecessary memory usage, provide progress reports, and more. | ||
In the following presentation, we’ll demonstrate the speed obtained with different approaches using a simulation function defined below | In the following presentation, we’ll demonstrate the speed obtained with different approaches using a simulation function defined below:<syntaxhighlight lang="r">my_k <- rep(1:14, each = 20) | ||
<syntaxhighlight lang="r">my_k <- rep(1:14, each = 20) | |||
my_simulation <- function(k) { | my_simulation <- function(k) { | ||
| Line 44: | Line 33: | ||
}</syntaxhighlight> | }</syntaxhighlight> | ||
== | == Single Core Analysis == | ||
We naturally want to compare a parallelization approach with the base single core analysis, which can be done | We naturally want to compare a parallelization approach with the base, single core analysis, which can be done using a standard for loop over various values of <code>k</code> :<syntaxhighlight lang="r">#base for-loop | ||
<syntaxhighlight lang="r">#base for-loop | |||
system.time({ | system.time({ | ||
storage_f <- c() | storage_f <- c() | ||
| Line 59: | Line 45: | ||
## 1.811 0.633 2.449</syntaxhighlight> | ## 1.811 0.633 2.449</syntaxhighlight> | ||
In the <code>foreach</code> package, we can also use just a single core in the following way which results in approximately the same run time as the previous code segment:<syntaxhighlight lang="r">#single-core `foreach` | |||
<syntaxhighlight lang="r">#single-core `foreach` | |||
library(foreach) | library(foreach) | ||
system.time({ | system.time({ | ||
| Line 72: | Line 55: | ||
## 1.876 0.640 2.522</syntaxhighlight> | ## 1.876 0.640 2.522</syntaxhighlight> | ||
It should be clear that the <code>foreach</code> function works very similar to the base <code>for</code>-loop; the difference in the syntax is mainly the fact that there is a <code>%do%</code> before the opening bracket. Additionally, notice that the loops of <code>k</code> for values in <code>my_k</code> is expressed differently than in a standard <code>for</code> loop (<code>k in my_k</code> vs <code>k = my_k</code>). | |||
== | == Multi-Core Analysis == | ||
To multiple cores with <code>foreach</code> we just change the above code to | To use multiple cores with <code>foreach</code> we just change the above code to:<syntaxhighlight lang="r">library(doParallel) | ||
<syntaxhighlight lang="r">library(doParallel) | |||
registerDoParallel(numCores-2) # use multicore, set to the number of our cores | registerDoParallel(numCores-2) # use multicore, set to the number of our cores | ||
| Line 91: | Line 71: | ||
## 3.513 2.520 0.601</syntaxhighlight> | ## 3.513 2.520 0.601</syntaxhighlight> | ||
The last number under “elapsed” suggest that although we used more core + user time overall we completed the overall process faster than without parallelization. | The last number under “elapsed” suggest that although we used more core + user time overall, we completed the overall process faster than without parallelization. The <code>%dopar%</code> represents "do parallelization." | ||
== | == Additional Parameters == | ||
#<code>.combine</code>: this parameter tells the system how you’d like to return | #<code>.combine</code>: this parameter tells the system how you’d like to return each step of the for-loop when combined at the end. Above we used <code>.combine = c</code> to get a vector back, but we could also use things like <code>rbind</code>, <code>cbind</code>. Note that the default is a list. | ||
# | #Memory and library passing: Sometimes you have a lot of functions or memory in your RAM before your run the parallelized for loop. The <code>foreach</code> function tries to reduce the amount memory used for each parallel process by constraining which functions are passed in. To specify which packages you’d like to use, use the <code>.packages</code> command in <code>foreach.</code> For example: <code>.packages = c("dplyr", "tidyr")</code>. Additionally, you can constrain what data is passed into each process with the parameter <code>.export</code> which uses strings relative to your global environment. | ||
#Progress bars: To have a running progress bar, one can use the package <code>doSNOW</code>. For the previous process we can add a progress bar with the following set of code: <syntaxhighlight lang="r">library(doSNOW) | |||
cl <- makeCluster(numCores-4) | cl <- makeCluster(numCores-4) | ||
registerDoSNOW(cl) | registerDoSNOW(cl) | ||
| Line 109: | Line 90: | ||
parallel::stopCluster(cl)</syntaxhighlight> | parallel::stopCluster(cl)</syntaxhighlight> | ||
<syntaxhighlight lang="r">|============================================================== | 83%</syntaxhighlight> | <syntaxhighlight lang="r">|============================================================== | 83%</syntaxhighlight> | ||
| | ||
= lapply style: <code>parallel</code> library (<code>mclapply</code>) = | = lapply style: <code>parallel</code> library (<code>mclapply</code>) = | ||
| Line 122: | Line 97: | ||
The second package we will look at is <code>parallel</code>, which includes the function <code>mclapply</code> which can be used very similar to an <code>lapply</code>. | The second package we will look at is <code>parallel</code>, which includes the function <code>mclapply</code> which can be used very similar to an <code>lapply</code>. | ||
== | == Single Core Analysis == | ||
For a single-core analysis of the same simulation seen in the previous section, we could run:<syntaxhighlight lang="r"> | |||
<syntaxhighlight lang="r"> | |||
system.time({ | system.time({ | ||
results <- lapply(my_k, my_simulation) | results <- lapply(my_k, my_simulation) | ||
| Line 136: | Line 108: | ||
## 1.819 0.641 2.467</syntaxhighlight> | ## 1.819 0.641 2.467</syntaxhighlight> | ||
== | == Multi-Core Analysis == | ||
Now, with the <code>mclapply</code> function we can do basically the same thing | Now, with the <code>mclapply</code> function we can do basically the same thing as before. The difference here is the same as the difference between using <code>for</code> and <code>lapply</code> in base <code>R</code>. <code>mclapply</code> applies each of the values in <code>my_k</code> to the simulation, and parallelizes it across <code>numCores</code> cores.<syntaxhighlight lang="r">system.time({ | ||
<syntaxhighlight lang="r">system.time({ | |||
results <- mclapply(my_k, my_simulation, mc.cores = numCores) | results <- mclapply(my_k, my_simulation, mc.cores = numCores) | ||
my_data <- data.frame(k = my_k, | my_data <- data.frame(k = my_k, | ||
| Line 149: | Line 118: | ||
## 2.208 1.636 0.522</syntaxhighlight> | ## 2.208 1.636 0.522</syntaxhighlight> | ||
== | == Extra Resources: == | ||
== | *[https://nceas.github.io/oss-lessons/parallel-computing-in-r/parallel-computing-in-r.html Matt Jones] | ||
*[https://blog.dominodatalab.com/multicore-data-science-r-python/ R and python more options] | |||
*<code>parallel</code> [https://stat.ethz.ch/R-manual/R-devel/library/parallel/doc/parallel.pdf manual] | |||
*<code>doParallel</code> + <code>foreach</code> [https://cran.r-project.org/web/packages/doParallel/vignettes/gettingstartedParallel.pdf vignette] | |||
== The Fastest One as of February 2024: <code>BiocParallel</code>== | |||
All of the above are good solutions, but from practical experience we have seen that sometimes | All of the above are good solutions, but from practical experience, we have seen that sometimes the previous methods can incur high overheads. A consistently faster solution comes from a package developed and maintained by the R bioinformatics community: <code>BioConductor</code>. The package is named <code>BiocParallel </code>and its interface is almost exactly the same as the one for the <code>parallel</code> package seen in the previous section. The only difference is that we need another package. | ||
First of all, install the | First of all, install the <code>BiocParallel</code> package manager via | ||
<pre tabindex="0">install.packages("BiocManager") | <pre tabindex="0">install.packages("BiocManager") | ||
</pre> | </pre> | ||
| Line 165: | Line 135: | ||
<pre>install("BiocParallel")</pre> | <pre>install("BiocParallel")</pre> | ||
We can then use a simple command. Assuming the function to run multiple times in parallel is named <code>foo</code>, we type | We can then use a simple command. Assuming the function to run multiple times in parallel is named <code>foo</code>, we type: | ||
<pre tabindex="0">library('BiocParallel') | <pre tabindex="0">library('BiocParallel') | ||
system.time( | system.time( | ||
Revision as of 14:18, 1 July 2025
There are a few ways to parallelize within R, generally broken up into 3 different approaches the capture different “for-loop” frameworks in R. They include:
- Parallelization in straight
for-loop-style withfoeach - Parallelization in
apply-style loops withmclapply - Parallelization with
tidyversenesting-style loops withmultidplyr
The final option in the above list is still too experimental and limited to truly encourage.
General Starting Point
To start parallelizing within R , we need to have some understanding of how many cores we have.
library(parallel)
numCores <- detectCores()
numCoresThis will tell you the total number of cores on your computer. We often recommend using less than the total number of cores when working on your personal computer or on the department servers to leave room for other jobs. You can always determine the current server system use by checking the hydra usage page.
for-loop style: foreach and doParallel library (foreach () %dopar%)
The combination of foreach and doParallel can convert for-loop style coding into parallel processing and provide a lot of additional parameters that can help reduce unnecessary memory usage, provide progress reports, and more.
In the following presentation, we’ll demonstrate the speed obtained with different approaches using a simulation function defined below:
my_k <- rep(1:14, each = 20)
my_simulation <- function(k) {
x <- runif(30,-3, 3)
y <- 2*x + 2* rnorm(n = 30)
x0 <- runif(10000,-3, 3)
y0 <- 2*x0 + 2*rnorm(n = 10000)
my_lm <- lm(y ~ poly(x,k))
y0_hat <- predict(my_lm, data.frame(x = x0))
return(mean((y0-y0_hat)^2))
}Single Core Analysis
We naturally want to compare a parallelization approach with the base, single core analysis, which can be done using a standard for loop over various values of k :
#base for-loop
system.time({
storage_f <- c()
for (k in my_k) {
storage_f <- c(storage_f, my_simulation(k))
}
})## user system elapsed
## 1.811 0.633 2.449
In the foreach package, we can also use just a single core in the following way which results in approximately the same run time as the previous code segment:
#single-core `foreach`
library(foreach)
system.time({
storage_fe <- foreach (k=my_k, .combine=c) %do% {
my_simulation(k)
}
})## user system elapsed
## 1.876 0.640 2.522It should be clear that the foreach function works very similar to the base for-loop; the difference in the syntax is mainly the fact that there is a %do% before the opening bracket. Additionally, notice that the loops of k for values in my_k is expressed differently than in a standard for loop (k in my_k vs k = my_k).
Multi-Core Analysis
To use multiple cores with foreach we just change the above code to:
library(doParallel)
registerDoParallel(numCores-2) # use multicore, set to the number of our cores
system.time({
storage_p <- foreach (k=my_k, .combine=c) %dopar% {
my_simulation(k)
}
})
stopImplicitCluster()## user system elapsed
## 3.513 2.520 0.601The last number under “elapsed” suggest that although we used more core + user time overall, we completed the overall process faster than without parallelization. The %dopar% represents "do parallelization."
Additional Parameters
.combine: this parameter tells the system how you’d like to return each step of the for-loop when combined at the end. Above we used.combine = cto get a vector back, but we could also use things likerbind,cbind. Note that the default is a list.- Memory and library passing: Sometimes you have a lot of functions or memory in your RAM before your run the parallelized for loop. The
foreachfunction tries to reduce the amount memory used for each parallel process by constraining which functions are passed in. To specify which packages you’d like to use, use the.packagescommand inforeach.For example:.packages = c("dplyr", "tidyr"). Additionally, you can constrain what data is passed into each process with the parameter.exportwhich uses strings relative to your global environment. - Progress bars: To have a running progress bar, one can use the package
doSNOW. For the previous process we can add a progress bar with the following set of code:library(doSNOW) cl <- makeCluster(numCores-4) registerDoSNOW(cl) pb <- txtProgressBar(max = length(my_k), style = 3) progress <- function(n) setTxtProgressBar(pb, n) opts <- list(progress = progress) storage_p <- foreach (k=my_k, .combine=c, .options.snow = opts) %dopar% { my_simulation(k) } parallel::stopCluster(cl)
|============================================================== | 83%
lapply style: parallel library (mclapply)
The second package we will look at is parallel, which includes the function mclapply which can be used very similar to an lapply.
Single Core Analysis
For a single-core analysis of the same simulation seen in the previous section, we could run:
system.time({
results <- lapply(my_k, my_simulation)
my_data <- data.frame(k = my_k,
values = unlist(results))
})## user system elapsed
## 1.819 0.641 2.467Multi-Core Analysis
Now, with the mclapply function we can do basically the same thing as before. The difference here is the same as the difference between using for and lapply in base R. mclapply applies each of the values in my_k to the simulation, and parallelizes it across numCores cores.
system.time({
results <- mclapply(my_k, my_simulation, mc.cores = numCores)
my_data <- data.frame(k = my_k,
values = unlist(results))
})## user system elapsed
## 2.208 1.636 0.522Extra Resources:
- Matt Jones
- R and python more options
parallelmanualdoParallel+foreachvignette
The Fastest One as of February 2024: BiocParallel
All of the above are good solutions, but from practical experience, we have seen that sometimes the previous methods can incur high overheads. A consistently faster solution comes from a package developed and maintained by the R bioinformatics community: BioConductor. The package is named BiocParallel and its interface is almost exactly the same as the one for the parallel package seen in the previous section. The only difference is that we need another package.
First of all, install the BiocParallel package manager via
install.packages("BiocManager")
This will make available an install command, which we use as follows:
install("BiocParallel")
We can then use a simple command. Assuming the function to run multiple times in parallel is named foo, we type:
library('BiocParallel')
system.time(
y <- bplapply(seq_len(100), foo)
)
See this article for more information.