← TinyGPT · docs · devlog · roadmap · speedup
source: docs/determinism.md · view on GitHub ↗

Determinism contract

tinygpt train --seed <UInt64> now seeds both of TinyGPT’s two randomness surfaces:

  1. MLXRandom — drives every MLX op that draws random numbers: model parameter init (He / Xavier / etc.), GPU-side dropout, embedding noise (NEFTune), and any other MLX-sourced randomness inside the forward/backward pass.
  2. BatchRng — a Splitmix64-backed host generator that wraps every corpus sampler’s window pick (ByteCorpus.sampleBatchRaw, TokenizedCorpus.sampleBatchRaw, IOSampler, SFTCorpus, PreferenceCorpus — see Sources/TinyGPTModel/BatchRng.swift).

Both are seeded together in Train.run. Two runs with the same --seed now produce:

The remaining caveat — prefetching

The prefetcher runs sampleBatchRaw on a background thread to overlap data prep with the previous step’s GPU compute. When seeded, that background thread still calls BatchRng.randomInt(in:), which is NSLock-guarded — so the individual draws are deterministic — but the interleaving between prefetch lookahead and the main loop’s foreground draws is scheduler-dependent. In practice this means:

Same seed → batches drawn in the same per-thread order, but which batches land in foreground vs prefetch can drift if the OS schedules differently. Observed: same-seed loss stays within ~1e-5; step-0 is bit-identical because the prefetcher hasn’t issued any draws by that point. A different seed diverges ~1e-1 — four orders of magnitude larger, so the harness below cleanly separates “reproducible” from “different run”.

There is currently no flag to disable the prefetcher, so bit-exact replay past step 0 is not available on the GPU path. For the common case — sanity-checking a spike, A/B sweeps — the ~1e-5 reproducibility is more than enough (the knob-under-test moves loss by far more).

Verifying determinism

The C9 harness runs the same training twice at one seed and asserts step-0 is bit-exact, the same-seed divergence stays within tolerance (DETERMINISM_TOL, default 1e-2), and a different seed produces a distinct trajectory:

bash evals/determinism-smoke.sh
# step-0 bit-exact: 6.103198
# same-seed max divergence: 2.67e-05 (<= 1e-02) — reproducible, not bit-exact past step 0
# cross-seed divergence:    1.85e-01 (distinct trajectory)

A real determinism regression (an unseeded sampler, a stray RNG draw) would diverge by O(1) and trip the harness; ordinary GPU FP noise (O(1e-5)) passes.

Unit tests pinning the contract live at Tests/TinyGPTModelTests/BatchRngTests.swift:

Where this matters

V1 → V2 changelog (for the curious)

See docs/PLAN.md §3 C9 for status.