Parallelization in r: Difference between revisions

From Stat DS Computing Wiki
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..."
 
No edit summary
 
(2 intermediate revisions by one other user not shown)
Line 1: Line 1:


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</code>. They include:
[[Category:Parallelization]]


#parallelization in straight <code>for</code>-loop-style with <code>foeach</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 <code>apply</code>-style loops with <code>mclapply</code>
#parallelization with <code>tidyverse</code> nesting-style loops with <code>multidplyr</code> (not discussed see: [https://blog.dominodatalab.com/multicore-data-science-r-python/ R and python parallelization] blog for discussion)


The final option the above list (as of 8/2021) is still too experimental and limited to truly encourage.
#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>


== General starting point ==
The final option in the above list is still too experimental and limited to truly encourage.


To start parallelizing within R we need to have some understanding of how many cores we have. To do figures with out we use the following lines of code:
== General Starting Point ==


&nbsp;
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 at least we less than the total number of cores when working on your personal computer or on the department servers - to leave room for other jobs / use.
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.
 
== 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]


= for-loop style: <code>foreach</code> and <code>doParallel</code> library (<code>foreach ()&nbsp;%dopar%</code>) =
= for-loop style: <code>foreach</code> and <code>doParallel</code> library (<code>foreach ()&nbsp;%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)
 
&nbsp;
<syntaxhighlight lang="r">my_k <- rep(1:14, each = 20)


my_simulation <- function(k) {
my_simulation <- function(k) {
Line 44: Line 35:
}</syntaxhighlight>
}</syntaxhighlight>


== single core analysis ==
== Single Core Analysis ==
 
We naturally want to compare a parallelization approach with the base single core analysis, which can be done with the code as follows:


&nbsp;
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 47:
##  1.811  0.633  2.449</syntaxhighlight>
##  1.811  0.633  2.449</syntaxhighlight>


Additionally, in the <code>foreach</code> package we can also use just a single core in the following way:
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`
 
&nbsp;
<syntaxhighlight lang="r">#single-core `foreach`
library(foreach)
library(foreach)
system.time({
system.time({
Line 72: Line 57:
##  1.876  0.640  2.522</syntaxhighlight>
##  1.876  0.640  2.522</syntaxhighlight>


This structure in the above code using <code>foreach</code> and <code>%do%</code> are very similar to the actual code we use, so it’s useful to understand. It should be clear that the <code>foreach</code> function works very similar to the base <code>for</code>-loop but we need to combined it with <code>%do%</code>. Additionally, notice that the <code>foreach</code> loops of <code>k</code> for values in <code>my_k</code> and we express that differently than in a <code>for</code> loop (<code>k in my_k</code> vs <code>k = my_k</code>).
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
== Multi-Core Analysis ==


&nbsp;
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 73:
##  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 ==
== Additional Parameters ==


#<code>.combine</code>: this parameter tells the system how you’d like to return of each step of the for-loop 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.  
#<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.
#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)
#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 92:
parallel::stopCluster(cl)</syntaxhighlight>
parallel::stopCluster(cl)</syntaxhighlight>
<syntaxhighlight lang="r">|==============================================================        | 83%</syntaxhighlight>
<syntaxhighlight lang="r">|==============================================================        | 83%</syntaxhighlight>
#memory and library passing: Sometimes you have a lot of functions or memory in your RAM before your run for 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 constraint what data is passed into each process with the parameter <code>.export</code> (which also uses strings relative to your global envirnoment).


&nbsp;
&nbsp;
== additional resources ==
<code>doParallel</code> + <code>foreach</code> [https://cran.r-project.org/web/packages/doParallel/vignettes/gettingstartedParallel.pdf vignette]


= lapply style: <code>parallel</code> library (<code>mclapply</code>) =
= lapply style: <code>parallel</code> library (<code>mclapply</code>) =
Line 122: Line 99:
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 ==
== Single Core Analysis ==
 
For a single-core analysis of the same analysis seen in the previous section, we could run


&nbsp;
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 110:
##  1.819  0.641  2.467</syntaxhighlight>
##  1.819  0.641  2.467</syntaxhighlight>


== multi core analysis ==
== 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({
 
&nbsp;
<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 120:
##  2.208  1.636  0.522</syntaxhighlight>
##  2.208  1.636  0.522</syntaxhighlight>


== additional resources ==
== Extra Resources: ==
 
*<code>parallel</code> [https://stat.ethz.ch/R-manual/R-devel/library/parallel/doc/parallel.pdf manual]
 


== '''The fastest one as of February 2024:&nbsp;<code>BiocParallel</code>''' ==
*[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:&nbsp;<code>BiocParallel</code>=


All of the above are good solutions, but from practical experience we have seen that sometimes they can incur in high overheads. A consistently faster solution comes from a package developed and maintainted by the (huge) bioinformatics community that works with R:&nbsp;&nbsp;<code>BioConductor</code>. The package is named <code>BiocParallel&nbsp;</code>and its interface is almost exactly the same as the one for the&nbsp;<code>Parallel</code> package seen in the previous section. The only difference is that we need another package.
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:&nbsp;&nbsp;<code>BioConductor</code>. The package is named <code>BiocParallel&nbsp;</code>and its interface is almost exactly the same as the one for the&nbsp;<code>parallel</code> package seen in the previous section. The only difference is that we need another package.


First of all, install the bioconductor package manager via
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 137:
<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(  

Latest revision as of 03:25, 29 June 2026


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:

  1. Parallelization in straight for-loop-style with foeach
  2. Parallelization in apply-style loops with mclapply
  3. Parallelization with tidyverse nesting-style loops with multidplyr

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()
numCores

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: 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.522

It 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.601

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 %dopar% represents "do parallelization."

Additional Parameters

  1. .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 = c to get a vector back, but we could also use things like rbind, cbind. Note that the default is a list.
  2. Memory and library passing: Sometimes you have a lot of functions or memory in your RAM before your run the parallelized for loop. The foreach 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 .packages command in foreach. For example: .packages = c("dplyr", "tidyr"). Additionally, you can constrain what data is passed into each process with the parameter .export which uses strings relative to your global environment.
  3. 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.467

Multi-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.522

Extra Resources:

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.