Skip to content

[BUG]: Missing Implementation for Loading after_scheduler Parameters #5119

Closed

Description

@imgaojun

🐛 Describe the bug

The current implementation of WarmupScheduler does not include the functionality to load the after_scheduler part of the parameters. This omission leads to a scenario where the learning rate cannot be restored to its saved state after loading from a checkpoint.

The relevant piece of code in question is as follows:

class WarmupScheduler(_LRScheduler):
    """Starts with a linear warmup lr schedule until it reaches N epochs then applies
    the specific scheduler (For example: ReduceLROnPlateau).

    Args:
        optimizer (:class:`torch.optim.Optimizer`): Wrapped optimizer.
        warmup_epochs (int): Number of epochs to linearly warmup lr until starting applying the scheduler.
        after_scheduler (:class:`torch.optim.lr_scheduler`): After target_epoch, use this scheduler.
        last_epoch (int, optional): The index of last epoch, defaults to -1. When last_epoch=-1,
            the schedule is started from the beginning or When last_epoch=-1, sets initial lr as lr.
    """

    def __init__(self, optimizer, warmup_epochs, after_scheduler, last_epoch=-1):
        self.warmup_epochs = int(warmup_epochs)
        self.after_scheduler = after_scheduler
        self.finished = False
        super().__init__(optimizer, last_epoch)

    def state_dict(self):
        state_dict = {key: value for key, value in self.__dict__.items() if key not in "optimizer"}
        if isinstance(state_dict["after_scheduler"], _LRScheduler):
            state_dict["after_scheduler_type"] = type(state_dict["after_scheduler"]).__name__
            state_dict["after_scheduler_dict"] = state_dict["after_scheduler"].state_dict()
            del state_dict["after_scheduler"]
        else:
            raise NotImplementedError()
        return state_dict

    def get_lr(self):
        if self.last_epoch >= self.warmup_epochs:
            if not self.finished:
                self.after_scheduler.base_lrs = self.base_lrs
                self.finished = True
            return self.after_scheduler.get_lr()

        return [(self.last_epoch + 1) / self.warmup_epochs * lr for lr in self.base_lrs]

    def step(self, epoch=None):
        if self.finished:
            if epoch is None:
                self.after_scheduler.step(None)
                self._last_lr = self.after_scheduler.get_last_lr()
            else:
                self.after_scheduler.step(epoch - self.warmup_epochs)
                self._last_lr = self.after_scheduler.get_last_lr()
        else:
            return super().step(epoch)

Environment

No response

Activity

  1. JazyJiang commented on Sep 24, 2026

    @JazyJiang
    Collaborator

    Thanks for the report! This was fixed by #5369 (merged 2024-02-06): WarmupScheduler now derives from TwoStageScheduler, whose state_dict() / load_state_dict() save and restore the after_scheduler state.

    We verified it on current main: a WarmupScheduler(warmup_epochs=3) wrapping CosineAnnealingLR(T_max=20), stepped 7 times, saved, then restored into a fresh optimizer + scheduler, gives the same learning rate as the original after 3 more steps (0.731498 vs 0.731498). The same check against the code before #5369 diverges (0.731498 vs 0.860513).

    Note that the optimizer state needs to be restored together with the scheduler, as with plain PyTorch schedulers, since CosineAnnealingLR updates the learning rate from the optimizer's current value.

    Closing as fixed. Please reopen if you still see this on the latest release.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions