Repository navigation
Conversation
Michael-RDev
left a comment
There was a problem hiding this comment.
No blocking issues found in the reviewed paths. The fallback matches the plugin’s declared default while also preserving False. All four new tests passed, along with existing distributed scheduler checks and accumulation factors of 1, 2, and 4 👍
|
Added a checkpoint-continuation test at Windows/Python 3.12, PyTorch 2.11 CPU: 2 new cases passed; 4 existing SchedulerAdjustmentTester cases passed / 9 deselected. Reverting scheduler.py and state.py to their parent gives 1 failed, 1 passed (the adjustment-enabled bookkeeping case fails); both files restored. Ruff checked. Uses in-memory state_dict copies; no filesystem Accelerator.save_state, multi-process or GPU claim. AI disclosure: prepared and tested with Codex assistance. Checkpoint-continuation regression patchdiff --git a/tests/test_scheduler_checkpoint_adjustment.py b/tests/test_scheduler_checkpoint_adjustment.py
new file mode 100644
index 00000000..41474cca
--- /dev/null
+++ b/tests/test_scheduler_checkpoint_adjustment.py
@@ -0,0 +1,64 @@
+# Copyright 2026 The HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import copy
+
+import pytest
+import torch
+
+from accelerate import Accelerator
+from accelerate.state import AcceleratorState, GradientState
+from accelerate.utils import GradientAccumulationPlugin
+
+
+@pytest.mark.parametrize("adjust", [False, True])
+def test_one_cycle_accumulation_checkpoint_roundtrip(adjust):
+ def make():
+ GradientState._reset_state()
+ AcceleratorState._reset_state(True)
+ accelerator = Accelerator(
+ cpu=True, gradient_accumulation_plugin=GradientAccumulationPlugin(num_steps=2, adjust_scheduler=adjust)
+ )
+ model = torch.nn.Linear(2, 1)
+ optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
+ scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=0.1, total_steps=4)
+ return accelerator, *accelerator.prepare(model, optimizer, scheduler)
+
+ def advance(parts, count):
+ accelerator, model, optimizer, scheduler = parts
+ rates = []
+ for _ in range(count):
+ with accelerator.accumulate(model):
+ accelerator.backward(model(torch.ones(1, 2)).sum())
+ optimizer.step()
+ scheduler.step()
+ optimizer.zero_grad()
+ rates.append(scheduler.get_last_lr())
+ return rates
+
+ try:
+ parts = make()
+ advance(parts, 4)
+ optimizer_state = copy.deepcopy(parts[2].state_dict())
+ scheduler_state = copy.deepcopy(parts[3].state_dict())
+ expected = advance(parts, 4)
+ resumed = make()
+ resumed[3].load_state_dict(scheduler_state)
+ resumed[2].load_state_dict(optimizer_state)
+ assert advance(resumed, 4) == expected
+ assert resumed[3].scheduler.last_epoch == 4
+ assert resumed[3].scheduler._step_count == (9 if adjust else 5)
+ finally:
+ GradientState._reset_state()
+ AcceleratorState._reset_state(True)
|
`GradientAccumulationPlugin.adjust_scheduler` defaults to `True` and is documented as such, but `KwargsHandler.to_kwargs()` only keeps values that differ from the dataclass defaults. As a result the default (and any explicit `True`) never reached `GradientState.plugin_kwargs`, and the `False` fallback in `GradientState.adjust_scheduler` made the scheduler adjustment branch in `AcceleratedScheduler.step` dead code for every user since it was introduced in huggingface#1187: the `_step_count` bookkeeping that accounts accumulation micro-steps for schedulers sized on the total number of batches (e.g. `OneCycleLR`) never ran. - Fall back to `True` in `GradientState.adjust_scheduler`, matching the plugin default (an explicit `False` is still honored, as it differs from the default and is kept by `to_kwargs()`). - Guard the `_step_count` bump for schedulers without one (e.g. `ReduceLROnPlateau`), which would otherwise raise `AttributeError`. - Count actual scheduler steps (`last_epoch`) instead of `_step_count` in the `OneCycleLR` guard so the restored micro-step bookkeeping does not shorten schedules sized on optimizer steps (stepping is identical to before for such schedulers). Signed-off-by: simpleqt <2015900818@qq.com> Adds the checkpoint-continuation regression test contributed by @JonnyFaeth in review: it saves optimizer and scheduler state at an accumulation boundary, recreates the Accelerator and its wrappers, restores both, and compares the remaining OneCycleLR sequence with uninterrupted execution. 2 passed here; with scheduler.py and state.py reverted to the parent, 1 fails (the adjustment-enabled bookkeeping case) and 1 passes.
d8ca022 to
457926e
Compare
|
Applied as I re-ran your claims on Windows / Python 3.11 / torch 2.15.0+cpu:
|
What does this PR do?
GradientAccumulationPlugin.adjust_scheduleris documented to default toTrue("Whether to adjust the scheduler steps to account for the number of steps being accumulated. Should beTrueif the used scheduler was not adjusted for gradient accumulation."), but the setting has silently never taken effect:Cause.
KwargsHandler.to_kwargs()only returns the attributes whose values differ from the dataclass defaults. Sinceadjust_scheduler's default isTrue, both the default and any explicitadjust_scheduler=Trueare stripped whenGradientStatestoresplugin_kwargs.GradientState.adjust_schedulerthen falls back toFalse:So
AcceleratedScheduler.stepnever bumpedself.scheduler._step_countduring gradient-accumulation micro-steps, i.e. the adjustment introduced in #1187 ("Make the Scheduler adjust the steps taken relative to the gradient accumulation steps") has been dead code for every user since March 2023, and schedulers sized on the total number of batches (e.g.OneCycleLRcreated without dividingtotal_steps) are not accounted for. An explicitFalseis honored (it differs from the default, soto_kwargs()keeps it) — the setting only ever worked in one direction.Fix. Minimal, three small changes:
GradientState.adjust_schedulerfalls back toTrue, matching the declared plugin default. An explicitadjust_scheduler=Falseis still honored._step_countbump inAcceleratedScheduler.stepis guarded withhasattr(self.scheduler, "_step_count"), because schedulers without a step counter (e.g.ReduceLROnPlateau) would otherwise raiseAttributeErrornow that the default is active.OneCycleLR-style guard compares againstself.scheduler.last_epoch(actual steps taken) instead of_step_count. Since_step_countnow also advances during micro-steps,_step_count <= total_stepswould stop such schedulers early;last_epoch < total_stepssteps them exactlytotal_stepstimes, which is identical to the behavior onmain(where the guard1 + steps_taken <= total_stepsalso yielded exactlytotal_stepssteps). Schedulers withouttotal_stepsare untouched.Test. New
SchedulerAdjustmentTesterintests/test_scheduler.py(CPU only, single process):test_adjust_scheduler_reaches_gradient_state: the defaultgradient_accumulation_stepspath and an explicitadjust_scheduler=Trueyieldgradient_state.adjust_scheduler is True; an explicitFalseyieldsFalse. Fails onmain(returnsFalse).test_accumulation_accounts_for_skipped_scheduler_steps: withgradient_accumulation_steps=2, the wrapped scheduler's_step_countadvances on non-sync micro-steps ([2, 3, 4, 5]after 4 micro-steps) whilelast_epochshows only the 2 real updates. Fails onmain([1, 2, 2, 3]).test_accumulation_with_plateau_scheduler:ReduceLROnPlateau+ accumulation keeps working (one LR update per optimizer step) — guards fix point 2.test_accumulation_with_one_cycle_scheduler:OneCycleLR(total_steps=4)under accumulation is still stepped exactly 4 times without raising — guards fix point 3.Results:
SchedulerAdjustmentTestermain(3008af9)Run with:
Note for reviewers: a previous attempt at this fix (#4262) was closed without review in September. This PR takes the same core diagnosis (the
Falsefallback contradicting the plugin default) with the two companion guards that keep the restored default safe for schedulers lacking_step_countand forOneCycleLR-styletotal_stepsbounds. Users who depended on the old effective default can keep the previous behavior explicitly withGradientAccumulationPlugin(num_steps=..., adjust_scheduler=False).Before submitting
Who can review? @SunMarc