Spaces:
Running on Zero
Running on Zero
Download cosmos_framework/utils/generator/optimizer.py from hugging-apps/greenwam: direct link, hf CLI and curl.
- Browser
- Download file 31.4 kB
-
https://huggingface.co/spaces/hugging-apps/greenwam/resolve/main/cosmos_framework/utils/generator/optimizer.py
- Command line
-
hf download hf://spaces/hugging-apps/greenwam/cosmos_framework/utils/generator/optimizer.py
-
curl -L -o optimizer.py https://huggingface.co/spaces/hugging-apps/greenwam/resolve/main/cosmos_framework/utils/generator/optimizer.py
31.4 kB
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: OpenMDW-1.1 | |
| import collections | |
| import copy | |
| from typing import Any, Iterator, NamedTuple | |
| import torch | |
| import torch.nn as nn | |
| from omegaconf import DictConfig, ListConfig, OmegaConf | |
| from torch.distributed.checkpoint.state_dict import StateDictOptions, get_optimizer_state_dict, set_optimizer_state_dict | |
| from torch.distributed.checkpoint.stateful import Stateful | |
| from torch.optim.lr_scheduler import LambdaLR, LRScheduler | |
| from cosmos_framework.utils.functional.lr_scheduler import LambdaLinearScheduler, LambdaWarmUpCosineScheduler, WSDScheduler | |
| from cosmos_framework.utils import log | |
| # Hybrid orthogonalizing optimizers (Muon / Dion2) that own their parameter | |
| # categorization and run their own collective communication. They must be built | |
| # as a single optimizer instance over all selected params (no per-device-mesh | |
| # split) and need ``categorize_params`` called after construction. | |
| _AUX_ADAMW_OPTIMIZERS = ("muonwithauxadamw", "dion2withauxadamw") | |
| class ParamMetadata(NamedTuple): | |
| lr: float | |
| enable_weight_decay: bool | |
| def _convert_omegaconf_to_python(obj: Any) -> Any: | |
| """Convert OmegaConf types to plain Python types. | |
| This is needed because PyTorch's checkpoint utilities don't handle | |
| OmegaConf types like ListConfig and DictConfig. | |
| """ | |
| if isinstance(obj, (ListConfig, DictConfig)): | |
| return OmegaConf.to_container(obj, resolve=True) | |
| return obj | |
| def _optimizer_cls( | |
| params: list[nn.Parameter] | list[dict[str, Any]], | |
| optimizer_type: str, | |
| **optimizer_kwargs: Any, | |
| ) -> torch.optim.Optimizer: | |
| """Dispatch to the requested optimizer class with the right kwargs. | |
| Supported ``optimizer_type`` values (case-insensitive): | |
| - ``"adam"`` / ``"adamw"``: forwarded to ``torch.optim.Adam`` / | |
| ``torch.optim.AdamW``. ``fused`` (if present in ``optimizer_kwargs``) | |
| flows through and selects the fused CUDA kernel. | |
| - ``"fusedadam"``: NVIDIA's :class:`cosmos_framework.utils.generator.fused_adam.FusedAdam`. | |
| It is fused by construction and rejects a ``fused`` kwarg, so any | |
| ``fused`` entry is popped before instantiation. We also force | |
| ``capturable=True`` and ``master_weights=True`` because those are the | |
| only settings exercised in our distributed training stack. | |
| Raises ``NotImplementedError`` for any other ``optimizer_type``. | |
| """ | |
| if optimizer_type.lower() == "adam": | |
| optimizer = torch.optim.Adam(params, **optimizer_kwargs) | |
| elif optimizer_type.lower() == "adamw": | |
| optimizer = torch.optim.AdamW(params, **optimizer_kwargs) | |
| elif optimizer_type.lower() == "fusedadam": | |
| from cosmos_framework.utils.generator.fused_adam import FusedAdam | |
| # FusedAdam is fused by construction and does not accept a ``fused`` kwarg. | |
| optimizer_kwargs.pop("fused", None) | |
| # Force ``capturable`` / ``master_weights`` on -- the only configuration | |
| # exercised in our distributed-training stack. Overwrite in-place | |
| # rather than passing as positional keywords, otherwise a caller that | |
| # also sets either flag would trigger a duplicate-kwarg ``TypeError``. | |
| optimizer_kwargs["capturable"] = True | |
| optimizer_kwargs["master_weights"] = True | |
| optimizer = FusedAdam(params, **optimizer_kwargs) | |
| elif optimizer_type.lower() == "muonwithauxadamw": | |
| from cosmos_framework.utils.generator.muon_with_aux_adamw import MuonWithAuxAdamW | |
| # Muon's AdamW side is the TE-fused kernel; it is fused by construction and | |
| # absorbs ``fused`` via **kwargs, but we pop it here to be explicit. We force | |
| # capturable + master_weights to match FusedAdam's mixed-precision setup. | |
| optimizer_kwargs.pop("fused", None) | |
| optimizer_kwargs["capturable"] = True | |
| optimizer_kwargs["master_weights"] = True | |
| optimizer = MuonWithAuxAdamW(params, **optimizer_kwargs) | |
| elif optimizer_type.lower() == "dion2withauxadamw": | |
| from cosmos_framework.utils.generator.dion2_with_aux_adamw import Dion2WithAuxAdamW | |
| optimizer_kwargs.pop("fused", None) | |
| optimizer_kwargs["capturable"] = True | |
| optimizer_kwargs["master_weights"] = True | |
| optimizer = Dion2WithAuxAdamW(params, **optimizer_kwargs) | |
| else: | |
| raise NotImplementedError(f"Optimizer {optimizer_type} not found.") | |
| return optimizer | |
| def _build_params_with_metadata( | |
| model: nn.Module, | |
| keys_to_select: list[str], | |
| lr_multipliers: dict[str, float], | |
| base_lr: float, | |
| disable_weight_decay_for_1d_params: bool, | |
| ) -> list[tuple[nn.Parameter, ParamMetadata]]: | |
| """Filter trainable parameters and tag each with its effective LR and weight-decay flag. | |
| Walks ``model.named_parameters()`` once and produces one | |
| ``(param, ParamMetadata)`` entry per parameter that survives the | |
| ``keys_to_select`` filter and belong to regular model. Has the side | |
| effect of freezing (``requires_grad=False``) every parameter that | |
| does NOT match ``keys_to_select`` when that list is non-empty. | |
| Selection rule: | |
| - If ``keys_to_select`` is empty, every parameter is kept. | |
| - Otherwise, a parameter is kept iff its dotted name contains at least | |
| one entry of ``keys_to_select`` as a substring. | |
| Effective LR: | |
| ``ParamMetadata.lr = base_lr * matched_multiplier`` where | |
| ``matched_multiplier`` is the value associated with the first | |
| ``lr_multipliers`` pattern whose key occurs as a substring in the | |
| parameter name (iteration order of the dict is significant); defaults | |
| to ``1.0`` if no pattern matches. | |
| Weight-decay flag: | |
| ``ParamMetadata.enable_weight_decay`` is ``False`` exactly when | |
| ``disable_weight_decay_for_1d_params`` is True AND the parameter has | |
| fewer than two dimensions (the standard heuristic for norm weights, | |
| biases, and other 1-D tensors). Otherwise it is ``True``. The flag is | |
| consumed downstream by :func:`_build_optimizer_internal`, which | |
| materializes the ``weight_decay=0.0`` override on the corresponding | |
| param group. | |
| Args: | |
| model: Module whose ``named_parameters()`` to scan. Only parameters | |
| under the ``net.`` subtree are considered; EMA-network parameters | |
| (``net_ema.*``) are intentionally skipped. | |
| keys_to_select: Substrings used to allowlist parameter names. Empty | |
| list = train everything. | |
| lr_multipliers: Ordered mapping from name-substring to LR multiplier. | |
| First match wins per parameter. | |
| base_lr: Base learning rate; each parameter's effective LR is | |
| ``base_lr * matched_multiplier``. | |
| disable_weight_decay_for_1d_params: When ``True``, 1-D parameters are | |
| tagged ``enable_weight_decay=False``. | |
| Returns: | |
| List of ``(nn.Parameter, ParamMetadata)`` pairs covering every kept | |
| parameter. Frozen parameters are not included. | |
| """ | |
| # Optimize only the regular ``net.`` subtree; the sibling ``net_ema`` | |
| # subtree is intentionally skipped. Materialize once so we can ``len()`` | |
| # the total below -- ``named_parameters()`` is a generator. | |
| net_params = dict(model.net.named_parameters()) | |
| param_dict = {pn: p for pn, p in net_params.items() if p.requires_grad} | |
| params_with_metadata: list[tuple[nn.Parameter, ParamMetadata]] = [] | |
| for pn, p in param_dict.items(): | |
| if len(keys_to_select) > 0 and not any(key in pn for key in keys_to_select): | |
| p.requires_grad = False | |
| continue | |
| # Find the matching multiplier for the parameter. | |
| matched_mult = 1.0 | |
| for pattern, mult in lr_multipliers.items(): | |
| if pattern in pn: | |
| matched_mult = mult | |
| break | |
| if disable_weight_decay_for_1d_params and p.dim() < 2: | |
| enable_weight_decay = False | |
| else: | |
| enable_weight_decay = True | |
| params_with_metadata.append( | |
| ( | |
| p, | |
| ParamMetadata( | |
| lr=base_lr * matched_mult, | |
| enable_weight_decay=enable_weight_decay, | |
| ), | |
| ) | |
| ) | |
| log.info( | |
| f"Total tensors: {len(net_params)}, " | |
| f"trainable tensors: {len(param_dict)}, " | |
| f"selected tensors: {len(params_with_metadata)}" | |
| ) | |
| return params_with_metadata | |
| def _build_optimizer_internal( | |
| params_with_metadata: list[tuple[nn.Parameter, ParamMetadata]], | |
| optimizer_type: str, | |
| **optimizer_kwargs: Any, | |
| ) -> torch.optim.Optimizer: | |
| """Bucket per-parameter metadata into PyTorch param groups and instantiate the optimizer. | |
| Parameters sharing the same ``ParamMetadata`` (i.e. identical effective LR | |
| *and* identical weight-decay enablement) are collapsed into a single | |
| ``torch.optim`` param group. Each resulting group carries an explicit | |
| ``"lr"``; groups whose metadata has ``enable_weight_decay=False`` also | |
| receive an explicit ``"weight_decay": 0.0`` override that suppresses the | |
| optimizer-wide ``weight_decay`` kwarg for those parameters only. | |
| Args: | |
| params_with_metadata: Output of :func:`_build_params_with_metadata` — | |
| one ``(param, ParamMetadata)`` entry per trainable parameter. | |
| optimizer_type: Optimizer kind string accepted by :func:`_optimizer_cls` | |
| (``"adam"``, ``"adamw"``, or ``"fusedadam"``). | |
| **optimizer_kwargs: Forwarded to the optimizer constructor. ``"lr"`` is | |
| ignored at the group level (each group sets its own LR) but must | |
| still be present because PyTorch optimizers require a default. | |
| Returns: | |
| A ``torch.optim.Optimizer`` configured with one param group per distinct | |
| ``ParamMetadata`` value found in ``params_with_metadata``. | |
| """ | |
| params_by_metadata: dict[ParamMetadata, list[nn.Parameter]] = collections.defaultdict(list) | |
| for param, metadata in params_with_metadata: | |
| params_by_metadata[metadata].append(param) | |
| param_groups: list[dict[str, Any]] = [] | |
| for metadata, params in sorted(params_by_metadata.items()): | |
| log.info( | |
| f"Param group (lr={metadata.lr}, WD={metadata.enable_weight_decay}): " | |
| f"{len(params):,} tensors, {sum(p.numel() for p in params):,} elements" | |
| ) | |
| param_group: dict[str, Any] = {"params": params, "lr": metadata.lr} | |
| if not metadata.enable_weight_decay: | |
| param_group["weight_decay"] = 0.0 | |
| param_groups.append(param_group) | |
| return _optimizer_cls(param_groups, optimizer_type, **optimizer_kwargs) | |
| class OptimizersContainer(Stateful): | |
| """ | |
| Utility for calling step/zero_grad on multiple optimizers, and | |
| saving/loading optimizer state_dict at checkpoint. | |
| """ | |
| def __init__( | |
| self, | |
| model: nn.Module, | |
| optimizer_type: str, | |
| **optimizer_kwargs: Any, | |
| ) -> None: | |
| """Build per-device-mesh optimizers and expose them as a single Stateful container. | |
| Parameters are first filtered + tagged with metadata via | |
| :func:`_build_params_with_metadata` and then sub-grouped by | |
| ``param.device_mesh.mesh_dim_names`` (or ``"default"`` when no mesh is | |
| attached). One ``torch.optim`` instance is created per resulting mesh | |
| bucket so fused kernels see only same-mesh DTensors — required for | |
| FSDP2-aware optimizer state-dict resharding. | |
| Args: | |
| model: Model to build the optimizer(s) for. ``model.parameters()`` | |
| provides the parameter universe and is also passed through to | |
| ``get_optimizer_state_dict`` / ``set_optimizer_state_dict`` for | |
| checkpointing. | |
| optimizer_type: Optimizer kind string accepted by | |
| :func:`_optimizer_cls` (``"adam"``, ``"adamw"``, ``"fusedadam"``). | |
| **optimizer_kwargs: Forwarded to the optimizer constructor, with the | |
| following container-level kwargs intercepted first: | |
| - ``fused`` (required): Must be truthy. Forwarded to | |
| ``torch.optim.AdamW`` / ``Adam`` so they use the fused CUDA | |
| kernel; ignored for ``"fusedadam"`` which is fused by | |
| construction. Eager optimizers are not supported. | |
| - ``lr`` (required): Base learning rate. Each param group's | |
| effective LR is ``lr * matched_multiplier``; ``lr`` is still | |
| required because PyTorch optimizers expect a default. | |
| - ``keys_to_select`` (optional, default ``[]``): List of | |
| substrings; only params whose names contain at least one are | |
| trained. Empty list = train every parameter. | |
| - ``lr_multipliers`` (optional, default ``{}``): Ordered dict | |
| mapping param-name substrings to LR multipliers. First match | |
| wins; unmatched selected params use multiplier ``1.0``. | |
| - ``disable_weight_decay_for_1d_params`` (optional, default | |
| ``False``): When true, parameters with ``dim() < 2`` (norm | |
| weights, biases, etc.) get ``weight_decay=0.0`` in their | |
| param group regardless of the optimizer-wide ``weight_decay``. | |
| """ | |
| self.model = model | |
| self.optimizers: list[torch.optim.Optimizer] = [] | |
| # Pop only the container-level kwargs. ``fused`` and ``lr`` are | |
| # intentionally NOT popped: ``fused`` must flow through to | |
| # ``torch.optim.AdamW`` / ``Adam`` so the fused CUDA kernel is actually | |
| # selected, and ``lr`` is needed both here (as the base for per-group | |
| # multipliers) and by the underlying optimizer (as the group-level | |
| # default). | |
| keys_to_select = optimizer_kwargs.pop("keys_to_select", []) | |
| lr_multipliers: dict[str, float] = optimizer_kwargs.pop("lr_multipliers", {}) | |
| disable_weight_decay_for_1d_params = optimizer_kwargs.pop("disable_weight_decay_for_1d_params", False) | |
| if not optimizer_kwargs.get("fused", False): | |
| raise ValueError("Optimizers with fused=False are not supported; pass fused=True in optimizer_kwargs.") | |
| if "lr" not in optimizer_kwargs: | |
| raise ValueError("`lr` is required in optimizer_kwargs (used as the base for per-group LR multipliers).") | |
| base_lr = optimizer_kwargs["lr"] | |
| params_with_metadata = _build_params_with_metadata( | |
| model, | |
| keys_to_select=keys_to_select, | |
| lr_multipliers=lr_multipliers, | |
| base_lr=base_lr, | |
| disable_weight_decay_for_1d_params=disable_weight_decay_for_1d_params, | |
| ) | |
| if optimizer_type.lower() in _AUX_ADAMW_OPTIMIZERS: | |
| # Muon / Dion2 categorize parameters into an orthogonalized-matrix group | |
| # and an auxiliary AdamW group internally, and run their own collective | |
| # communication across the FULL device mesh. They must therefore be a | |
| # single optimizer instance over all selected params (no per-mesh split). | |
| # | |
| # The factory's param grouping is preserved: ``_build_optimizer_internal`` | |
| # still produces the same (lr, weight_decay) param groups (from | |
| # ``lr_multipliers`` / ``disable_weight_decay_for_1d_params``), and | |
| # Muon/Dion2 read lr/weight_decay per group — degenerating to a single | |
| # global lr/wd when there is only one group (the reference behavior). | |
| optimizer = _build_optimizer_internal( | |
| params_with_metadata, | |
| optimizer_type, | |
| **optimizer_kwargs, | |
| ) | |
| # Categorize against the trainable network only, mirroring | |
| # ``_build_params_with_metadata`` which optimizes the ``net`` subtree | |
| # and skips ``net_ema``. | |
| optimizer.categorize_params(model.net) | |
| self.optimizers.append(optimizer) | |
| else: | |
| # Sub-group by device mesh so fused optimizers operate on same-mesh params. | |
| mesh_groups: dict[str, list[tuple[nn.Parameter, ParamMetadata]]] = collections.defaultdict(list) | |
| for param, metadata in params_with_metadata: | |
| if hasattr(param, "device_mesh"): | |
| # ``mesh_dim_names`` is ``tuple[str, ...] | None`` on DeviceMesh — | |
| # fall back to ``default`` when names weren't assigned. | |
| names = param.device_mesh.mesh_dim_names | |
| mesh_key = "-".join(names) if names else "default" | |
| else: | |
| mesh_key = "default" | |
| mesh_groups[mesh_key].append((param, metadata)) | |
| # Create one optimizer per mesh, each with per-LR,weight-decay param groups. | |
| for mesh_key, mesh_params in mesh_groups.items(): | |
| log.info(f"Building optimizer for mesh '{mesh_key}'") | |
| optimizer = _build_optimizer_internal( | |
| mesh_params, | |
| optimizer_type, | |
| **optimizer_kwargs, | |
| ) | |
| self.optimizers.append(optimizer) | |
| log.info(f"Created {len(self.optimizers)} optimizers") | |
| def __iter__(self) -> Iterator[torch.optim.Optimizer]: | |
| return iter(self.optimizers) | |
| def __len__(self) -> int: | |
| return len(self.optimizers) | |
| def step(self) -> None: | |
| # NOTE: This container does not forward a ``torch.amp.GradScaler`` to the | |
| # inner optimizers, and training here is bf16 with the scaler disabled | |
| # (``grad_scaler_args={"enabled": False}``), so step() always runs unscaled. | |
| # MuonWithAuxAdamW / Dion2WithAuxAdamW are intentionally bf16-only (their | |
| # AMP grad-scaler support was removed to keep the Newton-Schulz path simple); | |
| # only FusedAdam still carries a scaler hook. Enabling fp16 + dynamic loss | |
| # scaling end-to-end would require re-adding scaler passthrough here and in | |
| # the Muon/Dion2 step paths, which is deliberately not supported. | |
| for optimizer in self.optimizers: | |
| optimizer.step() | |
| def zero_grad(self, set_to_none: bool = True) -> None: | |
| # Default matches ``torch.optim.Optimizer.zero_grad`` (set_to_none=True | |
| # since PyTorch 1.7); zero-in-place is strictly slower and uses extra | |
| # memory, so callers must opt-in explicitly. | |
| for optimizer in self.optimizers: | |
| optimizer.zero_grad(set_to_none=set_to_none) | |
| def state_dict(self) -> dict[str, Any]: | |
| return get_optimizer_state_dict( | |
| model=self.model, | |
| optimizers=self.optimizers, | |
| options=StateDictOptions(flatten_optimizer_state_dict=True), | |
| ) | |
| def load_state_dict(self, state_dict: dict[str, Any]) -> None: | |
| set_optimizer_state_dict( | |
| model=self.model, | |
| optimizers=self.optimizers, | |
| optim_state_dict=state_dict, | |
| options=StateDictOptions(flatten_optimizer_state_dict=True), | |
| ) | |
| def build_optimizer( | |
| model: nn.Module, | |
| optimizer_type: str, | |
| **optimizer_kwargs: Any, | |
| ) -> "OptimizersContainer": | |
| """Build an :class:`OptimizersContainer` for a model. | |
| Returns the container — NOT a raw ``torch.optim.Optimizer`` — because the | |
| underlying implementation creates one optimizer per device mesh. Callers | |
| that need PyTorch ``param_groups`` must iterate ``container.optimizers``. | |
| Before instantiation, every kwarg is normalized via | |
| :func:`_convert_omegaconf_to_python` so that OmegaConf containers | |
| (``ListConfig`` / ``DictConfig``) become plain Python types — PyTorch's | |
| checkpoint utilities ``_copy_state_dict`` does not handle OmegaConf types. | |
| Args: | |
| model: The model to build an optimizer for. Trainable-parameter | |
| selection happens inside the container; parameters not matched by | |
| ``keys_to_select`` (when non-empty) are frozen as a side effect. | |
| optimizer_type: Optimizer kind string accepted by :func:`_optimizer_cls` | |
| (``"adam"``, ``"adamw"``, or ``"fusedadam"``). | |
| **optimizer_kwargs: Forwarded to :class:`OptimizersContainer`, which | |
| intercepts a few container-level kwargs and forwards the rest to | |
| the underlying ``torch.optim`` constructor. Container-level kwargs: | |
| - ``fused`` (required, truthy): Use the fused CUDA kernel. Eager | |
| optimizers are intentionally not supported. | |
| - ``lr`` (required): Base learning rate. Each param group's | |
| effective LR is ``lr * matched_multiplier``. | |
| - ``keys_to_select`` (optional, default ``[]``): Substrings used to | |
| allowlist parameter names; empty list = train everything. | |
| - ``lr_multipliers`` (optional, default ``{}``): Ordered dict | |
| mapping parameter-name substrings to LR multipliers. E.g. | |
| ``{"sound2llm": 5.0, "llm2sound": 5.0}`` gives those params 5x | |
| the base LR. First match wins; unmatched selected params use | |
| multiplier ``1.0``. | |
| - ``disable_weight_decay_for_1d_params`` (optional, default | |
| ``False``): If true, one-dimensional parameters such as norm | |
| weights and biases get ``weight_decay=0.0`` in their param group | |
| regardless of the optimizer-wide ``weight_decay``. | |
| Returns: | |
| An :class:`OptimizersContainer` wrapping one ``torch.optim.Optimizer`` | |
| per device mesh discovered on the model's parameters. | |
| """ | |
| # Convert OmegaConf types to plain Python types to avoid issues with checkpoint saving. | |
| # PyTorch's _copy_state_dict doesn't handle OmegaConf types like ListConfig. | |
| optimizer_kwargs = {k: _convert_omegaconf_to_python(v) for k, v in optimizer_kwargs.items()} | |
| return OptimizersContainer(model, optimizer_type, **optimizer_kwargs) | |
| def _lr_scheduler_cls( | |
| lr_scheduler_type: str, | |
| **lr_scheduler_kwargs: Any, | |
| ) -> LambdaLinearScheduler | LambdaWarmUpCosineScheduler | WSDScheduler: | |
| """Instantiate a lambda-style scheduler whose ``.schedule(step)`` returns an LR multiplier. | |
| Both returned classes expose a ``schedule(step) -> float`` callable that | |
| :class:`LRSchedulersContainer` wraps with ``torch.optim.lr_scheduler.LambdaLR`` | |
| to drive each optimizer's param-group LRs. ``lr_scheduler_type`` matching is | |
| case-insensitive; valid values are ``"lambdalinear"`` (linear decay), | |
| ``"lambdacosine"`` (warmup + cosine decay), and ``"wsd"`` | |
| (warmup-stable-decay). Any other value raises ``NotImplementedError``. | |
| All remaining ``**lr_scheduler_kwargs`` are forwarded verbatim to the | |
| underlying scheduler constructor (e.g. ``warm_up_steps``, ``cycle_lengths``, | |
| ``total_steps``, ``decay_steps``, ``f_start``, ``f_max``, ``f_min``, | |
| ``verbosity_interval``). | |
| """ | |
| if lr_scheduler_type.lower() == "lambdalinear": | |
| lr_scheduler = LambdaLinearScheduler(**lr_scheduler_kwargs) | |
| elif lr_scheduler_type.lower() == "lambdacosine": | |
| lr_scheduler = LambdaWarmUpCosineScheduler(**lr_scheduler_kwargs) | |
| elif lr_scheduler_type.lower() == "wsd": | |
| lr_scheduler = WSDScheduler(**lr_scheduler_kwargs) | |
| else: | |
| raise NotImplementedError(f"LR Scheduler {lr_scheduler_type} not found.") | |
| return lr_scheduler | |
| class LRSchedulersContainer(Stateful): | |
| """Wraps one ``torch.optim.lr_scheduler.LambdaLR`` per inner optimizer. | |
| Mirrors the structure of :class:`OptimizersContainer`: one scheduler is | |
| created per element of an ``OptimizersContainer`` (i.e. one per device | |
| mesh), and ``step()`` / ``state_dict()`` / ``load_state_dict()`` fan out | |
| over them. All schedulers share the same underlying lambda | |
| (``lr_scheduler.schedule``), so they advance in lock-step but multiply each | |
| of their optimizer's per-group ``base_lr`` values independently. | |
| Note: | |
| Subclasses must keep the ``step``, ``state_dict``, and | |
| ``load_state_dict`` signatures compatible with | |
| ``torch.optim.lr_scheduler.LRScheduler`` so that downstream training | |
| and checkpoint code can treat the container as a drop-in scheduler. | |
| Checkpoint format: | |
| ``LambdaLR.state_dict`` contains both scalar fields (``last_epoch``, | |
| ``_step_count``, ``verbose``, ``_get_lr_called_within_step`` — all | |
| identical across schedulers because they step in lock-step) and | |
| per-param-group lists (``base_lrs``, ``_last_lr``, ``lr_lambdas`` — | |
| whose length and values depend on the underlying optimizer's | |
| ``param_groups``). | |
| When ``len(self.schedulers) == 1`` (the common single-mesh case), | |
| :meth:`state_dict` returns scheduler-0's dict verbatim. This matches | |
| the historical on-disk shape, so single-scheduler checkpoints written | |
| by earlier versions of this class continue to round-trip. | |
| When ``len(self.schedulers) > 1`` (heterogeneous mesh case), the | |
| per-scheduler list fields differ in length and content across | |
| schedulers. :meth:`state_dict` augments scheduler-0's dict with a | |
| ``"schedulers"`` entry — a ``list[dict]`` of per-scheduler | |
| ``LambdaLR.state_dict`` values — so each scheduler's | |
| ``base_lrs`` / ``_last_lr`` / ``lr_lambdas`` are persisted and | |
| restored without corruption. | |
| Args: | |
| optimizers: The optimizer container to drive. Each inner optimizer | |
| gets its own ``LambdaLR`` instance sharing the same schedule | |
| lambda. | |
| """ | |
| schedulers: list[LRScheduler] | |
| def __init__( | |
| self, | |
| optimizers: OptimizersContainer, | |
| lr_scheduler_type: str, | |
| **lr_scheduler_kwargs: Any, | |
| ) -> None: | |
| if len(optimizers) == 0: | |
| raise ValueError("Must have at least one optimizer to create LRScheduler") | |
| lr_scheduler = _lr_scheduler_cls(lr_scheduler_type, **lr_scheduler_kwargs) | |
| self.schedulers = [LambdaLR(optimizer, lr_scheduler.schedule) for optimizer in optimizers] | |
| log.info(f"Created {len(self.schedulers)} schedulers") | |
| def __iter__(self) -> Iterator[LRScheduler]: | |
| return iter(self.schedulers) | |
| def __len__(self) -> int: | |
| return len(self.schedulers) | |
| def step(self) -> None: | |
| for scheduler in self.schedulers: | |
| scheduler.step() | |
| def state_dict(self) -> dict[str, Any]: | |
| # Single-scheduler: return the legacy shape verbatim so existing | |
| # on-disk checkpoints continue to round-trip with strict_resume=True. | |
| # Multi-scheduler: also persist per-scheduler state so heterogeneous | |
| # param-group structures (different base_lrs / _last_lr lengths) are | |
| # captured without corrupting one scheduler with another's data. | |
| state_dict = self.schedulers[0].state_dict() | |
| if len(self.schedulers) > 1: | |
| state_dict["schedulers"] = [scheduler.state_dict() for scheduler in self.schedulers] | |
| return state_dict | |
| def load_state_dict(self, state_dict: dict[str, Any]) -> None: | |
| # Inverse of ``state_dict``: presence of ``"schedulers"`` implies the | |
| # multi-scheduler shape and *exactly* matches ``len(self.schedulers)``; | |
| # absence implies the single-scheduler shape. ``state_dict`` is always | |
| # produced by ``self.state_dict()`` on either the same or a compatible | |
| # container, so a mismatch is a real config drift, not a recoverable | |
| # path — raise with an actionable message. | |
| per_scheduler_states = state_dict.get("schedulers") | |
| if per_scheduler_states is None: | |
| if len(self.schedulers) != 1: | |
| raise ValueError( | |
| f"LRSchedulersContainer.load_state_dict: checkpoint holds a single " | |
| f"scheduler's state, but this container has {len(self.schedulers)}. " | |
| "Resume from a checkpoint saved with a matching number of schedulers, " | |
| "or add the scheduler to `checkpoint.keys_not_to_resume` to start " | |
| "scheduler state fresh." | |
| ) | |
| self.schedulers[0].load_state_dict(copy.deepcopy(state_dict)) | |
| return | |
| if len(per_scheduler_states) != len(self.schedulers): | |
| raise ValueError( | |
| f"LRSchedulersContainer.load_state_dict: checkpoint has state for " | |
| f"{len(per_scheduler_states)} schedulers, but this container has " | |
| f"{len(self.schedulers)}. Resume from a checkpoint with a matching number " | |
| "of schedulers, or add the scheduler to `checkpoint.keys_not_to_resume` to " | |
| "start scheduler state fresh." | |
| ) | |
| for scheduler, sub_state in zip(self.schedulers, per_scheduler_states): | |
| # Deepcopy so nested mutable values (lists) are not aliased across | |
| # schedulers after ``LambdaLR.__dict__.update``. | |
| scheduler.load_state_dict(copy.deepcopy(sub_state)) | |
| def get_last_lr(self) -> list[float | torch.Tensor]: | |
| # Concatenate the per-optimizer ``get_last_lr()`` lists. Each | |
| # underlying scheduler reports one entry per param group, and | |
| # different optimizers (one per device mesh) may carry distinct | |
| # ``base_lr`` values via ``lr_multipliers``, so the full snapshot | |
| # requires aggregating across schedulers. | |
| return [lr for scheduler in self.schedulers for lr in scheduler.get_last_lr()] | |
| def get_lr(self) -> list[float | torch.Tensor]: | |
| # Same aggregation as ``get_last_lr``; see comment there. | |
| return [lr for scheduler in self.schedulers for lr in scheduler.get_lr()] | |
| def build_lr_scheduler( | |
| optimizer: OptimizersContainer, | |
| lr_scheduler_type: str, | |
| **lr_scheduler_kwargs: Any, | |
| ) -> LRSchedulersContainer: | |
| """Create an :class:`LRSchedulersContainer` for the given optimizer container. | |
| Thin factory used by Hydra/LazyConfig scheduler registrations (see | |
| ``cosmos_framework/configs/base/defaults/optimizer.py``). At | |
| instantiation time, ``optimizer`` is supplied by the model's | |
| ``init_optimizer_scheduler`` hook via | |
| ``lazy_instantiate(scheduler_config, optimizer=optimizer)``. | |
| Note: | |
| Callers who need different scheduler behavior can subclass | |
| :class:`LRSchedulersContainer` and write their own factory; pointing | |
| the Hydra ``_target_`` at that factory swaps it in. | |
| Args: | |
| optimizer: The optimizer container the scheduler should drive. One | |
| :class:`torch.optim.lr_scheduler.LambdaLR` will be built per | |
| element of this container. | |
| lr_scheduler_type: Scheduler kind accepted by :func:`_lr_scheduler_cls` | |
| — ``"lambdalinear"``, ``"lambdacosine"``, or ``"wsd"`` (case-insensitive). | |
| **lr_scheduler_kwargs: Forwarded verbatim to the underlying lambda | |
| scheduler constructor (e.g. ``warm_up_steps``, ``cycle_lengths``, | |
| ``f_start``, ``f_max``, ``f_min``, ``verbosity_interval``). | |
| Returns: | |
| An :class:`LRSchedulersContainer` wrapping one ``LambdaLR`` per inner | |
| optimizer. | |
| """ | |
| return LRSchedulersContainer(optimizer, lr_scheduler_type, **lr_scheduler_kwargs) | |