When you want to use random number generators (RNG) for parallel
computations, you need to make sure that the sequences of random numbers
used by the different processes do not overlap. There are two main
approaches to this problem:^{1}

- Partition the complete sequence of random numbers produced for one seed into non-overlapping sequences and assign each process one sub-sequence.
- Re-parametrize the generator to produce independent sequences for the same seed.

The RNGs included in `dqrng`

offer at least one of these
methods for parallel RNG usage. When using the R or C++ interface
independent streams can be accessed using the two argument
`dqset.seed(seed, stream)`

or
`dqset_seed(seed, stream)`

functions.

The Threefry engine uses internally a counter with \(2^{256}\) possible states, which can be
split into different substreams. When used from R or C++ with the two
argument `dqset.seed`

or `dqset_seed`

this counter
space is split into \(2^{64}\) streams
with \(2^{192}\) possible states each.
This is equivalent to \(2^{64}\)
streams with a period of \(2^{194}\)
each.

In the following example a matrix with random numbers is generated in parallel using the parallel package. The resulting correlation matrix should be close to the identity matrix if the different streams are independent:

```
library(parallel)
cl <- parallel::makeCluster(2)
res <- clusterApply(cl, 1:8, function(stream, seed, N) {
library(dqrng)
dqRNGkind("Threefry")
dqset.seed(seed, stream)
dqrnorm(N)
}, 42, 1e6)
stopCluster(cl)
res <- matrix(unlist(res), ncol = 8)
symnum(x = cor(res), cutpoints = c(0.001, 0.003, 0.999),
symbols = c(" ", "?", "!", "1"),
abbr.colnames = FALSE, corr = TRUE)
```

Correlation matrix:

```
[1,] 1
[2,] 1
[3,] ? 1
[4,] ? ? 1
[5,] ? ? 1
[6,] ? 1
[7,] ? 1
[8,] ? 1
attr(,"legend")
[1] 0 ‘ ’ 0.001 ‘?’ 0.003 ‘!’ 0.999 ‘1’ 1
```

As expected the correlation matrix for the different columns is almost equal to the identity matrix.

