Skip to content

Allocate optimizer state afresh on every rule invocation - #163

Merged
jessegrabowski merged 16 commits into
pymc-devs:mainfrom
jessegrabowski:rules-build-graphs
Sep 18, 2026
Merged

jessegrabowski merged 16 commits into
pymc-devs:mainfrom
jessegrabowski:rules-build-graphs

Conversation

@jessegrabowski

@jessegrabowski jessegrabowski commented Sep 18, 2026

Copy link
Copy Markdown
Member

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 a ContextVar, is deleted along with the registry behind it. Two training functions that share momentum are compiled from one dict, and compile_train accepts that dict in place of a rule.

Every learning_rate argument now accepts a schedule, in the *_updates functions and in scale as 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_schedule stays as the optax-named alias of scale.

skip_if now 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.

counter is removed in favor of step_counter. The other commits close gaps this contract exposed: a rule drops the gradients it does not descend, compile_train refuses a dict that leaves a collected parameter unwritten, nonfinite checks optimizer state and not only parameters, reduce_on_plateau refuses 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/

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.
@jessegrabowski
jessegrabowski merged commit 919d8fd into pymc-devs:main Sep 18, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configured update rules cannot be pickled: reuses_state wrapper captures the ContextVar

1 participant