Download src/part2/optimizers.py from Chiru20/ANLP_assignment2: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/Chiru20/ANLP_assignment2/resolve/main/src/part2/optimizers.py
- Command line
-
hf download hf://Chiru20/ANLP_assignment2/src/part2/optimizers.py
-
curl -L -o optimizers.py https://huggingface.co/Chiru20/ANLP_assignment2/resolve/main/src/part2/optimizers.py
17.5 kB
| """ | |
| Part 2: Custom Optimizers from Scratch | |
| Strictly subclassing torch.optim.Optimizer without predefined torch.optim modules. | |
| Covers all required categories from 'Fantastic Pretraining Optimizers and Where to Find Them' (2025): | |
| 1. Category 1: AdamW (Decoupled weight decay) | |
| 2. Category 2: Adan (Adaptive Nesterov Momentum - Variance-Reduced AdamW) | |
| 3. Category 3: Lion (Memory-Efficient Sign-Momentum) | |
| 4. Category 4: Muon (Matrix-Based Polar Newton-Schulz Optimizer) | |
| 5. Bonus: Sophia (Hessian-Based Clipped Stochastic Optimizer) | |
| """ | |
| import math | |
| from typing import Callable, Iterable, Optional, Tuple, Union, Dict, Any | |
| import torch | |
| from torch.optim import Optimizer | |
| class CustomAdamW(Optimizer): | |
| """ | |
| Category 1: AdamW with decoupled weight decay (Loshchilov & Hutter, 2019). | |
| theta_t = theta_{t-1} - lr * (m_hat / (sqrt(v_hat) + eps) + weight_decay * theta_{t-1}) | |
| """ | |
| def __init__( | |
| self, | |
| params: Iterable[torch.nn.Parameter], | |
| lr: float = 1e-3, | |
| betas: Tuple[float, float] = (0.9, 0.999), | |
| eps: float = 1e-8, | |
| weight_decay: float = 0.01 | |
| ): | |
| if lr < 0.0: | |
| raise ValueError(f"Invalid learning rate: {lr}") | |
| if not 0.0 <= betas[0] < 1.0: | |
| raise ValueError(f"Invalid beta1: {betas[0]}") | |
| if not 0.0 <= betas[1] < 1.0: | |
| raise ValueError(f"Invalid beta2: {betas[1]}") | |
| if eps < 0.0: | |
| raise ValueError(f"Invalid epsilon: {eps}") | |
| if weight_decay < 0.0: | |
| raise ValueError(f"Invalid weight_decay: {weight_decay}") | |
| defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay) | |
| super().__init__(params, defaults) | |
| def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]: | |
| loss = None | |
| if closure is not None: | |
| with torch.enable_grad(): | |
| loss = closure() | |
| for group in self.param_groups: | |
| lr = group["lr"] | |
| beta1, beta2 = group["betas"] | |
| eps = group["eps"] | |
| wd = group["weight_decay"] | |
| for p in group["params"]: | |
| if p.grad is None: | |
| continue | |
| grad = p.grad | |
| if grad.is_sparse: | |
| raise RuntimeError("CustomAdamW does not support sparse gradients") | |
| state = self.state[p] | |
| if len(state) == 0: | |
| state["step"] = 0 | |
| state["exp_avg"] = torch.zeros_like(p, memory_format=torch.preserve_format) | |
| state["exp_avg_sq"] = torch.zeros_like(p, memory_format=torch.preserve_format) | |
| state["step"] += 1 | |
| step = state["step"] | |
| exp_avg = state["exp_avg"] | |
| exp_avg_sq = state["exp_avg_sq"] | |
| # 1. Perform decoupled weight decay | |
| if wd != 0.0: | |
| p.mul_(1.0 - lr * wd) | |
| # 2. Update biased 1st and 2nd moment estimates | |
| exp_avg.mul_(beta1).add_(grad, alpha=1.0 - beta1) | |
| exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2) | |
| # 3. Bias corrections | |
| bias_correction1 = 1.0 - beta1 ** step | |
| bias_correction2 = 1.0 - beta2 ** step | |
| step_size = lr / bias_correction1 | |
| denom = (exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(eps) | |
| p.addcdiv_(exp_avg, denom, value=-step_size) | |
| return loss | |
| class CustomAdan(Optimizer): | |
| """ | |
| Category 2: Adan (Adaptive Nesterov Momentum Algorithm, Xie et al., 2022). | |
| Variance-reduced AdamW variant incorporating gradient differences to estimate | |
| future gradient trajectory and damp oscillations. | |
| """ | |
| def __init__( | |
| self, | |
| params: Iterable[torch.nn.Parameter], | |
| lr: float = 1e-3, | |
| betas: Tuple[float, float, float] = (0.98, 0.92, 0.99), | |
| eps: float = 1e-8, | |
| weight_decay: float = 0.02 | |
| ): | |
| if lr < 0.0: | |
| raise ValueError(f"Invalid learning rate: {lr}") | |
| if eps < 0.0: | |
| raise ValueError(f"Invalid epsilon: {eps}") | |
| defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay) | |
| super().__init__(params, defaults) | |
| def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]: | |
| loss = None | |
| if closure is not None: | |
| with torch.enable_grad(): | |
| loss = closure() | |
| for group in self.param_groups: | |
| lr = group["lr"] | |
| beta1, beta2, beta3 = group["betas"] | |
| eps = group["eps"] | |
| wd = group["weight_decay"] | |
| for p in group["params"]: | |
| if p.grad is None: | |
| continue | |
| grad = p.grad | |
| if grad.is_sparse: | |
| raise RuntimeError("CustomAdan does not support sparse gradients") | |
| state = self.state[p] | |
| if len(state) == 0: | |
| state["step"] = 0 | |
| state["exp_avg"] = torch.zeros_like(p) # m_k: EMA of grad | |
| state["exp_avg_diff"] = torch.zeros_like(p) # v_k: EMA of grad diff | |
| state["exp_avg_sq"] = torch.zeros_like(p) # n_k: second moment | |
| state["prev_grad"] = grad.clone() | |
| state["step"] += 1 | |
| step = state["step"] | |
| exp_avg = state["exp_avg"] | |
| exp_avg_diff = state["exp_avg_diff"] | |
| exp_avg_sq = state["exp_avg_sq"] | |
| prev_grad = state["prev_grad"] | |
| b1 = beta1 if beta1 > 0.5 else 1.0 - beta1 | |
| b2 = beta2 if beta2 > 0.5 else 1.0 - beta2 | |
| b3 = beta3 if beta3 > 0.5 else 1.0 - beta3 | |
| grad_diff = grad - prev_grad | |
| # Update 1st moment of grad | |
| exp_avg.mul_(b1).add_(grad, alpha=1.0 - b1) | |
| # Update 1st moment of grad difference | |
| exp_avg_diff.mul_(b2).add_(grad_diff, alpha=1.0 - b2) | |
| # Update 2nd moment of update vector | |
| update_term = grad + (1.0 - b2) * grad_diff | |
| exp_avg_sq.mul_(b3).addcmul_(update_term, update_term, value=1.0 - b3) | |
| bias_correction1 = 1.0 - b1 ** step | |
| bias_correction2 = 1.0 - b2 ** step | |
| bias_correction3 = 1.0 - b3 ** step | |
| m_hat = exp_avg / bias_correction1 | |
| v_hat = exp_avg_diff / bias_correction2 | |
| n_hat = exp_avg_sq / bias_correction3 | |
| denom = n_hat.sqrt().add_(eps) | |
| step_direction = (m_hat + (1.0 - b2) * v_hat) / denom | |
| # Apply parameter update and decoupled weight decay | |
| p.data.add_(step_direction, alpha=-lr) | |
| if wd != 0.0: | |
| p.data.div_(1.0 + lr * wd) | |
| prev_grad.copy_(grad) | |
| return loss | |
| class CustomLion(Optimizer): | |
| """ | |
| Category 3: Lion (EvoLved Sign Momentum, Chen et al., Google Brain 2023). | |
| Memory-efficient optimizer tracking only 1 momentum buffer (half the memory of AdamW). | |
| Uses coordinate-wise sign operation for uniform step magnitude. | |
| """ | |
| def __init__( | |
| self, | |
| params: Iterable[torch.nn.Parameter], | |
| lr: float = 1e-4, | |
| betas: Tuple[float, float] = (0.9, 0.99), | |
| weight_decay: float = 0.1 | |
| ): | |
| if lr < 0.0: | |
| raise ValueError(f"Invalid learning rate: {lr}") | |
| defaults = dict(lr=lr, betas=betas, weight_decay=weight_decay) | |
| super().__init__(params, defaults) | |
| def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]: | |
| loss = None | |
| if closure is not None: | |
| with torch.enable_grad(): | |
| loss = closure() | |
| for group in self.param_groups: | |
| lr = group["lr"] | |
| beta1, beta2 = group["betas"] | |
| wd = group["weight_decay"] | |
| for p in group["params"]: | |
| if p.grad is None: | |
| continue | |
| grad = p.grad | |
| if grad.is_sparse: | |
| raise RuntimeError("CustomLion does not support sparse gradients") | |
| state = self.state[p] | |
| if len(state) == 0: | |
| state["step"] = 0 | |
| state["exp_avg"] = torch.zeros_like(p) | |
| state["step"] += 1 | |
| exp_avg = state["exp_avg"] | |
| # 1. Weight decay | |
| if wd != 0.0: | |
| p.mul_(1.0 - lr * wd) | |
| # 2. Lion update direction: c_t = beta1 * m_{t-1} + (1 - beta1) * grad | |
| update = exp_avg.clone().mul_(beta1).add_(grad, alpha=1.0 - beta1) | |
| p.add_(torch.sign(update), alpha=-lr) | |
| # 3. Update momentum buffer: m_t = beta2 * m_{t-1} + (1 - beta2) * grad | |
| exp_avg.mul_(beta2).add_(grad, alpha=1.0 - beta2) | |
| return loss | |
| def newton_schulz5(G: torch.Tensor, steps: int = 5, eps: float = 1e-7) -> torch.Tensor: | |
| """ | |
| Quintic Newton-Schulz iteration for matrix orthogonalization (Muon). | |
| Computes an approximate polar decomposition (orthogonal projection of G). | |
| """ | |
| assert G.ndim == 2 | |
| a, b, c = (3.4445, -4.7750, 2.0315) | |
| X = G.bfloat16() if G.is_cuda else G.float() | |
| X /= (X.norm() + eps) | |
| if X.size(0) > X.size(1): | |
| X = X.T | |
| transposed = True | |
| else: | |
| transposed = False | |
| for _ in range(steps): | |
| A = X @ X.T | |
| B = b * A + c * A @ A | |
| X = a * X + B @ X | |
| if transposed: | |
| X = X.T | |
| return X.to(G.dtype) | |
| class CustomMuon(Optimizer): | |
| """ | |
| Category 4: Muon (Momentum Orthogonalized by Newton-Schulz, 2024/2025). | |
| Matrix-based optimizer. Applies orthogonalized momentum to 2D weight matrices, | |
| while falling back to standard AdamW for 1D vectors and non-matrix parameters. | |
| """ | |
| def __init__( | |
| self, | |
| params: Iterable[torch.nn.Parameter], | |
| lr: float = 0.02, | |
| momentum: float = 0.95, | |
| nesterov: bool = True, | |
| ns_steps: int = 5, | |
| weight_decay: float = 0.01, | |
| fallback_lr: float = 1e-3 | |
| ): | |
| defaults = dict( | |
| lr=lr, | |
| momentum=momentum, | |
| nesterov=nesterov, | |
| ns_steps=ns_steps, | |
| weight_decay=weight_decay, | |
| fallback_lr=fallback_lr, | |
| betas=(0.9, 0.999), | |
| eps=1e-8 | |
| ) | |
| super().__init__(params, defaults) | |
| def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]: | |
| loss = None | |
| if closure is not None: | |
| with torch.enable_grad(): | |
| loss = closure() | |
| for group in self.param_groups: | |
| lr = group["lr"] | |
| mu = group["momentum"] | |
| nesterov = group["nesterov"] | |
| ns_steps = group["ns_steps"] | |
| wd = group["weight_decay"] | |
| fallback_lr = group["fallback_lr"] | |
| for p in group["params"]: | |
| if p.grad is None: | |
| continue | |
| grad = p.grad | |
| state = self.state[p] | |
| is_matrix = (p.ndim == 2 and min(p.shape) >= 2) | |
| if len(state) == 0: | |
| state["step"] = 0 | |
| state["exp_avg"] = torch.zeros_like(p) | |
| if not is_matrix: | |
| state["exp_avg_sq"] = torch.zeros_like(p) | |
| state["step"] += 1 | |
| step = state["step"] | |
| if is_matrix: | |
| # 2D Matrix parameter: Muon Newton-Schulz update | |
| exp_avg = state["exp_avg"] | |
| if wd != 0.0: | |
| p.mul_(1.0 - lr * wd) | |
| exp_avg.mul_(mu).add_(grad) | |
| if nesterov: | |
| buf = grad + mu * exp_avg | |
| else: | |
| buf = exp_avg | |
| ortho_buf = newton_schulz5(buf, steps=ns_steps) | |
| # RMS-matched scaling: scales update by sqrt(max(n, m)) to match unit RMS | |
| scale = max(1.0, float(max(p.shape[-2:])) ** 0.5) | |
| p.add_(ortho_buf, alpha=-lr * scale) | |
| else: | |
| # 1D/Non-matrix parameter: Standard AdamW fallback | |
| exp_avg = state["exp_avg"] | |
| exp_avg_sq = state["exp_avg_sq"] | |
| beta1, beta2 = group["betas"] | |
| eps = group["eps"] | |
| if wd != 0.0: | |
| p.mul_(1.0 - fallback_lr * wd) | |
| exp_avg.mul_(beta1).add_(grad, alpha=1.0 - beta1) | |
| exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2) | |
| bias_correction1 = 1.0 - beta1 ** step | |
| bias_correction2 = 1.0 - beta2 ** step | |
| step_size = fallback_lr / bias_correction1 | |
| denom = (exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(eps) | |
| p.addcdiv_(exp_avg, denom, value=-step_size) | |
| return loss | |
| class CustomSophia(Optimizer): | |
| """ | |
| Bonus: Sophia (Second-order Clipped Stochastic Optimization, Liu et al., 2023). | |
| Hessian-based optimizer utilizing EMA of diagonal Hessian estimates and coordinate-wise clipping. | |
| """ | |
| def __init__( | |
| self, | |
| params: Iterable[torch.nn.Parameter], | |
| lr: float = 1e-3, | |
| betas: Tuple[float, float] = (0.96, 0.99), | |
| rho: float = 1.0, | |
| gamma: float = 0.01, | |
| eps: float = 1e-12, | |
| weight_decay: float = 0.01 | |
| ): | |
| if lr < 0.0: | |
| raise ValueError(f"Invalid learning rate: {lr}") | |
| if rho <= 0.0: | |
| raise ValueError(f"Invalid rho: {rho}") | |
| defaults = dict( | |
| lr=lr, | |
| betas=betas, | |
| rho=rho, | |
| gamma=gamma, | |
| eps=eps, | |
| weight_decay=weight_decay | |
| ) | |
| super().__init__(params, defaults) | |
| def update_hessian(self) -> None: | |
| """ | |
| Updates diagonal Hessian estimate using Hutchinson's estimator or gradient-squared approximation. | |
| """ | |
| for group in self.param_groups: | |
| beta2 = group["betas"][1] | |
| for p in group["params"]: | |
| if p.grad is None: | |
| continue | |
| state = self.state[p] | |
| if "hessian" not in state: | |
| state["hessian"] = torch.zeros_like(p) | |
| state["hessian_step"] = 0 | |
| state["hessian_step"] += 1 | |
| state["hessian_called_externally"] = True | |
| # Diagonal Hessian approximation via stochastic gradient outer product | |
| h_hat = p.grad.pow(2) | |
| state["hessian"].mul_(beta2).add_(h_hat, alpha=1.0 - beta2) | |
| def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]: | |
| loss = None | |
| if closure is not None: | |
| with torch.enable_grad(): | |
| loss = closure() | |
| for group in self.param_groups: | |
| lr = group["lr"] | |
| beta1, beta2 = group["betas"] | |
| rho = group["rho"] | |
| gamma = group["gamma"] | |
| eps = group["eps"] | |
| wd = group["weight_decay"] | |
| for p in group["params"]: | |
| if p.grad is None: | |
| continue | |
| grad = p.grad | |
| if grad.is_sparse: | |
| raise RuntimeError("CustomSophia does not support sparse gradients") | |
| state = self.state[p] | |
| if "step" not in state: | |
| state["step"] = 0 | |
| state["exp_avg"] = torch.zeros_like(p) | |
| if "hessian" not in state: | |
| state["hessian"] = torch.zeros_like(p) | |
| state["hessian_step"] = 0 | |
| state["step"] += 1 | |
| step = state["step"] | |
| exp_avg = state["exp_avg"] | |
| hessian = state["hessian"] | |
| # 1. Weight decay | |
| if wd != 0.0: | |
| p.mul_(1.0 - lr * wd) | |
| # 2. First moment update | |
| exp_avg.mul_(beta1).add_(grad, alpha=1.0 - beta1) | |
| # 3. Bias corrections | |
| bias_correction1 = 1.0 - beta1 ** step | |
| m_hat = exp_avg / bias_correction1 | |
| if state.get("hessian_called_externally", False): | |
| h_step = max(1, state.get("hessian_step", 1)) | |
| bias_correction2 = 1.0 - beta2 ** h_step | |
| h_hat = hessian / bias_correction2 | |
| else: | |
| # Dynamically update diagonal Hessian estimate from grad^2 each step | |
| hessian.mul_(beta2).add_(grad.pow(2), alpha=1.0 - beta2) | |
| bias_correction2 = 1.0 - beta2 ** step | |
| h_hat = hessian / bias_correction2 | |
| # 4. Scaled and clipped update step (Sophia: clip(m_hat / max(h_hat, gamma), rho)) | |
| denom = torch.clamp(h_hat, min=gamma) | |
| scaled_step = torch.clamp(m_hat / denom, min=-rho, max=rho) | |
| p.add_(scaled_step, alpha=-lr) | |
| return loss | |