Integration and parallel execution

Choose the state model first

Derive all independent keys before any sequential chain consumes them.

NeedUseState contract
Parallel, indexed, or addressed workkeys with splitrng, subrng, generate, or @keyedA key never advances
Pure sequential or work-item-local device codeRNGCursor with cursor nextrand* methodsReturn and thread the next cursor
Code expecting Random.AbstractRNGMutableRNG, mutably, or @mutablyOne active owner; checkpoint with freeze

MutableRNG(key) and Random.AbstractRNG(key) start a cursor at decimal position zero. They do not resume prior mutable state. Construct from a cursor to continue:

rng = MutableRNG(key)
result = foreign_code(rng)
cursor = freeze(rng)
@assert cursor_position(cursor) > 0

continued = MutableRNG(cursor)

freeze returns an immutable RNGCursor. restore!(rng, cursor) replaces the logical state and clears private caches. A bare key cannot encode sequential position. restore_cursor(key) and restore!(rng, key) throw IncompatibleCheckpoint.

mutably(f, key_or_cursor) and @mutably return (result, next_cursor). try_mutably returns MutablySuccess or MutablyFailure and retains the last known checkpoint after an exception. Use the qualified names ImmutableRNGs.issuccess and ImmutableRNGs.unwrap. They remain unexported to avoid ecosystem collisions.

The wrapper is not thread safe and permits one active owner. A task may receive the wrapper only if the sender stops using it until ownership returns. Concurrent tasks must derive separate keys and create separate wrappers.

Cursor steps and key steps

The same verbs support two state laws:

# Preserved split-per-call key continuation
x, next_key = nextrand(key)

# Strict sequential raw-word continuation
cursor = RNGCursor(key)
x, cursor = nextrand(cursor)
y, cursor = nextrandn(cursor)

Cursor steps match owned MutableRNG scalar and array calls from the same starting cursor. Key steps use a different stream. Changing a loop initializer from key to RNGCursor(key) changes its values.

Checkpoints and recovery

Use cursor_checkpoint(cursor) for a durable logical record. Use restore_cursor(record) to validate and restore it. The record exposes the complete key and position. It provides no authentication, confidentiality, substitution protection, or corruption recovery. Add those properties with a caller-owned envelope.

Exact external sampler replay needs more than a cursor. In a separate manifest, record the exact Julia, ImmutableRNGs, consumer, and dependency versions. Also record the model, callbacks, sampler configuration, chain and worker layout, and canonical starting record.

After an asynchronous failure, call freeze only if ImmutableRNGs.is_ready(rng) is true. A poisoned wrapper rejects draws and state reads. Recover it with caller-owned state through restore!, copy! on its destination, or supported integer seed!.

generate and macros

generate(f, key, T, dims...) gives element i the key subrng(key, i - 1). On CPU, f may use a local MutableRNG or variable work. Device bodies must remain pure, allocation-free, and kernel-safe.

values = generate(key, Float64, 100) do element_key
    randn(element_key)
end

@generate and @mutably bind a local wrapper. They rewrite bare or literal Random.rand, randn, and randexp calls that omit the bound RNG. @keyed derives each iteration from the loop-variable value. Plain, threaded, and Polyester loops therefore use the same addresses. Repeated addresses replay.

Threads and adaptive branches

Keys are immutable values and can be shared. Derive one child before each concurrent chain starts:

children = splitrng(key, Val(n))
@sync for child in children
    Threads.@spawn begin
        rng = MutableRNG(child)
        work(rng)
    end
end

Version 0.1.0 has no splitrng(::RNGCursor), subrng(::RNGCursor, ...), or independent cursor fork. Adaptive code may consume a UInt64 from the parent and construct a session-local child when it cannot plan branches. This statistical workaround is not structural separation. Replay requires the same fork sequence. Philox uses the consumed word under its modulo seed law. Squares conditions the seed and therefore has a different separation law. This recipe is not a stable fork API.

KernelAbstractions and CUDA

KernelAbstractions defines the package's kernel interface. Version 0.1.0 qualifies exactly these CUDA pairs:

  • Philox4x32 on CUDA.CUDABackend()
  • Threefry2x32 on CUDA.CUDABackend()

