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
Open
Make the seed settable, pre-register its reading, and launch both seed-variance campaigns (#51, #135)#155jonfroehlich wants to merge 1 commit into
jonfroehlich wants to merge 1 commit into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
seed: 0— the Ultralytics default — in all eightargs.yamlfiles. Train a supervised YOLO baseline (YOLO11 / YOLO26) on the RampNet dataset — isolate architecture vs. data #51's own rule is that differences under ~0.02 should not be read.Neither trainer could vary the seed at all:
train.pyhardcodedmanual_seed(42)with no flag, andrun_yolo_train_tillicum.slurmnever 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.
DistributedSamplercarries its ownseed(default 0) and derives each epoch's permutation fromseed + epochinsideset_epoch(), independent oftorch.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.pywritesbest_model.pthandlatest_checkpoint.pthto 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.slurmgives each replicate its ownRUNDIRandcds into it.run_train.slurmstays 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.mdfixes the decision rule on the sample SDsof the three Campaign A replicates:sThe 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_tilesat seeds 1/2/3, config identical to the as-runargs.yamlin every respect except the seed. Jobs 274367 (running, verifiedseed=1in its log header), 274369, 274370, each withCHAIN=5to cover the 24 hnormal-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--requeueplustrain.py's ownlatest_checkpoint.pth. Calendar, not money, is the risk:ckpt-all's duty cycle was 3.9% in 2026-08.One deliberate deviation:
save_period=1The #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:42literal survivesSEED— an unset replicate would be a silent duplicate of the published runRUNDIRisolation, and no checkpoints under klone's 10 GB home quotasys.argvunpack — an off-by-one there shiftsimgszintoepochsand trains a wrong model that finishes greenFull suite: 1,343 passed, 1 skipped.
--seedverified end to end in the real klonesidewalkcv2environment, andseed=1confirmed in job 274367's Ultralytics arg dump.Stated limitations
seed: 0arm 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.y11x_tilesis replicated — the measured SD is not automatically the other arms'.deterministic=Trueguarantee, 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])