Amuse optimizer - #1830
Conversation
polycrit
commented
Aug 3, 2026
- Added an implementation of AMUSE optimizer (mirrored from authors' github repository and adapted for RETURNN as returnn.torch.optim.amuse)
- Strip callables from the saved optimizer_opts metadata in updater.py so optimizer checkpoints are loadable under the torch >= 2.6 weights_only default
e41c0a2 to
ce30c8a
Compare
|
So currently we don't even have the much more popular Muon optimizer in RETURNN. I don't think adding AMUSE makes sense when we don't have Muon. But maybe we should add both then. See the failing code style errors also: |
was also thinking about this, makes total sense, I'll add it as well |
|
Re Muon, I always use |
I just found oud there's an already implemented Muon from pytorch (https://docs.pytorch.org/docs/2.13/generated/torch.optim.Muon.html). So I think maybe we don't need it separately implemented after all? Just a small adapter. |
|
Ah Muon seems to exist since PyTorch 2.10. Hm so good question. Maybe you are right. |
The problem is that in RETURNN you can't do something like: RETURNN’s PyTorch updater currently manages one optimizer object. So what we could implement there is a compact Muon/AdamW adapter that presents Muon and AdamW as that single object. |
|
Doesn't the Torch Muon already handle this automatically? At least |
|
Btw, I think it was discussed maybe in the past whether we somehow should have a possibility to use multiple optimizers for different params. Not sure? |
As far I understand, it doesn't, you have to explicitly call two separate optimizers. |
Can't remember this, but I think it's generally a very good idea, given we want to experiment with such multi-level optimizers. I will look into it. |
|
@albertz I imagine we can create an interface that does something like this: or:
similar to RETURNN's existing |
|
Yea looks good. Feel free to implement that. |
…er-sub weight decay split
There was a problem hiding this comment.
Pull request overview
This PR adds two new Torch optimizers to RETURNN—AMUSE (schedule-free) and MultiOptimizer (composite optimizer over disjoint parameter subsets)—and updates optimizer checkpoint metadata handling so checkpoints remain loadable with PyTorch’s weights_only=True default (>= 2.6). It also wires schedule-free optimizer train()/eval() switching into the Torch engine epoch boundaries and adds extensive tests + documentation.
Changes:
- Add
returnn.torch.optim.amuse.AMUSEandreturnn.torch.optim.multi.MultiOptimizer(plus helper filter) for mixed-optimizer setups. - Extend optimizer resolution + updater logic (multi-optimizer construction, param-group splitting on subsets, schedule-free mode switching, callable stripping in saved metadata).
- Add engine hooks, tests, and docs for the new optimizers and config interface.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_torch_engine.py | Adds coverage for multi-optimizer behavior, schedule-free hooks, AMUSE integration, and save/load contracts. |
| returnn/util/basic.py | Extends collect_class_init_kwargs to include keyword-only args/defaults. |
| returnn/torch/updater.py | Adds short-name resolution for shipped optimizers; implements multi optimizer construction; adds schedule-free mode switching; strips callables from saved optimizer metadata. |
| returnn/torch/optim/README.md | Documents short-name referencing for shipped optimizers. |
| returnn/torch/optim/multi.py | Introduces MultiOptimizer composite optimizer and a picklable hidden-matrix filter helper. |
| returnn/torch/optim/amuse.py | Adds AMUSE schedule-free optimizer implementation with selectable inner update type. |
| returnn/torch/engine.py | Switches schedule-free optimizers to train/eval modes at train epoch boundaries. |
| docs/configuration_reference/optimizer_settings.rst | Documents optimizer: {class: "multi", ...} in the config reference. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| optimizer_opts_to_save = self._optimizer_opts | ||
| if isinstance(optimizer_opts_to_save, dict): | ||
| optimizer_opts_to_save = _drop_callables_deep(optimizer_opts_to_save) |
| def __init__(self, sub_optimizers: Sequence[torch.optim.Optimizer]): | ||
| self._sub_optimizers = list(sub_optimizers) | ||
|
|
||
| def _owning_sub(self, param: torch.nn.Parameter) -> Optional[torch.optim.Optimizer]: | ||
| for sub in self._sub_optimizers: | ||
| for group in sub.param_groups: | ||
| for other in group["params"]: | ||
| if other is param: | ||
| return sub | ||
| return None |
| c_warmup = group.get("c_warmup", 1.0 / self.warmup_steps) | ||
| s_t = (ckp1 * (1.0 - c_warmup)) / (c_warmup * (1.0 - ckp1)) | ||
| return 1.0 - (s_t**self.rho) * (1.0 - self.beta1_init) |