Download colab_bundle/modules/es_forgetting_engine.py from CrashOverrideX/Quillan-Ronin: direct link, hf CLI and curl.
- Browser
- Download file 17 kB
-
https://huggingface.co/CrashOverrideX/Quillan-Ronin/resolve/main/colab_bundle/modules/es_forgetting_engine.py
- Command line
-
hf download hf://CrashOverrideX/Quillan-Ronin/colab_bundle/modules/es_forgetting_engine.py
-
curl -L -o es_forgetting_engine.py https://huggingface.co/CrashOverrideX/Quillan-Ronin/resolve/main/colab_bundle/modules/es_forgetting_engine.py
17 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| ES Forgetting Engine: Diagnostic & Analytical Framework for Catastrophic Forgetting in ES | |
| ======================================================================================== | |
| Paper 53: "Evolutionary Strategies lead to Catastrophic Forgetting in LLMs" | |
| Authors: Immanuel Abdi*, Akshat Gupta*, Micah Mok, Alexander Lu, Nicholas Lee, Gopala Anumanchipalli | |
| Affiliation: UC Berkeley | |
| arXiv: 2601.20861v1 (28 Jan 2026, 11 pages) | |
| Key Architectural & Analytical Findings: | |
| 1. High-Magnitude Parameter Drift (Frobenius Norm Explosion): | |
| - The Frobenius norm of model weight delta ||ΔW||_F = ||W_new - W_base||_F grows monotonically | |
| with training iterations. | |
| - After 500 iterations, ES updates are three orders of magnitude (~1000x) larger than GRPO updates, | |
| inducing severe drift away from the base manifold. | |
| 2. Global Lack of Update Sparsity: | |
| - Evaluated under absolute magnitude threshold tau = 10^-6: | |
| Sparsity(ΔW, tau) = (1 / |ΔW|) * sum(I[|ΔW_ij| < tau]) * 100% | |
| - GRPO updates exhibit high sparsity (~95%) across attention projections (Q, K, V, O) and MLP blocks, | |
| localizing modifications to a tiny fraction of critical weights. | |
| - Standard ES updates are globally dense with near-zero sparsity (<5-10%), perturbing virtually | |
| all stored parameters uniformly and destroying pre-existing capabilities. | |
| 3. Layerwise & Component-Wise Dissection: | |
| - Tracks parameter drift across key transformer architectural components: | |
| * Attention projections: Q, K, V (attn_qkv) | |
| * Attention output projection: O (attn_o) | |
| * Feed-Forward / MLP projections: Gate, Up, Down (mlp) | |
| * Layer Normalization: Input & Post-Attention Norm (norm) | |
| - Only LayerNorm exhibits sparsity in ES, but it accounts for a negligible fraction of total weights. | |
| 4. Continual Learning Instability & Pareto Degradation: | |
| - Even after new-task performance (e.g., Countdown) reaches plateau convergence (iter ~200), | |
| continued ES fine-tuning leads to an approximately 10% drop in prior task accuracy (e.g., HellaSwag). | |
| This module is part of the Quillan-Ronin Sovereign AI Architecture. | |
| """ | |
| import math | |
| from dataclasses import dataclass, field | |
| from typing import Dict, List, Optional, Tuple, Any | |
| import torch | |
| import torch.nn as nn | |
| class ESForgettingConfig: | |
| """Configuration for ES Catastrophic Forgetting Diagnostics.""" | |
| sparsity_threshold: float = 1e-6 # Absolute magnitude threshold tau for parameter sparsity | |
| norm_ratio_threshold: float = 100.0 # Expected ratio ES_norm / GRPO_norm (>100x - 1000x) | |
| grpo_target_sparsity: float = 90.0 # Expected sparsity percentage for GRPO (>90%) | |
| es_max_sparsity: float = 20.0 # Upper bound of expected sparsity for dense ES (<20%) | |
| forgetting_penalty_coeff: float = 0.05 # Penalty weight for prior task degradation estimation | |
| class ComponentMetrics: | |
| """Drift and sparsity metrics for a specific architectural component.""" | |
| name: str | |
| param_count: int | |
| frobenius_norm: float | |
| sparsity_pct: float | |
| mean_abs_delta: float | |
| max_abs_delta: float | |
| class ESDriftMetrics: | |
| """Comprehensive snapshot of model parameter drift and update sparsity.""" | |
| step: int | |
| total_params: int | |
| total_frobenius_norm: float | |
| global_sparsity_pct: float | |
| mean_abs_update: float | |
| component_metrics: Dict[str, ComponentMetrics] = field(default_factory=dict) | |
| estimated_prior_retention_pct: float = 100.0 | |
| def compute_frobenius_norm(delta: torch.Tensor) -> float: | |
| """ | |
| Computes Frobenius norm ||ΔW||_F = sqrt(sum(ΔW_ij^2)). | |
| """ | |
| return float(torch.linalg.norm(delta.float()).item()) | |
| def compute_parameter_sparsity(delta: torch.Tensor, tau: float = 1e-6) -> Tuple[float, int, int]: | |
| """ | |
| Computes sparsity percentage: | |
| Sparsity(ΔW, tau) = (Number of elements with |ΔW_ij| < tau) / (Total elements) * 100.0 | |
| Returns: | |
| (sparsity_percentage, count_below_tau, total_elements) | |
| """ | |
| total = delta.numel() | |
| if total == 0: | |
| return 100.0, 0, 0 | |
| below_tau = int((torch.abs(delta) < tau).sum().item()) | |
| sparsity_pct = (below_tau / total) * 100.0 | |
| return sparsity_pct, below_tau, total | |
| def classify_parameter_component(param_name: str) -> str: | |
| """ | |
| Categorizes a parameter name into canonical transformer architectural groups: | |
| - 'attn_qkv': Query, Key, Value projections | |
| - 'attn_o': Attention output projection | |
| - 'mlp': Feed-forward / Gate / Up / Down projections | |
| - 'norm': LayerNorm / RMSNorm weights and biases | |
| - 'other': Embeddings, classifier head, or miscellaneous | |
| """ | |
| p_lower = param_name.lower() | |
| if any(k in p_lower for k in ["q_proj", "k_proj", "v_proj", "qkv", "query", "key", "value"]): | |
| return "attn_qkv" | |
| elif any(k in p_lower for k in ["o_proj", "out_proj", "dense"]): | |
| return "attn_o" | |
| elif any(k in p_lower for k in ["mlp", "ffn", "gate_proj", "up_proj", "down_proj", "fc1", "fc2"]): | |
| return "mlp" | |
| elif any(k in p_lower for k in ["norm", "ln", "layernorm", "rmsnorm"]): | |
| return "norm" | |
| else: | |
| return "other" | |
| class ESForgettingDiagnosticTracker: | |
| """ | |
| Tracks parameter drift (Frobenius norm) and update sparsity across fine-tuning iterations. | |
| Compares the active model parameters against a frozen baseline checkpoint. | |
| """ | |
| def __init__( | |
| self, | |
| base_model: nn.Module, | |
| config: Optional[ESForgettingConfig] = None | |
| ) -> None: | |
| self.config = config or ESForgettingConfig() | |
| # Save a detached clone of all base parameters | |
| self.base_state: Dict[str, torch.Tensor] = { | |
| name: param.detach().clone().cpu() | |
| for name, param in base_model.named_parameters() | |
| } | |
| self.history: List[ESDriftMetrics] = [] | |
| def inspect_drift(self, current_model: nn.Module, step: int = 0) -> ESDriftMetrics: | |
| """ | |
| Calculates layerwise and global Frobenius norms and sparsity between the current model | |
| and the stored base baseline. | |
| """ | |
| tau = self.config.sparsity_threshold | |
| comp_deltas: Dict[str, List[torch.Tensor]] = { | |
| "attn_qkv": [], | |
| "attn_o": [], | |
| "mlp": [], | |
| "norm": [], | |
| "other": [] | |
| } | |
| total_sq_norm = 0.0 | |
| total_below_tau = 0 | |
| total_elements = 0 | |
| sum_abs_update = 0.0 | |
| for name, param in current_model.named_parameters(): | |
| if name not in self.base_state: | |
| continue | |
| base_p = self.base_state[name].to(param.device) | |
| delta = (param - base_p).detach() | |
| # Global aggregation | |
| sq_norm = float((delta ** 2).sum().item()) | |
| total_sq_norm += sq_norm | |
| abs_sum = float(torch.abs(delta).sum().item()) | |
| sum_abs_update += abs_sum | |
| below_cnt = int((torch.abs(delta) < tau).sum().item()) | |
| total_below_tau += below_cnt | |
| total_elements += delta.numel() | |
| group = classify_parameter_component(name) | |
| comp_deltas[group].append(delta) | |
| global_frob_norm = math.sqrt(total_sq_norm) | |
| global_sparsity = (total_below_tau / total_elements * 100.0) if total_elements > 0 else 100.0 | |
| mean_abs_update = (sum_abs_update / total_elements) if total_elements > 0 else 0.0 | |
| # Component metrics | |
| comp_metrics: Dict[str, ComponentMetrics] = {} | |
| for group, d_list in comp_deltas.items(): | |
| if not d_list: | |
| continue | |
| flat_delta = torch.cat([d.reshape(-1) for d in d_list]) | |
| c_norm = compute_frobenius_norm(flat_delta) | |
| c_spars, _, c_total = compute_parameter_sparsity(flat_delta, tau) | |
| c_mean_abs = float(torch.abs(flat_delta).mean().item()) if c_total > 0 else 0.0 | |
| c_max_abs = float(torch.abs(flat_delta).max().item()) if c_total > 0 else 0.0 | |
| comp_metrics[group] = ComponentMetrics( | |
| name=group, | |
| param_count=c_total, | |
| frobenius_norm=c_norm, | |
| sparsity_pct=c_spars, | |
| mean_abs_delta=c_mean_abs, | |
| max_abs_delta=c_max_abs | |
| ) | |
| # Estimate prior task capability retention: | |
| # Paper 53 Section 3.2: Dense perturbations + high Frobenius drift monotonically decay prior accuracy. | |
| # Theoretical retention model: Retention = 100% - penalty * ||ΔW||_F * (1 - Sparsity/100) | |
| density_factor = 1.0 - (global_sparsity / 100.0) | |
| drift_penalty = self.config.forgetting_penalty_coeff * global_frob_norm * density_factor | |
| retention_est = max(0.0, min(100.0, 100.0 - drift_penalty)) | |
| snapshot = ESDriftMetrics( | |
| step=step, | |
| total_params=total_elements, | |
| total_frobenius_norm=global_frob_norm, | |
| global_sparsity_pct=global_sparsity, | |
| mean_abs_update=mean_abs_update, | |
| component_metrics=comp_metrics, | |
| estimated_prior_retention_pct=retention_est | |
| ) | |
| self.history.append(snapshot) | |
| return snapshot | |
| class GradientVsESDriftSimulator: | |
| """ | |
| Simulates the mathematical divergence between gradient-based updates (GRPO) | |
| and gradient-free population perturbations (ES) over identical optimization steps. | |
| Mathematical Basis (Paper 53, Sections 3.2 - 3.3): | |
| - GRPO update: Sparse, low-rank, directional gradient along task subspace: | |
| ΔW_GRPO = alpha * G, where G is low-rank / sparse (nonzero only on critical path). | |
| - ES update: Full-parameter pseudo-gradient from isotropic Gaussian perturbations: | |
| ΔW_ES = (alpha / N) * sum(Z_n * eps_n), where eps_n ~ N(0, I) across all D dimensions. | |
| - Demonstrates: | |
| 1. ||ΔW_ES||_F >> ||ΔW_GRPO||_F (up to 1000x difference) | |
| 2. Sparsity(ΔW_GRPO, tau) >> Sparsity(ΔW_ES, tau) (~95% vs <10%) | |
| """ | |
| def __init__( | |
| self, | |
| d_in: int = 128, | |
| d_out: int = 128, | |
| seed: int = 42 | |
| ) -> None: | |
| torch.manual_seed(seed) | |
| self.d_in = d_in | |
| self.d_out = d_out | |
| self.w_base = torch.randn(d_out, d_in) * 0.02 | |
| def simulate_steps( | |
| self, | |
| steps: int = 100, | |
| population_size: int = 30, | |
| sigma: float = 0.001, | |
| lr_es: float = 0.0005, | |
| lr_grpo: float = 0.0005, | |
| tau: float = 1e-6 | |
| ) -> Dict[str, Any]: | |
| """ | |
| Executes simulated optimization runs for both ES and GRPO and extracts drift metrics. | |
| """ | |
| w_es = self.w_base.clone() | |
| w_grpo = self.w_base.clone() | |
| # Task subspace: only rank-2 active direction for the target task | |
| task_u = torch.randn(self.d_out, 2) | |
| task_v = torch.randn(self.d_in, 2) | |
| task_u, _ = torch.linalg.qr(task_u) | |
| task_v, _ = torch.linalg.qr(task_v) | |
| for step in range(steps): | |
| # GRPO: Directional, highly sparse gradient in task subspace | |
| # Gradient is concentrated on active singular vectors with small noise | |
| grad_core = torch.matmul(task_u, task_v.T) * 0.1 | |
| # Add sparse mask: only top 5% weights receive direct gradient update | |
| flat_grad = grad_core.reshape(-1) | |
| k = int(flat_grad.numel() * 0.05) | |
| topk_vals, topk_idx = torch.topk(torch.abs(flat_grad), k) | |
| sparse_mask = torch.zeros_like(flat_grad) | |
| sparse_mask[topk_idx] = 1.0 | |
| grad_sparse = (flat_grad * sparse_mask).reshape_as(grad_core) | |
| w_grpo = w_grpo + lr_grpo * grad_sparse | |
| # ES: In-place isotropic perturbation across all D dimensions (Algorithm 2 & Section 3.3) | |
| # eps_n ~ N(0, I) | |
| epsilons = torch.randn(population_size, self.d_out, self.d_in) | |
| # Evaluate synthetic reward: alignment with task subspace + Gaussian noise | |
| rewards = torch.zeros(population_size) | |
| for n in range(population_size): | |
| perturbed = w_es + sigma * epsilons[n] | |
| alignment = (perturbed * grad_core).sum().item() | |
| rewards[n] = alignment + float(torch.randn(1).item() * 0.01) | |
| # Z-score normalization | |
| r_mean = rewards.mean() | |
| r_std = rewards.std() + 1e-8 | |
| z_rewards = (rewards - r_mean) / r_std | |
| # Digested update: theta <- theta + alpha * (1/N) * sum(Z_n * eps_n) | |
| delta_es = lr_es * torch.mean(z_rewards.view(population_size, 1, 1) * epsilons, dim=0) | |
| w_es = w_es + delta_es | |
| delta_w_es = w_es - self.w_base | |
| delta_w_grpo = w_grpo - self.w_base | |
| frob_es = compute_frobenius_norm(delta_w_es) | |
| frob_grpo = compute_frobenius_norm(delta_w_grpo) | |
| spars_es, _, _ = compute_parameter_sparsity(delta_w_es, tau=tau) | |
| spars_grpo, _, _ = compute_parameter_sparsity(delta_w_grpo, tau=tau) | |
| return { | |
| "steps": steps, | |
| "es_frobenius_norm": frob_es, | |
| "grpo_frobenius_norm": frob_grpo, | |
| "norm_ratio_es_to_grpo": frob_es / (frob_grpo + 1e-8), | |
| "es_sparsity_pct": spars_es, | |
| "grpo_sparsity_pct": spars_grpo, | |
| "delta_w_es": delta_w_es, | |
| "delta_w_grpo": delta_w_grpo | |
| } | |
| def verify_es_forgetting_invariants() -> Dict[str, Any]: | |
| """ | |
| Self-verification function testing all core mathematical invariants of Paper 53: | |
| 1. Frobenius norm calculation accuracy. | |
| 2. Sparsity metric under threshold tau = 1e-6. | |
| 3. Component classification across transformer projections. | |
| 4. Divergence between ES and GRPO: ES Frobenius norm >> GRPO and ES sparsity << GRPO. | |
| """ | |
| # 1. Toy Transformer Module | |
| class TinyTransformerBlock(nn.Module): | |
| def __init__(self, d_model: int = 64): | |
| super().__init__() | |
| self.q_proj = nn.Linear(d_model, d_model, bias=False) | |
| self.k_proj = nn.Linear(d_model, d_model, bias=False) | |
| self.v_proj = nn.Linear(d_model, d_model, bias=False) | |
| self.o_proj = nn.Linear(d_model, d_model, bias=False) | |
| self.gate_proj = nn.Linear(d_model, d_model * 2, bias=False) | |
| self.up_proj = nn.Linear(d_model, d_model * 2, bias=False) | |
| self.down_proj = nn.Linear(d_model * 2, d_model, bias=False) | |
| self.ln1 = nn.LayerNorm(d_model) | |
| self.ln2 = nn.LayerNorm(d_model) | |
| model = TinyTransformerBlock() | |
| tracker = ESForgettingDiagnosticTracker(model) | |
| # 2. Apply simulated dense ES perturbation | |
| with torch.no_grad(): | |
| for name, param in model.named_parameters(): | |
| if "ln" in name: | |
| # LayerNorm gets very small perturbation (Paper 53 Section 3.3) | |
| param.add_(torch.randn_like(param) * 1e-7) | |
| else: | |
| param.add_(torch.randn_like(param) * 1e-3) | |
| metrics = tracker.inspect_drift(model, step=100) | |
| # Invariant 1: Frobenius norm must be strictly positive | |
| assert metrics.total_frobenius_norm > 0.0, "Total Frobenius norm must be positive" | |
| # Invariant 2: Attention and MLP layers must have low sparsity under dense perturbation | |
| assert metrics.component_metrics["attn_qkv"].sparsity_pct < 20.0, "Attention projections should be dense in ES" | |
| assert metrics.component_metrics["mlp"].sparsity_pct < 20.0, "MLP projections should be dense in ES" | |
| # Invariant 3: LayerNorm with < 1e-6 deltas must show high sparsity | |
| assert metrics.component_metrics["norm"].sparsity_pct > 80.0, "LayerNorm with < tau deltas must show high sparsity" | |
| # 3. Simulate ES vs GRPO optimization divergence | |
| sim = GradientVsESDriftSimulator(d_in=64, d_out=64, seed=123) | |
| res = sim.simulate_steps(steps=50, population_size=20, sigma=0.001, lr_es=0.001, lr_grpo=0.001, tau=1e-6) | |
| # Invariant 4: ES Frobenius norm must exceed GRPO Frobenius norm | |
| assert res["es_frobenius_norm"] > res["grpo_frobenius_norm"], ( | |
| f"ES norm ({res['es_frobenius_norm']}) should exceed GRPO norm ({res['grpo_frobenius_norm']})" | |
| ) | |
| # Invariant 5: GRPO sparsity must substantially exceed ES sparsity | |
| assert res["grpo_sparsity_pct"] > res["es_sparsity_pct"], ( | |
| f"GRPO sparsity ({res['grpo_sparsity_pct']}%) should exceed ES sparsity ({res['es_sparsity_pct']}%)" | |
| ) | |
| return { | |
| "status": "PASS", | |
| "total_frobenius_norm": metrics.total_frobenius_norm, | |
| "global_sparsity_pct": metrics.global_sparsity_pct, | |
| "norm_ratio_es_to_grpo": res["norm_ratio_es_to_grpo"], | |
| "es_sparsity_pct": res["es_sparsity_pct"], | |
| "grpo_sparsity_pct": res["grpo_sparsity_pct"], | |
| "estimated_retention": metrics.estimated_prior_retention_pct | |
| } | |
| if __name__ == "__main__": | |
| result = verify_es_forgetting_invariants() | |
| print("=== ES Forgetting Engine Verification ===") | |
| for k, v in result.items(): | |
| print(f" {k}: {v}") | |