Skip to content

6h-step stochastic bake-off subset: arms 1,2,3,6,7 + arm9 (7 + γ0.5 whitening) - #1392

Draft
mcgibbon wants to merge 8 commits into
experiment/2026-07-16-stochastic-ace-bakeofffrom
experiment/2026-07-28-stochastic-ace-bakeoff-6h
Draft

6h-step stochastic bake-off subset: arms 1,2,3,6,7 + arm9 (7 + γ0.5 whitening)#1392
mcgibbon wants to merge 8 commits into
experiment/2026-07-16-stochastic-ace-bakeofffrom
experiment/2026-07-28-stochastic-ace-bakeoff-6h

Conversation

@mcgibbon

@mcgibbon mcgibbon commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Configs only — nothing is launched from this PR; launches are Jeremy's call after review.

Reruns the stochastic-ACE bake-off protocol at a 6h step, to test whether the daily arms' small-scale precipitation power deficit (reports#51) is a daily-timestep artifact. Based on the daily bake-off branch at its exact launch SHA (42ab578), so the training code — including the unmerged spectral-power-CRPS term — is identical to what produced the daily arms, and the PR diff is the new configs/baselines/stochastic-ace-bakeoff-6h/ directory alone.

Arms (per the daily numbering): 1 (0.9/0.1 + EC), 2 (0.9/0.1, no EC), 3 (0.5/0.5 + EC), 6 (0.8/0.1/0.1 + EC), 7 (0.9/0/0.1 + EC), and new arm 9 = arm 7 + γ0.5 per-sample whitening — the cell reports#51 left untested (with energy_score_weight: 0, the shared whitening operator applies to the spectral-power term alone; verified against the loss-builder code path at this SHA).

Each config is its daily counterpart with three mechanical edits (generated by an assertion-checked script, not hand-edited) plus one recipe change requested in review — global_mean_removal (kind: shared, append_as_input: true) is dropped from all six arms:

  1. Dataset: the daily wrapper directory 2026-03-19-era5-1deg-8layer-daily-1940-2025.zarr/data_path: /climate-default/, with the unchanged file_pattern selecting the 6-hourly store 2026-03-19-era5-1deg-8layer-1940-2025.zarr directly — the 6-hourly store has no wrapper directory, and this is the form the prior 6h stochastic runs (4s0rnth6) trained with. Same variable set, same ACE2 train/val/inference split, same 06Z IC dates (the 6h zarr spans 1940-01-01T12–2025-12-31T18 at 00/06/12/18Z, so all ICs and horizons are in range).
  2. Normalization: daily 1990–2019 stats → 6-hourly 1990–2019 stats, via beaker dataset andrep/2026-03-19-era5-1deg-8layer-stats-1990-2019 mounted at /statsdata (same stats the prior 6h stochastic pretrains used; residual scaling is timestep-dependent — e.g. PRESsfc residual std 250.4 at 6h vs 641.5 daily — so the daily stats can't be reused). The mount rides the run script's existing # arg: header mechanism.
  3. Inference horizons ×4 at fixed lead time: 10-year 3652 → 14608 steps, ACE2-comparable 5-year 1826 → 7304, 5-day weather 5 → 20 steps, day-5 step_means/ensembles index 5 → 20. forward_steps_in_memory (a memory/IO chunk size, not a horizon) is unchanged everywhere.

Arm 9 additionally adds the energy_score_whitening block in arm 8's exact syntax.

Held fixed deliberately (timestep and the global-mean-removal drop are the only differences vs the daily arms): model (fg16/sr0.125 NoiseConditionedSFNO), 1-step training, seed 0, max_epochs: 80, batch size 8, LR 1e-4, EMA decay 0.999, inline-inference cadence (every 2 epochs). Review consequences: 80 epochs at 6h is ~4× the optimizer steps and wall time per arm — the 6h zarr has 125,646 timesteps vs the daily 31,411 (exactly 4.0×), and daily arm 1 took ~54 h on 4 GPUs, so expect ~9 days per arm on 4 GPUs (24 GPUs held ~9 days for the set of six). EMA/LR-schedule shapes also differ in step terms at fixed epoch count, and each inline inference pass is ~4×. If step-matching (20 epochs) is preferred over epoch-matching, say so and I'll adjust.

Validation done: all six configs pass fme.ace.validate_config --config_type train at this SHA (schema only — it does not touch the filesystem, hence the path bug pre-review caught); all six stepper_training.loss blocks were built through the real LossConfig.build path, confirming the sp-term/whitening wiring per arm; and the data/stats path forms match the prior finished 6h run.

run-train.sh is the daily script with only the launch-target block changed (guardrails byte-identical); job names follow ace2s-bakeoff-6h-arm*-rs0.

🤖 Generated with Claude Code

… whitening)

