tandem rng tandem-torch GitHub

Design

Fills

On CUDA every dtype has its own fill kernel in tandem.cuh, including bool, the 8-bit and 16-bit types and float16. bfloat16 is the 16-bit word fill followed by the scaling.

Bounded integers

randint(low, high, size, dtype=torch.int64, device="cpu") draws on [low, high). A range of at most 2^32 takes one 32-bit draw per element (tandem_fill_u32_below on CPU, tandem::fill_u32_below on CUDA), a larger range one 64-bit draw. Both use Lemire's method and a rejected draw retries on a fallback stream keyed by its global draw index, so a fill cut at any element equals the whole fill and CPU and CUDA give the same values and every element consumes exactly one draw. randperm(n) is Fisher-Yates from the end with one sequential scalar bounded draw per swap, tandem_u32_below over the stream, so the CPU defines it and a CUDA result is copied from there. shuffle(x, dim=0) indexes x with a randperm. None of these is in the specification, and randperm needs n < 2^32.

Normals

randn follows Appendix A of the specification (tandem_fill_normal_f64 and _f32 on CPU, tandem::fill_normal_f64 and _f32 on CUDA).

float64 is the 1024-layer ziggurat. Element i comes from 64-bit draw i, so a fill cut at any element equals the whole fill. A draw outside the inner rectangles, about 0.4 %, continues on a fallback stream keyed by its global draw index. Both devices equal tandem-c bit for bit. An empty float64 fill aligns the position to 64, as section 5 of the specification says. On CUDA the fill keeps its miss list between calls, as tandem-cuda does from 6ad0817 on, so the extension leaves the memory pools alone. A list from cudaMallocAsync in each fill went back to the default pool at every synchronize, and the extension used to raise the pool's release threshold for that. Without it, the median A100 fill with one synchronize per call writes 1016 to 1028 GiB/s at 2^27 elements and 714 to 744 at 2^24, as the raised threshold did.

float32 is Box-Muller. Elements 2j and 2j + 1 are the cos and sin halves from the float32 uniforms 2j and 2j + 1, so an odd count still consumes both uniforms of its last pair. The CPU equals tandem-c bit for bit. The CUDA fill uses logf and the fast __sincosf and agrees to 16 ulps. float16 and bfloat16 round the float32 normal. Empty bounded and float32 normal fills leave the position alone.

Exponentials

exponential is -log(1 - u) of uniform draw i for element i, with the polynomial log of the normals (tandem_fill_exponential_f64 and _f32 on CPU, tandem::fill_exponential_f64 and _f32 on CUDA). Both devices equal tandem-c bit for bit, float32 included. float16 and bfloat16 round the float32 value, and an empty fill leaves the position alone. It is not in the specification.