Skip to content

Update lr_schedules.py - #4563

Merged
tjruwase merged 14 commits into
deepspeedai:masterfrom
CoinCheung:master
Nov 10, 2023
Merged

Update lr_schedules.py#4563
tjruwase merged 14 commits into
deepspeedai:masterfrom
CoinCheung:master

Conversation

@CoinCheung

Copy link
Copy Markdown
Contributor

add cosine annealing scheduler

this scheduler is widely used in image classification task, and many llm (e.g. llama) use this also.

add cosine annealing scheduler
@CoinCheung

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

@tjruwase

Copy link
Copy Markdown
Contributor

@CoinCheung, thanks for the PR. A few items to address.

To fix formatting issues use this guide.

Please add unit test: example

Inspect failing CI tests.

@CoinCheung
CoinCheung requested a review from mrwyattii as a code owner October 26, 2023 03:41
@CoinCheung

Copy link
Copy Markdown
Contributor Author

@tjruwase @wjessup @dfyz @manuelciosici I have no experience with pr for deepspeed, what is the status of this now? Is there any further operation that needs me to work on?

@tjruwase

Copy link
Copy Markdown
Contributor

@CoinCheung, thanks for making the changes. We will review and merge once the CI passes.

@CoinCheung

Copy link
Copy Markdown
Contributor Author

Hi @tjruwase ,

I have made some fixes, would you please help me launch CI test one more time?

@CoinCheung

Copy link
Copy Markdown
Contributor Author

@tjruwase Would you please launch CI one more time ?

@CoinCheung

Copy link
Copy Markdown
Contributor Author

Hi @jeffra @mrwyattii I think the problem is not with my fix, it is a inference error, but my fix is about training learning rate scheduler. Can this fix be merged ? Or is there other things that need me to commit?
image

@tjruwase

Copy link
Copy Markdown
Contributor

@CoinCheung, sorry for the delay. It seems the issue is with our CI system. Please bear with us while we resolve the problem.

@tjruwase
tjruwase added this pull request to the merge queue Oct 31, 2023
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 31, 2023
@CoinCheung

Copy link
Copy Markdown
Contributor Author

Hi @tjruwase , What is the status of this thread?
image

@tjruwase

tjruwase commented Nov 7, 2023

Copy link
Copy Markdown
Contributor

@CoinCheung, I have restarted CI. Let's see how it goes.

@CoinCheung

Copy link
Copy Markdown
Contributor Author

Hi @tjruwase ,

Is this associated with my changes?
image

@tjruwase

tjruwase commented Nov 8, 2023

Copy link
Copy Markdown
Contributor

@CoinCheung, no I don't think it is related to your changes.

@CoinCheung
CoinCheung requested a review from tjruwase November 10, 2023 01:11
@tjruwase
tjruwase added this pull request to the merge queue Nov 10, 2023
Merged via the queue into deepspeedai:master with commit 4388a60 Nov 10, 2023
@kmn1024

kmn1024 commented Nov 16, 2023

Copy link
Copy Markdown

kmn1024 added a commit to kmn1024/axolotl that referenced this pull request Nov 16, 2023
@tjruwase

Copy link
Copy Markdown
Contributor

Should WarmupCosineLR inherit from WarmupLR? https://github.com/microsoft/DeepSpeed/blob/master/deepspeed/runtime/lr_schedules.py#L774

Yes, you are correct. It should.

@CoinCheung, are you able to refactor your changes? Thanks!

@CoinCheung

CoinCheung commented Nov 17, 2023

Copy link
Copy Markdown
Contributor Author

Hi @tjruwase @kmn1024 , I do not think WarmupCosineLR can be interited from this WarmupLR in this case.

SInce they use different methods to determine the learning rates. For WarmupCosineLR, I use "ratio of original lr values", which I think should be more scientific, while WarmupLR uses specific lr values.

For example, when using WarmupLR, by setting warmup_min_lr=1e-5, warmup_num_steps=100, the scheduler will set lr from 1e-5 to max lr within 100 steps.
When using WarmupCosineLR, by setting warmup_min_ratio=0.1, warmup_num_steps=100, and we assume lr=1e-3 when we defined the optimizer, the scheduler would set lr from 0,1 * lr = 1e-4 to max lr within 100 steps. We do not set specific lr values to a scheduler.

The reason why I feel using ratio is better: we do not need to set specific lr values everywhere in both optimizer and scheduler. When we define an optimizer, we need to consider the learning rate. When we define a scheduler, what we only need to do is to determine the shape of the learning rate curves, rather than its specific values. When you want to keep shape of lr curve and only tune peak lr, you only need to change one place. This follows the principle of "each module only does its own work, and their settings are not impacted by each other".

From my experience of tuning models, this method is less likely to cause mistakes that I change optimizer lr but forgot to change scheduler.

Also in some paper, if I recally correctly, they claimed that they use CosineLR to train their model, and the learning rate anneals from max_ lr to 0.1 * max_lr. I think many other people accept this method of tuning learning rates.

