Download code/optimizer/eigen_block_basis_optimizer.py from yuxing6/optimizer-model-consistency-pretrained: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/yuxing6/optimizer-model-consistency-pretrained/resolve/main/code/optimizer/eigen_block_basis_optimizer.py
- Command line
-
hf download hf://yuxing6/optimizer-model-consistency-pretrained/code/optimizer/eigen_block_basis_optimizer.py
-
curl -L -o eigen_block_basis_optimizer.py https://huggingface.co/yuxing6/optimizer-model-consistency-pretrained/resolve/main/code/optimizer/eigen_block_basis_optimizer.py
11.6 kB
| import torch | |
| import math | |
| from torch.optim.optimizer import Optimizer | |
| from optimizer.numerical_methods import zeropower_via_newtonschulz5 | |
| class EigenBlockOptimizer(Optimizer): | |
| """ | |
| Implements Eigen-Block Basis Optimizer. | |
| Features: | |
| - Hessian Approximation via Shampoo/SOAP style preconditioners (L, R). | |
| - Eigen-Block Rotation: Groups close eigenvalues and rotates within blocks | |
| to maximize L1 sparsity (using Newton-Schulz). | |
| - Momentum: Applies momentum to gradients in the original space. | |
| - Dual Mode: Supports both SignSGD (default) and Adam updates in the optimal basis. | |
| """ | |
| def __init__(self, params, lr=1e-3, betas=(0.9, 0.95), weight_decay=0.0, | |
| eigen_tol=0.1, eps=1e-8, use_adam=False): | |
| """ | |
| Args: | |
| lr (float): Learning rate. | |
| betas (tuple): (beta1, beta2). | |
| beta1: Momentum coefficient. | |
| beta2: Preconditioner EMA coefficient (and Adam 2nd moment if use_adam=True). | |
| eigen_tol (float): Relative tolerance for grouping eigenvalues. | |
| Groups lambdas where (lambda_i - lambda_start) < tol * max_lambda. | |
| use_adam (bool): If True, uses Adam updates in the projected space. | |
| If False, uses SignSGD updates in the projected space. | |
| """ | |
| defaults = dict(lr=lr, betas=betas, weight_decay=weight_decay, | |
| eigen_tol=eigen_tol, eps=eps, use_adam=use_adam) | |
| super(EigenBlockOptimizer, self).__init__(params, defaults) | |
| def step(self, closure=None): | |
| 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'] | |
| weight_decay = group['weight_decay'] | |
| eigen_tol = group['eigen_tol'] | |
| eps = group['eps'] | |
| use_adam = group['use_adam'] | |
| for p in group['params']: | |
| if p.grad is None: | |
| continue | |
| grad = p.grad | |
| if grad.dim() != 2: | |
| # Fallback for 1D vectors (bias/layernorm): Standard Adam or SignSGD | |
| state = self.state[p] | |
| if len(state) == 0: | |
| state['step'] = 0 | |
| state['exp_avg'] = torch.zeros_like(p) | |
| if use_adam: | |
| state['exp_avg_sq'] = torch.zeros_like(p) | |
| state['step'] += 1 | |
| exp_avg = state['exp_avg'] | |
| # Update momentum | |
| exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1) | |
| if weight_decay > 0: | |
| p.mul_(1 - lr * weight_decay) | |
| if use_adam: | |
| exp_avg_sq = state['exp_avg_sq'] | |
| exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2) | |
| denom = exp_avg_sq.sqrt().add_(eps) | |
| # Bias correction | |
| bias_correction1 = 1 - beta1 ** state['step'] | |
| bias_correction2 = 1 - beta2 ** state['step'] | |
| step_size = lr * math.sqrt(bias_correction2) / bias_correction1 | |
| p.addcdiv_(exp_avg, denom, value=-step_size) | |
| else: | |
| # SignSGD with momentum | |
| p.add_(exp_avg.sign(), alpha=-lr) | |
| continue | |
| # --- Handle 2D Matrices (Weights) --- | |
| state = self.state[p] | |
| # Init state | |
| if len(state) == 0: | |
| state['step'] = 0 | |
| state['momentum_buffer'] = torch.zeros_like(p) # 1st moment (M) | |
| # Shampoo/SOAP style preconditioners | |
| state['L'] = torch.zeros((grad.size(0), grad.size(0)), device=grad.device, dtype=grad.dtype) | |
| state['R'] = torch.zeros((grad.size(1), grad.size(1)), device=grad.device, dtype=grad.dtype) | |
| if use_adam: | |
| # Adam 2nd moment (V') in the PROJECTED space | |
| # Note: We store this same size as p, assuming basis consistency over time | |
| state['adam_v_prime'] = torch.zeros_like(p) | |
| state['step'] += 1 | |
| L = state['L'] | |
| R = state['R'] | |
| momentum = state['momentum_buffer'] | |
| # 1. Update Momentum (M_t) in Original Space | |
| momentum.mul_(beta1).add_(grad, alpha=1 - beta1) | |
| # 2. Update Preconditioners (Hessian Approximation) | |
| # Using EMA of GG^T and G^TG | |
| L.mul_(beta2).add_(grad @ grad.T, alpha=1 - beta2) | |
| R.mul_(beta2).add_(grad.T @ grad, alpha=1 - beta2) | |
| # 3. Eigen Decomposition | |
| # Add 1e-12 for numerical stability | |
| L_evals, U_L = torch.linalg.eigh(L + 1e-12 * torch.eye(L.size(0), device=L.device)) | |
| R_evals, U_R = torch.linalg.eigh(R + 1e-12 * torch.eye(R.size(0), device=R.device)) | |
| # 4. Rotate Momentum to Eigen-basis: M_tilde = U_L^T M U_R | |
| # We rotate M instead of G because we are doing Momentum-based updates | |
| M_tilde = U_L.T @ momentum @ U_R | |
| # 5. Compute Optimal Basis Q_L (Left) | |
| Q_L = U_L.clone() | |
| # Use max eigenvalue to scale tolerance | |
| tol_L = eigen_tol * (L_evals.abs().max().item() + 1e-6) | |
| boundaries_L = self._find_block_boundaries(L_evals, tol_L) | |
| # --- NEW: Record L Block Stats --- | |
| self._store_block_stats(state, 'L', boundaries_L, L_evals.shape[0]) | |
| for start, end in boundaries_L: | |
| if end - start <= 1: | |
| continue | |
| # Extract rows corresponding to this block from M_tilde | |
| # We use M_tilde to determine the rotation N within the block | |
| M_sub = M_tilde[start:end, :] | |
| # Formula: N = msign(A * sign(A)^T) | |
| # Using M_sub aligns the basis with the momentum direction's sparsity | |
| A = M_sub @ M_sub.sign().T | |
| N_block = zeropower_via_newtonschulz5(A) | |
| # Update Q_L columns: Q_L_new = Q_L_old @ N_block | |
| Q_L[:, start:end] = U_L[:, start:end] @ N_block | |
| # 6. Compute Optimal Basis Q_R (Right) | |
| Q_R = U_R.clone() | |
| tol_R = eigen_tol * (R_evals.abs().max().item() + 1e-6) | |
| boundaries_R = self._find_block_boundaries(R_evals, tol_R) | |
| # --- NEW: Record R Block Stats --- | |
| self._store_block_stats(state, 'R', boundaries_R, R_evals.shape[0]) | |
| for start, end in boundaries_R: | |
| if end - start <= 1: | |
| continue | |
| M_sub = M_tilde[:, start:end] | |
| # Formula: N = msign(sign(A)^T * A) | |
| A = M_sub.sign().T @ M_sub | |
| N_block = zeropower_via_newtonschulz5(A) | |
| Q_R[:, start:end] = U_R[:, start:end] @ N_block | |
| # 7. Project Momentum and Gradient into the FULL optimal basis Q | |
| # M_opt = Q_L^T M Q_R | |
| M_opt = Q_L.T @ momentum @ Q_R | |
| if use_adam: | |
| # If using Adam, we also need the projected Gradient to update V' | |
| # G_opt = Q_L^T G Q_R | |
| G_opt = Q_L.T @ grad @ Q_R | |
| adam_v_prime = state['adam_v_prime'] | |
| # Update V'_t using projected gradient square | |
| adam_v_prime.mul_(beta2).addcmul_(G_opt, G_opt, value=1 - beta2) | |
| # Bias Correction | |
| bias_correction1 = 1 - beta1 ** state['step'] | |
| bias_correction2 = 1 - beta2 ** state['step'] | |
| M_hat = M_opt / bias_correction1 | |
| V_hat = adam_v_prime / bias_correction2 | |
| # Adam Update in Basis | |
| Update_inner = M_hat / (V_hat.sqrt() + eps) | |
| else: | |
| # SignSGD with Momentum in Basis | |
| Update_inner = M_opt.sign() | |
| # 8. Rotate update back to original space | |
| # D_t = Q_L * Update_inner * Q_R^T | |
| Update_step = Q_L @ Update_inner @ Q_R.T | |
| if weight_decay > 0: | |
| p.mul_(1 - lr * weight_decay) | |
| p.add_(Update_step, alpha=-lr) | |
| return loss | |
| def _find_block_boundaries(self, evals, tol): | |
| """ | |
| Groups eigenvalues based on difference from the FIRST element in the block. | |
| Iterates through sorted eigenvalues. If evals[i] - evals[start] > tol, | |
| a new block is started at i. | |
| """ | |
| # evals is expected to be sorted (torch.linalg.eigh returns ascending) | |
| boundaries = [0] | |
| n = len(evals) | |
| if n == 0: return [] | |
| start_idx = 0 | |
| for i in range(1, n): | |
| # Check difference against the anchor (start_idx) | |
| diff = evals[i] - evals[start_idx] | |
| if diff > tol: | |
| # Close current block, start new one at i | |
| boundaries.append(i) | |
| start_idx = i | |
| boundaries.append(n) | |
| # Convert to (start, end) tuples | |
| blocks = [] | |
| for k in range(len(boundaries) - 1): | |
| blocks.append((boundaries[k], boundaries[k+1])) | |
| return blocks | |
| def _store_block_stats(self, state, name, blocks, total_dim): | |
| """ | |
| 计算 10%, 25%, 50%, 75%, 90%, 100% 处的特征值索引属于第几个 Block,并存储到 state 中。 | |
| """ | |
| if 'eigen_stats' not in state: | |
| state['eigen_stats'] = {} | |
| # 1-based block index (第几个格子) | |
| stats = {} | |
| percentiles = [0.10, 0.25, 0.50, 0.75, 0.90, 1.00] | |
| for p in percentiles: | |
| # 计算对应的 index (0-based) | |
| # 例如 100% 对应最后一个元素 (total_dim - 1) | |
| target_idx = int(p * total_dim) - 1 | |
| target_idx = max(0, min(target_idx, total_dim - 1)) | |
| # 查找该 index 属于哪个 block | |
| found_block_idx = 0 # default | |
| for i, (start, end) in enumerate(blocks): | |
| if start <= target_idx < end: | |
| found_block_idx = i + 1 # 使用 1-based 计数,方便观察 | |
| break | |
| # 记录 metric,例如 'p10': 1 | |
| stats[f'p{int(p*100)}'] = found_block_idx | |
| stats['total_blocks'] = len(blocks) | |
| stats['total_dim'] = total_dim | |
| state['eigen_stats'][name] = stats |