Skip to content

val: subsampled metric loaders + pinned contiguous video loaders - #642

Draft
RyanPCo wants to merge 1 commit into
ryanco/infra-portfrom
ryanco/eval-loaders
Draft

RyanPCo wants to merge 1 commit into
ryanco/infra-portfrom
ryanco/eval-loaders

Conversation

@RyanPCo

@RyanPCo RyanPCo commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

The unshuffled val loaders were truncated by limit_val_batches (80 x 64 =
5120 frames) to their leading episode-hash-sorted slice, so the seen-op head
scored 25 of 70 episodes and 6 of 11 operators, and the unseen head 33 of 191
episodes with an 81/19 operator mix on a 52/48 split. The loaders are
unshuffled only so the overlay video is coherent, and the video renders just
the first viz_max_batches (10) batches anyway.

Split each val head in two:

  • metric loader = EvenStrideDataset(split, frames_per_episode=K) with K from
    data.metric_frames_per_episode[head], so every episode -- and so every
    operator -- is scored, at the same 80 batches and with no rendering;
  • video loader = _video, the head's split restricted to the episode
    hashes pinned in data.video_episodes[head] (one per operator), contiguous
    and unsubsampled, logging no metrics. The pinned episodes are taken out of
    the already-resolved split, so resolve-once still holds.

EvalVideo.on_validation_step gains a mode argument (metrics / video / both);
ModelWrapper.validation_step routes _video to the same evaluator in
video mode and skips it entirely on a non-viz epoch, before the forward pass.
Heads with no pins are called exactly as before, so a data config that
declares neither key behaves as today.

K = floor(5120 / episodes-in-split), from the measured splits of
mecka_fold_flagship_opsplit_hpt_6d: valid 73 (70 eps), train_viz 3 (1346 eps),
unseen_op_valid 26 (191 eps). The same keys for the single-operator flagship
config are kept out of this PR along with that config.

Expect a shift in the per-batch Frechet _min/_max curves: they now span many
episodes instead of ~2 s of one, so they are not comparable with older runs.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

🤖 Generated with Claude Code

https://claude.ai/code/session_01HjCQBqQCr2igFEMR7yUovw

RyanPCo commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@RyanPCo
RyanPCo changed the base branch from ryanco/infra-port to graphite-base/642 September 17, 2026 14:48
@RyanPCo
RyanPCo force-pushed the ryanco/eval-loaders branch from c314000 to 690b00f Compare September 17, 2026 14:52
@RyanPCo
RyanPCo changed the base branch from graphite-base/642 to ryanco/infra-port September 17, 2026 14:53
@RyanPCo
RyanPCo force-pushed the ryanco/eval-loaders branch 2 times, most recently from 9c8662e to 76facc6 Compare October 12, 2026 21:50
@RyanPCo
RyanPCo force-pushed the ryanco/eval-loaders branch from 76facc6 to 0093b4e Compare October 12, 2026 23:19
@RyanPCo RyanPCo mentioned this pull request Oct 19, 2026
@RyanPCo
RyanPCo force-pushed the ryanco/eval-loaders branch from 0093b4e to e63242c Compare October 19, 2026 19:38
@RyanPCo
RyanPCo force-pushed the ryanco/eval-loaders branch from e63242c to 939f249 Compare October 30, 2026 18:27

@RyanPCo RyanPCo left a comment

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.

Automated adversarial review (read-only; findings were checked by a second skeptical pass before posting): 1 medium, 2 low, 2 nit.

Comment thread egomimic/trainHydra.py
# config nodes itself (eval mode included).
datamodule_kwargs["unseen_op_valid_datasets"] = unseen_op_valid_datasets
# Built above; hydra has nothing to instantiate for these.
datamodule_kwargs["video_datasets"] = video_datasets

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.

medium (correctness): In eval mode video_datasets is still built and passed here, so with data.video_episodes.valid set (e.g. mecka_fold_flagship_opsplit_hpt_6d) val_dataloader() returns [valid, valid_video]. But model.val_loader_names is assigned only in the mode == "train" branch, so ModelWrapper falls back to ["valid", "train_viz"]. As a result, valid_video is loaded up to limit_val_batches and then dropped as a train_viz batch with no evaluator, and valid runs in mode "both", which renders the video from the EvenStride-subsampled frames. Fix by moving model.val_loader_names = datamodule.val_loader_names() above the mode branch, or by skipping _video_datasets outside train mode.

Comment thread egomimic/trainHydra.py
raise ValueError(
f"data.metric_frames_per_episode.{head} must be a positive int, got {k}"
)
return k * _trainer_world_size(cfg)

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.

low (correctness): K is scaled by devices * num_nodes from the training trainer config, but in eval mode trainHydra later forces devices=1, num_nodes=1 and replaces limit_val_batches with the evaluator's value (400 by default). With a 4-GPU config, eval keeps 4x73 frames per episode on a single rank, so the 'every episode inside limit_val_batches' guarantee no longer holds. Apply the eval trainer overrides before building and subsampling the val datasets, or skip metric subsampling when mode != train.