@tjruwase

tjruwase commented Nov 17, 2023

Copy link
Copy Markdown
Contributor

@CoinCheung, thanks for your response. I agree with the differences that you identify between WarmupLR and WarmupCosineLR, but these differences are to me simply in the implementation and logic. At the high-level they are similar because of they provide two phases of lr changes: (1) initial phase of warmup/increase, and (2) final phase of no change or decay. Looking more closely we observe significant similarity or duplication in many of the methods including step, state_dict, load_state_dict, get_last_lr, _format_param. These similarities suggest to me opportunities for to code refactor and reuse.

@CoinCheung

CoinCheung commented Nov 17, 2023

Copy link
Copy Markdown
Contributor Author

@tjruwase Would it be acceptable if we change args (init args used for define the scheduler object) of WarmupLR ? It has only one sub-class WarmupDecayLR, and I think its usage frequency is not very high.

mauryaavinash95 pushed a commit to mauryaavinash95/DeepSpeed that referenced this pull request Feb 17, 2024
add cosine annealing scheduler

this scheduler is widely used in image classification task, and many llm
(e.g. llama) use this also.

---------

Co-authored-by: Olatunji Ruwase <olruwase@microsoft.com>
Co-authored-by: Logan Adams <114770087+loadams@users.noreply.github.com>
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Aug 11, 2026
…ai#8201)

## The bug

`OneCycle` documents four of its arguments as accepting a
per-param-group list:

```
cycle_min_lr (float or list): Initial learning rate which is the
    lower boundary in the cycle for each parameter group.
cycle_max_lr (float or list): Upper learning rate boundaries in the cycle
    for each parameter group.
cycle_min_mom (float or list): Initial momentum which is the
    lower boundary in the cycle for each parameter group.
cycle_max_mom (float or list): Upper momentum boundaries in the cycle
    for each parameter group.
```

`_initialize_lr` and `_initialize_momentum` only ever broadcast a
scalar:

```python
self.min_lrs = [cycle_min_lr] * len(optimizer.param_groups)
...
self.min_moms = [(cycle_min_mom, 0.99)] * len(optimizer.param_groups)
```

so the documented list is written whole into every param group, and the
optimizer is left holding a list where it expects a number:

```
param_group lrs after construction:   [[0.001, 0.002], [0.001, 0.002]]
param_group betas after construction: [([0.8, 0.85], 0.99), ([0.8, 0.85], 0.99)]

scheduler.step()  -> TypeError: unsupported operand type(s) for -: 'list' and 'list'
optimizer.step()  -> TypeError: unsupported operand type(s) for -: 'int' and 'list'
```

The second line matters: the optimizer is corrupt from construction, so
even a plain `optimizer.step()` fails before the scheduler is stepped at
all.

This is reachable from a plain JSON config, not just the Python API.
`engine.py:1550` does `scheduler(optimizer, **scheduler_params)`, so
`"cycle_min_lr": [0.001, 0.002]` in `ds_config` deserializes to a Python
list and lands directly in `OneCycle.__init__`.

A wrong-length list is also accepted silently, where the siblings raise:

```
OneCycle:     accepted 3 values for 2 param groups, no error
LRRangeTest:  ValueError expected 2 lr_range_test_min_lr, got 3
WarmupLR:     ValueError expected 2 value for min_lr, got [0.0, 0.1, 0.2]
```

## Why implement it rather than delete the docstring lines

Deleting the four "or list" claims would be a smaller diff, but the rest
of `OneCycle` is already per-group end to end: `_get_cycle_lr` zips
`min_lrs` with `max_lrs`, `_get_cycle_mom` zips `min_moms` with
`max_moms`, and `update_lr` walks the param groups. Only the two
initializers collapse the input. Both sibling schedulers in this file
implement the same documented contract, and the two most recent
multi-group fixes here (deepspeedai#7969 for `WarmupCosineLR`, deepspeedai#8171 for
`WarmupLR`) went in the same direction. This reads as an unfinished port
rather than a design decision.

## The fix

Reuse `_format_param`, which is how the siblings already honour this
contract. It was defined twice, identically: as a method on `WarmupLR`,
and again on `WarmupCosineLR` where nothing calls it (`_format_param`
appears in only two files repo-wide, and in the test file only inside a
comment). I promoted the single copy to module level next to `update_lr`
and `get_torch_optimizer`, dropped the dead one, and pointed `WarmupLR`
and `OneCycle` at it. Net result is 19 added, 22 removed, and one
implementation of this logic instead of two.

I chose promoting over leaving one-line delegate methods behind because
`_format_param` is private and has no callers outside this file, so a
delegate would be indirection with no consumer; happy to switch to
delegates if you would rather not remove the methods.

Three details worth calling out rather than leaving for review:

**The momentum call has to wrap the scalar, not the tuple.**
`_format_param` accepts tuples, and the default `cycle_min_mom` pairs
with `0.99` into a length-2 tuple, so wrapping the existing
`(cycle_min_mom, 0.99)` expression would raise at construction for 1 and
3 param groups, and for exactly 2 groups would silently write
`group['betas'] = 0.8` as a float and blow up later in `_get_cycle_mom`.
The correct form, which is what this PR uses, formats the scalar first:

```python
self.min_moms = [(mom, 0.99) for mom in _format_param(optimizer, cycle_min_mom, 'cycle_min_mom')]
```

**Both bounds are now validated before the optimizer is touched.**
`_initialize_lr` used to compute `min_lrs`, write `group['lr']`, and
only then look at `cycle_max_lr`, so a bad-length `cycle_max_lr` left
the param groups half updated. Moving the second `_format_param` call
above the mutation loop makes the constructor all-or-nothing:

```
before: lrs after a failed ctor = [[0.001, 0.002], [0.001, 0.002]]
after:  ValueError, lrs after a failed ctor = [0.1, 0.2]   (untouched)
```

**One token in `_format_param`'s error message.** Both copies
interpolate `FileNotFoundError(param_value)` where the wording promises
a count, so `WarmupLR` currently reports `expected 2 value for min_lr,
got [0.0, 0.1, 0.2]`. Since the two copies are collapsing into one
shared helper, I corrected it to `len(param_value)` rather than carry
the typo into the surviving copy. It is the only change to `WarmupLR`'s
behaviour and nothing asserts on that message (no `pytest.raises(...,
match=...)` anywhere in the file); say the word and I will drop it back
to verbatim.

**Not claiming this is strictly safer for momentum.** Because
`_format_param` accepts tuples, a betas-shaped `cycle_min_mom=(0.8,
0.999)` on a two-group optimizer goes from a loud `TypeError` to
silently training with per-group momenta. That hazard already exists
identically in `WarmupLR`, so I kept the behaviour symmetric rather than
diverging, but it is a real trade rather than a pure win.

## Tests

Added to `tests/unit/runtime/test_lr_schedulers.py` as module-level
functions, matching the existing plain tests there:

- `test_one_cycle_accepts_per_group_lr_and_momentum_lists`: two param
groups, per-group lists for all four arguments, asserting the
constructor sets each group's own lr and `betas[0]`, that the cycle peak
reaches each group's own `cycle_max_lr` with momentum at its own
`cycle_min_mom`, and that the bottom of the cycle returns each group to
its own `cycle_max_mom`.
- `test_one_cycle_rejects_wrong_length_per_group_lists`, parametrized
over all four arguments.

It uses `Adam` rather than `SGD` on purpose: `_initialize_momentum`
returns early when `'betas' not in optimizer.defaults`, so the momentum
half of the test would silently never run under SGD.

`pytest` cannot start on my machine (no GPU, and the `tests/unit`
conftest pulls in the distributed harness), so I ran the module-level
tests in this file directly against the real `lr_schedules.py`, with the
`DistributedTest` classes stripped and only `deepspeed.utils.logger`
stubbed. Three runs:

```
control     upstream lr_schedules.py + upstream tests    21 passed, 0 failed
before      upstream lr_schedules.py + these tests       21 passed, 5 failed
after       this branch                                  26 passed, 0 failed
```

All 5 failures before are the new tests, and the 21 pre-existing ones
are unchanged by this diff. The `DistributedTest` OneCycle coverage
(`TestOneCycle.test_lr`, `test_mom`) and the other scalar-momentum users
(`test_fp16.py`, `test_bf16.py`, `test_pipeline.py`,
`test_other_optimizer.py`) all pass scalars, which take the unchanged
broadcast path; I am relying on CI for those since they need a GPU.

Lint: `yapf` 0.40.0 with the repo's `.style.yapf` reports no diff on
both files, and `flake8` with the repo's `.flake8` is clean on both
(also confirmed clean on the unmodified files, so that is a real result
rather than a config that checks nothing).

## Prior art

No open or closed PR implements list support here. `--search` over
`lr_schedules`, `_format_param`, `OneCycle`, `cycle_min_lr` and `lr
scheduler list param groups` turns up deepspeedai#8151, deepspeedai#8166, deepspeedai#8171, deepspeedai#7969, deepspeedai#8179,
deepspeedai#1455 and deepspeedai#4563, all merged and none touching these two initializers. No
open issue covers it either; the only open `OneCycle` issue is deepspeedai#3492, a
request for `CosineAnnealingLR` support.

This follows deepspeedai#8179 in the same class, so to be upfront about it: that
one was about the cycle shape (`_initialize_cycle` and
`_get_scale_factor`), this one is about the two value initializers, and
I did not see it while in there. If you would rather batch further
`lr_schedules.py` work, tell me and I will hold the rest.

Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
Co-authored-by: Zhipeng Wang <zhipeng.rainbowserie@gmail.com>
Co-authored-by: Masahiro Tanaka <81312776+tohtana@users.noreply.github.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.

4 participants