The Xoshiro256+/++/** generators has a period of \(2^{256} -1\) and offers \(2^{128}\) sub-sequences that are \(2^{128}\) random draws apart as well as
\(2^{64}\) streams that are \(2^{192}\) random draws apart. The
Xoroshiro128+/++/** generators has a period of \(2^{128} -1\) and offers \(2^{64}\) sub-sequences that are \(2^{64}\) random draws apart as well as
\(2^{32}\) streams that are \(2^{98}\) random draws apart. You can go
from one sub-sequence to the next using the `jump()`

or
`long_jump()`

method and the convenience wrapper
`jump(int n)`

or `long_jump(int n)`

, which
advances to the `n`

th sub-sequence. When used from R or C++
with the two argument `dqset.seed`

and
`dqset_seed`

you get \(2^{64}\) streams that are \(2^{192}\) and \(2^{64}\) random draws apart for
Xoshiro256+/++/** and Xoroshiro128+/++/**, respectively.

As an example using C++ we draw and sum a large number of uniformly
distributed numbers. This is done several times sequentially as well as
using OpenMP for parallelisation. Care has been taken to keep the global
RNG `rng`

usable outside of the parallel block.

```
#include <Rcpp.h>
// [[Rcpp::depends(dqrng, BH)]]
#include <dqrng_distribution.h>
// [[Rcpp::plugins(openmp)]]
#include <omp.h>
// [[Rcpp::export]]
std::vector<double> random_sum(int n, int m) {
dqrng::uniform_distribution dist(0.0, 1.0); // Uniform distribution [0,1)
auto rng = dqrng::generator<dqrng::xoshiro256plusplus>(); // seeded from R's RNG
std::vector<double> res(m);
for (int i = 0; i < m; ++i) {
double lres(0);
for (int j = 0; j < n; ++j) {
lres += dist(*rng);
}
res[i] = lres / n;
}
return res;
}
// [[Rcpp::export]]
std::vector<double> parallel_random_sum(int n, int m, int ncores) {
dqrng::uniform_distribution dist(0.0, 1.0); // Uniform distribution [0,1)
auto rng = dqrng::generator<dqrng::xoshiro256plusplus>(); // seeded from R's RNG
std::vector<double> res(m);
// ok to use rng here
#pragma omp parallel num_threads(ncores)
{
// make thread local copy of rng and advance it by 1 ... ncores jumps
auto lrng = rng->clone(omp_get_thread_num() + 1);
#pragma omp for
for (int i = 0; i < m; ++i) {
double lres(0);
for (int j = 0; j < n; ++j) {
lres += dist(*lrng);
}
res[i] = lres / n;
}
}
// ok to use rng here
return res;
}
/*** R
bm <- bench::mark(
parallel_random_sum(1e7, 8, 4),
random_sum(1e7, 8),
check = FALSE
)
bm[,1:4]
*/
```

Result:

```
expression min median `itr/sec`
<bch:expr> <bch:tm> <bch:tm> <dbl>
1 parallel_random_sum(1e+07, 8, 4) 98.3ms 99ms 10.1
2 random_sum(1e+07, 8) 270.2ms 271ms 3.68
```

Note that the thread local RNG uses `std::unique_ptr`

instead of `Rcpp::Xptr`

since it is used in parallel
code.

From the PCG family we will look at pcg64, a 64-bit generator with a
period of \(2^{128}\). It offers the
function `advance(int n)`

,
which is equivalent to `n`

random draws but scales as \(O(ln(n))\) instead of \(O(n)\). In addition, it offers \(2^{127}\) separate streams that can be
enabled via the function `set_stream(int n)`

or the two
argument constructor with `seed`

and `stream`

.
When used from R or C++ with the two argument `dqset.seed`

and `dqset_seed`

you get \(2^{64}\) streams out of the possible \(2^{127}\) separate streams.

In the following example a matrix with random numbers is generated in
parallel using RcppParallel. Instead of using the more traditional
approach of generating the random numbers from a certain distribution,
we are using the fast sampling methods from `dqrng_sample.h`

.
The resulting correlation matrix should be close to the identity matrix
if the different streams are independent:

```
#include <Rcpp.h>
// [[Rcpp::depends(dqrng, BH)]]
#include <dqrng_generator.h>
#include <dqrng_sample.h>
// [[Rcpp::depends(RcppParallel)]]
#include <RcppParallel.h>
struct RandomFill : public RcppParallel::Worker {
RcppParallel::RMatrix<int> output;
uint64_t seed;
RandomFill(Rcpp::IntegerMatrix output, const uint64_t seed) : output(output), seed(seed) {};
void operator()(std::size_t begin, std::size_t end) {
auto rng = dqrng::generator<pcg64>(seed, end);
for (std::size_t col = begin; col < end; ++col) {
auto sampled = dqrng::sample::sample<std::vector<int>, uint32_t>(*rng, 100000, output.nrow(), true);
RcppParallel::RMatrix<int>::Column column = output.column(col);
std::copy(sampled.begin(), sampled.end(), column.begin());
}
}
};
// [[Rcpp::export]]
Rcpp::IntegerMatrix parallel_random_matrix(const int n, const int m, const int ncores) {
Rcpp::IntegerMatrix res(n, m);
RandomFill randomFill(res, 42);
RcppParallel::parallelFor(0, m, randomFill, m/ncores + 1);
return res;
}
/*** R
res <- parallel_random_matrix(1e6, 8, 4)
head(res)
symnum(x = cor(res), cutpoints = c(0.001, 0.003, 0.999),
symbols = c(" ", "?", "!", "1"),
abbr.colnames = FALSE, corr = TRUE)
*/
```

Head of the random matrix:

```
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,] 67984 16279 69262 7126 21441 37720 51107 51045
[2,] 69310 21713 82885 81157 54051 5261 91165 17833
[3,] 76742 31232 78953 4626 94939 29416 85652 78296
[4,] 76349 47427 1770 37957 33888 59134 94591 65793
[5,] 85008 89224 43493 7925 60866 2464 14080 10763
[6,] 38017 88509 51195 73086 1883 68193 75259 62216
```

Correlation matrix:

```
[1,] 1
[2,] 1
[3,] ? 1
[4,] ? 1
[5,] 1
[6,] ? ? ? 1
[7,] ? 1
[8,] ? 1
attr(,"legend")
[1] 0 ‘ ’ 0.001 ‘?’ 0.003 ‘!’ 0.999 ‘1’ 1
```

So as expected the correlation matrix is almost equal to the identity matrix.

So far we have used our own RNG and either seeded it from R’s RNG or
with an explicit seed. As an alternative, we can also make use of
dqrng’s global RNG. This is exemplified in the template function
`parallel_generate<>()`

defined in the header file
`dqrng_extra/parallel_generate.h`

. A simple way to use this
template would be the following functions which generate random variates
according to the uniform, normal, or exponential distribution:

```
#include <Rcpp.h>
// [[Rcpp::depends(dqrng, BH, RcppParallel)]]
// [[Rcpp::plugins(openmp)]]
#include <dqrng_extra/parallel_generate.h>
#include <dqrng_distribution.h>
using dqrng::extra::parallel_generate;
// [[Rcpp::export]]
Rcpp::NumericVector runif_para(std::size_t n, double min = 0, double max = 1,
std::size_t threads = 2, std::size_t streams = 24) {
return parallel_generate<dqrng::uniform_distribution>(n, threads, streams, min, max);
}
// [[Rcpp::export]]
Rcpp::NumericVector rnorm_para(std::size_t n, double mean = 0, double sd = 1,
std::size_t threads = 2, std::size_t streams = 24) {
return parallel_generate<dqrng::normal_distribution>(n, threads, streams, mean, sd);
}
// [[Rcpp::export]]
Rcpp::NumericVector rexp_para(std::size_t n, double rate = 1,
std::size_t threads = 2, std::size_t streams = 24) {
return parallel_generate<dqrng::exponential_distribution>(n, threads, streams, rate);
}
```

Generating `n`

numbers is split into (about) equal
`streams`

streams that are processed by `threads`

threads. For an efficient distribution of the workload it is important
that `streams`

is a multiple of `threads`

, since
then every thread gets to process the same number of streams. The
default `streams = 24`

can be used together with 1, 2, 3, 4,
6, 8, 12, and 24 threads. The result for a *given number of
streams* is *independent of the number of threads*:

```
dqset.seed(42); norm1 <- rnorm_para(22, threads = 1)
dqset.seed(42); norm2 <- rnorm_para(22, threads = 4)
identical(norm1, norm2)
#> [1] TRUE
```

At the same time, the serial version is almost as fast as using the
normal `dqrng::dqr<dist>`

function and therefore much
faster than the `stats::r<dist>`

function. Using
multiple threads gives a clear speed up.

```
n <- 1e6
bench::mark(stats::runif(n),
dqrng::dqrunif(n),
runif_para(n, threads = 2L),
runif_para(n, threads = 1L),
check = FALSE)[, 1:6]
bench::mark(stats::rnorm(n),
dqrng::dqrnorm(n),
rnorm_para(n, threads = 2L),
rnorm_para(n, threads = 1L),
check = FALSE)[, 1:6]
bench::mark(stats::rexp(n),
dqrng::dqrexp(n),
rexp_para(n, threads = 2L),
rexp_para(n, threads = 1L),
check = FALSE)[, 1:6]
```

Here the actual implementation of the template function:

```
// Copyright 2024 Ralf Stubner
// Copyright 2024 Philippe Grosjean
//
// This file is part of dqrng.
//
// dqrng is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// dqrng is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with dqrng. If not, see <http://www.gnu.org/licenses/>.
#include <sstream>
#include <dqrng.h>
#include <RcppParallel/RVector.h>
#include <omp.h>
namespace dqrng {
namespace extra {
template<typename Dist, typename... Params>
Rcpp::NumericVector parallel_generate(std::size_t n,
std::size_t threads,
std::size_t streams,
Params&&... params) {
if (n < streams)
streams = n;
std::size_t stream_size = n / streams;
std::size_t remainder = n % streams;
// use RcppParallel::RVector as thread safe accessor
Rcpp::NumericVector res(Rcpp::no_init(n));
RcppParallel::RVector<double> work(res);
// use global RNG from dqrng
dqrng::random_64bit_accessor rng{};
std::stringstream buffer;
#ifdef _OPENMP
std::size_t maxthreads = omp_get_num_procs();
if (threads > maxthreads)
threads = maxthreads;
// No need for more threads than there are streams
if (threads > streams)
threads = streams;
#endif
#pragma omp parallel num_threads(threads)
{
std::size_t start,end;
#pragma omp for schedule(static,1)
for (std::size_t i = 0; i < streams; ++i) {
if (i < remainder) {
start = i * stream_size + i;
end = start + stream_size + 1;
} else {
start = i * stream_size + remainder;
end = start + stream_size;
}
// private RNG in each stream; RNG with i == 0 is identical to global RNG
auto prng = rng.clone(i);
prng->generate<Dist>(std::begin(work) + start, std::begin(work) + end,
std::forward<Params>(params)...);
if (i == 0) {// Save the state of the global RNG's clone
buffer << *prng;
}
}
}
// Make sure that the global RNG advances as well by applying the state
// of the global RNG's clone to the global RNG
buffer >> rng;
return res;
}
} // namespace extra
} // namespace dqrng
```

Besides the distribution of the work into (about) equal streams, the
pattern for RNG access is similar to the OpenMP example above with the
important difference that the local RNGs are not *thread-* but
*stream-specific*. This way, the result becomes independent of
the used amount of parallelism. However, one has to consider one
important aspect: After the parallel for loop, the global RNG has not
advanced at all. Calling the function repeatedly would lead to identical
results. To circumvent this, one of the stream specific RNGs is an exact
clone of the global RNG (`clone(stream=0)`

) and the state of
this RNG after processing its stream is saved and used to overwrite the
global RNG’s state.^{2} This way, the global RNG’s state advances
as if it had processed one of the streams and successive calls to
`parallel_generate()`

produce different random numbers as
expected.

```
dqset.seed(153)
runif_para(30)
#> [1] 0.87693642 0.14323366 0.33129746 0.07856319 0.80991119 0.37524485
#> [7] 0.90387542 0.38746776 0.30473153 0.01102334 0.21272306 0.11975609
#> [13] 0.98440547 0.13373340 0.82823735 0.87196225 0.14920422 0.27723804
#> [19] 0.59308120 0.07853078 0.63040483 0.21707435 0.25876379 0.81296194
#> [25] 0.53645030 0.29976254 0.37159454 0.38683266 0.03737063 0.03359113
runif_para(30) # Different values, as expected
#> [1] 0.90407135 0.73543499 0.09026296 0.90321975 0.66162669 0.51716146
#> [7] 0.74186366 0.41125413 0.17581202 0.68547734 0.11766549 0.82316789
#> [13] 0.40565668 0.44854610 0.95477820 0.64388593 0.31991691 0.02239872
#> [19] 0.13687388 0.32476719 0.67093851 0.05564081 0.76817620 0.49502455
#> [25] 0.07459706 0.25493312 0.14019729 0.89704659 0.40548199 0.53800443
```

Pitfall: should you use `parallel_generate()`

in
concurrent threads or streams, make sure to seed each of them with
enough separate streams to avoid overlap. For instance, with
`parallel_generate(..., stream = 4)`

, you could seed first
thread with something like `dqset.seed(546, 1)`

, but you must
seed second thread at least on stream 5 (previous stream plus the number
of streams you reserve for `parallel_generate()`

). If you use
`dqset.seed(546, 2)`

on the second thread, there will be an
overlap like this:

```
# Seed used in the first thread
dqset.seed(546, 1); (v1 <- rnorm_para(8, streams = 4))
#> [1] 0.01904358 0.57750157 0.39156879 -1.72594164 1.24949453 -0.87535133
#> [7] -0.49878776 0.26077249
# Seed used in the second thread
dqset.seed(546, 2); (v2 <- rnorm_para(8, streams = 4))
#> [1] 0.3915688 -1.7259416 1.2494945 -0.8753513 -0.4987878 0.2607725
#> [7] 1.2018189 -0.1060487
```

Note how values 1-6 of v2 are identical to values 3-8 of v1 because
of an overlap of the streams consumed in each call to
`parallel_generate()`

. You will get the same result if the
two lines of code were run in separate threads or even, separate R
processes. Here, if second `dqset.seed()`

is shifted by 4
streams or more from first one, then there is no overlap any more:

```
# Seed used in the first thread
dqset.seed(546, 1); (v1 <- rnorm_para(8, streams = 4))
#> [1] 0.01904358 0.57750157 0.39156879 -1.72594164 1.24949453 -0.87535133
#> [7] -0.49878776 0.26077249
# Seed used in the second thread
dqset.seed(546, 5); (v2 <- rnorm_para(8, streams = 4))
#> [1] 1.2018189 -0.1060487 -0.8532641 0.6531933 -0.8304053 -0.4745548
#> [7] -0.4211618 -0.5871540
```

See for example https://www.pcg-random.org/posts/critiquing-pcg-streams.html.↩︎

Note that

**here**in contrast to the default PCG implementation, streams are counted from the current stream, i.e. setting`stream`

to`0`

will give the same RNG. This brings PCG in line with the other RNGs.↩︎