Skip to content

Make a run reproducible

Reproducibility is available, not automatic: some of what varies is seeded by default and some deliberately is not — the ported algorithms seed neither their replay draw nor their Fisher sample, faithfully to the originals.

Set the seeds that are not set for you:

run: {seed: 0}
algorithm:
  name: ppo
  params:
    replay_seed: 0      # the hindsight-replay draw
    fisher_seed: 0      # the EWC Fisher sample
  policy:
    id: skyfall_crl.train.backends.policy:language_policy
    kwargs: {model: ./sft-ckpt, seed: 0}

run.seed reaches the environment and nothing else. Everything else the seed should reach is written where it belongs — the policy's kwargs, the algorithm's params — and in a sweep that is what ${seed} does: substituted wherever it appears, so every cell's seed reaches whatever the document says it reaches. A misplaced key is an error, not a no-op: a document that puts policy: at the top level is refused at load, naming the key.

Prove it, don't assume it. Tutorial L5 does exactly this, gate-verified: the same document run twice produces 600 byte-identical trace rows. The equivalent check for your own run is two runs into two paths and a comparison of the deterministic JSON:

skyfall-crl eval --traces first.jsonl --format json --output a.json
skyfall-crl eval --traces second.jsonl --format json --output b.json
diff a.json b.json

The JSON carries no timestamp and no absolute path, so identical runs produce byte-identical files — an empty diff is the proof.

What can still differ. A reward component that reads a clock scores by observed_at when the row carries one — traces recorded without it replay against now. Unseeded inference differs every time, which is why a scored serve names --seed. And identical results across seeds is its own warning sign: if every seeded run matches, the seed reached nothing.