Skip to content

validation rollout sliced - #770

Open
amogh-gulati wants to merge 8 commits into
mainfrom
validation_rollout
Open

validation rollout sliced#770
amogh-gulati wants to merge 8 commits into
mainfrom
validation_rollout

Conversation

@amogh-gulati

@amogh-gulati amogh-gulati commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

This PR adds long-horizon autoregressive rollout validation to the training loop, complementing the existing single-step validation.

Previously, validation only checked one-step-ahead prediction error each epoch. This change allows training to periodically roll the model forward autoregressively over the validation period and log rollout RMSE at configured horizons, such as 90 days and 360 days.

The implementation supports multiple rollout horizons in a single rollout. For example, with rollout_validation_days: [360, 90], the code rolls out once to the maximum horizon, records the 90-day metrics at the intermediate cutoff, and then continues to 360 days. This avoids launching a separate 90-day rollout.

Metrics are logged separately by horizon, variable, depth level, and depth band using raw, un-normalized fields. Rollout validation currently runs only on rank 0, with other ranks waiting at a barrier, since the validation window is not sharded across workers. The rollout is processed in bounded chunks to avoid materializing the full forecast horizon’s targets at once.

This is currently wired up for the standard single-scale training schedule and is a no-op for FOMO’s multi-scale schedule.

  • Added rollout validation config options:

    • rollout_validation_days
    • rollout_validation_steps
    • rollout_validation_freq
    • rollout_validation_steps_forward

Config

In configs/samudra_om4_v2/train.yaml, rollout validation is enabled with:

rollout_validation_days: [360, 90]
rollout_validation_steps_forward: 4

test run here - https://wandb.ai/ocean_emulators/default/runs/e7r70ugc?nw=nwuseramoghgulati
metrics present in rollout_val for 90 days and 360 days

@amogh-gulati
amogh-gulati requested a review from jder June 25, 2026 22:01
@jder

jder commented Jun 26, 2026

Copy link
Copy Markdown
Member

@codex review

@jder

jder commented Jun 26, 2026

Copy link
Copy Markdown
Member

Thanks @amogh-gulati! One high-level question: why is this separate from the existing inference_one_epoch code? ie should those be the same thing, generalized to cover the new things you're adding here like supporting multiple lengths and removing the strange multi-GPU behavior the inference code currently has?

Also looks like you have some (real) CI failures now that I've finally fixed the spurious ones.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a182db0acc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/samudra/train.py

def should_log_validation_images(epoch: int, frequency: int) -> bool:
"""Return whether to log validation images for a 1-based training epoch."""
def should_run_on_epoch_freq(epoch: int, frequency: int) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore should_log_validation_images

The old should_log_validation_images helper was removed by this rename, but tests/test_trainer.py still imports and exercises it. Any run that collects that module now fails with ImportError before the tests execute, so this breaks the existing test suite; keep a wrapper/export or update those callers in the same commit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this still a valid concern? I agree with codex.

Comment thread src/samudra/train.py Outdated
Comment on lines +822 to +823
if self.distributed is not None:
torch.distributed.barrier()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid blocking nonzero ranks before long rollout

When rollout validation is enabled under NCCL/DDP for the intended long horizons, non-main ranks enter this barrier before rank 0 runs the entire validation rollout, so they can sit inside a distributed collective for the full rollout duration. Since init_distributed_mode does not configure a longer process-group timeout, quarter-degree 90/360-day rollouts that exceed the default timeout can abort otherwise healthy training; configure a timeout that covers this path or avoid starting the collective until the long rank-0 work is done.

Useful? React with 👍 / 👎.

Amogh Gulati added 2 commits August 3, 2026 11:48
The epoch argument to should_run_rollout_validation was previously ignored, so rollout validation ran every epoch regardless of config.
Uses a lazily created gloo process group with a 12h timeout so the default NCCL watchdog cannot abort healthy training during long rollouts.
@amogh-gulati
amogh-gulati requested a review from alxmrs August 3, 2026 15:54
Amogh Gulati added 2 commits August 3, 2026 12:28
The old should_log_validation_images name was still imported by test_trainer.py, failing CI at collection.

@alxmrs alxmrs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review part 1/N. Will return to this when I can.

Comment thread src/samudra/train.py
from typing import Any, NamedTuple

