Why do withr::with_seed and R.utils::withSeed generate different results when used in async code?

211 views Asked by At

I'm trying to learn async programming using R so that I can implement an app that requires generation of random numbers with specified seeds (always with specified seeds). I've been using R.utils::withSeed for this, but I know that withr::with_seed also exists so I thought I might check that out.

I know random number generation is tricky, so I've been trying to run simple examples to try to understand how things work. I need:

  • ...to always get the same random numbers when using the same seed
  • ...to get the same random numbers with the same seed regardless of whether I'm using the async framework (so I should be able to run the code outside a promise and get the same answer)

In the code below, I define two functions to generate random numbers, settings the seed with either withr::with_seed or R.utils::withSeed.

  • These two functions give me the same answer when run outside the promise.
  • These two functions give different answers when run inside the promise.
  • The withr::with_seed version gives the same answer inside or outside the promise.
  • The R.utils::withSeed version gives different answers inside or outside the promise.

The answers seem to be consistent across multiple runs, however.

My question is: why? Is this a bug in R.utils::withSeed, or am I misunderstanding something?


Code


library(future)
library(promises)

plan(multisession)


s0_R = function(seed = 1, n = 1){
  R.utils::withSeed(expr = {
    rnorm(n)
  }, seed = seed)
}

s0_w = function(seed = 1, n = 1){
  withr::with_seed(
     seed = seed,
     code = {
       rnorm(n)
     })
}


s_R = function(seed = 1, n = 1){
  future_promise(
    {
    Sys.sleep(5)
    s0_R(seed, n)
    }, 
    seed = TRUE
  )
} 

s_w = function(seed = 1, n = 1){
  future_promise(
    {
      Sys.sleep(5)
      s0_w(seed, n)
    }, 
    seed = TRUE
  )
} 

s0_R(123) %>%
  paste(" (R.utils::withSeed)\n") %>%
  cat()
# -0.560475646552213  (R.utils::withSeed)

s0_w(123) %>%
  paste(" (withr::with_seed)\n") %>%
  cat()
# -0.560475646552213  (withr::with_seed)

s_R(123) %...>%
  paste(" (async, R.utils::withSeed)\n") %...>%
  cat()

s_w(123) %...>%
  paste(" (async, withr::with_seed)\n") %...>%
  cat()

# Results occur later...
# -0.968592726552943  (async, R.utils::withSeed)
# -0.560475646552213  (async, withr::with_seed)
1

There are 1 answers

0
Pavel Obraztcov On BEST ANSWER

The future package sets the default RNG kind to L'Ecuyer-CMRG, whereas R's default is Mersenne-Twister. withr::with_seed resets the RNG kind to "default" (i.e. Mersenne-Twister) unless it is explicitly specified in the .rng_kind argument. R.utils::withSeed, on the other hand, does not do anything about RNG kind by default, but the RNG kind can be specified using the ... argument list passed to set.seed. In your example, the s0_R can be modified as follows to get the same results inside and outside the promise.

s0_R = function(seed = 1, n = 1){
  R.utils::withSeed(expr = {
    rnorm(n)
  }, seed = seed, kind = "default")
}