Skip to content

Make the seed settable, pre-register its reading, and launch both seed-variance campaigns (#51, #135) - #155

Open
jonfroehlich wants to merge 1 commit into
mainfrom
exp/seed-variance-51-135
Open

Make the seed settable, pre-register its reading, and launch both seed-variance campaigns (#51, #135)#155
jonfroehlich wants to merge 1 commit into
mainfrom
exp/seed-variance-51-135

Conversation

@jonfroehlich

Copy link
Copy Markdown
Member

Makes the training seed settable in both trainers, pre-registers how its variance will be read, and launches the two campaigns that measure it. Both campaigns are running as of 2026-09-03 — job IDs below.

Why now

#51 and #135 hit the same wall from opposite sides.

Neither trainer could vary the seed at all: train.py hardcoded manual_seed(42) with no flag, and run_yolo_train_tillicum.slurm never passed one to Ultralytics.

Everything downstream inherits this. With no noise floor, no RampNet 2.0 improvement can be called real either.

The half that was easy to get wrong

Stage 2 has two sources of run-to-run randomness and only one was ever set. DistributedSampler carries its own seed (default 0) and derives each epoch's permutation from seed + epoch inside set_epoch(), independent of torch.manual_seed. A sweep that moved only the torch seed would reuse one data order across every replicate — understating the spread, and doing it silently, since no log line distinguishes the two cases.

So both seeds move together, with one exception that has to be exact: at the historical seed 42 the sampler must stay at its historical 0, or the default stops reproducing published runs. That pairing is rampnet/seeding.py::sampler_seed_for, tested rather than commented.

Why the klone launcher is a new file

train.py writes best_model.pth and latest_checkpoint.pth to the current directory, not to --checkpoint-dir. Three replicates launched from one directory would overwrite each other's best model and — worse — each other's resume state: a resume file from another seed loads silently as if it were this run's own, converging the arms onto one lineage with nothing in the log to say so. run_train_seed.slurm gives each replicate its own RUNDIR and cds into it. run_train.slurm stays untouched as the preserved record of the published run and #135's rungs, the same reasoning that kept the Tillicum YOLO launcher separate from its klone original.

Pre-registered, before any replicate finished

docs/seed_variance_51_135.md fixes the decision rule on the sample SD s of the three Campaign A replicates:

s reading
≤ 0.010 0.039 is ≈4σ — the architecture advantage is small but real
≥ 0.020 0.039 is inside 2σ — #51 closes with "the supervised baseline is statistically indistinguishable from RampNet at matched operating points"
0.010–0.020 ambiguous; report the interval, claim neither, combine with Campaign B

The second branch makes our own headline smaller and is accepted in advance.

What is running

Campaign A — YOLO seed variance, Tillicum. Three replicates of y11x_tiles at seeds 1/2/3, config identical to the as-run args.yaml in every respect except the seed. Jobs 274367 (running, verified seed=1 in its log header), 274369, 274370, each with CHAIN=5 to cover the 24 h normal-QoS ceiling. ~3.0 h/epoch on one H200; ~$21.60 per 24 h link, ≤$389 for all three, against a $1,500/month cap with $0 used this cycle.

Tillicum was chosen over free klone for a measured reason: the tiles arm consumes 8.5 MB/s there against a filesystem measured at 8.3–11.8 MB/s — it sits on the ceiling — so three concurrent replicates would contend for the wall itself. On Tillicum the same arm uses 4% of available bandwidth and is genuinely GPU-bound.

Campaign B — RampNet seed variance, klone. Three replicates of the committed recipe (1 epoch / 9,378 steps, constant lr 1e-5, global batch 16) at seeds 1/2/3. Jobs 39515025/26/27 on ckpt-all, free and preemptable, resumed by --requeue plus train.py's own latest_checkpoint.pth. Calendar, not money, is the risk: ckpt-all's duty cycle was 3.9% in 2026-08.

One deliberate deviation: save_period=1

The #51 arms ran save_period: -1, which is exactly why the epoch-curve follow-up had to be retracted — no per-epoch weights exist for any arm and they cannot be recovered. Keeping every epoch costs ~20 GB against a 1 TB allocation and buys back an analysis that is currently foreclosed.

Tests

tests/test_seeding.py (21 tests) checks the plumbing at the source level, because every failure mode here is silent:

  • the default still maps to sampler seed 0, so reproductions are unchanged
  • all three RNGs follow the flag, and no 42 literal survives
  • the launcher refuses to default SEED — an unset replicate would be a silent duplicate of the published run
  • per-seed RUNDIR isolation, and no checkpoints under klone's 10 GB home quota
  • the Tillicum heredoc's positional list still lines up with its sys.argv unpack — an off-by-one there shifts imgsz into epochs and trains a wrong model that finishes green

Full suite: 1,343 passed, 1 skipped. --seed verified end to end in the real klone sidewalkcv2 environment, and seed=1 confirmed in job 274367's Ultralytics arg dump.

Stated limitations

  • n=3 gives a wide interval on the SD itself (95% CI on σ spans ~0.5σ̂–3.7σ̂). A 4th and 5th replicate are ~$119 each and are the pre-registered response if the result lands in the ambiguous band.
  • Campaign A's replicates are Tillicum H200; the existing seed: 0 arm is klone L40S. The SD is over the three same-hardware replicates only; the seed-0 arm is a separate cross-hardware check, not a fourth sample.
  • Only y11x_tiles is replicated — the measured SD is not automatically the other arms'.
  • Seed variance is not the same as training-run variance; Campaign B has no deterministic=True guarantee, so its spread is an upper bound on seed effect alone.

Related: #154 (the parity result this exists to adjudicate), #51, #135.

🤖 Generated with Claude Code (claude-opus-5[1m])

…ad to mean (#51, #135)

Both issues are blocked on the same missing number. #51's matched-operating-point read
put the RampNet-vs-YOLO residual at 0.039 F1, and #51's own rule is that differences
under ~0.02 should not be read -- but every arm in that comparison is seed 0, the
ultralytics default, in all eight args.yaml. #135's power analysis reached the same wall
from the other side and said so outright: the binding limit is unmeasured seed variance,
n=1. Nothing downstream can be called real without it, RampNet 2.0 included.

Neither trainer could vary the seed at all. train.py hardcoded manual_seed(42) with no
flag; run_yolo_train_tillicum.slurm never passed one to ultralytics.

THE HALF THAT WAS EASY TO GET WRONG. Stage 2 has TWO sources of run-to-run randomness,
and only one was ever set. DistributedSampler carries its own seed (default 0) and
derives each epoch's permutation from seed + epoch in set_epoch(), independent of
torch.manual_seed. A sweep that moved only the torch seed would reuse one data order
across every replicate, understating the spread -- and would do it silently, since no
log line distinguishes the two cases.

So both seeds move together, with one exact exception: at the historical seed 42 the
sampler must stay at its historical 0, or the DEFAULT stops reproducing published runs.
That pairing is rampnet/seeding.py::sampler_seed_for, tested rather than commented.

Also load-bearing, and the reason the klone launcher is a new file rather than a flag on
run_train.slurm: train.py writes best_model.pth and latest_checkpoint.pth to the CURRENT
DIRECTORY, not to --checkpoint-dir. Three replicates launched from one directory would
overwrite each other's best model and, worse, each other's resume state -- a resume file
from another seed loads silently as if it were this run's own, converging the arms onto
one lineage with nothing in the log to say so. run_train_seed.slurm gives each replicate
its own RUNDIR and cd's into it.

PRE-REGISTERED, before any replicate finished: docs/seed_variance_51_135.md fixes the
decision rule. SD <= 0.010 and the architecture advantage is real at ~4 sigma; SD >= 0.020
and #51 closes with "the supervised baseline is statistically indistinguishable from
RampNet at matched operating points"; between is ambiguous and needs both campaigns. The
second branch makes our own headline smaller and is accepted in advance.

One deliberate deviation from the #51 protocol: save_period=1. The arms ran -1, which is
exactly why the epoch-curve follow-up had to be retracted -- no per-epoch weights exist
anywhere and cannot be recovered. ~20 GB against 1 TB buys back that analysis.

tests/test_seeding.py (21) checks the plumbing at the source level, because every failure
here is silent: the default still mapping to sampler seed 0, all three RNGs following the
flag, the launcher refusing to default SEED, per-seed RUNDIR isolation, and the Tillicum
heredoc's positional list still lining up with its unpack -- an off-by-one there shifts
imgsz into epochs and trains a wrong model that finishes green.

Full suite: 1,343 passed, 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jonfroehlich added a commit that referenced this pull request Sep 3, 2026
… B is decided against (#135)

#135 exists to decide whether to spend 1,675-3,350 GPU-hours on the annealed arm. Every
input to that decision has been in place since 2026-08-29 -- the rung completed, the LR
schedule verified over 100% of the run, manual_gold scored and tied at every epoch -- but
the gate written into the pre-registration was never actually applied to the numbers, so
the issue stayed open with the decision unmade.

scripts/analysis/run_b_gate_135.py applies it from the committed summaries rather than
restating it in prose, and writes docs/data/run_b_gate_135.json:

  PRIMARY   max-F1(cosine ep8) - max-F1(Run A ep8) = +0.002994
            |z| = 1.02 to 1.86        -> not significant
  SECONDARY Run A ep3->ep8 -0.006624 ; cosine -0.005158, arrested? NO
            difference of declines +0.001466, |z| = 0.50 to 0.91 -> not significant
  VERDICT   JUDGMENT CALL

The primary misses significance AT THE FAVOURABLE END of the measured s.e. bracket, so the
reading does not depend on which value inside it is chosen. That robustness is the reason
the bracket is honest enough to decide on: a true paired bootstrap between the two arms
needs both arms' per-pano detections and the cosine arm's are not committed, so this uses
the s.e. #138 measured across 28 Run A epoch pairs on the same panos and GT. Stated in the
script's own docstring, not buried.

The pre-registration says a tie on both is explicitly NOT an automatic cancellation, so the
decision is recorded as the judgment it is. Not running Run B, because:

- The mechanism that justified it is the one that failed. Run B's case was #51's annealed
  tail; the rung tested exactly that at matched budget, seed and data order and moved
  manual_gold by nothing measurable. A 30-epoch arm changes length AND schedule, so it
  could not attribute a difference even if it found one.
- The gain is real and in the wrong place: up to 3.98% better auto-label val loss, none of
  it reaching human F1. That is #84's exchange rate replicating.
- THE ARGUMENT THAT WAS NOT AVAILABLE WHEN RUN B WAS SPECIFIED: Run B is n=1, and this
  issue itself established seed variance as the binding limit. The plausible effect (~0.003
  of late-epoch damping) sits below the ~0.01 that is "measured but not attributable"
  without a seed control, so a single 30-epoch run is uninterpretable at any length. A
  readable Run B is 3 seeds = 5,025-10,050 GPU-h, ~$4,500-9,000.
- Opportunity cost, measured: #151 just produced a +0.115 F1 rig effect on the same model.

What is NOT claimed is written down too: not that annealing does nothing (the ~0.003 damping
is unresolved, not refuted), not that a 30-epoch run would fail (it was never run), and not
epoch 7's +0.0042 -- the largest gap anywhere, which WOULD clear 1.96 at the favourable end
of the bracket but is not the pre-registered comparison. The artifact flags it so nobody
quotes it as the result.

Reopening condition is concrete and already in flight: the seed campaign (PR #155, klone
39515025/26/27) prices it directly. Seed SD <= ~0.002 max-F1 and a 0.003-0.008 effect
becomes readable at n=1.

Two amendments so the repo does not carry contradictory advice:
- stage2_run_b_power_135.md's "run the 30-epoch arm" recommendation predates the rung's
  results; marked SUPERSEDED in place rather than rewritten.
- stage2_epoch_curve_84.md's "the gate does not cancel Run B" section gets the later
  outcome inline, since that doc is where a reader looks for Run A/Run B.

Also states a deliberate omission: #135 asked for the benchmark splits as well as
manual_gold, and only manual_gold was scored -- because #138 measured the nine city splits
pooled at an unpaired MDE of 0.0219 against manual_gold's 0.0117, so they cannot resolve
what manual_gold cannot.

tests/test_run_b_gate_135.py (10) covers the branches that did NOT fire, since an
implementation that only ever emits the observed verdict is untestable by its own output.

Full suite: 1,371 passed, 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant