API
Use
Put include/ on the include path, for example
nvcc -I<tandem-cuda>/include.
#include "tandem.cuh"
const uint32_t key[4] = {1, 2, 3, 4};
double *x;
cudaMalloc(&x, n * sizeof(double));
uint64_t pos = tandem::fill_f64(key, 0, 32, x, n); // the spec's Float64 fill from position 0
uint32_t *die; // 6-sided dice, n draws from the UInt32 stream
cudaMalloc(&die, n * sizeof(uint32_t));
pos = tandem::fill_u32_below(key, pos, 32, 6, die, n);
float *z; // standard normals
cudaMalloc(&z, n * sizeof(float));
pos = tandem::fill_normal_f32(key, pos, 32, z, n);
pos = tandem::fill_exponential_f32(key, pos, 32, z, n); // standard exponentials, in place of z
tandem::generator g = tandem::generator::seed(42); // the same, with the position kept for you
g.fill_f64(x, n);
g.fill_u32_below(6, die, n);
__global__ void kernel(uint32_t k0, uint32_t k1, uint32_t k2, uint32_t k3, float *out) {
const uint32_t key[4] = {k0, k1, k2, k3};
tandem::device_rng rng = tandem::device_rng::from_key(key, 0, 32).split(blockIdx.x * blockDim.x + threadIdx.x);
out[threadIdx.x] = rng.next_f32();
}Reference
tandem.cuh: the CUDA fills anddevice_rng.include/tandem/core.hpp: the portable core thattandem.cuhbuilds on. It holds the step, the seeding function, the stream layout, the float mappings, child keys, an eight-lane row, the scalar generatortandem::Rngand the normal ziggurat, without CUDA types.include/tandem/normal_tables.hppholds the ziggurat's tables, generated from the specification. Its functions areTANDEM_FN:KOKKOS_INLINE_FUNCTIONunder Kokkos,__host__ __device__ inlineunder nvcc and hipcc,inlineotherwise.tandem::fill_u32/u64/f32/f64(key, pos, K, device_ptr, n, stream): fill device memory from a key and stream position, as the specification's fill defines, and return the position after the fill. One thread per chunk walks itsKblocks. ForK >= 8a block of 32 groups stages eight steps in shared memory and writes 512 contiguous bytes per warp. For smallerKeach group stores its own 128-byte line per step.tandem::fill_u8/u16/f16_bits/booltake the same arguments.fill_f16_bitswrites the binary16 bit patterns of the specification's Float16 draws,(raw >> 5) * 2^-11, into auint16_tbuffer.fill_boolwrites one byte, 0 or 1, per stream bit. The 8 and 16 bit fills use the tile kernel like the wider ones.tandem::fill_i8/i16/i32/i64: the unsigned fill of the same width read in two's complement, as the specification defines signed integers.tandem::fill_u32_below(key, pos, K, range, out, n, stream),fill_u64_below:ndraws uniform on[0, range), asRng::urand(range). Not part of the specification.tandem::fill_u32_below(key, pos, K, range, low, out, n, stream)andfill_u64_belowwith a low bound:out[e] = low + draw[e], summed in the output type and wrapping, so the output can beuint32_torint32_t, and for a 32-bit range alsouint64_torint64_t, which widens the 32-bit draw into an 8-byte element. The draws and the consumed bits are those of the plain fill, and the offset and the widening are fused into the store, so there is no second pass.fill_u64_belowtakes auint64_torint64_tlow bound and output.tandem::fill_normal_f64(key, pos, K, out, n, stream):nstandard normals by the 1024-layer ziggurat, one UInt64 draw each, asRng::normal()calls, bit identical to tandem-c. From 2^16 elements it uses a list of misses ofn / 8bytes, which the library keeps per device between fills, see design.fill_normal_f32: both Box-Muller halves per two Float32 uniforms, as the flattenedRng::normalf2()calls. Appendix A of the specification.tandem::fill_exponential_f64/f32(key, pos, K, out, n, stream):nstandard exponentials-ln(1 - u), one uniform each, asRng::exponential()orexponentialf()calls, bit identical to tandem-c. Not part of the specification.tandem::fill_choice(key, pos, K, table, out, n, stream):nweighted choice indices asuint32_t, elementifrom UInt64 drawithrough an alias table, asRng::choice(table)calls, bit identical to tandem-c'standem_fill_choice. Build the table on the host withtandem::choice_build(table, weights, m, cut, alias)fromcore.hpp, which fills the caller'smcut andmalias entries and returns false for invalid weights, then copy the two arrays to the device and pointtable.cutandtable.aliasat the copies.tandem::choice_of(table, r)maps one draw on hosts and devices. Appendix C of the specification.Every fill, free or through
tandem::generator, checks its endalign(pos, w) + w non the host before it launches. Where the end reaches 2^64 it throwsstd::length_error, writes nothing and leaves the generator's position unchanged.tandem::fill_endincore.hppis that check, for other host entry points.tandem::generator: a host handle with publickey,posandK, built byfrom_keyorseed. Itsfill_*methods (every fill above, with the range first for the bounded ones) take the output pointer,nand a stream, fill frompos, and setposto the position the fill returns, so successive fills continue one stream without the caller tracking positions.tandem::device_rng: a per-thread generator for kernels that draw scalars. It holds the transport form (public fieldskey,pos,K) and one cached chunk state, about 20 registers.from_key,seed,skip_to,next_bool/u8/u16/u32/u64/f16_bits/f32/f64. Its draws follow the specification's scalar rule for the same key and position, mixed widths included. The rest of the draw API istandem::Rng's, shared throughDraws<D>incore.hpp: bounded drawsurand(range)andurand64(range)by Lemire's method, andbelow(range), which names only the result type and so draws 32 bits forrange <= 2^32and 64 bits above, as Appendix A requires,normal()(one ziggurat draw) andnormal2()(two),normalf()/normalf2()by Box-Muller in float from two f32 uniforms,exponential()andexponentialf(),at_urand/urand64/frand/drand(i),choice(table),child,split,subandfork. Bounded draws, normals, exponentials and weighted choice are not part of the specification.tandem::Rng::set_position(p)andfrom_keyreject a startp >= 2^63:set_positionreturns false and keeps the state, andfrom_keykeeps position 0.advance_to(p)moves to the end of draws or a fill made from the generator without the check, since an end may lie at or past 2^63. They matchtandem_u32_below,tandem_u64_below,tandem_normal_f64andtandem_exponential_f64and_f32andtandem_choiceof the C library.tandem_thrust.cuh: Thrust and CUB adapters.tandem::uniform<T>(u32, u64, f32, f64),tandem::below<T>(u32, u64;below<uint64_t>takes its draw width from the range, 32 bits forrange <= 2^32) andtandem::normal<T>(f32, f64) are functors from an indexito elementiof the fill of the same type, built on the device generator's random access.tandem::make_iterator(f, first)wraps one in atransform_iteratorover acounting_iterator, forthrust::reduce,thrust::copy_n,thrust::transformand CUB. Anormal<double>yields the ziggurat of UInt64 drawi, andnormal<float>the cos half at2jand the sin half at2j + 1of the uniforms2jand2j + 1. Header only, and Thrust ships with the toolkit.tandem::normal_f64(r, key, K, g): the ziggurat normal of one UInt64 drawrwith global draw indexgunder a generator's key andK, elementg - align(pos, 64) / 64of a fill.normal_f64_fast(r, hit)is its table step alone.tandem::T,F,F_keyed,block: the specification's building blocks, host and device, fromcore.hpp.
Thrust
#include "tandem_thrust.cuh"
auto it = tandem::make_iterator(tandem::uniform<uint64_t>(key, 32)); // element i of fill_u64
uint64_t sum = thrust::reduce(it, it + n, (uint64_t)0); // no bufferParallel 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.