Stop Optim.step(scale_by) mutating the caller's gradients - #92
Merged
Conversation
'set(g, g * scale_by)' wrote the scaled value back into the caller's gradient Params, so reusing a gradient tree compounded the scaling. Return a scaled copy instead.
cemde
force-pushed
the
fix/67-optim-scale-by
branch
from
August 9, 2026 18:23
51f8d8f to
c1ce300
Compare
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.
Bug
Optim.step(scale_by=k)scaled each gradient withset(g, g * scale_by).setwrites the new value into theParamobject it is given, and that object is the caller's, not a copy. So the call mutated the gradient tree the caller still holds.Impact
Stepping the same gradient tree twice compounds the scaling. With
lr=1.0,scale_by=0.5,g=1.0,w0=1.0, two steps must give1 - 2*(1.0*0.5*1.0) = 0.0; PCX gives0.25, because the gradient is 0.5 on the second step and 0.25 on the third.Every tutorial hits this: they compute one gradient tree and feed it to both
optim_handoptim_wwithscale_by=1.0/batch_size. The effective learning rate shrinks geometrically with nothing to show for it but slow convergence.The same mechanism also compounds within a single step on a
pxnn.sharedmodel, since the outertree_mapvisits an aliasedParamonce per reference.Fix
tree_mapdescends into theParam(it is a registered pytree), scales the value, and rebuilds a newParamvia the registered unflatten. The caller's object is untouched.Why
tree_mapand nottype(g)(g * scale_by)The registered unflatten copies
__dict__minus_value, so the subclass and all its attributes survive. Constructing through__init__does not: a rebuiltVodeParamwould losefrozen, the attribute the tutorials set on the output node and thatM_hasnot(VodeParam, frozen=True)reads. A mask over the scaled tree would then select the wrong parameters.OptimTree.stepdelegates here, so it inherits the fix.Closes #67