Last updated: 2023-12-20

Checks: 7 0

Knit directory: muse/

This reproducible R Markdown analysis was created with workflowr (version 1.7.1). The Checks tab describes the reproducibility checks that were applied when the results were created. The Past versions tab lists the development history.


Great! Since the R Markdown file has been committed to the Git repository, you know the exact version of the code that produced these results.

Great job! The global environment was empty. Objects defined in the global environment can affect the analysis in your R Markdown file in unknown ways. For reproduciblity it’s best to always run the code in an empty environment.

The command set.seed(20200712) was run prior to running the code in the R Markdown file. Setting a seed ensures that any results that rely on randomness, e.g. subsampling or permutations, are reproducible.

Great job! Recording the operating system, R version, and package versions is critical for reproducibility.

Nice! There were no cached chunks for this analysis, so you can be confident that you successfully produced the results during this run.

Great job! Using relative paths to the files within your workflowr project makes it easier to run your code on other machines.

Great! You are using Git for version control. Tracking code development and connecting the code version to the results is critical for reproducibility.

The results in this page were generated with repository version 49be9e8. See the Past versions tab to see a history of the changes made to the R Markdown and HTML files.

Note that you need to be careful to ensure that all relevant files for the analysis have been committed to Git prior to generating the results (you can use wflow_publish or wflow_git_commit). workflowr only checks the R Markdown file, but you know if there are other scripts or data files that it depends on. Below is the status of the Git repository when the results were generated:


Ignored files:
    Ignored:    .Rproj.user/
    Ignored:    r_packages_4.3.2/

Untracked files:
    Untracked:  script/fresh.sh

Note that any generated files, e.g. HTML, png, CSS, etc., are not included in this status report because it is ok for generated content to have uncommitted changes.


These are the previous versions of the repository in which changes were made to the R Markdown (analysis/parallel.Rmd) and HTML (docs/parallel.html) files. If you’ve configured a remote Git repository (see ?wflow_git_remote), click on the hyperlinks in the table below to view the files as they were in that past version.

File Version Author Date Message
Rmd 49be9e8 Dave Tang 2023-12-20 Update
html 0b6b70f Dave Tang 2023-07-27 Build site.
Rmd e6c246e Dave Tang 2023-07-27 Worker environment
html 2f4cb47 Dave Tang 2023-07-26 Build site.
Rmd 9367b80 Dave Tang 2023-07-26 pbapply
html 130d11f Dave Tang 2022-11-17 Build site.
Rmd b2043f3 Dave Tang 2022-11-17 Parallel computation in R

Introduction

As stated in the foreach vignette:

Much of parallel computing comes to doing three things: splitting the problem into pieces, executing the pieces in parallel, and combining the results back together.

There are several packages that make it easy to run tasks in parallel:

  • The parallel package that comes with R.
  • The doParallel package is a parallel backend for the foreach package and acts as an interface between foreach and the parallel package.
  • The BiocParallel package tailored for use with Bioconductor.
  • The furrr package parallelises mapping functions from the purrr package.
  • The pbapply package, which is a package for adding a progress bar, but supports parallel execution.

Timing processes with system.time

From ?proc.time:

The “user time” is the CPU time charged for the execution of user instructions of the calling process.

The “system time” is the CPU time charged for execution by the system on behalf of the calling process.

Elapsed time is the amount of time that has elapsed/passed. The user and system time while sleeping is close to zero because the CPU is idly waiting and not executing anything.

system.time(
  Sys.sleep(5)
)
   user  system elapsed 
  0.001   0.001   5.006 

More information is provided on Stack Overflow:

“User CPU time” gives the CPU time spent by the current process (i.e., the current R session and outside the kernel)

“System CPU time” gives the CPU time spent by the kernel (the operating system) on behalf of the current process. The operating system is used for things like opening files, doing input or output, starting other processes, and looking at the system clock: operations that involve resources that many processes must share.

Testing data

Create a list of 100 data frames each with 5,000 observations across 100 variables.

create_df <- function(n, m, seed = 1984){
  set.seed(seed)
  as.data.frame(
    matrix(
      data = rnorm(n = n * m),
      nrow = n,
      ncol = m
    )
  )
}

my_list <- lapply(1:100, function(x) create_df(5000, 100, x))
length(my_list)
[1] 100

Number of threads used

This is a parameterised notebook; the number of threads used for the code examples is 8.

params$threads
[1] 8

parallel

Load the parallel package.

library(parallel)

Create a summary of each variable in each data frame without parallelisation.

system.time(
  my_sum <- lapply(my_list, summary)
)
   user  system elapsed 
  3.720   0.014   3.743 

The mclapply function can be used to process a list in parallel. Note that this function uses forking, which is not available on Windows.

system.time(
  my_sum_mc <- mclapply(my_list, summary, mc.cores = params$threads)
)
   user  system elapsed 
  0.997   0.458   0.698 

Compare the two summaries.

identical(my_sum, my_sum_mc)
[1] TRUE

Another way to run the jobs in parallel is via sockets. For Windows users, you will need to use this method for parallelisation. In addition, you need to use the parLapply function instead of mclapply.

cl <- makeCluster(params$threads)
system.time(
  my_sum_sock <- parLapply(cl, my_list, summary)
)
   user  system elapsed 
  0.601   0.297   1.870 
stopCluster(cl)

identical(my_sum_mc, my_sum_sock)
[1] TRUE