Reruns the stochastic-ACE bake-off protocol at a 6h step to test whether
the daily arms' small-scale precipitation power deficit is a timestep
artifact. Each config is its daily counterpart with only the 6-hourly
dataset, 6-hourly normalization stats (beaker dataset mounted at
/statsdata), and x4 inference step counts at fixed lead time; arm 9 adds
g0.5 whitening to arm 7 (whitening the spectral-power term alone, the
cell the daily bake-off left untested).
@mcgibbon

Copy link
Copy Markdown
Contributor Author

Claude: independent pre-review (fresh context, no authoring history). One run-killing bug, plus two judgment calls and a doc nit. Not ready to launch as-is.


1. BLOCKER — every data_path points inside the zarr store; all 6 arms die at dataset construction

All 20 data_path occurrences per file became:

data_path: /climate-default/2026-03-19-era5-1deg-8layer-1940-2025.zarr
file_pattern: 2026-03-19-era5-1deg-8layer-1940-2025.zarr

get_raw_paths is a plain glob with no .zarr special case (fme/core/dataset/xarray.py:329-333 at the base SHA):

def get_raw_paths(path, file_pattern):
    fs = _get_fs(path)
    glob_paths = sorted(fs.glob(os.path.join(path, file_pattern)))

so this resolves to /climate-default/2026-03-19-era5-1deg-8layer-1940-2025.zarr/2026-03-19-era5-1deg-8layer-1940-2025.zarr, which does not exist → ValueError: No files found matching ... (xarray.py:562-565). It fires for the train loader, the validation loader, and all five inference loaders, on all six arms, immediately after GPU allocation.

Why the daily form isn't transferable. In the daily config, data_path's .zarr suffix is part of a container directory name, not a store:

/climate-default/2026-03-19-era5-1deg-8layer-daily-1940-2025.zarr/   <- directory (data_path)
    2026-03-19-era5-1deg-8layer-1940-2025.zarr/                     <- the actual store (file_pattern)

