[Fix] Keep SignSGD momentum buffer across steps - #516
Merged
Conversation
`init_group()` runs on every `step()` and reassigned `state['momentum_buffer']` to zeros, so the buffer never persisted. With `momentum > 0` the update reduced to `sign((1 - momentum) * grad)`, which equals `sign(grad)`, making Signum behave as plain SignSGD and leaving `momentum` with no effect. Initialise the buffer only when it is absent, matching `SGDW` and `AccSGD`.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #516 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 128 128
Lines 12442 12442
=========================================
Hits 12442 12442
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
kozistr
approved these changes
Aug 23, 2026
kozistr
left a comment
Owner
There was a problem hiding this comment.
Hi! Thanks for your contribution. looks good to me!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem (Why?)
SignSGD.init_group()is called fromstep()on every optimizer step, and whenmomentum > 0itreassigns
state['momentum_buffer'] = torch.zeros_like(p)unconditionally. The buffer is zeroedbefore every update, so it never carries state from one step to the next.
Since the update is
sign(buf)andbufcollapses to(1 - momentum) * grad, the positive scalefactor is discarded by
sign. Signum therefore behaves as plain SignSGD, andmomentumhas noeffect on training.
momentum=0.9is the constructor default, soSignSGD(params, lr=...)isaffected.
A four-value check on a small network, 50 steps each, gives identical loss trajectories and zero
parameter difference:
With a constant gradient the buffer stays at
(1 - momentum) * gradinstead of accumulating, and atan exactly zero gradient the optimizer makes no update where Signum would still step along the
carried moment.
Solution (What/How?)
Initialise the buffer only when it is absent.
SGDW(sgd.py:183) andAccSGD(sgd.py:71) inthe same module already use the
if len(state) == 0pattern.Other changes (bug fixes, small refactors)
Added
test_sign_sgd_preserves_momentum_buffer, parametrised over bothforeachpaths. After twosteps with gradients
1.0then-0.1andmomentum=0.9, the buffer should be0.9 * 0.1 + 0.1 * (-0.1) = 0.08. Onmainit is-0.01.Notes
While looking at this I also noticed that
weight_decayandweight_decoupleare validated andstored in
defaultsbut never applied in eitherSignSGDstep path. I have left that out of thisPR because fixing it changes behaviour for anyone currently passing a non-zero
weight_decay, whichseems like your call rather than mine. I opened a separate issue for it and am happy to send a patch
if you want one.
SGDSaIhas the same unguarded initialisation, though it may be intentional there since its stateis set up during warmup. I have not touched it.
Checklist
just formatbefore commitjust checkshows no errors)just testruff checkpasses on both changed files. I did not runpyright, so please flag anything itcatches. The docs box is unticked because I do not think this needs a docs change, but say if it
does.
Test suite before the change: 2486 passed, 2 failed, 433 skipped. After: 2488 passed, 2 failed, 433
skipped. The two failures are
test_complex_optimizers[FTRL_...]andtest_parse_version; bothalso fail on an unmodified checkout of
mainand are unrelated to this change.