Make the OneCycle stair counts actually change the schedule - #8179
Conversation
cycle_first_stair_count and cycle_second_stair_count are documented in the
1Cycle tutorial, exposed as --cycle_first_stair_count / --cycle_second_stair_count,
plumbed through get_config_from_args into the scheduler params, and used in four
of the repo's own test configs. _initialize_cycle stores them as
self.first_stair_count and self.second_stair_count, and nothing ever reads them.
_get_scale_factor is therefore always continuous, so every stair count produces
a byte-identical schedule:
cycle_first_step_size=8, cycle_second_step_size=8
stair_count=0 -> [0.002125, 0.00325, 0.004375, 0.0055, ...]
stair_count=2 -> identical
stair_count=4 -> identical
stair_count=8 -> identical
Quantise the half-cycle scale factor into stair_count steps, matching the
floor() that LRRangeTest._staircase_interval already uses, so lr and momentum
hold flat across each stair instead of moving every batch. A count of 0 keeps
the continuous schedule, and a count equal to the half-cycle length is the
continuous schedule as well.
Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aca90b02fc
ℹ️ 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".
| # instead of moving them every batch, the same floor() the LR range test staircase | ||
| # uses. A count of 0 keeps the continuous schedule. | ||
| if stair_count > 0: | ||
| scale_factor = math.floor(scale_factor * stair_count) / stair_count |
There was a problem hiding this comment.
Preserve the peak when quantizing stairs
When stair counts are enabled with asymmetric cycle halves, this floor can quantize away the exact peak because the continuous scale at cycle_first_step_size is often just below 1 due to floating point math. For example, cycle_first_step_size=10, cycle_second_step_size=21, and cycle_first_stair_count=10 gives scale_factor=0.9999999999999998, which is floored to 0.9, so the scheduler never reaches cycle_max_lr at the cycle boundary. Please clamp values near 1 or quantize from integer batch offsets before flooring.
Useful? React with 👍 / 👎.
tohtana
left a comment
There was a problem hiding this comment.
Hi @vineethsaivs,
Thank you for submitting this PR!
The Codex's comment seems reasonable, but I think the required fix is broader: the same floating-point rounding can shift any mathematically exact interior stair boundary, not only values near 1.0.
My codex suggested this change to address it. Could you check it?
Flooring the normalised scale_factor can drop a whole stair when the float scale at a half-cycle boundary lands just under an exact stair. With cycle_first_step_size=10 and cycle_second_step_size=21, the scale at the peak is 0.9999999999999998, so a stair count of 10 floored it to 0.9 and the schedule never reached cycle_max_lr. Compute the stair position from the integer batch offset within the half cycle instead, so batch-aligned boundaries are exact by construction rather than by luck of the rounding. A stair count equal to its half-cycle length now reproduces the continuous schedule exactly in the asymmetric case too, which it did not before. Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
|
Thanks @tohtana, you are right that my version was too narrow, and your change is better than what I would have written. Pushed it in 22779f2. To be concrete about why yours is the right shape: my I re-ran the numbers against the patched source. With
So it is not only the peak. Any exact interior boundary could shift down a stair, which is the broader point you made. I also confirmed the change does not weaken the original test: on the symmetric 8/8 cycle, counts 0 and 8 still reproduce the continuous schedule and counts 2 and 4 still hold the lr flat across each stair. Your One note for the record: your suggested diff is what landed here, so the credit for the approach is yours. |
tohtana
left a comment
There was a problem hiding this comment.
Hi @vineethsaivs,
Thank you for the update and kind note! No worries, I really appreciate your contribution.
Signed-off-by: Masahiro Tanaka <mtanaka@anyscale.com>
Problem
cycle_first_stair_countandcycle_second_stair_countdo nothing. Every value produces the same learning rate schedule.They are not obscure knobs. They are documented in the 1Cycle tutorial:
exposed as CLI flags (
--cycle_first_stair_count,--cycle_second_stair_count), plumbed throughget_config_from_argsinto the scheduler params, documented onOneCycleitself ("Number of stairs in first half of cycle phase. This means lr/mom are changed in staircase fashion. Default 0, means staircase disabled."), and set in four of the repo's own test configs (tests/unit/runtime/half_precision/test_fp16.py,tests/unit/v1/half_precision/test_bf16.py,tests/unit/checkpoint/test_pipeline.py,tests/unit/checkpoint/test_other_optimizer.py)._initialize_cyclestores them:and
self.first_stair_count/self.second_stair_countare read nowhere in the repository._get_scale_factoris unconditionally continuous:Reproduction
cycle_min_lr=0.001,cycle_max_lr=0.01,cycle_first_step_size=8,cycle_second_step_size=8:A user who sets a stair count silently gets the continuous schedule, with no warning.
Fix
Quantise the half-cycle scale factor into
stair_countsteps, using the samefloor()thatLRRangeTest._staircase_intervalalready uses for the LR range test staircase:Because
_get_scale_factorbacks both_get_cycle_lrand_get_cycle_mom, this makes lr and momentum staircase together, which is what the docstring describes.After the fix, on the same config:
stair_count=0is byte-identical to before, so the default path is untouched. A stair count equal to the half-cycle length is also the continuous schedule, which is the correct degenerate case: quantising 8 batches into 8 steps changes nothing.Documentation needs no update. The tutorial and the docstring already describe this behaviour; only the code was missing.
The exact quantisation is the one design choice here. I matched
LRRangeTest, which is the only other staircase in this file, and kept it to a single expression so it is easy to change if you would rather the stairs be derived from the batch index than from the scale factor.Testing
test_one_cycle_stair_count_holds_lr_flatintests/unit/runtime/test_lr_schedulers.py, added next to the existing non-distributed scheduler tests. It asserts the continuous schedule still varies every batch, that a stair count of 2 differs from it, changes the lr less often, and is flat within a stair, and that a stair count equal to the half-cycle length reproduces the continuous schedule. It fails on master withAssertionError: stair count still has no effectand passes with this change.yapf --style .style.yapfandflake8 --config .flake8clean on both files. Commit is signed off.