Skip to content

Commit f2492c5

Browse files
authored
Feature/parallel generate (#82)
* Add header file for parallel variate generation * Make clone(0) work also for PCG * Move seeding from R into dqrng::generator<RNG>() * Rework Parallel RNG vignette * use dqrng::generator to create the (main) RNG and rng->clone() for the local versions * remove the first two examples that make use of the global RNG * add a section on global RNG usage which discusses dqrng_extra/parallel_generate.h
1 parent 243dbfe commit f2492c5

11 files changed

Lines changed: 276 additions & 185 deletions

File tree

DESCRIPTION

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
Package: dqrng
22
Type: Package
33
Title: Fast Pseudo Random Number Generators
4-
Version: 0.3.2.5
4+
Version: 0.3.2.6
55
Authors@R: c(
6-
person("Ralf", "Stubner", email = "ralf.stubner@gmail.com", role = c("aut", "cre")),
6+
person("Ralf", "Stubner", email = "ralf.stubner@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0009-0009-1908-106X")),
77
person("daqana GmbH", role = "cph"),
88
person("David Blackman", role = "cph", comment = "Xoroshiro / Xoshiro family"),
99
person("Melissa O'Neill", email = "oneill@pcg-random.org", role = "cph", comment = "PCG family"),
1010
person("Sebastiano Vigna", email = "vigna@acm.org", role = "cph", comment = "Xoroshiro / Xoshiro family"),
1111
person("Aaron", "Lun", role="ctb"),
1212
person("Kyle", "Butts", role = "ctb", email = "kyle.butts@colorado.edu"),
13-
person("Henrik", "Sloot", role = "ctb")
13+
person("Henrik", "Sloot", role = "ctb"),
14+
person("Philippe", "Grosjean", role = c("ctb"), comment = c(ORCID = "0000-0002-2694-9471"))
1415
)
1516
Description: Several fast random number generators are provided as C++
1617
header only libraries: The PCG family by O'Neill (2014