(GCS mirrors this: gs://vcm-ml-intermediate/2026-03-19-era5-1deg-8layer-daily-1940-2025/2026-03-19-era5-1deg-8layer-1940-2025.zarr/.) The 6-hourly source has no such wrapper — it is the store itself, directly at /climate-default/2026-03-19-era5-1deg-8layer-1940-2025.zarr. Two independent confirmations:

  • Both prior 6h runs used data_path: /climate-default/ + file_pattern: 2026-03-19-era5-1deg-8layer-1940-2025.zarr and trained: 4s0rnth6 (finished), 2rlkte27.
  • gsutil ls gs://vcm-ml-intermediate/2026-03-19-era5-1deg-8layer-1940-2025.zarr/ shows zarr.json plus variable directories and no nested .zarr.

Fix: set data_path: /climate-default in all 20 places per file and leave file_pattern alone (it is already the correct store name in the daily configs — this is a one-token change, not a re-derivation). Do not drop file_pattern: _default_file_pattern_check rejects the *.nc default under engine: zarr. data_path: /climate-default is also the dominant idiom elsewhere in configs/ (~84 occurrences on the base branch).

Note that fme.ace.validate_config cannot catch this — it never touches the filesystem, and /climate-default doesn't exist on the launch host. "All six pass validate_config" is not evidence about paths.


2. forward_steps_in_memory: 5 → 20 in the weather blocks costs 4× inference memory for no scientific gain

forward_steps_in_memory is documented as "number of forward steps to take before re-reading data from disk" (fme/ace/train/train_config.py:122-123) — a memory/IO knob, not a horizon. Scaling n_forward_steps 5 → 20 is required by the fixed-lead-time intent; scaling this one is not.

With n_ensemble_per_ic: 8, the resident window goes from 8×5 = 40 to 8×20 = 160 member-steps — 4× the daily arms, and 4× the two 10-year blocks (which correctly kept 40 × 1 member). If it OOMs it does so inside inline inference, hours into training, on all six arms at once. Leaving it at 5 is behaviourally identical (20 % 5 == 0, four chunks) and strictly safer. Worth a deliberate decision rather than a mechanical ×4.


3. Cost, concretely — worth putting the numbers in the description

The 6h zarr has 125,646 timesteps vs the daily's 31,411 (exactly 4.0×), so 80 epochs really is ~4× the optimizer steps. Anchoring that to the daily arms: qhv9zf95 (arm1) took 53.6 h wall on 4 GPUs for 80 epochs / 244,640 steps; 9vecgb9s (arm7) 50.5 h. So expect ~9 days per arm, i.e. 24 GPUs held ~9 days for the set — inline inference scales ~4× too, so there's no offsetting saving. The description says "~4× wall time" qualitatively; stating 53.6 h → ~215 h makes the epoch-match vs step-match (80 vs 20 epochs) decision concrete.


4. Nit — README "Horizons" bullet is narrower than the diff

It says "day-5 step_means index 5 → 20", but the same 5 → 20 edit also lands on the ensembles: entries in both weather blocks (plus forward_steps_in_memory). Mentioning the ensembles entry lets a reader reconcile the bullet against the 45/41-line diff without opening a config.


Verified clean

  • No drift in the five carried-over arms. Filtering the full -U0 daily→6h diff for anything outside {header comment, data_path, global_means_path/global_stds_path, - step: 5|20, n_forward_steps, forward_steps_in_memory} returns empty for arms 1, 2, 3, 6, 7 — all at an identical 45 insertions / 41 deletions.
  • - step: 520 hit only the intended entries. ema_checkpoint_save_epochs.step: 5 is untouched in all six files, and is the only remaining step: 5 in the directory (line 12 of each).
  • Step arithmetic exact. 14608 × 6 h = 3652 d; 7304 × 6 h = 1826 d; 20 × 6 h = 5 d. The 6h zarr runs 1940-01-01T12 … 2025-12-31T18 at dt = 6 h on 00/06/12/18Z, so every 06Z IC exists and every horizon (longest: 2015-01-01T06 + 3652 d = 2024-12-31T06) stays in range. All in_names/out_names are present in the 6h store.
  • arm9 is exactly arm7 + whitening, in daily arm8's byte-identical syntax, with 0.9/0.0/0.1 preserved.
  • arm9's premise holds at the base SHA. EnsembleLoss passes energy_score_whitening into SpectralPowerCRPSLoss under if spectral_power_crps_weight > 0 (loss.py:830-836) regardless of energy_score_weight, and forward() skips the energy-score component when its weight is 0 (loss.py:855). The crps_weight + energy_score_weight == 0 guard (loss.py:811) is satisfied by crps 0.9.
  • Stats mount is right. andrep/2026-03-19-era5-1deg-8layer-stats-1990-2019 = 01KPC6VHH4H3D800KV4VX83KK9, description names the 6-hourly source over 1990-2019, and the four files sit at the dataset root — so /statsdata/centering.nc resolves (no nested source-name subdirectory as in the daily /climate-default layout). All in_names/out_names are covered by all three files. The 6h residual scaling is genuinely distinct from the daily (PRESsfc 250.4 vs 641.5; air_temperature_7 1.41 vs 2.66), confirming the README's "cannot be reused".
  • # arg: mechanism works. Ran the exact extraction loop under set -euo pipefail: yields the two tokens --dataset / andrep/…:/statsdata and exits 0 (the trailing non-matching line does not trip set -e). --dataset owner/name:/mount matches gantry's documented dataset-name:/mount/location.
  • run-train.sh differs from the daily script only in the six launch lines; the GUARDRAILS block is byte-identical.

… values

Pre-review findings: the daily data_path is a wrapper directory containing
the store named by file_pattern, but the 6-hourly store has no wrapper, so
the substituted path pointed inside the store and every loader's glob would
come up empty. Use data_path: /climate-default/ + unchanged file_pattern
(the form the prior 6h stochastic runs trained with). Also revert the
weather blocks' forward_steps_in_memory 5->20 (a memory/IO chunk size, not
a horizon) and note the ensembles step-index edit in the README.
@mcgibbon

Copy link
Copy Markdown
Contributor Author

