Skip to content

fix(train): don't rebuild resumed schedulers with last_epoch - #1049

Open
zihanlin-ai wants to merge 2 commits into
vllm-project:mainfrom
zihanlin-ai:fix/resume-scheduler-initial-lr
Open

fix(train): don't rebuild resumed schedulers with last_epoch#1049
zihanlin-ai wants to merge 2 commits into
vllm-project:mainfrom
zihanlin-ai:fix/resume-scheduler-initial-lr

Conversation

@zihanlin-ai

Copy link
Copy Markdown
Contributor

Purpose

Fixes #1048: every distributed resume (DDP and FSDP) crashes in setup_optimizer with KeyError: "param 'initial_lr' is not specified in param_groups[0]".

The last_epoch=checkpointer.previous_epoch passed at scheduler construction is redundant — load_scheduler_state_dict() restores the scheduler's real state three lines later — and wrong twice over: these are per-step schedulers, so an epoch index is the wrong unit, and last_epoch >= 0 makes LRScheduler.__init__ demand initial_lr in every param group, which the distributed optimizer load (set_optimizer_state_dict) does not round-trip. The fix is to drop it: construct with the default last_epoch=-1 and let load_scheduler_state_dict() restore the true position. Net −4 lines in trainer.py.

Unmasking the crash exposed a second gap, also fixed here: the checkpoint_dir fixture saved a never-stepped AdamW, so test_distributed_resume[ddp]'s "optimizer state restored" assertion compared an empty dict to an empty dict ([fsdp] only passed it because DCP initializes state as a side effect). The fixture now takes one real optimizer step before saving.

One behavioral note: a legacy checkpoint with no scheduler file previously got a scheduler positioned at last_epoch = epoch_index — an epoch count where a step count is expected, so not a meaningful position. Such checkpoints now restart the schedule from step 0.

Tests

test_distributed_resume[ddp/fsdp] go red → green on a multi-GPU machine. Full tests/unit:

platform result
8x GPU, torch 2.11, transformers 5.8.1 792 passed, 0 skipped
1 GPU (CI-equivalent), same 787 passed, 5 skipped
Ascend NPU (aarch64), torch 2.9 + torch_npu, transformers 4.57.6 745 passed, 47 skipped
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan/results, such as providing test command and pasting the results.
  • (Optional) The necessary documentation update.
  • I (a human) have written or reviewed the code in this pr to the best of my ability.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b6834b72-11f5-4e1c-82a6-4fdb09d3e266

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d4ab2683-5b83-40a2-8d1d-281d8056325c

📥 Commits

Reviewing files that changed from the base of the PR and between 7a58fc5 and dc212e4.

📒 Files selected for processing (2)
  • src/speculators/train/trainer.py
  • tests/unit/train/test_setup_model.py
💤 Files with no reviewable changes (1)
  • src/speculators/train/trainer.py

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Resume training

Layer / File(s) Summary
Resume optimizer and scheduler setup
src/speculators/train/trainer.py
Optimizer state loading remains conditional on checkpoint resume. Linear and cosine schedulers no longer receive last_epoch during construction.
Populate checkpoint optimizer state
tests/unit/train/test_setup_model.py
The checkpoint fixture performs an AdamW step, clears gradients, resets trainable weights to 42.0, and saves the checkpoint.

Merge Risk: ⚪ Minimal · up to dc212

The change fixes distributed checkpoint resume failures while preserving saved scheduler state and improving test coverage; no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main fix: removing last_epoch when rebuilding resumed schedulers.
Description check ✅ Passed The description explains the distributed resume failure, the scheduler fix, the checkpoint fixture update, and the test results.
Linked Issues check ✅ Passed The changes satisfy issue #1048 by constructing resumed schedulers with the default last_epoch and loading the saved scheduler state separately, preventing the missing initial_lr KeyError in DDP and F…
Out of Scope Changes check ✅ Passed The changes remain within scope. The checkpoint fixture update directly strengthens the optimizer-state restoration test for the reported resume behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files.
Full details: Linked Issues check

Explanation

The changes satisfy issue #1048 by constructing resumed schedulers with the default last_epoch and loading the saved scheduler state separately, preventing the missing initial_lr KeyError in DDP and FSDP resume.

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mergify

mergify Bot commented Aug 27, 2026

Copy link
Copy Markdown

Merge Protections

🔴 1 of 1 protections blocking · waiting on 👀 reviews

Protection Waiting on
🔴 Require approval from approved reviewers list 👀 reviews

🔴 Require approval from approved reviewers list

Waiting for any of

  • approved-reviews-by = dsikka
  • approved-reviews-by = fynnsu
  • approved-reviews-by = orestis-z
  • approved-reviews-by = rahul-tuli
  • approved-reviews-by = shanjiaz
This rule is failing.

All pull requests must have at least one approving review from a member of the approved reviewers list before merging.

  • any of:
    • approved-reviews-by = dsikka
    • approved-reviews-by = fynnsu
    • approved-reviews-by = orestis-z
    • approved-reviews-by = rahul-tuli
    • approved-reviews-by = shanjiaz

On resume, setup_optimizer constructed fresh schedulers with
last_epoch=checkpointer.previous_epoch. That argument is redundant: three
lines later load_scheduler_state_dict() restores the scheduler's real
state (last_epoch, base_lrs, _step_count). It is also wrong twice over:
these are per-step schedulers, so an epoch index is the wrong unit, and
constructing with last_epoch >= 0 makes LRScheduler.__init__ demand
initial_lr in every param group. The single-device optimizer load happens
to restore initial_lr inside its saved param groups, but the distributed
path (set_optimizer_state_dict) does not round-trip it, so every resume
under DDP/FSDP crashes with
KeyError: "param 'initial_lr' is not specified in param_groups[0]".
CI never sees this because its single-GPU runners skip the multi-GPU
resume tests.

Always construct with the default last_epoch=-1 and let
load_scheduler_state_dict() restore the true position.

Unmasking the crash exposed a second gap: the checkpoint_dir fixture
saved a never-stepped AdamW, so test_distributed_resume[ddp]'s
"optimizer state restored" assertion was checking an empty dict against
an empty dict ([fsdp] only passed it because DCP initializes state as a
side effect of set_optimizer_state_dict). The fixture now takes one real
optimizer step before saving, then re-pins the weights, so the assertion
has teeth on both paths.

Fixes vllm-project#1048

Signed-off-by: Zihan Lin <linzihan.ai@gmail.com>
@zihanlin-ai
zihanlin-ai force-pushed the fix/resume-scheduler-initial-lr branch from 4381ad7 to dc212e4 Compare August 28, 2026 03:19
@zihanlin-ai
zihanlin-ai marked this pull request as ready for review August 28, 2026 03:19

@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: dc212e400b

ℹ️ 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/speculators/train/trainer.py
… scheduler bases on them

Loading the optimizer state overwrites each param group's lr with the
decayed value from the checkpoint, and scheduler construction stamps the
group lr as its base (initial_lr). With a scheduler state file present
that base is corrected by load_scheduler_state_dict; without one (legacy
checkpoints, or an interruption between the optimizer and scheduler
writes) the decayed LR silently became the base forever - a linear
schedule saved near LR zero would keep training at zero. Restore the
configured LRs after the optimizer load, so the missing-state path
restarts the schedule from the configured base.

Raised by review on the PR.

Signed-off-by: Zihan Lin <linzihan.ai@gmail.com>
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.

test_distributed_resume[ddp/fsdp] fails on any 2+ GPU machine: resumed scheduler lacks initial_lr in param_groups

1 participant