NEWS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
* The default RNG has changed from Xoroshiro128+ to Xoroshiro128++. The older generators Xoroshiro128+ and Xoshiro256+ are still available but should only be used for backward compatibility or for generating floating point numbers, i.e. not sampling etc. ([#57](https://github.com/daqana/dqrng/pull/57) fixing [#56](https://github.com/daqana/dqrng/issues/56))
66
* The `dqrng::rng64_t` type has been changed to use `Rcpp::XPtr` instead of `std::shared_ptr` and the functions from `dqrng_sample.h` now expect a reference to `dqrng::random_64bit_generator` instead of `dqrng::rng64_t` ([#70](https://github.com/daqana/dqrng/pull/70) fixing [#63](https://github.com/daqana/dqrng/issues/63))
7-
* The two argument constructor and `seed` function from PCG has [surprising properties](https://github.com/imneme/pcg-cpp/issues/91): it is not identical to the one argument version followed by `set_stream(stream)`. For consistency with the new `clone(stream)` method, the two argument versions are no longer used. This influences code the uses multiple stream with PCG together with the tooling from this package, e.g. the example code in the vignette on parallel RNG usage.
7+
* The two argument constructor and `seed` function from PCG has [surprising properties](https://github.com/imneme/pcg-cpp/issues/91): it is not identical to the one argument version followed by `set_stream(stream)`. For consistency with the new `clone(stream)` method, the two argument versions are no longer used. This influences code that uses multiple streams with PCG together with the tooling from this package, e.g. the example code in the vignette on parallel RNG usage. In addition, setting the stream on PCG64 via `dqset.seed(seed, stream)` or at the C++ level using the interface provided by dqrng will be relative to the current stream, i.e. setting `stream=0` will not change the RNG. This is for consistency with the other provided RNGs. You still get the standard behaviour if you are using the C++ classes for PCG directly.
88

99
## Other changes
1010

@@ -18,6 +18,7 @@
1818
* A `clone(stream)` method to allow using the global RNG state for parallel computation
1919
* New methods `variate<dist>(param)`, `generate<dist>(container, param)` etc. using and inspired by [`randutils`](https://www.pcg-random.org/posts/ease-of-use-without-loss-of-power.html).
2020
* The scalar functions `dqrng::runif`, `dqrng::rnorm` and `dqrng::rexp` available from `dqrng.h` have been deprecated and will be removed in a future release. Please use the more flexible and faster `dqrng::random_64bit_accessor` together with `variate<Dist>()` instead. The same applies to `dqrng::uniform01` from `dqrng_distribution.h`, which can be replaced by the member function `dqrng::random_64bit_generator::uniform01`.
21+
* New template function `dqrng::extra::parallel_generate` in `dqrng_extra/parallel_generate.h` as an example for using the global RNG in a parallel context (fixing [#77](https://github.com/daqana/dqrng/issues/77) in [#82](https://github.com/daqana/dqrng/issues/82) together with Philippe Grosjean)
2122

2223

2324
# dgrng 0.3.2

README.Rmd

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,9 @@ Intermediate releases can also be obtained via
3838
[r-universe](https://rstub.r-universe.dev/dqrng):
3939

4040
```r
41-
options(repos = c(
41+
install.packages('dqrng', repos = c(
4242
rstub = 'https://rstub.r-universe.dev',
4343
CRAN = 'https://cloud.r-project.org'))
44-
install.packages('dqrng')
4544
```
4645

4746
## Example

README.md

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ Intermediate releases can also be obtained via
3232
[r-universe](https://rstub.r-universe.dev/dqrng):
3333

3434
``` r
35-
options(repos = c(
35+
install.packages('dqrng', repos = c(
3636
rstub = 'https://rstub.r-universe.dev',
3737
CRAN = 'https://cloud.r-project.org'))
38-
install.packages('dqrng')
3938
```
4039

4140
## Example
@@ -61,8 +60,8 @@ bm[, 1:4]
6160
#> # A tibble: 2 × 4
6261
#> expression min median `itr/sec`
6362
#> <bch:expr> <bch:tm> <bch:tm> <dbl>
64-
#> 1 rnorm(N) 608.9µs 692.8µs 1342.
65-
#> 2 dqrnorm(N) 82.7µs 86.2µs 10227.
63+
#> 1 rnorm(N) 606.2µs 654.7µs 1456.
64+
#> 2 dqrnorm(N) 82.8µs 85.9µs 10984.
6665
```
6766

6867
This is also true for the provided sampling functions with replacement:
@@ -79,10 +78,10 @@ bm[, 1:4]
7978
#> # A tibble: 4 × 4
8079
#> expression min median `itr/sec`
8180
#> <bch:expr> <bch:tm> <bch:tm> <dbl>
82-
#> 1 sample.int(m, n, replace = TRUE) 7.03ms 7.46ms 133.
83-
#> 2 sample.int(1000 * m, n, replace = TRUE) 8.87ms 9.4ms 104.
84-
#> 3 dqsample.int(m, n, replace = TRUE) 292.69µs 316.75µs 2821.
85-
#> 4 dqsample.int(1000 * m, n, replace = TRUE) 408.94µs 446.19µs 1987.
81+
#> 1 sample.int(m, n, replace = TRUE) 6.88ms 7.07ms 140.
82+
#> 2 sample.int(1000 * m, n, replace = TRUE) 8.57ms 8.81ms 112.
83+
#> 3 dqsample.int(m, n, replace = TRUE) 289.69µs 296.86µs 2834.
84+
#> 4 dqsample.int(1000 * m, n, replace = TRUE) 407.45µs 489.33µs 1645.
8685
```
8786

8887
And without replacement:
@@ -100,11 +99,11 @@ bm[, 1:4]
10099
#> # A tibble: 5 × 4
101100
#> expression min median `itr/sec`
102101
#> <bch:expr> <bch:tm> <bch:tm> <dbl>
103-
#> 1 sample.int(m, n) 25.43ms 27.8ms 31.8
104-
#> 2 sample.int(1000 * m, n) 12.99ms 17.4ms 57.0
105-
#> 3 sample.int(m, n, useHash = TRUE) 10.44ms 12.8ms 77.2
106-
#> 4 dqsample.int(m, n) 617.83µs 762.9µs 882.
107-
#> 5 dqsample.int(1000 * m, n) 1.51ms 2.5ms 334.
102+
#> 1 sample.int(m, n) 40.98ms 42.8ms 23.1
103+
#> 2 sample.int(1000 * m, n) 12.01ms 13.3ms 66.9
104+
#> 3 sample.int(m, n, useHash = TRUE) 9.35ms 10.4ms 92.4
105+
#> 4 dqsample.int(m, n) 616.34µs 679.1µs 1265.
106+
#> 5 dqsample.int(1000 * m, n) 1.42ms 1.7ms 501.
108107
```
109108

110109
Note that sampling from `10^10` elements triggers “long-vector support”
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2024 Ralf Stubner
2+
// Copyright 2024 Philippe Grosjean
3+
//
4+
// This file is part of dqrng.
5+
//
6+
// dqrng is free software: you can redistribute it and/or modify it
7+
// under the terms of the GNU Affero General Public License as published by
8+
// the Free Software Foundation, either version 3 of the License, or
9+
// (at your option) any later version.
10+
//
11+
// dqrng is distributed in the hope that it will be useful, but
12+
// WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with dqrng. If not, see <http://www.gnu.org/licenses/>.
18+
19+
#include <sstream>
20+
#include <dqrng.h>
21+
#include <RcppParallel/RVector.h>
22+
#include <omp.h>
23+
24+
namespace dqrng {
25+
namespace extra {
26+
template<typename Dist, typename... Params>
27+
Rcpp::NumericVector parallel_generate(std::size_t n,
28+
std::size_t threads,
29+
std::size_t streams,
30+
Params&&... params) {
31+
if (n < streams)
32+
streams = n;
33+
std::size_t stream_size = n / streams;
34+
std::size_t remainder = n % streams;
35+
36+
// use RcppParallel::RVector as thread safe accessor
37+
Rcpp::NumericVector res(Rcpp::no_init(n));
38+
RcppParallel::RVector<double> work(res);
39+
40+
// use global RNG from dqrng
41+
dqrng::random_64bit_accessor rng{};
42+
std::stringstream buffer;
43+
44+
#ifdef _OPENMP
45+
std::size_t maxthreads = omp_get_num_procs();
46+
if (threads > maxthreads)
47+
threads = maxthreads;
48+
// No need for more threads than there are streams
49+
if (threads > streams)
50+
threads = streams;
51+
#endif
52+
53+
#pragma omp parallel num_threads(threads)
54+
{
55+
std::size_t start,end;
56+
57+
#pragma omp for schedule(static,1)
58+
for (std::size_t i = 0; i < streams; ++i) {
59+
if (i < remainder) {
60+
start = i * stream_size + i;
61+
end = start + stream_size + 1;
62+
} else {
63+
start = i * stream_size + remainder;
64+
end = start + stream_size;
65+
}
66+
// private RNG in each stream; RNG with i == 0 is identical to global RNG
67+
auto prng = rng.clone(i);
68+
prng->generate<Dist>(std::begin(work) + start, std::begin(work) + end,
69+
std::forward<Params>(params)...);
70+
if (i == 0) {// Save the state of the global RNG's clone
71+
buffer << *prng;
72+
}
73+
}
74+
}
75+
// Make sure that the global RNG advances as well by applying the state
76+
// of the global RNG's clone to the global RNG
77+
buffer >> rng;
78+
return res;
79+
}
80+
} // namespace extra
81+
} // namespace dqrng

inst/include/dqrng_generator.h

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
#include <dqrng_types.h>
2525
#include <xoshiro.h>
2626
#include <pcg_random.hpp>
27+
#include <boost/multiprecision/cpp_int.hpp>
2728
#include <Rcpp.h>
29+
#include <convert_seed.h>
30+
#include <R_randgen.h>
2831

2932
#if defined(__cpp_lib_make_unique) && (__cpp_lib_make_unique >= 201304)
3033
using std::make_unique;
@@ -98,19 +101,35 @@ inline void random_64bit_wrapper<::dqrng::xoshiro256starstar>::set_stream(result
98101

99102
template<>
100103
inline void random_64bit_wrapper<pcg64>::set_stream(result_type stream) {
101-
gen.set_stream(stream);
104+
// set the stream relative to the current stream, i.e. stream = 0 does not change the RNG
105+
boost::multiprecision::uint128_t number;
106+
std::vector<boost::multiprecision::uint128_t> state;
107+
std::stringstream iss;
108+
iss << gen;
109+
while (iss >> number)
110+
state.push_back(number);
111+
// state[1] is the current stream
112+
// PCG will do 2*stream + 1 to make sure stream is odd; need to revert that here
113+
gen.set_stream(pcg_extras::pcg128_t(state[1]/2) + stream);
102114
}
103115

116+
uint64_t get_seed_from_r() {
117+
Rcpp::RNGScope rngScope;
118+
Rcpp::IntegerVector seed(2, dqrng::R_random_int);
119+
return dqrng::convert_seed<uint64_t>(seed);
120+
}
121+
122+
104123
template<typename RNG = default_64bit_generator>
105124
typename std::enable_if<!std::is_base_of<random_64bit_generator, RNG>::value, rng64_t>::type
106125
generator () {
107-
return rng64_t(new random_64bit_wrapper<RNG>());
126+
return rng64_t(new random_64bit_wrapper<RNG>(get_seed_from_r()));
108127
}
109128

110129
template<typename RNG = default_64bit_generator>
111130
typename std::enable_if<std::is_base_of<random_64bit_generator, RNG>::value, rng64_t>::type
112131
generator () {
113-
return rng64_t(new RNG());
132+
return rng64_t(new RNG(get_seed_from_r()));
114133
}
115134

116135
template<typename RNG = default_64bit_generator>

man/dqrng-package.Rd

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/dqrng.cpp

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,10 @@
2424
#include <xoshiro.h>
2525
#include <pcg_random.hpp>
2626
#include <dqrng_threefry.h>
27-
#include <convert_seed.h>
28-
#include <R_randgen.h>
2927

3028
namespace {
31-
dqrng::rng64_t rng = dqrng::generator();
29+
dqrng::rng64_t rng = dqrng::generator(56478348);
3230
std::string rng_kind = "default";
33-
34-
void init() {
35-
Rcpp::RNGScope rngScope;
36-
Rcpp::IntegerVector seed(2, dqrng::R_random_int);
37-
rng->seed(dqrng::convert_seed<uint64_t>(seed));
38-
}
3931
}
4032

4133
// [[Rcpp::interfaces(r, cpp)]]
@@ -44,7 +36,7 @@ void init() {
4436
void dqset_seed(Rcpp::Nullable<Rcpp::IntegerVector> seed,
4537
Rcpp::Nullable<Rcpp::IntegerVector> stream = R_NilValue) {
4638
if (seed.isNull()) {
47-
init();
39+
rng = dqrng::generator();
4840
} else {
4941
uint64_t _seed = dqrng::convert_seed<uint64_t>(seed.as());
5042
if (stream.isNotNull()) {

tests/testthat/test-external-generator.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ test_that("cloned external Xoshiro256++ gives different result only when a diffe
7272
test_that("cloned external PCG64 gives different result", {
7373
dqrng::dqRNGkind("PCG64")
7474
dqset.seed(use_seed)
75-
# PCG64 gives a different result as long as you are not selecting the default stream
76-
expect_false(cloned_calls(stream = 0))
75+
expect_true(cloned_calls(stream = 0))
7776
expect_false(cloned_calls(stream = 1))
7877
dqrng::dqRNGkind("default")
7978
})

vignettes/dqrng.Rmd

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,6 @@ Here a minimal [SplitMix](https://xoroshiro.di.unimi.it/splitmix64.c) generator
203203
#include <Rcpp.h>
204204
// [[Rcpp::depends(dqrng, BH)]]
205205
#include <dqrng_distribution.h>
206-
// [[Rcpp::plugins(cpp11)]]
207-
#include <cstdint>
208206
209207
class SplitMix {
210208
public:
@@ -254,7 +252,6 @@ For example, here the 64 bit Threefry engine with 13 rounds from package sitmo i
254252
// [[Rcpp::depends(dqrng, BH, sitmo)]]
255253
#include <dqrng_distribution.h>
256254
#include <threefry.h>
257-
// [[Rcpp::plugins(cpp11)]]
258255
259256
// [[Rcpp::export]]
260257
Rcpp::NumericVector threefry_rnorm(const int n, const double mean = 0.0, const double sd = 1.0) {
@@ -281,11 +278,10 @@ For example, this function generates random numbers according to the normal dist
281278
// [[Rcpp::depends(dqrng)]]
282279
#include <dqrng_generator.h>
283280
#include <xoshiro.h>
284-
// [[Rcpp::plugins(cpp11)]]
285281
286282
// [[Rcpp::export]]
287283
Rcpp::NumericVector std_rnorm(const int n, const double mean = 0.0, const double sd = 1.0) {
288-
auto rng = dqrng::generator<dqrng::xoroshiro128plus>(42);
284+
auto rng = dqrng::generator<dqrng::xoroshiro128plusplus>(42);
289285
Rcpp::NumericVector result(n);
290286
rng->generate<std::normal_distribution>(result, mean, sd);
291287
return result;
@@ -334,25 +330,17 @@ The parameters of the distributions are adjusted for every draw using `<distribu
334330
// [[Rcpp::depends(dqrng, BH)]]
335331
#include <boost/random/binomial_distribution.hpp>
336332
#include <dqrng_distribution.h>
337-
#include <xoshiro.h>
338-
// [[Rcpp::plugins(cpp11)]]
339333
340-
// aliases for the used distributions
341-
using binomial = boost::random::binomial_distribution<int>;
342-
using normal = dqrng::normal_distribution;
343334
// [[Rcpp::export]]
344335
Rcpp::NumericMatrix multiple_distributions(int n) {
345336
auto rng = dqrng::generator<dqrng::xoshiro256plusplus>(42);
346-
// distributions with default parameters
347-
binomial bernoulli;
348-
normal normal;
349337
Rcpp::NumericMatrix out(n, 3);
350338
double p = 0.0;
351339
for (int i = 0; i < n; ++i) {
352340
p = double(i) / double(n);
353-
out(i,0) = bernoulli(*rng, binomial::param_type(1, p));
354-
out(i,1) = normal(*rng, normal::param_type(p, 1.0));
355-
out(i,2) = normal(*rng, normal::param_type(4.0, 3.0 - p));
341+
out(i,0) = rng->variate<boost::random::binomial_distribution<int>>(1, p);
342+
out(i,1) = rng->variate<dqrng::normal_distribution>(p, 1.0);
343+
out(i,2) = rng->variate<dqrng::normal_distribution>(4.0, 3.0 - p);
356344
}
357345
Rcpp::colnames(out) = Rcpp::CharacterVector::create("Bernoulli", "Normal1", "Normal2");
358346
return out;
@@ -375,24 +363,17 @@ RNG engine of `dqrng`. Please note that the included RNG will be invalidated if
375363
#include <boost/random/binomial_distribution.hpp>
376364
#include <dqrng.h>
377365
#include <dqrng_distribution.h>
378-
// [[Rcpp::plugins(cpp11)]]
379366
380-
// aliases for the used distributions
381-
using binomial = boost::random::binomial_distribution<int>;
382-
using normal = dqrng::normal_distribution;
383367
// [[Rcpp::export]]
384368
Rcpp::NumericMatrix multiple_distributions(int n) {
385369
auto rng = dqrng::random_64bit_accessor{};
386-
// distributions with default parameters
387-
binomial bernoulli;
388-
normal normal;
389370
Rcpp::NumericMatrix out(n, 3);
390371
double p = 0.0;
391372
for (int i = 0; i < n; ++i) {
392373
p = double(i) / double(n);
393-
out(i,0) = bernoulli(rng, binomial::param_type(1, p));
394-
out(i,1) = normal(rng, normal::param_type(p, 1.0));
395-
out(i,2) = normal(rng, normal::param_type(4.0, 3.0 - p));
374+
out(i,0) = rng.variate<boost::random::binomial_distribution<int>>(1, p);
375+
out(i,1) = rng.variate<dqrng::normal_distribution>(p, 1.0);
376+
out(i,2) = rng.variate<dqrng::normal_distribution>(4.0, 3.0 - p);
396377
}
397378
Rcpp::colnames(out) = Rcpp::CharacterVector::create("Bernoulli", "Normal1", "Normal2");
398379
return out;

0 commit comments

Comments
 (0)