Claude: All four findings addressed in efec091:

  1. data_path blocker — fixed to data_path: /climate-default/ + unchanged file_pattern (the 4s0rnth6 form) in all six configs, all 20 occurrences per file.
  2. forward_steps_in_memory — reverted to the daily values everywhere (weather blocks back to 5; the 10-year/5-year blocks were already 40).
  3. Cost — the ~9 days/arm on 4 GPUs (24 GPUs ≈ 9 days for the set) figure is now in the PR description, tied to your 4.0× timestep-count ratio and the 53.6 h daily-arm1 datum.
  4. README nit — the Horizons bullet now names the ensembles step-index edit and states that forward_steps_in_memory is unchanged.

Also noted in the description that validate_config is schema-only and doesn't touch the filesystem.

@mcgibbon

Copy link
Copy Markdown
Contributor Author

Claude: re-review of efec091 (fresh fetch of the branch head). All four findings are correctly resolved and no new drift crept in. Ready for review.

Blocker fix verified

All 20 data_path occurrences in each of the six configs are now data_path: /climate-default/, with file_pattern untouched — so the glob resolves to /climate-default/2026-03-19-era5-1deg-8layer-1940-2025.zarr, byte-identical to the data_path/file_pattern pair that 4s0rnth6 trained with. Grepping for any data_path: line in the directory that is not the bare /climate-default/ returns nothing (the only other hit is the README prose describing the change).

No drift

  • Filtering each arm's full -U0 daily→6h diff for anything outside {header comment, data_path, global_means_path/global_stds_path, - step: 5|20, n_forward_steps} returns empty for arms 1, 2, 3, 6, 7. Note I removed forward_steps_in_memory from the allowed set for this pass, so any residual change to it would have surfaced — none did.
  • All five are at an identical 43 insertions / 39 deletions, down from 43/45-39/41 exactly the 4 lines the forward_steps_in_memory revert removes. That is the count the intended edit set predicts: 4 header + 20 data_path + 4 stats paths + 10 step indices + 5 n_forward_steps.
  • forward_steps_in_memory is back to the daily values everywhere: 40 × 3 (both 10-year blocks and the 5-year) and 5 × 2 (the two weather blocks).
  • ema_checkpoint_save_epochs.step: 5 still untouched, and still the only step: 5 in the directory (line 12 of each of the six).
  • arm9 is still exactly 6h arm7 plus the three-line energy_score_whitening block, nothing else.
  • run-train.sh still differs from the daily script only in the six launch lines (6/8); all six # arg: --dataset …:/statsdata headers intact.

Independent invariant check

Parsed all six configs and asserted, per inference block: every step_means/ensembles step index ≤ n_forward_steps, forward_steps_in_memoryn_forward_steps, and the latest IC + n_forward_steps × 6 h inside the zarr's range (ends 2025-12-31T18). All pass. Loss weights read back as 0.9/0.1/0, 0.9/0.1/0, 0.5/0.5/0, 0.8/0.1/0.1, 0.9/0.0/0.1, 0.9/0.0/0.1 + {per_sample, 0.5}, and the corrector column as EC on for all but arm2 — both matching the README's knob table exactly; seed: 0 / max_epochs: 80 across all six.

I did not re-run validate_config: the only env with fme here is pinned to an unrelated commit (958f779), and validating against a mismatched schema would be weaker evidence than what the diff already gives. The six configs differ from the daily files — proven valid by 80 epochs of real training — only in scalar value substitutions, plus one structural addition (arm9's whitening block) that is byte-identical to daily arm8's, itself also trained for 80 epochs. Schema validity follows from the diff shape; the semantic validity of the new values (store path, stats files, step indices, IC/horizon ranges) I checked directly against the data and the beaker dataset.

Description claims spot-checked against measurements

claim measured
daily arm1 ~54 h on 4 GPUs 53.6 h (qhv9zf95)
125,646 vs 31,411 timesteps, ~4.0× 125,646 / 31,411 = 4.00006
PRESsfc residual std 250.4 (6h) vs 641.5 (daily) matches
6h zarr spans 1940-01-01T12–2025-12-31T18 at 00/06/12/18Z matches
based on the daily arms' exact launch SHA 42ab578 qhv9zf95 and 9vecgb9s both record commit 42ab57804a10990720b3275eb749f7f395ae3adf
forward_steps_in_memory unchanged everywhere confirmed, 40/40/40/5/5

The README's Data and Horizons bullets now match the diff, including the ensembles step-index edit and the explicit "forward_steps_in_memory … is unchanged everywhere".

Comment on lines +580 to +582
global_mean_removal:
kind: shared
append_as_input: true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get rid of the global mean removal in all configurations, we didn't mean to use it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@-

mcgibbon added 6 commits July 29, 2026 17:14
Jeremy: the daily arms' global_mean_removal (kind: shared,
append_as_input: true) was not meant to be part of this recipe. Removed
from all six configs; README notes it as a deliberate divergence from
the daily arms alongside the timestep.
Arms 1, 7 and 9 were killed by host-RAM exhaustion in the first launch
wave. Set num_data_workers 8 -> 4 on every loader (training, validation,
all five inference) and the training loader's prefetch_factor 4 -> 2, then
relaunch. Both are queue-depth knobs; sample order comes from the seeded
distributed sampler, so they are expected to be results-neutral.

