Since current computers are essentially deterministic, random number generators used for stochastic simulation aren't truly random at all, and so are more correctly referred to as pseudo-random number generators.
Random number generators typically consist of three main components:
- An internal state,
$s\in S$ - A deterministic function for transforming the internal state,
$f: S\rightarrow S$ - A deterministic function for turning the current internal state into a pseudo-random number (say, a realisation of a
$U(0,1)$ random variable),$g: S\rightarrow [0,1)$
The state space,
Typically,
How these functions are used and accessed depends on the language. In imperative languages, many of the details are often hidden. eg. The internal state might be hidden in a global mutable variable. Then a function, say, random
, could first update the internal state by calling
In some languages/libraries the internal state is stored in a mutable variable, but it is not global. Then you use a seeding function to create a pointer/reference to an internal state that you then need to pass in to any random functions. But again, the random function updates the mutable internal state at the given reference and then turns the new state into a random value to be returned to the user.
In pure functional languages we don't have mutable variables, so we must be more explicit in our handling of the state. The standard way to do this is to have a generator function random
:
def random(s0: S): (S, Double) =
val s1 = f(s0)
(s1, g(s1))
We then need to unpack the result of this function, and be sure to pass the updated state into subsequent calls to the generator. There are ways to make this process more elegant and convenient by using the state monad, but this is again tangential to the current discussion.
A major problem with all of the approaches discussed so far is that they are fundamentally sequential, and don't adapt trivially to a parallel context. Since the state is necessarily finite, repeated application of
An arguably much better approach to parallel random number generation than old-school PPRNGs is to use splittable generators. There are many variations on this approach, but the main idea is to replace the advancement function
with a split function
so that instead of just getting one new state, the split function returns two (or more) states. These states are different to each other and the input state, and the split function is carefully chosen so that if either of the returned states are subsequently split, and so on, the tree of states generated is unlikely to contain any repeats. For historical reasons, the states of a splittable generator are often referred to as "keys".
In this case, the random
function is simply the deterministic function
To understand why splittable generators solve the concurrency and parallelism problem, consider two functions, my_random1
and my_random2
that each use the splittable generator functions
k0 = new_key 42 -- initial key from a seed
[k1, k2] = split_key k0
res1 = my_random1 k1
res2 = my_random2 k2
...
Since the functions are pure and there is no dependence between them, there is no reason why my_random
and my_random2
cannot be executed in parallel. But this argument works at every level of the program. Arbitrary concurrency and parallelism can be employed throughout. From a type safety viewpoint, there is also the added bonus that the intended randomness of any function is clearly indicated in its type signature, since it must take a state/key as input. In languages with support for currying, it is most convenient to always have the random key as the final input parameter.
The downside of splittable generators is that the programmer is responsible for ensuring that no key is ever reused. But it is quite easy to accidentally re-use a key, leading to incorrect behaviour, and bugs in Monte Carlo codes are hard to track down. Smart people are thinking about ways to address this drawback, but for now you just have to be careful!
- Pseudo Random Numbers in JAX
- JAX uses a threefry counter PRNG (described in Salmon et al, 2011)) with a functional array-oriented splitting model (described in Claessen and Palka, 2013)
- Salmon et al (2011) Parallel Random Numbers: As Easy as 1, 2, 3, SC '11: Proceedings of 2011 International Conference for High Performance Computing, Networking, Storage and Analysis, 16. PDF
- Claessen and Palka (2013) Splittable pseudorandom number generators using cryptographic hashing, Haskell '13: Proceedings of the 2013 ACM SIGPLAN symposium on Haskell, 47-58. PDF
- Steele at al (2014) Fast splittable pseudorandom number generators, ACM SIGPLAN Notices, 49(10):453-472. PDF