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 — each parameter below exists on the backend named beside it, and nothing else takes one:

run: {seed: 0}            # the environment's reset, and nothing else
algorithm:
  name: ppo_ewc
  params:
    seed: 0               # torch's generator: weight init and sampling (ppo and ppo_ewc)
    replay_seed: 0        # the hindsight-replay draw (ppo and ppo_ewc)
    fisher_seed: 0        # the EWC Fisher sample (ppo_ewc only -- plain ppo has no Fisher)
  policy:
    id: skyfall_crl.train.backends.policy:language_policy
    kwargs: {model: ./sft-ckpt}

run.seed reaches the environment and nothing else. Everything else the seed should reach is written where it belongs — the algorithm's params, as above — 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 — and a parameter the backend does not take (fisher_seed on plain ppo, say) is refused when the backend is built. One caveat on seed: itself: it calls torch.manual_seed, which is global to the process — right for a reproduction run, wrong to rely on with two seeded backends alive at once.

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 — the same document twice, with run.trace_path set to first.jsonl and then second.jsonl — 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.