API
Use
import Metal
import Tandem
let kernels = try TandemKernels(device: MTLCreateSystemDefaultDevice()!) // compiles the shader
var rng = Tandem(seed: 42) // 128-bit seed through the spec's whitening
let buffer = kernels.device.makeBuffer(length: 4 << 20, options: .storageModePrivate)!
rng.fill(.u32, count: 1 << 20, buffer: buffer, kernels: kernels) // blocks until done
let cb = kernels.queue.makeCommandBuffer()!
rng.fill(.f32, count: 1 << 20, buffer: buffer, kernels: kernels, commandBuffer: cb) // only encodes
cb.commit()
rng.fill(.u32Below(range: 6), count: 1000, buffer: buffer, kernels: kernels) // in [0, 6)
rng.fill(.u32BelowWide(range: 6, low: UInt64(bitPattern: -3)), count: 1000, // Int64 in [-3, 3)
buffer: buffer, kernels: kernels)
rng.fill(.normalF32, count: 1 << 20, buffer: buffer, kernels: kernels)
let worker = rng.split(7) // by index, from the key alone
let phase = rng.purpose(2) // by purpose identifier, from the key alone
let children = rng.fork(4) // from the current block; rng moves past it
var x = [Double](repeating: 0, count: 1000) // the CPU path, f64 included
rng.fillNormalF64(&x)
let u = rng.nextF64(), k = rng.nextU64(below: 10), e = rng.nextExponentialF64()Reference
tandem.metal: the building blocksT,F,F_keyed,block,split_key,purpose_keyand the fills, as functions in namespacetandem, and the kernelsfill_u32,fill_f32,fill_below32,fill_below32_wide,fill_below64,fill_normal_f32,fill_normal_f32_odd,fill_exponential_f32andfill_choice, which wrap them. One thread walks one chunk and stores each 16-byte block of draws with one vector store. It is the one shader source of the Metal ports: tandem-mlx vendors it unchanged and leaves the kernels out withTANDEM_NO_KERNELS.Sources/Tandem: theTandemtype, its CPU draws and fills, andfill(_:count:buffer:...), which encodes a kernel.Shader.swiftembedstandem.metal(tools/embed.sh), so the package needs no resource bundle.
Metal has no double type. Float64 uniforms, normals and exponentials
run on the CPU, and the GPU fills u64 words, which
(raw >> 11) 2^-53 maps to Float64 on the host.
A Tandem is the transport form of section 7,
key, position and chunkLength,
plus a cache of the current row. Copies draw the same values, and
equality compares the transport form. Set position to move
a generator, at the same cost at any distance.
fill takes a GPUDraw:
| case | element | values |
|---|---|---|
.u32, .u64 |
4, 8 bytes | stream words |
.f32 |
4 bytes | (raw >> 8) 2^-24 |
.u32Below(range:low:) |
4 bytes | low + Lemire on [0, range) from 32-bit
draws |
.u32BelowWide(range:low:) |
8 bytes | the same 32-bit draws, stored as 64-bit values |
.u64Below(range:low:) |
8 bytes | low + Lemire on [0, range) from 64-bit
draws |
.normalF32 |
4 bytes | Box-Muller pairs of f32 uniforms |
.exponentialF32 |
4 bytes | -ln(1 - u) of f32 uniforms |
.choice(table) |
4 bytes | indices of a ChoiceTable from 64-bit draws |
ChoiceTable(weights:) builds the integer alias table of
Appendix
C on the host from finite, nonnegative weights, not all zero, and
throws otherwise. An index has probability proportional to its weight.
Each element maps one 64-bit draw by integer operations and never
retries, so the indices equal every other port's, on the CPU and the
GPU, and an empty fill aligns the position to 64 bits. A GPU choice fill
copies the table to the device.
fill writes count elements at
offset bytes, a multiple of the element size, and moves the
position as the CPU fill of the same kind does. Without
commandBuffer it runs on the kernels' queue and returns
when the fill is done. With one it only encodes, so fills can follow
each other in one command buffer, since each call returns with the new
position. The low bound adds in the output type with wrap-around, so a
signed output takes the bit pattern of its bound.
The CPU draws are nextU32, nextU64,
nextF32, nextF64,
nextU32(below:), nextU64(below:),
nextNormalF64, nextNormalF32,
nextNormalPairF32, nextExponentialF64,
nextExponentialF32 and nextChoice(_:). The CPU
fills are fillU32, fillU64,
fillF32, fillF64,
fillU32(_:below:low:) into 32-bit or 64-bit arrays,
fillU64(_:below:low:), fillNormalF64,
fillNormalF32, fillExponentialF64,
fillExponentialF32 and fillChoice(_:table:).
Tandem.stepT, Tandem.seedF and
Tandem.block expose the building blocks for conformance
tests. Tandem is a RandomNumberGenerator whose
next() is nextU64(), so
Int.random(in:using:) and shuffled(using:)
take it, with the standard library's mappings rather than those of
Appendix A.
Parallel use
Element i of a fill is draw i, so threads,
command buffers 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. Start every range of a Float32 normal
fill at an even element.