tandem rng tandem-kokkos GitHub

Design

Fills

On host execution spaces a work item steps the eight chunks of a group together, a whole 1024-bit row per step. With GCC 12+ or clang the row lives in 128-bit vectors (NEON or SSE/AVX) and leaves registers by a 4x4 transpose. Define TANDEM_NO_SIMD for the scalar loop. The host float normal fills turn the pairs of each row into normals with normal_block_f32 from core.hpp, a Box-Muller without libm that the compiler vectorizes, so a host fill equals the scalar normalf2() calls bit for bit. Double normals map each u64 draw with normal_f64, the ziggurat, on every space. On devices a team stores the ziggurat's fast path and queues the misses, 0.43 % of draws, in scratch memory, then continues them together every eight steps, which keeps the slow path out of the stepping loop. On device spaces one thread steps one chunk. For K >= 8 a team of 256 threads stages eight steps of 32 groups in 32 KiB of scratch memory and writes them as 512 contiguous bytes per warp. For smaller K each thread stores its blocks directly. Both pick a 16-byte store when the output's blocks are 16-byte aligned.

Portable core

tandem/core.hpp lives in tandem-cuda, which builds its CUDA kernels on it, and depends on nothing but the C++ standard library. TANDEM_FN expands to KOKKOS_INLINE_FUNCTION when Kokkos is included first, to __host__ __device__ inline under nvcc or hipcc, and to inline otherwise. It holds T, F, F_keyed, block, the stream position arithmetic, the float mappings including to_f16_bits, Rng on the Draws<D> base that device generators share, GenState, the bounded-fill helpers below_u32 and below_u64 with PURPOSE_BELOW32 and PURPOSE_BELOW64, the ziggurat normal_f64 with its tables in normal_tables.hpp, box_muller2_f32 with the host block normal_block_f32, exponential_f64 and exponential_f32, the weighted choice table build choice_build and draw map choice_of, and Row, the eight lanes of a group. HIP and SYCL ports can reuse it and add only their fill kernels.