Repository navigation
feat(pretrain): per-hyperparameter multiplier schedules - #4071
Merged
Merged
Conversation
Collaborator
Author
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.
Stacked on #4062 (
feat/pretrain).What
The pretrain trainer stops using the shared torch LR scheduler and gets per-hyperparameter schedules. SFT, RL and
trainer/scheduler.pyare unchanged.Design
f(step) -> multiplier,step= optimizer updates completed so far. Updatestepusesbase_g * f(step), withbase_geach param group's own base value, so ratios between groups hold at every step (the sharedCosineAnnealingLRdecays every group to one absoluteeta_min).f(0) = start_ratio, the warmup reaches 1 at updatewarmup - 1(warmup 0 or 1 starts at 1), the decay reachesend_ratioat the last updatemax_steps - 1. Both ratios default to 0.wsd(default),cosine,constant,custom(import_path+kwargs, called asfn(step, total_steps=max_steps, **kwargs)). Plain functions bound withfunctools.partial.schedules.optimkeys areoptimfield names. Each optimizer config declares its schedulable fields as data (schedulableClassVar: field -> param group key, tuple index,algorithmfilter): AdamWlr,weight_decay,betas1/betas2(->betas[0/1]); Muonlr,weight_decay,mu(Muon groups),betas1/betas2(->beta1/beta2, AdamW groups); SignSGDlr,weight_decay. There is noepsfield on these configs on this branch, soepsis not schedulable (Dion's Muon-groupepsilonis the Newton-Schulz epsilon anyway). APretrainConfigvalidator rejects keys the chosen optimizer can't schedule,clip_normwithoutmax_norm, decays/fractions withoutmax_steps, and phases longer thanmax_steps.Schedulesis built right after the optimizer and captures each group's base values before any checkpoint load.schedules.apply(step)at the top of each step writes the values (by group index, since loading an optimizer state dict replaces the group dicts) and returns the clip norm plus metrics. Nothing is checkpointed: values are recomputed from the step on resume.ckpt.skip_schedulerrestarts the schedules at the resumed step over the remaining steps.optim/<name>(first targeted group's value;optim/lras before) andschedule/<name>multipliers, plusoptim/max_norm/schedule/clip_norm.[scheduler] cosine, warmup_steps = N, min_lr = X->[schedules.optim.lr] cosine, warmup = N, end_ratio = X / lr.Validation
uv run pytest tests/unit/train/pretrain tests/unit/test_configs.py -q-> 260 passed (5 new tests intests/unit/train/pretrain/test_schedules.py;test_configs.pyloads all TOMLs including the updated ladder examples).examples/pretrain/llama3/17m.tomlwith 60 steps, batch 32,lrwsd (warmup = 6,decay = 0.3,end_ratio = 0.1),betas2cosine (warmup = 0,end_ratio = 1.04),clip_normcosine (warmup = 0,end_ratio = 0.5), checkpoint every 30 steps. It finished and the logged values match the convention exactly: LR 0 at step 1, rising by 6e-4 per step to 3e-3 at step 6, flat through step 42, then linear down to 3e-4 at step 60.betas2went from 0.95 to 0.988 andmax_normfrom 1.0 to 0.5 at the last step. Loss 11.82 -> 7.56.resume.dirset to the step-30 checkpoint logs the same scheduled values at steps 31-60. Step 31 matches bit for bit (loss 7.852322, grad norm 0.323856), and losses later agree to about 1e-4 (normal GPU nondeterminism).Notes / open questions
wsdis warmup 0.1, decay 0.9, linear,end_ratio = 0: linear decay to 0 over all updates after the warmup. On our ablations (CompleteP + Muon, 537M tokens, 65k-token batches), WSD with 10/20/40/60/80% decay lost +0.069/+0.041/+0.018/+0.007/+0.001 nats to it at each schedule's best LR, and cosine tied. This matches Bergsma et al. 2025, Schaipp et al. 2025 and Wen et al. 2025 (Muon). For cooldown branches, set a shorterdecay(e.g. 0.2) withdecay_shape = "1-sqrt"(−0.010 vs linear at 20%). Unit tests after the change: 260 passed (CPU job on the research cluster).max_stepsmust now set a schedule without decay/fractions (e.g.constant). Previously the default scheduler was constant.min_lr(torchLinearLRstart factor); the new ones start atstart_ratio = 0. Setstart_ratio = 0.1to keep the old start.schedules.optimin TOML replaces the default dict, so a config that schedules onlyweight_decayhas a constant LR. Documented.--schedules.optim.lr.end-ratio 0.2, adding--schedules.optim.weight-decay.type constant). Changing thetypeof an entry merges with the TOML fields, so switching acosineentry toconstantfrom the CLI fails on the leftover fields (same as other unions).CheckpointConfig.skip_schedulerkeeps its shared name; for pretraining it means "restart the schedules at the resumed step".