Spaces:
Running on Zero
Running on Zero
Download cosmos_framework/utils/reasoner/optimizer.py from hugging-apps/greenwam: direct link, hf CLI and curl.
- Browser
- Download file 13 kB
-
https://huggingface.co/spaces/hugging-apps/greenwam/resolve/main/cosmos_framework/utils/reasoner/optimizer.py
- Command line
-
hf download hf://spaces/hugging-apps/greenwam/cosmos_framework/utils/reasoner/optimizer.py
-
curl -L -o optimizer.py https://huggingface.co/spaces/hugging-apps/greenwam/resolve/main/cosmos_framework/utils/reasoner/optimizer.py
13 kB
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: OpenMDW-1.1 | |
| import collections | |
| import functools | |
| import itertools | |
| import math | |
| from copy import deepcopy | |
| from typing import Any, Optional | |
| import attrs | |
| import torch | |
| import torch.nn as nn | |
| 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 | |
| from cosmos_framework.utils.config import make_freezable | |
| from cosmos_framework.utils.lazy_config import LazyDict | |
| from cosmos_framework.utils import log | |
| from cosmos_framework.utils.reasoner.fused_adam import FusedAdam | |
| class OptimizerConfig: | |
| name: str = "FusedAdam" | |
| lr: float = 2e-6 | |
| init_lr: float = 1e-7 | |
| end_lr: float = 1e-6 | |
| fused: bool = False | |
| weight_decay: float = 0.1 | |
| betas: tuple[float, float] = (0.9, 0.95) | |
| lr_multiplier: LazyDict = LazyDict(dict(vision_encoder=0.1, mm_projector=1.0, llm=1.0)) | |
| # model freeze config | |
| freeze_vision_encoder: bool = False | |
| freeze_mm_projector: bool = False | |
| freeze_llm: bool = False | |
| freeze_llm_moe_gates: bool = False | |
| def _optimizer_cls(params: list[nn.Parameter], optimizer_kwargs: dict[str, Any], name: str): | |
| if name.lower() == "adam": | |
| optimizer = torch.optim.Adam(params, **optimizer_kwargs) | |
| elif name.lower() == "adamw": | |
| optimizer = torch.optim.AdamW(params, **optimizer_kwargs) | |
| elif name.lower() == "fusedadam": | |
| optimizer = FusedAdam( | |
| params, | |
| lr=optimizer_kwargs["lr"], | |
| weight_decay=optimizer_kwargs["weight_decay"], | |
| betas=optimizer_kwargs["betas"], | |
| capturable=True, | |
| master_weights=True, | |
| ) | |
| else: | |
| raise NotImplementedError(f"Optimizer {name} not added.") | |
| return optimizer | |
| class OptimizersContainer(Stateful): | |
| """Util for calling step/zero_grad on multiple optimizers needed for virtual pipeline stages | |
| and saving/loading optimizer state_dict at checkpoint. | |
| """ | |
| def __init__( | |
| self, | |
| model_parts: list[nn.Module], | |
| optimizer_kwargs: dict[str, Any], | |
| name: str, | |
| lr_multiplier: dict[str, float], | |
| model_part_names: list[str], | |
| ) -> None: | |
| assert len(model_parts) == len(model_part_names), "model_parts and model_part_names must have the same length" | |
| self.model_parts = model_parts | |
| self.optimizers = [[] for _ in self.model_parts] | |
| self.model_part_names = model_part_names | |
| for model_id, model in enumerate(self.model_parts): | |
| optimizer_kwargs_copy = deepcopy(optimizer_kwargs) | |
| optimizer_kwargs_copy["lr"] *= lr_multiplier[model_part_names[model_id]] | |
| log.info( | |
| f"model_id: {model_id} | model_part_names: {model_part_names[model_id]} | lr_multiplier: {lr_multiplier[model_part_names[model_id]]} | lr: {optimizer_kwargs_copy['lr']}" | |
| ) | |
| if optimizer_kwargs_copy["fused"]: | |
| # Group the parameters by device mesh to do optimizer fusion. | |
| parameters_by_mesh = collections.defaultdict(list) | |
| for p in model.parameters(): | |
| if p.requires_grad: | |
| device_mesh = p.device_mesh if hasattr(p, "device_mesh") else "default" | |
| parameters_by_mesh[device_mesh].append(p) | |
| for params in parameters_by_mesh.values(): | |
| optimizer = _optimizer_cls(params, optimizer_kwargs_copy, name) | |
| self.optimizers[model_id].append(optimizer) | |
| else: | |
| for p in model.parameters(): | |
| if p.requires_grad: | |
| optimizer = _optimizer_cls([p], optimizer_kwargs_copy, name) | |
| self.optimizers[model_id].append(optimizer) | |
| def __iter__(self) -> torch.optim.Optimizer: | |
| return iter(itertools.chain(*self.optimizers)) | |
| def step(self) -> None: | |
| for optimizer in itertools.chain(*self.optimizers): | |
| optimizer.step() | |
| def zero_grad(self, set_to_none: bool = False) -> None: | |
| for optimizer in itertools.chain(*self.optimizers): | |
| optimizer.zero_grad(set_to_none=set_to_none) | |
| def state_dict(self) -> dict[str, Any]: | |
| result = {} | |
| for idx, (model_part, optimizer) in enumerate(zip(self.model_parts, self.optimizers)): | |
| sd = get_optimizer_state_dict( | |
| model=model_part, | |
| optimizers=optimizer, | |
| options=StateDictOptions(flatten_optimizer_state_dict=True), | |
| ) | |
| prefix = f"optimizer_{idx}/" | |
| result.update({f"{prefix}{k}": v for k, v in sd.items()}) | |
| return result | |
| def load_state_dict(self, state_dict: dict[str, Any]) -> None: | |
| for idx, (model, optimizers) in enumerate(zip(self.model_parts, self.optimizers)): | |
| prefix = f"optimizer_{idx}/" | |
| optimizer_state = { | |
| k[len(prefix) :]: v # Remove prefix | |
| for k, v in state_dict.items() | |
| if k.startswith(prefix) | |
| } | |
| set_optimizer_state_dict( | |
| model=model, | |
| optimizers=optimizers, | |
| optim_state_dict=optimizer_state, | |
| options=StateDictOptions(flatten_optimizer_state_dict=True), | |
| ) | |
| # consider split between PP and non-PP | |
| def build_optimizers( | |
| model_parts: list[nn.Module], | |
| config: OptimizerConfig, | |
| model_part_names: list[str], | |
| ) -> OptimizersContainer: | |
| """Wrap one optimizer per model part in an OptimizersContainer which provides a single | |
| step() and zero_grad() method for all the child optimizers. | |
| """ | |
| lr_multiplier = config.lr_multiplier | |
| for part_name in model_part_names: | |
| assert part_name in lr_multiplier, f"lr_multiplier must have the key {part_name}" | |
| name = config.name | |
| lr = config.lr | |
| fused = config.fused | |
| optimizer_kwargs = { | |
| "lr": lr, | |
| "betas": config.betas, | |
| "weight_decay": config.weight_decay, | |
| "fused": fused, | |
| "foreach": not fused, | |
| } | |
| return OptimizersContainer(model_parts, optimizer_kwargs, name, lr_multiplier, model_part_names) | |
| class SchedulersContainer(Stateful): | |
| """Util for calling step on multiple learning rate schedulers needed for virtual pipeline stages""" | |
| def __init__(self, optimizers: OptimizersContainer, lr_lambda) -> None: | |
| self.schedulers = [] | |
| for optimizer in optimizers: | |
| self.schedulers.append(LambdaLR(optimizer, lr_lambda=lr_lambda)) | |
| def step(self) -> None: | |
| for id, scheduler in enumerate(self.schedulers): | |
| scheduler.step() | |
| def state_dict(self) -> dict[str, Any]: | |
| # Currently, we have one scheduler per optimizer. However, when using MultiSchedule PP or optimizer-in-backward, | |
| # there are multiple optimizers and schedulers, but the scheduler state_dict remains the same for all. | |
| # Therefore, we only save the first one and later load it for all. | |
| assert len(self.schedulers) > 0, "Must have at least one scheduler to save state_dict" | |
| return self.schedulers[0].state_dict() | |
| def load_state_dict(self, state_dict: dict[str, Any]) -> None: | |
| # Load the same state_dict for all schedulers. The key value we're concerned with in scheduler.state_dict() is `last_epoch`, | |
| # which is an integer that will be automatically copied. As long as `training.steps` and `training.warmup_iters` remain | |
| # unchanged when resuming from a checkpoint, this approach is safe. We call `.copy()` here to ensure extra safety. | |
| last_epoch = state_dict["last_epoch"] # Extract last known epoch | |
| _step_count = state_dict["_step_count"] | |
| log.info(f"Resuming schedulers by stepping them to last_epoch: {last_epoch}; _step_count: {_step_count}") | |
| # Manually step all schedulers to match the saved state -- this is a workaround for the inherited issue in the state dict saving (only saved the first scheduler) | |
| # But we have different learning rate for each scheduler, so we need to step them separately instead of loading the state dict | |
| # The benefit of this approach is that we can resume from a checkpoint even if the learning rate is changed | |
| for idx, scheduler in enumerate(self.schedulers): | |
| for step in range(_step_count): | |
| scheduler.step() # Step forward to match previous training state | |
| log.info(f"Scheduler {idx + 1}/{len(self.schedulers)} stepped {_step_count} times.") | |
| log.info(f"Updated learning rate: {scheduler.get_last_lr()}") | |
| def get_last_lr(self) -> list[float]: | |
| return [scheduler.get_last_lr() for scheduler in self.schedulers] | |
| def linear_warmup_linear_decay(warmup_iters: int, decay_steps: int, current_step: int) -> float: | |
| """Computes linear warmup followed by linear decay. | |
| Per LambdaLR requirement, this is accomplished by returning | |
| a multiplicative factor to adjust the learning rate to | |
| create the desired schedule. | |
| """ | |
| if current_step < warmup_iters: | |
| # linear warmup | |
| # 0-indexed step, hence + 1 adjustments | |
| current_step += 1 | |
| curr_adjustment = float(current_step / (warmup_iters + 1)) | |
| else: | |
| # linear decay | |
| normalized_step = decay_steps - (current_step - warmup_iters) | |
| curr_adjustment = 1 - (decay_steps - normalized_step) / decay_steps | |
| return curr_adjustment | |
| def linear_warmup(warmup_iters: int, current_step: int) -> float: | |
| """Computes linear warmup only | |
| Per LambdaLR requirement, this is accomplished by returning | |
| a multiplicative factor to adjust the learning rate to | |
| create the desired schedule. | |
| """ | |
| if current_step < warmup_iters: | |
| # linear warmup | |
| # 0-indexed step, hence + 1 adjustments | |
| current_step += 1 | |
| curr_adjustment = float(current_step / (warmup_iters + 1)) | |
| else: | |
| curr_adjustment = 1 | |
| return curr_adjustment | |
| def linear_warmup_cosine_cooldown( | |
| warmup_iters: int, cooldown_steps: int, current_step: int, base_lr: float, init_lr: float, end_lr: float | |
| ) -> float: | |
| """This scheduler will warmup the learning rate from init_lr to base_lr for warmup_iters, | |
| then decay the learning rate from base_lr to end_lr for cooldown_steps. After cooldown_steps + warmup_iters, | |
| the learning rate will be set to end_lr. | |
| Per LambdaLR requirement, this is accomplished by returning | |
| a multiplicative factor to adjust the learning rate to | |
| create the desired schedule. | |
| Args: | |
| warmup_iters (int): The number of steps to warmup the learning rate. | |
| cooldown_steps (int): The number of steps to decay the learning rate. | |
| current_step (int): The current step. | |
| base_lr (float): The base learning rate. | |
| init_lr (float): The initial learning rate before warmup. | |
| end_lr (float): The final learning rate after cooldown. | |
| Returns: | |
| float: The multiplicative factor to adjust the learning rate. | |
| """ | |
| total_steps = warmup_iters + cooldown_steps | |
| # Normalize | |
| init_multiplier = init_lr / base_lr | |
| end_multiplier = end_lr / base_lr | |
| if current_step <= warmup_iters: | |
| progress = float(current_step / warmup_iters) | |
| return init_multiplier + (1.0 - init_multiplier) * progress | |
| elif current_step <= total_steps: | |
| progress = (current_step - warmup_iters) / cooldown_steps | |
| return end_multiplier + 0.5 * (1.0 - end_multiplier) * (1 + math.cos(math.pi * progress)) | |
| else: | |
| return end_multiplier | |
| def build_lr_schedulers( | |
| optimizers: OptimizersContainer, | |
| name: str, | |
| lr: float, | |
| warmup_iters: int, | |
| lr_decay_iters: Optional[int] = None, | |
| init_lr: Optional[float] = None, | |
| end_lr: Optional[float] = None, | |
| ) -> SchedulersContainer: | |
| decay_steps = float(max(1, lr_decay_iters - warmup_iters)) if lr_decay_iters is not None else None | |
| if name == "warmup_cosine_lr": | |
| assert init_lr is not None and end_lr is not None, "init_lr and end_lr must be provided for warmup_cosine_lr" | |
| assert lr_decay_iters is not None, "lr_decay_iters must be provided for warmup_cosine_lr" | |
| lr_lambda = functools.partial( | |
| linear_warmup_cosine_cooldown, | |
| warmup_iters, | |
| decay_steps, | |
| base_lr=lr, | |
| init_lr=init_lr, | |
| end_lr=end_lr, | |
| ) | |
| elif name == "lambdalinear": | |
| assert lr_decay_iters is not None, "lr_decay_iters must be provided for lambdalinear" | |
| lr_lambda = functools.partial(linear_warmup_linear_decay, warmup_iters, decay_steps) | |
| else: | |
| lr_lambda = functools.partial(linear_warmup, warmup_iters) | |
| return SchedulersContainer(optimizers, lr_lambda) | |