Random numbers and reproducible seeds
Understand local seeded generators, versioned random streams, seed ownership, validation evidence and the limits of exact replay across scientific workflows.
Implemented in developmentAner's formal scientific path uses explicit, deterministic pseudorandom numbers. A seed belongs to a particular operation or a documented simulation identity. Repeating the same operation with the same seed restarts its stream; it does not continue an invisible process wide generator. This makes replay and concurrency easier to control, provided that the experiment also records its inputs, operation order, numerical backend, and implementation.
The October 17, 2026 audit found a sound foundation: local seed state, explicit 53 bit uniform conversion, rejection sampling for integer bounds, and worker independent simulation identities. The main maintenance weakness was duplicated generator code and scattered explanations of stream ownership. The formal change consolidates those primitives, pins their compatibility with regression tests, and establishes the policy below. It preserves existing seeded sequences. It does not certify statistical independence or replace every component with a new generator.
Physical entropy capture is a separate experimental investigation. The regular dataset, neural, generative, quantum simulator, and parallel APIs do not read a physical device or silently reseed themselves.
Practical rules
- Write down a seed for each purpose before running the experiment: partitioning, initialization, batch order, dropout, fitting, and final sampling. Reuse a seed only when repeated or deliberately paired random inputs are intended.
- Treat a seed as an exact integer. Save it as an integer or decimal string; a large Int64 seed cannot always survive conversion through Float64 or a JavaScript number.
- Remember the ownership model.
nn.randomandnn.dropoutrestart a local stream per call.parallel.randomandparallel.normalselect an explicit draw identity without advancing state. Training APIs derive their own step or epoch streams. - Assign different draw identities to different random decisions, including decisions using different distribution functions. Keep the assignment stable when comparing workers, implementations, and scenarios.
- Preserve the source, input identity and row order, settings, build, backend, dtype, checkpoints, and outputs needed for replay. A seed alone is an incomplete experiment record.
- Use multiple prespecified seeds for scientific sensitivity analysis. Keep every outcome; a favorable seed is not evidence of a robust result.
Established reproducibility guidance recommends explicitly owned generators and explains why a fixed seed cannot make an entire numerical program portable across software stacks. Aner applies this ownership principle through seeded calls and explicit draw identities in its native runtime. NumPy NEP 19
Current implementation and seed contract
| Operation | Engine and mapping | Owner and seed domain |
|---|---|---|
nn.random, tensor.random, Xavier/He uniform initialization | SplitMix64 words; top 53 bits become a Float64 uniform, then a symmetric scale | Local to each call; signed Int64 seed is converted to its UInt64 bit pattern. |
nn.dropout | The same uniform mapping; a value below probability is dropped, retained values are scaled by 1/(1-probability) | Local to each call; signed Int64. Repeating the seed repeats the mask for the same flattened shape. |
| Dataset split, partition, folds, batches | SplitMix64 with rejection before modulo; Fisher and Yates where shuffling is required | Local to construction; signed Int64. Input row order and grouping/stratification settings affect the result. |
data.shuffle contextual batches | The same bounded draw and Fisher and Yates recipe | Local to the call; signed Int64. Features, targets, and context are gathered together. |
| K means initial centers | Bounded SplitMix64 selection of distinct training row indices | Local to fitting; signed Int64. Equal feature values can still occur at different selected indices. |
| Dense pipeline initialization | nn.random with the configured uniform strategy and zero biases | Base seed plus Dense index; checked Int64 overflow. The recipe is retained for compatibility. |
| Dense mini batch training | Dataset batch schedules with shuffle_seed + completed_epoch | Training configuration plus completed epoch; checked overflow and continuation constraints. |
| Decoder initialization, training, and generation | SplitMix64, bounded permutations, seeded dropout, and cumulative token sampling | Separate explicit model/training/generation inputs; signed Int64. Step, layer tag, epoch, or generation row further identify the operation. |
| Scientific generative models | SplitMix64, bounded timestep/row draws, and a fixed Box Muller transform | Explicit initialization, training, calibration, or sampling seed; signed Int64. Training derives streams from the saved update identity. |
parallel.random and parallel.normal | Existing keyed SplitMix64 mixer schedule; uniform53 or Box Muller | Explicit nonnegative Int64 seed and draw; the default draw is zero. |
parallel.repeat | Existing master/iteration seed derivation; positive 63 bit task seeds | Nonnegative Int64 master seed and zero based iteration identity. The task owns its draw identities. |
| Native Monte Carlo and analytic logistic fixtures | Existing mixer recipes indexed by seed and sample identity | Explicit seed and global sample identity, independent of chunk assignment. These remain distinct documented workload recipes. |
| Quantum shot simulation | std::mt19937_64 plus explicit top 53 bit mapping and cumulative probabilities | Local to quantum.run; signed Int64 seed. This is a seeded classical simulator. |
Exact entry points and parameter names remain in neural primitives, sequence operations, datasets, generative models, parallel repetitions, and quantum simulation.
There is no new public aner.random module, global seed setter, or automatically advancing random object in this change. The shared C++ header is an internal implementation component.
The shared primitive protocols
include/aner/random.hpp owns the common implementation under aner::rng:
| Identifier | Meaning |
|---|---|
aner.splitmix64/v1 | UInt64 state increment and finalizer used by the existing local streams. |
aner.uniform53/v1 | double(word >> 11) * 2^-53, a 53 bit grid in [0,1). |
aner.bounded-rejection/v1 | Reject words below (0-bound)%bound using UInt64 arithmetic, then return word%bound. |
aner.parallel-keyed/v1 | Existing master/iteration seed derivation and keyed (seed, stream, draw) mixer recipe. |
aner.parallel-box-muller/v1 | Existing keyed stream zero radius and stream one angle transform for parallel.normal. |
UInt64 arithmetic intentionally wraps modulo 2^64. The SplitMix64 increment is 0x9e3779b97f4a7c15; the two finalizer multipliers are 0xbf58476d1ce4e5b9 and 0x94d049bb133111eb. The shared mix operation performs one stateless increment/finalization, while next mutates caller owned state. These definitions preserve the former local implementations.
The bounded helper requires a positive bound and rejects zero explicitly. Rejection is essential: direct modulo would assign unequal probabilities when the bound does not divide 2^64. Fisher and Yates calls it with the number of remaining positions. The existing call sites already used this correct rejection recipe; consolidation reduces the risk that a later copy diverges.
The primitive identifiers do not completely specify a higher level distribution. To replay a dropout mask, shuffle, normal sample, or token sample, retain its operation recipe and parameters too. The quantum engine, parallel key derivation, scientific normal transform, and model seed schedules retain their own existing semantics.
SimulationResults.summary() now includes rng_engine, rng_uniform, rng_schedule, and rng_normal with the corresponding identifiers, alongside the master seed. Its rng_scope is task-seed-and-parallel-helpers: these fields describe task seed derivation and the parallel helper recipes, not every operation an arbitrary task may call. Save that summary with exported iteration/seed/result rows. Other APIs still require their recipe and seed information to be retained explicitly.
The constants and word transform agree with the SplitMix64 reference implementation. Aner keeps state owned by the caller instead of using that reference file's global example state.
Ranges and distribution details
nn.random(rows, cols, seed, scale) maps the unit grid to [-scale, scale). Scale must be finite and nonnegative. Zero scale returns zeros. The implementation clamps an upward rounded value below the excluded positive bound, including for subnormal scales. At very small scales, floating point rounding can concentrate values on a few representable outputs; a continuous mathematical distribution is not representable there.
Dropout accepts a finite probability in [0,1). Inference mode and zero probability return the input. A fresh call with the same seed, probability, and shape reproduces the same mask; it is not a fresh random experiment.
parallel.normal uses two keyed uniform values and Box Muller arithmetic. Its first uniform is the same stream zero value returned by parallel.random for that seed and draw. Therefore parallel.random(seed, draw) and parallel.normal(seed, draw) are coupled. Give separate scientific decisions separate draw identities even when the distribution functions differ.
Scientific models retain their earlier two word Box Muller recipe. Its logarithm input uses (double(word >> 11)+0.5)/2^53 to avoid zero. Floating point rounding can reach one at the upper extreme; this yields a finite zero radius. This compatibility transform is a discrete approximation and must not be described as an exact continuous normal or an exactly open (0,1) conversion.
Token sampling depends on model logits, temperature, top-k/top-p settings, candidate ordering, and the generation seed. The seed controls the random threshold; it cannot preserve tokens when logits or selection settings change. Greedy generation does not need a random threshold.
Explicit seed plans with existing Aner APIs
Separate purposes visibly in the source. These human selected seed values are an experiment plan, not a claim that adjacent or different seeds establish statistical independence:
import aner.dataset
import aner.nn
let partition_seed = 20261017
let initialization_seed = 42017
let shuffle_seed = 81017
let dropout_seed = 91017
let flowers = dataset.load("iris")
let parts = dataset.partition(flowers,
validation: 0.2, test: 0.2, seed: partition_seed)
let training_rows = dataset.train(parts)
let schedule = dataset.batches(training_rows,
size: 16, seed: shuffle_seed, shuffle: true)
let weights = nn.random(4, 8, seed: initialization_seed, scale: 0.2)
let masked = nn.dropout(weights,
probability: 0.1, seed: dropout_seed, training: true)
print(dataset.provenance(training_rows))
print(dataset.count(schedule))The example constructs one mask. A hand written training loop must deliberately select another dropout seed for a new step and retain that schedule. The built in generative trainers already derive dropout streams from their saved training step and layer tag. Dense pipeline training separately advances its documented epoch shuffle seed. Do not add an implicit wall clock seed to either workflow.
For simulations, use the task seed supplied by parallel.repeat and reserve a nonoverlapping draw range for each decision:
import aner.parallel
fn one_path(parameters: List, iteration: Int64, seed: Int64) -> List {
var total = 0.0
var step = 0
while step < 32 {
// Even IDs: normal shocks. Odd IDs: a separate uniform decision.
let shock = parallel.normal(seed: seed, draw: 2 * step)
let decision = parallel.random(seed: seed, draw: 2 * step + 1)
if decision < 0.5 { total = total + shock }
step = step + 1
}
return List(total)
}
let results = parallel.repeat(iterations: 1000, task: one_path,
parameters: List(0), execution: parallel.local(), seed: 20261017)
print(results.mean())
print(results.seed(iteration: 0))In a larger simulation, document the maximum step/decision counts and check that draw arithmetic cannot overflow Int64 or reuse an identity. A retry should retain its iteration and draw identities so it replays the same work. Never derive scientific streams from a worker number, thread ID, completion order, process ID, or elapsed time.
When comparing two parameter settings with the same random inputs, reusing the master seed and iteration/draw identities is an intentional common random number design. Those paired results are dependent. When estimating independent replications, plan separate seeds and assess sensitivity; the mere fact that seeds differ is insufficient evidence of independence.
Independence, collisions, and the migration boundary
SplitMix64 provides a deterministic 64 bit state cycle. Seeds choose positions in that cycle. In particular, seeds separated by a multiple of the increment select streams related by an offset. Separate seed values do not imply separate cycles or prove statistical independence. Reusing a seed across two calls can also reuse the same underlying words even when their outputs are transformed differently.
The legacy Dense base_seed + layer_index and mini batch shuffle_seed + epoch schedules remain reproducible recipes. Do not generalize them into a worker or experiment spawning algorithm: neighboring base seeds can give overlapping sets of layer or epoch seeds across runs. Established parallel guidance explains this additive seed overlap and describes structured identity mixing, spawning, and counter based alternatives. Aner has not implemented SeedSequence or Philox in this change. NumPy parallel RNG guidance
parallel.repeat mixes master seed and iteration identity, then maps into positive 63 bit task seeds. This is stable across worker counts; it does not guarantee collision free seeds over all possible inputs. Its keyed draw helper is an existing custom mixer recipe, not a separately certified counter based generator.
A future scientific RNG proposal should evaluate a well studied counter based engine with explicit experiment, purpose, replicate, sample, step, and decision identities; wider seed/key material; documented stream budgets; and migration tests against today's protocols. That is research work. It must be an explicit, versioned choice with measured statistical and performance evidence. Changing the default engine or seed schedule silently would invalidate historical experiments.
CPU, accelerators, concurrency, and checkpoints
Neural initialization and dropout masks are generated on the host, then converted or uploaded for the selected placement. There is no independent cuRAND, Metal, or MPS random state in these paths. This avoids another implicit seed lifecycle. Float32 conversion, accelerator kernels, matrix libraries, reductions, and compiler settings can still change numerical results even when the random words agree.
Parallel repetitions derive task seeds from iteration identity and retain output rows in iteration order. Worker assignment and completion order do not affect those identities. Native Monte Carlo fixtures index draws by global sample identity, so changing work chunks does not change samples. The same guarantee does not automatically cover arbitrary user code, distributed floating point reductions, or every neural backend.
Decoder and scientific model checkpoints retain training seeds, update/epoch/cursor state, optimizer state, bound data identity, and execution provenance. Continuation reconstructs the random schedule from those identities rather than serializing a hidden global generator. Existing tests compare interrupted and uninterrupted decoder training, including dropout and partial batches. Dense mini batch checkpoints retain their shuffle configuration, source row order, completed epochs, and optimizer state; continuation validates the relevant inputs.
Generative continuation also checks implementation identity. The shared RNG header is included in that build identity. This refactor therefore changes the identity even though the seeded draws are preserved. A checkpoint made by a prior implementation can remain usable for supported inference while exact continuation rejects the identity mismatch. Preserve the original runtime for exact historical continuation; do not remove or bypass that check merely because the random sequence matches.
Raw UInt64 vectors and uniform53 conversion have tightly specified bit level behavior. Normal transforms depend on native log, sqrt, sin/cos or related math, and training depends on many further numerical operations. Bitwise equality across operating systems, math libraries, dtypes, accelerator models, or different software builds is not a general promise. Pin the environment for exact replay and state tolerances for numerical portability. NumPy NEP 19
Minimum experiment record
| Record | Why it matters |
|---|---|
| Experiment identity, code, runtime version, implementation/build identity | Distinguishes the algorithm and numerical implementation. |
| Every seed as an exact integer, its purpose, and its derivation | Prevents accidental reuse and loss of high bits. |
| Engine/mapping identifiers and higher level operation recipe | A seed has meaning only relative to the generator and transformation. |
| Data hashes, selected row IDs/order, split/fold/group settings, schedule fingerprint | Data order and eligible rows affect random selection and training. |
| Model dimensions, initialization strategy, distribution parameters, batch size, training and generation settings | Call shape and consumption order affect outputs. |
| Device, dtype, numerical backend, compiler/platform and relevant thread/reduction settings | Distinguishes random bit replay from numerical result replay. |
| Checkpoints, completed step/epoch/cursor, optimizer state, and generation row seeds | Makes interrupted execution and per row generation reviewable. |
| Prespecified seed set, full results, acceptance criteria, and known failed runs | Supports scientific validation without seed selection bias. |
Some APIs already retain parts of this record. The table is a research workflow contract; it is not a claim that one automatic manifest currently captures every field for every Aner operation. Legacy checkpoints can retain less provenance than current formats. Inspect their summaries and preserve the experiment source alongside them.
Validation evidence and remaining limits
The focused aner.randomness test target covers shared raw word known answers, exact uniform53 conversion and endpoints, seed restart semantics, integer bounds including a forced rejection and zero rejection, representative neural initialization and dropout masks, dataset batch order/fingerprint, the existing keyed parallel schedule, and deterministic distribution diagnostics. Its purpose is to catch implementation regressions and gross mistakes. The established quantum tests continue to check its separate engine behavior.
Existing component tests add fixed dataset split memberships, same/different seed behavior, finite normal draws, one versus many worker equality, dropout gradients/inference behavior, model checkpoint continuation, and backend specific numerical behavior. The relevant repository sources include tests/randomness_tests.cpp, tests/dataset_tests.cpp, tests/partition_tests.cpp, tests/sequence_tests.cpp, tests/parallel_repeat_runtime.cpp, tests/gen_tests.cpp, and tests/training_tests.cpp.
Passing deterministic diagnostics establishes behavior for the checked fixtures, sample sizes, and build. It does not prove independence, tail accuracy for every scientific use, correctness of a scientific model, or the quality of a physical entropy source. No TestU01/PractRand campaign, independent statistical certification, or physical device validation is claimed by this change. Accelerator parity must be checked on the actual available hardware; host based mask construction alone does not establish whole model parity.
The October 17, 2026 macOS arm64 CPU check used 100,000 samples and fixed seed 20261017. Release and Debug builds passed the focused contracts. The Release smoke observations below are rounded; thresholds were fixed in the test source before execution. Uniform samples alternate with bounded draws on one local stream; normal samples use the keyed parallel recipe.
| Diagnostic | Observed | Prespecified smoke condition |
|---|---|---|
| Uniform mean | 0.500917 | Distance from 0.5 at most 0.005 |
| Uniform population variance | 0.0834344 | Distance from 1/12 at most 0.002 |
| Ten bin bounded draw chi square statistic | 9.8492 | Less than 40 |
| Normal mean | −0.00243747 | Absolute value at most 0.025 |
| Normal population variance | 1.00501 | Distance from 1 at most 0.04 |
These broad deterministic diagnostics supplement exact vectors and forced rejection tests. They are regression alarms, not a statistical acceptance study for scientific deployment. The existing seeded neural values, dropout mask, dataset batch ordering and schedule fingerprint also match the pre consolidation runtime fixtures exactly. Actual accelerator execution was not part of this audit run.
Before changing an engine, distribution transform, seed derivation, or draw consumption order, maintainers must identify affected callers, update the documented protocol/recipe, add independent known answer and boundary tests, run the relevant component and continuation tests, and publish the stream compatibility impact. A performance only refactor should preserve existing golden outputs. A necessary correctness fix may change outputs, but must make that change explicit and preserve the environment needed to replay prior published work.
Security and the physical entropy boundary
Scientific SplitMix64 streams, keyed simulation helpers, and the quantum simulator are unsuitable for passwords, tokens, session secrets, or cryptographic keys. More unpredictable seeding does not turn these deterministic engines into cryptographic generators. Operational std::random_device uses in job/session identity creation are separate from scientific sampling; that C++ facility alone does not establish a physical device or entropy certification claim.
Physical entropy work must describe its noise source, acquisition method, failure behavior, conditioning, retained provenance, and entropy assessment. Statistical appearance alone cannot establish the source's unpredictability. NIST SP 800 to 90B addresses entropy source design and validation, including health testing and conditioning, as a separate concern from deterministic generation. This project does not claim NIST validation. NIST SP 800 to 90B
The experimental entropy guide defines the side implementation and its limits. It must remain opt in and explicit, preserve captured bytes and source provenance for replay, and fail visibly on acquisition problems. It must not introduce a hidden fallback, periodic reseeding, or a device dependency into the formal scientific APIs.