The qualified host-controlled mutable surface is rand!, randn!, and randexp! for Float32 and Float64. The host wrapper validates and commits the complete span before launch. Only the immutable start address reaches the kernel. A later kernel error leaves the committed cursor in place.

Raw words, transform inputs, and ending cursors match CPU scalar draws exactly. Values match the same-backend scalar path exactly. CPU and CUDA normal and exponential values may differ within the calibrated ULP bound. No range fill, other generator, or other backend is claimed in 0.1.0.

Pure addressed keys are the general parallel model. An accelerator work item may own and thread a local cursor derived from its key. Device code can use the public, unexported try_nextrand* and try_advance_cursor forms for terminal handling without exceptions. Always branch on ok.

GPUArrays' rand_native! and randn_native! use backend-native streams for throughput. They do not use the portable package stream.

Reactant and Enzyme

Reactant tracing requires explicit cursor state. A top-level MutableRNG argument must write its next cursor back after each compiled invocation. Repeated calls must not replay the traced range or route through ReactantRNG. A closed-over wrapper without a graph state path is unsupported.

Reactant support in 0.1.0 is an experimental functional preview rather than a qualified accelerator backend. XLA CUDA acceptance on an A100 covers continuation, recovery, terminal handling, and HLO work bounds. Reactant has no performance guarantee. In one Reactant 0.2.279 A100 run, the full mutable lifecycle cost 5.05 to 8.43 times the immutable graph across twelve length-2^20 rows. A separate minimal device-resident Philox rand(Float32) shape without transfers cost 1.1964 times as much. Use the qualified KernelAbstractions CUDA path for supported host-dispatched GPU fills.

Enzyme treats bundled keys and cursors as inactive random state while gradients flow through caller data. Host Enzyme rules cover rand!, randn!, and randexp!. They advance the cursor as a direct fill would, zero single and batched destination shadows, and preserve caller gradients. The extension does not cover AD entries that bypass these rules.

The owned Enzyme rules passed functional CuArray CUDA acceptance on the same A100. The result verifies accelerator execution and cursor state, but not Reactant or Enzyme performance.

Distributions

The optional Distributions extension owns fixed cursor paths for Uniform{Float32/64}, Normal{Float32/64}, Exponential{Float32/64}, Bernoulli{Float32/64}, and supported DiscreteUniform. Cursor steps and the wrapper agree for these distributions.

Generic distributions use the normal mutable interface. They may consume a variable number of primitives. Sequential distribution arrays use one cursor; pure immutable arrays retain one subrng per logical sample. These streams are intentionally different.

Uniform{Float32} uses an owned Float64 grid before converting the final value. Rounding may return the upper endpoint exactly.

Turing and AbstractMCMC

The isolated interop suite tests Turing Prior, MH, and NUTS with this execution matrix:

ModeSource wrapper after success
one direct chainafter that chain
MCMCSerialafter the final reseeded chain
MCMCThreadsafter generating the per-chain seeds
same-version MCMCDistributedafter generating the per-chain seeds

AbstractMCMC copies and integer-reseeds wrappers for ensemble chains. Per-chain results for the same generated seeds agree between serial and threaded modes. The source continuation differs because the modes use it differently.

Standard threaded and distributed APIs do not expose every failed chain's RNG and sampler state. They cannot recover every failed chain. Own and checkpoint each chain's wrapper and sampler state when recovery matters. See examples/18_turing_bridge.jl for explicit orchestration.

Distributed workers must match Julia, word size, ImmutableRNGs, Turing, AbstractMCMC, generator support, and stream ID. They must load modules that define models, samplers, callbacks, and captured user types. Captured values, initial parameters, sampler state, and callbacks must serialize or be rebuilt on every worker. ARS5 requires AES support on every worker.

For a ready MutableRNG, Julia's process serializer stores only the cursor. Deserialization creates a ready wrapper with an empty cache and Reactant state. Inflight and poisoned wrappers reject serialization. This is same-version process transport. Use the canonical data-only cursor record for durable storage.

CachingPool transports a captured wrapper once to each worker. Chains on one worker share one deserialized wrapper, which AbstractMCMC reseeds for each chain. pmap serializes calls on each worker and preserves the single-owner rule. Distributed replay across versions is unsupported.

LoopVectorization works after randomness is materialized. Fill an array, then transform it with @turbo. Direct RNG calls inside @turbo are unsupported.