% \documentclass[a4paper,12pt]{article} %\VignetteIndexEntry{Using the package doRNG} %\VignetteDepends{doRNG,doParallel,knitr,doRedis,rbenchmark,pkgmaker} %\VignetteCompiler{knitr} %\VignetteEngine{knitr::knitr} \usepackage{a4wide} \usepackage{xspace} \usepackage[colorlinks]{hyperref} % for hyperlinks \usepackage{tocloft} \renewcommand{\cftsecleader}{\cftdotfill{\cftdotsep}} \usepackage[toc]{multitoc} % add preamble from pkgmaker <>= pkgmaker::latex_preamble() @ % REFERENCES \usepackage[minnames=1,maxnames=2,backend=bibtex]{biblatex} \AtEveryCitekey{\clearfield{url}} <>= pkgmaker::latex_bibliography('doRNG') @ \newcommand{\citet}[1]{\citeauthor{#1}~\cite{#1}} %% \newcommand{\graphwidth}{0.9\columnwidth} % clever references \usepackage[noabbrev, capitalise, nameinlink]{cleveref} \newcommand{\dorng}{\code{\%dorng\%}\xspace} \title{Using the \code{doRNG} package\\ {\small \Rpkg{doRNG} -- Version \Sexpr{packageVersion('doRNG')}}} \author{Renaud Gaujoux} \begin{document} \maketitle \tableofcontents \section*{Introduction} \addcontentsline{toc}{section}{Introduction} Research reproducibility is an issue of concern, e.g. in bioinformatics \cite{Hothorn2011,Stodden2011,Ioannidis2008}. Some analyses require multiple independent runs to be performed, or are amenable to a split-and-reduce scheme. For example, some optimisation algorithms are run multiple times from different random starting points, and the result that achieves the least approximation error is selected. The \citeCRANpkg{foreach} provides a very convenient way to perform parallel computations, with different parallel environments such as MPI or Redis, using a transparent loop-like syntax: <>= options(width=90) library(pkgmaker) library(knitr) opts_chunk$set(size = "footnotesize") knit_hooks$set(try = pkgmaker::hook_try) @ <>= # load and register parallel backend for multicore computations library(doParallel) cl <- makeCluster(2) registerDoParallel(cl) # perform 5 tasks in parallel x <- foreach(i=1:5) %dopar% { i + runif(1) } unlist(x) @ For each parallel environment a \emph{backend} is implemented as a specialised \code{\%dopar\%} operator, which performs the setup and pre/post-processing specifically required by the environment (e.g. export of variable to each worker). The \code{foreach} function and the \code{\%dopar\%} operator handle the generic parameter dispatch when the task are split between worker processes, as well as the reduce step -- when the results are returned to the master worker. When stochastic computations are involved, special random number generators must be used to ensure that the separate computations are indeed statistically independent -- unless otherwise wanted -- and that the loop is reproducible. In particular, standard \code{\%dopar\%} loops are not reproducible: <>= # with standard %dopar%: foreach loops are not reproducible set.seed(123) res <- foreach(i=1:5) %dopar% { runif(3) } set.seed(123) res2 <- foreach(i=1:5) %dopar% { runif(3) } identical(res, res2) @ A random number generator commonly used to achieve reproducibility is the combined multiple-recursive generator from \citet{Lecuyer1999}. This generator can generate independent random streams, from a 6-length numeric seed. The idea is then to generate a sequence of random stream of the same length as the number of iteration (i.e. tasks) and use a different stream when computing each one of them. The \citeCRANpkg{doRNG} provides convenient ways to implement reproducible parallel \code{foreach} loops, independently of the parallel backend used to perform the computation. We illustrate its use, showing how non-reproducible loops can be made reproducible, even when tasks are not scheduled in the same way in two separate set of runs, e.g. when the workers do not get to compute the same number of tasks or the number of workers is different. The package has been tested with the \CRANpkg*{doParallel} and \CRANpkg*{doMPI} packages \citepkg{Rpackage:doMPI,Rpackage:doParallel}, but should work with other backends such as provided by the \citeCRANpkg{doRedis}. \section{The \texttt{\%dorng\%} operator} The \Rpkg{doRNG} defines a new generic operator, \code{\%dorng\%}, to be used with foreach loops, instead of the standard {\%dopar\%}. Loops that use this operator are \emph{de facto} reproducible. <>= # load the doRNG package library(doRNG) # using %dorng%: loops _are_ reproducible set.seed(123) res <- foreach(i=1:5) %dorng% { runif(3) } set.seed(123) res2 <- foreach(i=1:5) %dorng% { runif(3) } identical(res, res2) @ \subsection{How it works} For a loop with $N$ iterations, the \code{\%dorng\%} operator internally performs the following tasks: \begin{enumerate} \item generate a sequence of random seeds $(S_i)_{1\leq i\leq N}$ for the \proglang{R} random number generator \code{"L'Ecuyer-CMRG"} \cite{Lecuyer1999}, using the function \code{nextRNGStream} from the \citeCRANpkg{parallel}, which ensure the different RNG streams are statistically independent; \item modify the loop's \proglang{R} expression so that the random number generator is set to \code{"L'Ecuyer-CMRG"} at the beginning of each iteration, and is seeded with consecutive seeds in $(S_n)$: iteration $i$ is seeded with $S_i$, $1\leq i\leq N$; \item call the standard \code{\%dopar\%} operator, which in turn calls the relevant (i.e. registered) foreach parallel backend; \item store the whole sequence of random seeds as an attribute in the result object: <>= attr(res, 'rng') @ \end{enumerate} \subsection{Seeding computations} Sequences of random streams for \code{"L'Ecuyer-CMRG"} are generated using a 6-length integer seed, e.g.,: <>= nextRNGStream(c(407L, 1:6)) @ However, the \code{\%dorng\%} operator provides alternative -- convenient -- ways of seeding reproducible loops. \begin{description} \item[\code{set.seed}:] as shown above, calling \code{set.seed} before the loop ensure reproducibility of the results, using a single integer as a seed. The actual 6-length seed is then generated with an internal call to \code{RNGkind("L'Ecuyer-CMRG")}. \item[\code{.options.RNG} with single integer:] the \dorng operator support options that can be passed in the \code{foreach} statement, containing arguments for the internal call to \code{set.seed}: <>= # use a single numeric as a seed s <- foreach(i=1:5, .options.RNG=123) %dorng% { runif(3) } s2 <- foreach(i=1:5, .options.RNG=123) %dorng% { runif(3) } identical(s, s2) @ \noindent \textbf{Note}: calling \code{set.seed} before the loop is equivalent to passing the seed in \code{.options.RNG}. See \cref{sec:set_seed} for more details. \medskip The kind of Normal generator may also be passed in \code{.options.RNG}: <>= ## Pass the Normal RNG kind to use within the loop # results are identical if not using the Normal kind in the loop optsN <- list(123, normal.kind="Ahrens") resN.U <- foreach(i=1:5, .options.RNG=optsN) %dorng% { runif(3) } identical(resN.U[1:5], res[1:5]) # Results are different if the Normal kind is used and is not the same resN <- foreach(i=1:5, .options.RNG=123) %dorng% { rnorm(3) } resN1 <- foreach(i=1:5, .options.RNG=optsN) %dorng% { rnorm(3) } resN2 <- foreach(i=1:5, .options.RNG=optsN) %dorng% { rnorm(3) } identical(resN[1:5], resN1[1:5]) identical(resN1[1:5], resN2[1:5]) @ \item[\code{.options.RNG} with 6-length:] the actual 6-length integer seed used for the first RNG stream may be passed via \code{options.RNG}: <>= # use a 6-length numeric s <- foreach(i=1:5, .options.RNG=1:6) %dorng% { runif(3) } attr(s, 'rng')[1:3] @ \item[\code{.options.RNG} with 7-length:] a 7-length integer seed may also be passed via \code{options.RNG}, which is useful to seed a loop with the value of \code{.Random.seed} as used in some iteration of another loop\footnote{Note that the RNG kind is then always required to be the \code{"L'Ecuyer-CMRG"}, i.e. the first element of the seed must have unit 7 (e.g. 407 or 107).}: <>= # use a 7-length numeric, used as first value for .Random.seed seed <- attr(res, 'rng')[[2]] s <- foreach(i=1:5, .options.RNG=seed) %dorng% { runif(3) } identical(s[1:4], res[2:5]) @ \item[\code{.options.RNG} with complete sequence of seeds:] the complete description of the sequence of seeds to be used may be passed via \code{options.RNG}, as a list or a matrix with the seeds in columns. This is useful to seed a loop exactly as desired, e.g. using an RNG other than \code{"L'Ecuyer-CMRG"}, or using different RNG kinds in each iteration, which probably have different seed length, in order to compare their stochastic properties. It also allows to reproduce \code{\%dorng\%} loops without knowing their seeding details: <>= # reproduce previous %dorng% loop s <- foreach(i=1:5, .options.RNG=res) %dorng% { runif(3) } identical(s, res) ## use completely custom sequence of seeds (e.g. using RNG "Marsaglia-Multicarry") # as a matrix seedM <- rbind(rep(401, 5), mapply(rep, 1:5, 2)) seedM sM <- foreach(i=1:5, .options.RNG=seedM) %dorng% { runif(3) } # same seeds passed as a list seedL <- lapply(seq(ncol(seedM)), function(i) seedM[,i]) sL <- foreach(i=1:5, .options.RNG=seedL) %dorng% { runif(3) } identical(sL, sM) @ \end{description} \subsection{Difference between \texttt{set.seed} and \texttt{.options.RNG}} \label{sec:set_seed} While it is equivalent to seed \dorng loops with \code{set.seed} and \code{.options.RNG}, it is important to note that the result depends on the current RNG kind \footnote{See \cref{sec:issues} about a bug in versions < 1.4 on this feature.}: <>= # default RNG kind RNGkind('default') def <- foreach(i=1:5, .options.RNG=123) %dorng% { runif(3) } # Marsaglia-Multicarry RNGkind('Marsaglia') mars <- foreach(i=1:5, .options.RNG=123) %dorng% { runif(3) } identical(def, mars) # revert to default RNG kind RNGkind('default') @ This is a ``normal'' behaviour, which is a side-effect of the expected equivalence between \code{set.seed} and \code{.options.RNG}. This should not be a problem for reproducibility though, as R RNGs are stable across versions, and loops are most of the time used with the default RNG settings. In order to ensure seeding is independent from the current RNG, one has to pass a 7-length numeric seed to \code{.options.RNG}, which is then used directly as a value for \code{.Random.seed} (see below). \section{Parallel environment independence} An important feature of \code{\%dorng\%} loops is that their result is independent of the underlying parallel physical settings. Two separate runs seeded with the same value will always produce the same results. Whether they use the same number of worker processes, parallel backend or task scheduling does not influence the final result. This also applies to computations performed sequentially with the \code{doSEQ} backend. The following code illustrates this feature using 2 or 3 workers. <>= # define a stochastic task to perform task <- function() c(pid=Sys.getpid(), val=runif(1)) # using the previously registered cluster with 2 workers set.seed(123) res_2workers <- foreach(i=1:5, .combine=rbind) %dorng% { task() } # stop cluster stopCluster(cl) # Sequential computation registerDoSEQ() set.seed(123) res_seq <- foreach(i=1:5, .combine=rbind) %dorng% { task() } # # Using 3 workers # NB: if re-running this vignette you should edit to force using 3 here cl <- makeCluster( if(isManualVignette()) 3 else 2) length(cl) # register new cluster registerDoParallel(cl) set.seed(123) res_3workers <- foreach(i=1:5, .combine=rbind) %dorng% { task() } # task schedule is different pid <- rbind(res1=res_seq[,1], res_2workers[,1], res2=res_3workers[,1]) storage.mode(pid) <- 'integer' pid # results are identical identical(res_seq[,2], res_2workers[,2]) && identical(res_2workers[,2], res_3workers[,2]) @ \section{Reproducible \texttt{\%dopar\%} loops} The \Rpkg{doRNG} also provides a non-invasive way to convert \code{\%dopar\%} loops into reproducible loops, i.e. without changing their actual definition. It is useful to quickly ensure the reproducibility of existing code or functions whose definition is not accessible (e.g. from other packages). This is achieved by registering the \code{doRNG} backend: <>= set.seed(123) res <- foreach(i=1:5) %dorng% { runif(3) } registerDoRNG(123) res_dopar <- foreach(i=1:5) %dopar% { runif(3) } identical(res_dopar, res) attr(res_dopar, 'rng') @ \section{Reproducibile sets of loops} Sequences of multiple loops are reproducible, whether using the \code{\%dorng\%} operator or the registered \code{doRNG} backend: <>= set.seed(456) s1 <- foreach(i=1:5) %dorng% { runif(3) } s2 <- foreach(i=1:5) %dorng% { runif(3) } # the two loops do not use the same streams: different results identical(s1, s2) # but the sequence of loops is reproducible as a whole set.seed(456) r1 <- foreach(i=1:5) %dorng% { runif(3) } r2 <- foreach(i=1:5) %dorng% { runif(3) } identical(r1, s1) && identical(r2, s2) # one can equivalently register the doRNG backend and use %dopar% registerDoRNG(456) r1 <- foreach(i=1:5) %dopar% { runif(3) } r2 <- foreach(i=1:5) %dopar% { runif(3) } identical(r1, s1) && identical(r2, s2) @ \section{Nested and conditional loops} \label{sec:nested} Nested and conditional foreach loops are currently not supported and generate an error: <>= # nested loop try( foreach(i=1:10) %:% foreach(j=1:i) %dorng% { rnorm(1) } ) # conditional loop try( foreach(i=1:10) %:% when(i %% 2 == 0) %dorng% { rnorm(1) } ) @ In this section, we propose a general work around for this kind of loops, that will eventually be incorporated in the \code{\%dorng\%} operator -- when I find out how to mimic its behaviour from the operator itself. \subsection{Nested loops} The idea is to create a sequence of RNG seeds before the outer loop, and use each of them successively to set the RNG in the inner loop -- which is exactly what \code{\%dorng\%} does for simple loops: <>= # doRNG must not be registered registerDoParallel(cl) # generate sequence of seeds of length the number of computations n <- 10; p <- 5 rng <- RNGseq( n * p, 1234) # run standard nested foreach loop res <- foreach(i=1:n) %:% foreach(j=1:p, r=rng[(i-1)*p + 1:p]) %dopar% { # set RNG seed rngtools::setRNG(r) # do your own computation ... c(i, j, rnorm(1)) } # Compare against the equivalent sequential computations k <- 1 res2 <- foreach(i=1:n) %:% foreach(j=1:p) %do%{ # set seed rngtools::setRNG(rng[[k]]) k <- k + 1 # do your own computation ... c(i, j, rnorm(1)) } stopifnot( identical(res, res2) ) @ The following is a more complex example with unequal -- but \textbf{known \emph{a priori}} -- numbers of iterations performed in the inner loops: <>= # generate sequence of seeds of length the number of computations n <- 10 rng <- RNGseq( n * (n+1) / 2, 1234) # run standard nested foreach loop res <- foreach(i=1:n) %:% foreach(j=1:i, r=rng[(i-1)*i/2 + 1:i]) %dopar%{ # set RNG seed rngtools::setRNG(r) # do your own computation ... c(i, j, rnorm(1)) } # Compare against the equivalent sequential computations k <- 1 res2 <- foreach(i=1:n) %:% foreach(j=1:i) %do%{ # set seed rngtools::setRNG(rng[[k]]) k <- k + 1 # do your own computation ... c(i, j, rnorm(1)) } stopifnot( identical(res, res2) ) @ \subsection{Conditional loops} The work around used for nested loops applies to conditional loops that use the \code{when()} clause. It ensures that the RNG seed use for a given inner iteration does not depend on the filter, but only on its index in the unconditional-unfolded loop: <>= # un-conditional single loop resAll <- foreach(i=1:n, .options.RNG=1234) %dorng%{ # do your own computation ... c(i, rnorm(1)) } # generate sequence of RNG rng <- RNGseq(n, 1234) # conditional loop: even iterations resEven <- foreach(i=1:n, r=rng) %:% when(i %% 2 == 0) %dopar%{ # set RNG seed rngtools::setRNG(r) # do your own computation ... c(i, rnorm(1)) } # conditional loop: odd iterations resOdd <- foreach(i=1:n, r=rng) %:% when(i %% 2 == 1) %dopar%{ # set RNG seed rngtools::setRNG(r) # do your own computation ... c(i, rnorm(1)) } # conditional loop: only first 2 and last 2 resFL <- foreach(i=1:n, r=rng) %:% when(i %in% c(1,2,n-1,n)) %dopar%{ # set RNG seed rngtools::setRNG(r) # do your own computation ... c(i, rnorm(1)) } # compare results stopifnot( identical(resAll[seq(2,n,by=2)], resEven) ) stopifnot( identical(resAll[seq(1,n,by=2)], resOdd) ) stopifnot( identical(resAll[c(1,2,n-1,n)], resFL) ) @ \subsection{Nested conditional loops} Conditional nested loops may use the same work around, as shown in this intricate example: <>= # generate sequence of seeds of length the number of computations n <- 10 rng <- RNGseq( n * (n+1) / 2, 1234) # run standard nested foreach loop res <- foreach(i=1:n) %:% when(i %% 2 == 0) %:% foreach(j=1:i, r=rng[(i-1)*i/2 + 1:i]) %dopar%{ # set RNG seed rngtools::setRNG(r) # do your own computation ... c(i, j, rnorm(1)) } # Compare against the equivalent sequential computations k <- 1 resAll <- foreach(i=1:n) %:% foreach(j=1:i) %do%{ # set seed rngtools::setRNG(rng[[k]]) k <- k + 1 # do your own computation ... c(i, j, rnorm(1)) } stopifnot( identical(resAll[seq(2,n,by=2)], res) ) @ \section{Performance overhead} The extra setup performed by the \code{\%dorng\%} operator leads to a slight performance over-head, which might be significant for very quick computations, but should not be a problem for realistic computations. The benchmarks below show that a \code{\%dorng\%} loop may take up to two seconds more than the equivalent \code{\%dopar\%} loop, which is not significant in practice, where parallelised computations typically take several minutes. <>= # load rbenchmark library(rbenchmark) # comparison is done on sequential computations registerDoSEQ() rPar <- function(n, s=0){ foreach(i=1:n) %dopar% { Sys.sleep(s) } } rRNG <- function(n, s=0){ foreach(i=1:n) %dorng% { Sys.sleep(s) } } # run benchmark cmp <- benchmark(rPar(10), rRNG(10) , rPar(25), rRNG(25) , rPar(50), rRNG(50) , rPar(50, .01), rRNG(50, .01) , rPar(10, .05), rRNG(10, .05) , replications=5) # order by increasing elapsed time cmp[order(cmp$elapsed), ] @ \section{Known issues} \label{sec:issues} \begin{itemize} \item Nested and/or conditional foreach loops using the operator \code{\%:\%} are not currently not supported (see \cref{sec:nested} for a work around). \item An error is thrown in \code{doRNG} 1.2.6, when the package \code{iterators} was not loaded, when used with \code{foreach} >= 1.4. \item There was a bug in versions prior to \code{1.4}, which caused \code{set.seed} and \code{.options.RNG} not to be equivalent when the current RNG was \code{"L'Ecuyer-CMRG"}. This behaviour can still be reproduced by setting: <>= doRNGversion('1.3') @ To revert to the latest default behaviour: <>= doRNGversion(NULL) @ \end{itemize} \section{News and changes} {\scriptsize \begin{verbatim} <>= cat(paste(readLines(system.file('NEWS', package='doRNG')), collapse="\n")) @ \end{verbatim} } \section*{Cleanup} <>= stopCluster(cl) @ \section*{Session information} \addcontentsline{toc}{section}{Session information} <>= sessionInfo() @ \printbibliography[heading=bibintoc] \end{document}