Feat/val split#72
Conversation
New --val_split flag (train phase only): a value <1 holds out that fraction of the training set, >=1 holds out that many examples. The split is a seeded shuffle of the combined dataset, so every DDP rank holds out the identical set. When set, per-epoch validation loss drives best-checkpoint selection and is logged to wandb as val/loss; on multi-GPU it is reduced across ranks to cover the full val set. Skipped for RL, where LM loss is not the training objective. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The held-out val set was built with the same train-mode args, so --augment_ecg/--augment_rgb randomly augmented val inputs every epoch, making val/loss non-deterministic — a poor signal for the best-checkpoint selection and early stopping it now drives. Build the val dataset through a copied args namespace with augmentation forced off (build_data_representation gains an optional args override, defaulting to self.args so the train path is unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lower --patience_delta from 0.1 to 0.0001 so early stopping reacts to smaller improvements; patience stays at 5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Early stopping now triggers only when a held-out validation loss exists (val_loss is not None); otherwise it would stop on train loss, which is not a valid generalization signal. No-op when --val_split is unset or in RL, where val_loss is None. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Overall Comments: I think this is much more complicated than necessary. Meaning, it changes a lot of the fundamental logic, for example i dont like how our I think we should do the splitting in our I also don't understand why the validation runner needs to be so different from the training runner. Shouldn't it just be the same but just with the validation data? ChatGPT suggested something like this for splitting And handling the tuple vs single instance return outside in the main_trainer (isinstance check if output is tuple). |
c418300 to
88959bf
Compare
Addresses reviewer feedback (keep DatasetMixer single-purpose, split in BuildDataLoader, don't diverge the validation runner from training): - DatasetMixer returns a single dataset again; the train/val split moves to BuildDataLoader via torch.utils.data.random_split with a seeded generator (still identical across DDP ranks). - Fold run_validation into run_train: with no optimizer passed it runs a no-grad eval pass (eval mode + cross-rank loss reduction). main_trainer's epoch loop is simplified accordingly. - Exclude RL from the split (no validation loss for RL). - Preserve augmentation-immunity via a no-augment view of the val subset. - Fix: random_split wraps the dataset in a Subset, so read the tokenizer from BuildDataLoader.dataset instead of loader.dataset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f3c6c65 to
919786a
Compare
|
DatasetMixer returns a single dataset again; split moved to BuildDataLoader via seeded random_split. Kept a val_dataloader attribute instead of a (train, val) tuple + isinstance because it avoids the isinstance branch and leaves main_evaluator untouched. Can switch if you prefer. Shuffling disabling is necessary for determinism, especially for uneven batches. Augment disabling is needed for similar reasons, though I think we run into that case less. |
|
I think we can do a cleaner implementation. Lets try this. as for the validation runner, i want it to be a separate file called validator.py, then just copy paste the train runner function and modify it as needed (no optimizer, no backward). we already get the average loss in the train runner function so in the validator runner we can just log that with the all reduce logic as well. For determining augment, we should put it in the init.py of base.py dataset, where we just check if its train or not then augment based on that. we can also put the indication of train or not in the class args as well if thats easier. let me know if your confused on anything. Also in the future, make sure you either answer or address via code all comments I make. This makes it helpful for me to align what has been addressed and what has not. |
Move the split to the data level and reuse the existing pipeline, add a separate validator runner, and gate augmentation with an is_train flag. - DatasetMixer.split_train_val splits the loaded/mixed data (seeded, so identical across ranks) and builds train + val through the same build_data_representation; build_torch_dataset returns (train, val). - runners/validator.py: run_validation is run_train minus optimizer and backward, with eval() + cross-rank loss reduction. run_train reverted to its original form. - Base.__init__ gets self.is_train (default True); the val dataset's is set to False; representations gate augmentation on it. Drops random_split / Subset / without_augmentation. - Tokenizer read from dataloader.dataset again (full datasets now, no Subset wrapper). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Ok I gave it another shot; I think I handled all your comments The one thing is that we split in DatasetMixer again due to "get the hf dataset from load_dataset then split that into train and validation splits" and contrary to "dont like how our DatasetMixer class now returns train or val loader. I want it to be simply a class to return a single dataset" from the first comment. There's surely ways to make it so both are simultaneously true but I feel like that is more complicated. Let me know if I did something wrong here. |
9826848 — Add held-out validation split for training
Introduces --val_split (train phase only): a value <1 holds out that fraction of the training set, >=1 holds out that many examples. The split is a seeded shuffle of the combined dataset, so every DDP rank holds out the identical set. When set, per-epoch validation loss drives best-checkpoint selection and is logged to wandb as val/loss; on multi-GPU it's reduced across ranks (new all_reduce_sum helper) to cover the full val set. Skipped for RL.
The reduction across ranks is necessary. Without it, val/loss would reflect only rank 0's shard, which wasn't too much of a problem before because we were just logging using train loss for vibes but now were probably stopping runs with val loss so we should make it precise and not waste other ranks' data.
13298d6 — Disable augmentation for the validation split
The held-out set was built with the same train-mode args, so --augment_ecg / --augment_rgb randomly augmented val inputs every epoch, making val/loss non-deterministic (it should be deterministic). The val dataset is now built through a copied args namespace with augmentation forced off (build_data_representation gains an optional args override, defaulting to self.args so the train path is unchanged). May not actually be used all too often, but just letting you know this problem exists in case we want to use augment signal ever. No need to merge if not needed.
d136869 — Tighten early-stopping delta default to 0.0001
Lowers --patience_delta from 0.1 to 0.0001 so early stopping reacts to smaller improvements; --patience stays at 5. This is to prepare for next commit, where 1e-4 delta is standard.
959bf — Gate early stopping on an active validation split
Early stopping now triggers only when a held-out validation loss exists (val_loss is not None); otherwise it would stop on train loss, which we I think we should fully discontinue because early stopping without validation is sort of crazy. Early stopping is now no-op when --val_split is unset or in RL, where val_loss is None. Not necessary to merge but I think it should probably work this way.