Note that forking is faster.

Worker environment

If you run the code below:

cl <- makeCluster(4)
system.time(
  test <- parLapply(cl, 1:4, function(x){
    class(my_list)
  })
)
stopCluster(cl)

you will get the following error:

Error in checkForRemoteErrors(val) : 
  4 nodes produced errors; first error: object 'my_list' not found

This is because each worker is using a different environment. To make the my_list object available to each worker, we use the clusterExport() function.

cl <- makeCluster(4)
clusterExport(cl, list("my_list"))
system.time(
  test2 <- parSapply(cl, 1:4, function(x){
    class(my_list)
  })
)
   user  system elapsed 
  0.001   0.000   0.042 
stopCluster(cl)

test2
[1] "list" "list" "list" "list"

pbapply

Parallelisation with a progress bar! From the help page of pblapply:

Parallel processing can be enabled through the cl argument. parLapply is called when cl is a ‘cluster’ object, mclapply is called when cl is an integer. Showing the progress bar increases the communication overhead between the main process and nodes / child processes compared to the parallel equivalents of the functions without the progress bar. The functions fall back to their original equivalents when the progress bar is disabled (i.e. getOption(“pboptions”)$type == “none” or dopb() is FALSE). This is the default when interactive() if FALSE (i.e. called from command line R script).

library(pbapply)
cl <- makeCluster(params$threads)
system.time(
  my_sum_pb <- pblapply(my_list, summary, cl = cl)
)
   user  system elapsed 
  0.763   0.313   1.928 
stopCluster(cl)

identical(my_sum_mc, my_sum_pb)
[1] TRUE

Use mclapply.

system.time(
  my_sum_pb_fork <- pblapply(my_list, summary, cl = params$threads)
)
   user  system elapsed 
  0.530   0.210   0.699 
identical(my_sum_pb, my_sum_pb_fork)
[1] TRUE

doParallel

Load the doParallel package.

library(doParallel)
Loading required package: foreach
Loading required package: iterators

Using foreach.

cl <- makeCluster(params$threads)
registerDoParallel(cl)

system.time(
  my_sum_dopar <- foreach(l = my_list) %dopar% {
    summary(l)
  }
)
   user  system elapsed 
  0.884   0.378   2.439 
stopCluster(cl)

identical(my_sum_mc, my_sum_dopar)
[1] TRUE

BiocParallel

Load BiocParallel.

library(BiocParallel)

Using bplapply.

param <- SnowParam(workers = params$threads, type = "SOCK")
system.time(
  my_sum_bp <- bplapply(my_list, summary, BPPARAM = param)
)
   user  system elapsed 
  0.823   0.298   7.841 
identical(my_sum_mc, my_sum_bp)
[1] TRUE

furrr

Load required libraries.

library(furrr)
Loading required package: future
library(purrr)

Attaching package: 'purrr'
The following objects are masked from 'package:foreach':

    accumulate, when

Map without parallelisation.

system.time(
  my_sum_pur <- map(my_list, summary)
)
   user  system elapsed 
  4.118   0.028   4.155 
identical(my_sum_mc, my_sum_pur)
[1] TRUE

Map with parallelisation.

plan(multisession, workers = params$threads)
system.time(
  my_sum_fur <- future_map(my_list, summary)
)
   user  system elapsed 
  0.766   0.516   3.106 
identical(my_sum_pur, my_sum_fur)
[1] TRUE

Summary

So, which package should you use? BiocParallel and furrr are tailored for use with Bioconductor and purrr, so use those packages accordingly.

For parallelisation over a list, use parallel. The foreach function provides more flexibility when parallelising, so use the doParallel package if you have a more complicated task.


sessionInfo()
R version 4.3.2 (2023-10-31)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 22.04.3 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.20.so;  LAPACK version 3.10.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] purrr_1.0.2         furrr_0.3.1         future_1.33.0      
[4] BiocParallel_1.36.0 doParallel_1.0.17   iterators_1.0.14   
[7] foreach_1.5.2       pbapply_1.7-2       workflowr_1.7.1    

loaded via a namespace (and not attached):
 [1] sass_0.4.8        utf8_1.2.4        stringi_1.8.3     listenv_0.9.0    
 [5] digest_0.6.33     magrittr_2.0.3    evaluate_0.23     fastmap_1.1.1    
 [9] rprojroot_2.0.4   jsonlite_1.8.8    processx_3.8.3    whisker_0.4.1    
[13] ps_1.7.5          promises_1.2.1    httr_1.4.7        fansi_1.0.6      
[17] codetools_0.2-19  jquerylib_0.1.4   cli_3.6.2         rlang_1.1.2      
[21] parallelly_1.36.0 cachem_1.0.8      yaml_2.3.8        tools_4.3.2      
[25] httpuv_1.6.13     globals_0.16.2    vctrs_0.6.5       R6_2.5.1         
[29] lifecycle_1.0.4   git2r_0.33.0      stringr_1.5.1     fs_1.6.3         
[33] pkgconfig_2.0.3   callr_3.7.3       pillar_1.9.0      bslib_0.6.1      
[37] later_1.3.2       glue_1.6.2        Rcpp_1.0.11       xfun_0.41        
[41] tibble_3.2.1      rstudioapi_0.15.0 knitr_1.45        htmltools_0.5.7  
[45] snow_0.4-4        rmarkdown_2.25    compiler_4.3.2    getPass_0.2-4