tandem rng tandem-kokkos GitHub

API

Use

#include <tandem/kokkos.hpp>

tandem::Rng rng(42);                       // 128-bit seed as two halves, default K = 32
Kokkos::View<double *> x("x", n);
tandem::fill(x, rng);                      // the spec's Float64 fill, on x's execution space
double u = rng.drand();                    // continues the stream after the fill
tandem::Rng w = rng.split(7);              // child stream from the key alone
Kokkos::View<uint32_t *> die("die", n);
tandem::fill_below(die, rng, 6u);          // uniform on [0, 6)
Kokkos::View<float *> g("g", n);
tandem::fill_normal(g, rng);               // standard normals
tandem::fill_exponential(g, rng);          // standard exponentials

// Weighted choice: build the alias table on the host, copy it to the device's memory.
const double w[] = {1, 2, 3, 4};
Kokkos::View<uint64_t *> cut("cut", 4);
Kokkos::View<uint32_t *> alias("alias", 4);
auto hcut = Kokkos::create_mirror_view(cut);
auto halias = Kokkos::create_mirror_view(alias);
tandem::ChoiceTable t;
tandem::choice_build(t, w, 4, hcut.data(), halias.data());
Kokkos::deep_copy(cut, hcut);
Kokkos::deep_copy(alias, halias);
t.cut = cut.data();
t.alias = alias.data();
Kokkos::View<uint32_t *> idx("idx", n);
tandem::fill_choice(idx, rng, t);          // index i with probability w[i] / 10

Kokkos::View<float *> y("y", m);
Kokkos::parallel_for(m, KOKKOS_LAMBDA(int i) {
    tandem::Rng r = rng.split(i);          // one generator per work item
    y(i) = r.frand() + r.frand();
});

Reference

Rng draws:

bit(), urand(), urand64(), frand(), drand() the specification's Bool, UInt32, UInt64, Float32 and Float64 draws
at_urand(i), at_urand64(i), at_frand(i), at_drand(i) element i of the fill that would start here, without advancing
urand(range), urand64(range), rand(start, end), rand64(start, end), frand(range), drand(start, end), ... bounded draws, uniform by Lemire's multiply and reject
below(range) a bounded draw whose width comes from the range: 32 bits for range <= 2^32, else 64
normal(), normal(mean, sd) a ziggurat normal from one UInt64 draw
normal2() two ziggurat normals from two UInt64 draws, as a pair z0, z1
normalf(), normalf2() the cos half, or both halves, of a Box-Muller step from two Float32 draws
exponential(), exponentialf() -log(1 - u) of one Float64 or Float32 draw
choice(table) a weighted choice index from one UInt64 draw
split(i), sub(purpose), fork(children, n) child generators as the specification defines them
key(), position(), set_position(p), chunk_length() transport form. set_position returns false and changes nothing for p >= 2^63, and from_key with such a start keeps position 0

Signed integers hold the two's complement of the unsigned draw of the same width. A complex value takes two draws, the real and then the imaginary component. half_t needs a Kokkos with a half type, which the conda-forge build for macOS lacks: there half_t is float and fill writes Float32 draws. A View that is not contiguous, such as a column of a LayoutRight matrix, throws std::invalid_argument. A fill whose end align(p, w) + w n reaches 2^64 throws std::length_error before it launches, and leaves the View and the generator unchanged.

Bounded, normal and exponential draws follow the specification's non-normative Appendix A and the same fills in tandem-cuda. core.hpp computes log, sin and cos with polynomials whose multiply-adds are explicit fused ones, so every backend writes tandem-c's bits for bounded integers, double normals and exponentials, and host backends do for float normals. CUDA float normals take the fast __sincosf and agree to 16 ulps + 1e-6. Build with -ffp-contract=off, and with -mfma on x86 so the fused operations stay inline. The method names and the MAX_* constants follow the Kokkos generators, so Kokkos::rand<tandem::Rng, T>::draw(rng, ...) works. Its draws with a range or bounds name only the result type, so tandem/kokkos.hpp specializes it for the 64-bit integer types to take the width from the range through Rng::below(range): 32 bits for range <= 2^32, else 64.

No Kokkos::fill_random pool

Kokkos::fill_random(view, pool, range) calls pool.get_state() without an index, and the Kokkos pools hand out states by thread id or atomic locks. Which state draws which element then depends on scheduling, so no pool adaptor can make that fill reproducible. tandem::fill replaces it, and in your own kernels rng.split(i) keyed by the work item index gives every item its own stream.

Parallel use

Element i of a fill is draw i, so ranks, threads or devices that start at the position of their first element, or draw from split(task), reproduce a serial run for any decomposition, as Appendix B of the specification shows.