Reach through the DeepSpeed optimizer wrapper in AcceleratedOptimizer.eval() - #4130
Conversation
….eval() train() handles the case where DeepSpeed wraps the user optimizer one level deeper, but eval() never got the matching branch, so a schedule-free optimizer under DeepSpeed is switched into train mode and never switched back. There is no error, just eval running with train-mode weights. Mirror the train() branch in eval() and add CPU regression tests for the wrapped, unwrapped and no-mode-support cases.
|
This issue has been automatically marked as stale because it has not had recent activity. If you think this still needs to be addressed please comment on this thread. Please note that issues that do not follow the contributing guidelines are likely to be ignored. |
|
This looks correct and worth merging — it's the exact symmetric counterpart to the DeepSpeed wrapper reach-through that #3266 added to Real-world impact isn't cosmetic: without this, It's been open over a month with no review and just got the stale-bot comment. @muellerzr @BenjaminBossan — worth a look before it gets auto-closed? |
The control called train()/eval() on a plain optimizer and asserted nothing, so it only proved the calls did not raise. Run it over the wrapper shape as well, which is the negative case for the reach-through this PR adds, and assert the inner optimizer never grows a mode it did not have. Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
|
Still applies, and thank you @verma8076 for the read: if hasattr(self.optimizer, "eval") and callable(self.optimizer.eval):
self.optimizer.eval()Re-verified today against I also gave the third test something to assert. It was the no-mode-support control and it only called Not stale from my side. Happy to rebase whenever a maintainer has a moment. |
SunMarc
left a comment
There was a problem hiding this comment.
A minor fix but worth merging i guess
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
|
@bot /style |
|
Style bot fixed some files and pushed the changes. |
What this fixes
AcceleratedOptimizer.train()has a branch for the case where DeepSpeed wraps the user optimizer one level deeper:eval()carries the same docstring but never got the matching branch, so under DeepSpeed a schedule-free optimizer is put into train mode bytrain()and never taken back out byeval(). Nothing raises; evaluation just silently runs with train-mode weights, which for a schedule-free optimizer means the wrong parameters.The history suggests this was an oversight rather than a deliberate asymmetry. #2631 introduced
train()/eval()as a mirrored pair, #3055 changed both together in one diff, and #3266 ("support for wrapped schedulefree optimizer when using deepspeed") added the unwrap totrain()only, with its patch hunk ending on the linedef eval(self):.The fix
Mirror the
train()branch ineval(), comment included, so the two stay symmetric.Tests
CPUOptimizerTesterhad notrain()/eval()coverage at all, so this adds three CPU tests covering the DeepSpeed-style double-wrapped optimizer, the plain schedule-free optimizer, and an optimizer with no train/eval support (which should stay a no-op).The double-wrap test is the regression test for this bug; it fails on
mainwith the optimizer still reportingtrainaftereval()and passes with the fix. The other two pass either way and are there so the unwrapped paths cannot regress. Reproduces on CPU with no GPU or DeepSpeed install needed.