Allocate optimizer state afresh on every rule invocation - #163
Merged
jessegrabowski merged 16 commits intoSep 18, 2026
Merged
Conversation
A rule is a graph builder, and the updates dict it returns is the state-bearing object, so two functions that share state are compiled from one dict. The memoizing scope this replaces closed over a ContextVar, which is why a configured rule could not be pickled. Closes pymc-devs#160.
A rule's clock counts the updates it applied, and a skipped step was not one: a count that advanced anyway bias-corrects the next step against a moment that never saw it. A caller-held clock is not the rule's state and keeps counting calls.
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.
A rule such as
adam(1e-3)now allocates fresh optimizer state on every invocation, and the updates dict it returns is the only state-bearing object.reuses_state, the decorator that memoized a rule's buffers across invocations through aContextVar, is deleted along with the registry behind it. Two training functions that share momentum are compiled from one dict, andcompile_trainaccepts that dict in place of a rule.Every
learning_rateargument now accepts a schedule, in the*_updatesfunctions and inscaleas well as the aliases. A rule given a schedule reads it off the clock it already counts its own steps on and writes that clock into its updates, so a scheduled rule holds one clock and a checkpoint taken from the dict resumes the schedule where it left off.scale_by_schedulestays as the optax-named alias ofscale.skip_ifnow holds a rule's clock back through a skipped step along with the rest of the rule's state. A clock that advanced anyway bias-corrected the next step against a moment that never saw it, which damped adam's first step after a skip by about half. A clock the caller holds and passes into a schedule is not the rule's state and keeps counting.counteris removed in favor ofstep_counter. The other commits close gaps this contract exposed: a rule drops the gradients it does not descend,compile_trainrefuses a dict that leaves a collected parameter unwritten,nonfinitechecks optimizer state and not only parameters,reduce_on_plateaurefuses a scale no step reads, and guard and policy counters are int64.Closes #160
📚 Documentation preview 📚: https://pytensor-ml--163.org.readthedocs.build/en/163/