tandem rng

One generator, every language.

Tandem8x32 is a noncryptographic pseudorandom number generator with one specified stream that eighteen ports reproduce bit for bit, fast on CPUs and GPUs alike.

Pick your language

Julia

DocsTandemRNG.jl
using Pkg; Pkg.add(url="https://github.com/tandem-rng/TandemRNG.jl")
using TandemRNG
rng = Tandem8x32(42)
x, rng = rand_next(rng, Float64)    # value and next generator

C and C++

Docstandem-c
$ git clone https://github.com/tandem-rng/tandem-c && make -C tandem-c
#include "tandem.h"
tandem_rng rng = tandem_seed(42, 0, 0);
double x = tandem_next_f64(&rng);

Rust

Docstandem-rs
$ cargo add tandem-rng --git https://github.com/tandem-rng/tandem-rs
let mut rng = tandem_rng::Tandem::new(42);
let x = rng.next_f64();
let worker = rng.split(7);          // by index, from the key

Python, NumPy

Docstandem-numpy
$ pip install git+https://github.com/tandem-rng/tandem-numpy
from tandem_rng import TandemGenerator
g = TandemGenerator(42)
z = g.standard_normal(10**6)

CUDA

Docstandem-cuda
$ git clone https://github.com/tandem-rng/tandem-cuda
#include "tandem.cuh"
auto g = tandem::generator::seed(42);
g.fill_f64(x, n);                   // x in device memory

JAX

Docstandem-jax
$ pip install git+https://github.com/tandem-rng/tandem-jax
import jax, tandem_jax as tj
key = tj.key(42)
z = jax.random.normal(key, (1000,)) # any jax.random function

R

Docstandem-r
remotes::install_github("tandem-rng/tandem-r")
library(tandemrng)
rng <- tandem(42)
u <- tandem_runif(rng, 1e6)

PyTorch

Docstandem-torch
$ pip install --no-build-isolation git+https://github.com/tandem-rng/tandem-torch
from tandem_torch import Tandem
t = Tandem(42)
z = t.randn(1000)                   # also device="cuda"

JavaScript, WebGPU

Docstandem-webgpu
$ npm install github:tandem-rng/tandem-webgpu
import { seed, Tandem } from "tandem-webgpu";
const rng = new Tandem(seed(42n));
const xs = rng.fillF64(1 << 20);

Fortran

Docstandem-fortran
! fpm.toml: tandem_rng = { git = "https://github.com/tandem-rng/tandem-fortran" }
use tandem_rng
rng = tandem_new(42_int64)
x = rng%next_real64()

Kokkos

Docstandem-kokkos
$ git clone --recurse-submodules https://github.com/tandem-rng/tandem-kokkos
#include <tandem/kokkos.hpp>
tandem::Rng rng(42);
tandem::fill(x, rng);               // any contiguous View

Java

Docstandem-java
$ git clone https://github.com/tandem-rng/tandem-java && mvn -f tandem-java install
Tandem rng = Tandem.seed(42);
double x = rng.nextDouble();
Tandem worker = rng.split(7);

Mojo

Docstandem-mojo
$ git clone https://github.com/tandem-rng/tandem-mojo && cd tandem-mojo && pixi install
from tandem import Tandem
var rng = Tandem(42)
var x = rng.next_f64()

SYCL

Docstandem-sycl
$ git clone --recurse-submodules https://github.com/tandem-rng/tandem-sycl
#include <tandem/sycl.hpp>
tandem::Rng rng(42);
tandem::fill(q, x, n, rng);         // USM memory on queue q

Swift, Metal

Docstandem-metal
// Package.swift: .package(url: "https://github.com/tandem-rng/tandem-metal", branch: "main")
import Tandem
var rng = Tandem(seed: 42)
let x = rng.nextF64()               // GPU fills take a Metal buffer

MLX

Docstandem-mlx
$ pip install git+https://github.com/tandem-rng/tandem-mlx
import mlx.core as mx, tandem_mlx as tm
key = tm.key(42)
z = tm.normal(key, (1000,))         # Metal kernel on the GPU

Haskell

Docstandem-hs
-- cabal.project: source-repository-package, location: https://github.com/tandem-rng/tandem-hs
import System.Random.Tandem qualified as T
let g0 = T.seed 42
    (x, g1) = T.nextDouble g0       -- value and next generator

OCaml

Docstandem-ml
$ git clone https://github.com/tandem-rng/tandem-ml && cd tandem-ml && opam install . && dune build
let g = Tandem.seed 42
let x, g = Tandem.float g
let worker = Tandem.split g 7       (* by index, from the key *)

Speed

porthardwareFloat64 uniform fill, GiB/sFloat64 normal fill, GiB/ssource
tandem-cApple M4 Pro, one thread16.5 std::mt19937_64 5.17.6 std::normal_distribution 0.90docs/speed.md at b031bed
TandemRNG.jlApple M4 Pro, one task17.2 Xoshiro 19.87.74 Xoshiro 7.19performance.md at 0410c6a
tandem-cudaNVIDIA A100 40 GB1388 cuRAND 7781064 cuRAND 567docs/speed.md at 59e7bcc

GiB/s written to memory, with the baseline from the same page where it has one. The A100 fills run near the card's memory bandwidth. All three Float64 normal fills use the ziggurat.

Every port

How it works

Eight 32-bit words

The state is two halves of four words. The hidden half runs a bijective clock and takes feedback from the exposed half. The exposed half is a Philox-shaped Feistel step whose multipliers come from the hidden half. Each step emits the 128 exposed bits.

Split, fork, purpose

A keyed seeding function starts every chunk of 32 steps, and eight chunks interleave into one 1024-bit row, so eight GPU lanes write one contiguous row. split(i) derives a child from the key and an index, fork(n) a batch from the current block, and purpose(u) a named sub-stream. A child costs at most one seeding.

Element i is draw i

Element i of a fill equals draw i, so any split across threads, ranks or GPUs reproduces a serial run. Bounded integers, normals and exponentials follow one shared appendix, so the ports agree on those too.