Arms 2, 3 and 6 are unchanged (3 and 6 are still running).
…ed config

Arm 3 died 2026-08-05 at epoch 69/80 to a 30-min NCCL allreduce watchdog
timeout, four minutes after a second full-footprint arm landed on its node.
Its beaker autoResume job carried the pre-fix config and starved 5.5 h
unscheduled; it was cancelled in favour of this continuation.

arm3-50-50-ec-resume-e69.yaml adds resume_results (existing_dir
/prior-results, resume_wandb true) over result dataset
01KYS0V4WTP34MX4MERGRAY7ZD, and takes the same num_data_workers 8->4 /
prefetch_factor 4->2 as arms 1/7/9. max_epochs stays 80. No fme/ code differs
between the checkpoint's commit (95364df) and this base, so the checkpoint
is compatible.

The launch line reuses the ORIGINAL wandb run name so resume_wandb keeps the
run id without renaming its display name.
The bake-off base recipe is the pre-training donor nzccs8zd, which is the
main ERA5 baseline with exactly filter_num_groups: 16 and spectral_ratio:
0.125 carried over. It sets neither residual_prediction nor
global_mean_removal. Both reached these arms because the daily configs
adopted the 4-degree/daily v2 architecture block wholesale rather than
building base + those two knobs; the arms' builder.config is byte-for-byte
v2's, minus clip_latent_global_means. global_mean_removal was caught and
dropped from the 6h subset on 2026-07-29; residual_prediction was not.

6h subset (all six arms):
- residual_prediction: true -> false.
- Loader knobs made uniform at num_data_workers 4 / prefetch_factor 2. Only
  arms 1, 7 and 9 carried the measured fix; arms 2, 3 and 6 still ran the
  pre-fix 8/4 that caused the host-RAM OOM and wedge failures.
- 8 GPUs per arm (was 4), so a job holds a whole node and the co-scheduling
  behind those failures cannot recur. batch_size is the global batch, so the
  effective batch and gradient are unchanged.
- Drop arm3-50-50-ec-resume-e69.yaml: it resumed an epoch-69 checkpoint
  trained with residual prediction, which is not loadable on this recipe.
- Job names gain a "-nores" suffix to separate them from the superseded wave.

Daily wave: add arm1-90-10-ec-nores.yaml, arm 1 with residual_prediction
false and nothing else changed, as a one-knob A/B against the completed
arm 1 (qhv9zf95) to test whether that wave's small-scale precipitation power
deficit is caused by residual prediction. global_mean_removal and the 8/4
loader knobs are kept so the comparison stays one-knob. The eight completed
daily arms are untouched.

Also corrects both READMEs, which credited nzccs8zd with residual prediction
and shared global-mean removal. It has neither.

Launch targets: ai2/ace workspace, urgent priority, ai2/jupiter, with
CM_PRIORITY=high for the external balancer.
Jeremy's call (2026-08-07): run the ablation on the corrected bake-off recipe
rather than hold global_mean_removal to match the old arm. residual_prediction
and global_mean_removal are both settings the daily wave inherited from the
4-degree/daily v2 architecture block; the donor nzccs8zd sets neither.

Against the completed arm 1 (qhv9zf95) the diff is therefore two knobs,
residual and GMR, so a change in the small-scale precipitation spectrum is
attributable to the pair rather than to residual prediction alone. Noted in
the README and the launcher comment so the result is not over-read.

Job renamed to ...-nores-nogmr-rs0 so it does not collide with the cancelled
GMR-retaining run (wandb do85dswp). Loader knobs unchanged at 8/4.
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