Comment thread egomimic/pl_utils/pl_model.py Outdated
if video_only:
mode = "video"
# Non-viz epoch: skip before the forward pass, it would be wasted.
should_viz = getattr(active, "_should_viz", None)

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.

low (simplification): validation_step reaches into the evaluator's private _should_viz through getattr/callable, and EvalVideo.on_validation_step(mode="video") already returns on the same condition. Either add a public should_viz() to the Eval interface (default True) and call that, or drop this check and let the evaluator decide. As written, the viz gating lives in two classes and only one of them applies the viz_max_batches cap.

Comment thread tests/unit/test_eval_loaders.py Outdated
),
}

# Episode counts these K values were computed from (measured 2026-09-15).

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.

nit (test-quality): This test copies the YAML's K table and pinned hashes, plus a dated episode count ("measured 2026-09-15"), so it mostly checks that the YAML equals a copy of itself, and it will break on every re-measurement. The docstring also says "Both flagship configs", but only one config is parametrized, and lines 117-118 repeat the same values. Assert the structural properties instead (keys are VAL_HEADS, K > 0, pins are lists of str, _metric_frames_per_episode round-trips) and move the K derivation into the YAML comment.

Comment thread tests/unit/test_eval_loaders.py Outdated
assert wrapped.base is split

per_episode = {name: 0 for name in sizes}
starts = {}

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.

nit (dead-code): starts and offset are computed and never used. Delete the loop.

The unshuffled val loaders were truncated by limit_val_batches (80 x 64 x W
frames) to their leading episode-hash-sorted slice, so at W = 1 the seen-op
head scored 25 of 70 episodes and 6 of 11 operators, and the unseen head 33 of
191 episodes with an 81/19 operator mix on a 52/48 split (measured
2026-09-15, single-rank). The loaders are unshuffled only so the
overlay video is coherent, and the video renders just the first
viz_max_batches (10) batches anyway.

Split each val head in two:

* metric loader = EvenStrideDataset(split, frames_per_episode=K) with K from
  data.metric_frames_per_episode[head], so every episode -- and so every
  operator -- is scored, at the same 80 batches and with no rendering;
* video loader = <head>_video, the head's split restricted to the episode
  hashes pinned in data.video_episodes[head] (one per operator), contiguous
  and unsubsampled, logging no metrics. The pinned episodes are taken out of
  the already-resolved split, so resolve-once still holds.

EvalVideo.on_validation_step gains a mode argument (metrics / video / both);
ModelWrapper.validation_step routes <head>_video to the same evaluator in
video mode and skips it entirely on a non-viz epoch, before the forward pass.
Heads with no pins are called exactly as before, so a data config that
declares neither key behaves as today.

K = floor(5120 / episodes-in-split), from the measured splits: opsplit
valid 73 (70 eps), train_viz 3 (1346 eps), unseen_op_valid 26 (191 eps).
Those are SINGLE-RANK
numbers and trainHydra scales them by trainer.devices x num_nodes, because
DistributedSampler strides rather than chunks: the W ranks between them walk
the whole split, so the pre-split window was already 80 x 64 x W frames wide.
Without the scaling the shipped 4-GPU ddp_pi run would score 5,110 frames of
the seen-val head where it previously scored all 13,744 -- a regression on
the one head small enough to score whole. EvenStrideDataset keeps an episode
entire when K exceeds its length, so the scaling cannot overrun a short split.

Expect a shift in the per-batch Frechet _min/_max curves: they now span many
episodes instead of ~2 s of one, so they are not comparable with older runs.

Eval.on_validation_step, the abstract base, takes the mode kwarg too: the
metrics/video path passes it unconditionally, so an evaluator that predates
the split would have raised TypeError at the first val step of any config
that declares video_episodes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBmhpVy7ZcMtsBJWh38cJp
@RyanPCo

RyanPCo commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Review findings: status after the fixup pass. The fixes are squashed into this PR's commits, and the whole stack was force-pushed.

Fixed

  • Eval mode builds a valid_video loader, but ModelWrapper never learns the loader names: model.val_loader_names is now set before the mode branch, so it applies in both modes. Not unit-tested: it sits inside train().
  • metric_frames_per_episode is scaled by the training world size and limit_val_batches even in eval mode: Eval overrides moved into _apply_eval_overrides, which runs before the val datasets are built (it moves pre-existing eval-override code). Added a test of the override order; trainHydra: metric_frames_per_episode: auto, and warn on empty prompts #671 later stops scaling K by world size altogether.
  • The non-viz-epoch skip is implemented in both ModelWrapper and EvalVideo: Added public Eval.should_viz() (default True), implemented in EvalVideo. ModelWrapper calls it instead of getattr on _should_viz. eval: build the pinned-episode video batches only where they are used #673's video-loader gate reads should_viz() as well.
  • Test hard-codes a dated cluster measurement and copies the YAML values: Replaced with structural checks (K>0 int, heads in VAL_HEADS, pins are lists of str, read back by trainHydra). The K derivation note moved into the yaml comment.
  • Unused starts/offset bookkeeping in the test: Removed.

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