import dask
import numpy as np

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm surprised we didn't have numpy before!

Comment thread src/samudra/train.py

def should_log_validation_images(epoch: int, frequency: int) -> bool:
"""Return whether to log validation images for a 1-based training epoch."""
def should_run_on_epoch_freq(epoch: int, frequency: int) -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this still a valid concern? I agree with codex.

Comment thread src/samudra/train.py
return requested_steps


class RolloutValidationSpec(NamedTuple):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🐑 I prefer using data classes over named tuples. Though, if tuples make this cleaner, that's ok too (I could lack context).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this is a public contract, it would be nice to add a docstring here to explain what this is for and what it does.

Comment thread src/samudra/train.py
return float(delta)


def resolve_rollout_validation_day_spec(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think these new methods and classes should live in util somewhere to not clutter train.

Comment thread src/samudra/train.py
Comment on lines +382 to +386
self.rollout_validation_steps = cfg.rollout_validation_steps
self.rollout_validation_days = cfg.rollout_validation_days
self.rollout_validation_steps_forward = cfg.rollout_validation_steps_forward
self.rollout_validation_freq = cfg.rollout_validation_freq
self._rollout_validation_pg: torch.distributed.ProcessGroup | None = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should pull these up into a single rollout validation config object, not as top lvl train settings.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

feel free to pull in related, existing vars from the train config into here.

Comment thread src/samudra/train.py
Comment on lines +495 to +500
if self.should_run_rollout_validation(epoch):
rollout_val_stats = self.validate_rollout_one_epoch(epoch)
end_epoch_rollout_val_time = time.perf_counter()
else:
rollout_val_stats = {}
end_epoch_rollout_val_time = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the train time cost of this operation (approximately)?

Comment thread src/samudra/train.py
logger.info(f"Aggregating validation logs")
return val_aggregator.get_logs(label="val")

def should_run_rollout_validation(self, epoch: int) -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we have a rollout validation config object, then this could just be a null check (does the object exist?) at the call site -- this function would go away.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To be clear, the config file would have a config object that builds a new data class or ADT. The ADT could live in util and would be built at the top of train.py.

Comment thread src/samudra/train.py
0, available_steps
).values
specs = [
resolve_rollout_validation_day_spec(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe this method could live as a static build method on the RolloutValidationSpec data class?

Comment thread src/samudra/train.py
torch.distributed.barrier(group=group)
yield False

def validate_rollout_one_epoch(self, epoch):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm torn if a lot of the contents of this method should live in the stepper (like validate_one_epoch) or not. That would follow convention, but I'm not sure what the right convention is. The way this is done has some advantages.

@alxmrs alxmrs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

2/2. Looks pretty good so far! My main note of feedback is on how we package this new process.

Comment thread src/samudra/config.py
"a value of 10 logs on epochs 1, 11, 21, ..."
),
)
rollout_validation_steps: int = Field(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See comment in train.py about packaging these new options differently.

Comment thread src/samudra/stepper.py
return ValBatchOutput(loss, loss_per_channel, input_data, label, outs, batch.ctx)


def _get_rollout_step_chunks(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this function could be simpler if we used batched:

https://docs.python.org/3/library/itertools.html#itertools.batched

Comment on lines +82 to +88
hist: int,
area_weights: torch.Tensor,
wet: torch.Tensor,
num_prognostic_channels: int,
normalize: Normalize,
tensor_map: TensorMap,
distributed_reduce: bool = True,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🐑 I could be wrong, but could we use the DatasetSpec here instead? Many of the arguments we need are there.

Do we need to include TensorMap and Normalize? We're generally trying to move away from including those multitions.

if len(data.target) == 0:
raise ValueError("No target values in data")

_, target_unnorm = get_aggregator_dicts(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🐑 optional: We may be able to create a replacement to this function and the one below to use "modern" normalization code that manages state better so we could avoid adding the multitons.

@alxmrs

alxmrs commented Aug 12, 2026

Copy link
Copy Markdown
Member

Hey @fomo-bot, will you address the concerns that I've raised in my review of Amogh's code? Where there is ambiguity, please let me know your thinking and I'll help resolve it. Feel free to push back on my suggestions where that is appropriate, given your reading of the code in the branch. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

3 participants