Random number generators in JOREK
For the JOREK-particles extension many random numbers are required, for example in the particle initialization routines. To meet the combined demands of speed, statistical quality, and parallel correctness, JOREK uses the PCG family of pseudo-random number generators alongside an optional Sobol' quasi-random sequence generator.
Why not use the Fortran built-in RNG?
The Fortran intrinsic random_number has several limitations that make it unsuitable for JOREK's use case. The builtin random-number generators in Fortran are compiler-dependent and often of low quality. Furthermore they are not thread-safe and cannot be used in OpenMP parallel regions without external locking. Additionally they do not offer strided sampling and jump-ahead to specific values. Those attributes are very useful when generating related sets of random numbers on different MPI tasks.
Implementation
A common abstract interface for all generators is defined in tools/mod_rng.f90, making it straightforward to swap generators without changing calling code. The interface is currently implemented by:
- pcg32 (
tools/mod_pcg32_rng.f90usingtools/mod_pcg32.f90andtools/pcg_basic.c) - Sobol sequence (
tools/mod_sobseq_rng.f90usingtools/mod_sobseq.f90)
Interface
An implementation of the RNG interface must provide the following routines:
initialize(rng, n_dims, seed, n_streams, i_stream, ierr)next(rng, out)jump_ahead(rng, delta)
whose names should be self-explanatory.
Usage
Parameters for rng%initialize:
| Parameter | Description |
|---|---|
n_dims | Number of random values produced per call to next. |
seed | Integer seed for the generator. |
n_streams | Total number of independent streams being initialised (across all threads and ranks). |
i_stream | Index of the stream assigned to this instance, in the range [1, n_streams]. |
ierr | Returns 0 on success, non-zero on failure. |
Serial context
Please consider the following example in a serial context (only one OpenMP thread):
use mod_pcg32_rng
use mod_random_seed
real*8 :: rans(7)
type(pcg32_rng) :: rng ! or class(type_rng), allocatable :: rng ! and allocate(pcg32_rng::rng)
call rng%initialize(n_dims=7, seed=random_seed(), n_streams=1, i_stream=1, ierr=ifail)
! create 7 random numbers per call on 1 thread selecting the first stream
call rng%next(rans)
OpenMP parallel context
In the parallel context, a number of streams of random numbers are defined, where each thread selects one stream. The following OpenMP example is adapted from particles/examples/W_sputtering_rad_coll.f90:
type(pcg32_rng), dimension(:), allocatable :: rng
real*8 :: ran(6)
integer :: n_stream, i_rng, i, j
seed = random_seed()
n_stream = 1
!$ n_stream = omp_get_max_threads()
allocate(rng(n_stream))
do i=1,n_stream
call rng(i)%initialize(6, seed, n_stream, i)
end do
!$omp parallel default(private), shared(rng)
i_rng = 1
!$ i_rng = omp_get_thread_num()+1
!$omp do
do j=1,size(particles,1)
call rng(i_rng)%next(ran)
end do
!$omp end do
!$omp end parallel
Hybrid MPI + OpenMP context
To use this with a hybrid MPI-OpenMP architecture we need to calculate the stream number and index across the processors. This can be done easily with a few MPI calls combined with the above example.
call MPI_COMM_RANK(MPI_COMM_WORLD, my_id, ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, n_cpu, ierr)
! Calculate a single random seed and communicate it over MPI
if (my_id .eq. 0) seed = random_seed()
call MPI_Bcast(seed, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, ierr)
! Set up rngs
n_threads = 1
!$ n_threads = omp_get_max_threads()
allocate(rng(0:n_threads-1), source=pcg32_rng())
n_streams = n_cpu*n_threads
do i_thread=0, n_threads-1
seq=my_id*n_threads + i_thread + 1
call rng(i_thread)%initialize(5, seed, n_streams, seq, ierr)
if(ierr .ne. 0) call MPI_ABORT(MPI_COMM_WORLD, -1, ierr)
end do
Seeding
A module to create seed numbers for the generator is provided from /dev/urandom or by xor-ing the time and current process pid in tools/mod_random_seed.f90. If reproducibility across runs is required (e.g. for debugging), replace random_seed() with a fixed integer.
Sobol' sequence QRNG
The sobseq generator has some properties which need to be treated with care.
Output stream correlation
The strength of the Sobol' sequence is the correlation in the output stream, but this causes some problems too. For instance in the first stream of the sobseq generator every even number is > 0.5. This has consequences for rejection sampling in parallel, where we use the strided generator to provide multiple streams. It is very important to try an equal number of samples on every thread, instead of looking for an equal number of accepted proposals.
Strided generation
Due to the strided implementation of the sobseq generator it can only accept a number of streams which is a power of 2.
Seeding
The Sobol' series has no possibility of seeding with any number. The seed parameter is ignored in calls to rng%initialize.