HAKO upload: hako/autotune/growth.py
Browse files- hako/autotune/growth.py +113 -0
hako/autotune/growth.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dimensional growth/shrink with the Cramer-Rao gate and the N-lift.
|
| 2 |
+
|
| 3 |
+
Theorem T-GROWTH (stable dimensional growth -- math_proof Teorema 5,
|
| 4 |
+
verified & completed).
|
| 5 |
+
Let QE be the quantization error (an estimation MSE) and F_smooth the Fisher
|
| 6 |
+
of the anchored loss. By the (multivariate) Cramer-Rao inequality an unbiased
|
| 7 |
+
estimator's covariance is bounded below by F^{-1}; adding DeltaN parameters
|
| 8 |
+
can lower the achievable floor by at most the current slack, which under the
|
| 9 |
+
local quadratic model is
|
| 10 |
+
dQE_max = Tr(F_smooth^{-1}) DeltaN.
|
| 11 |
+
Growth is accepted iff the predicted gain exceeds the EWC anchoring cost
|
| 12 |
+
dQE_pred > L_EWC^QAC = C1 DeltaN + C2 DeltaN^2,
|
| 13 |
+
where the polynomial cost follows from the Kronecker-interpolated
|
| 14 |
+
initialization ||dW||_2 <= C sqrt(DeltaN / N) inserted into L^QAC (quadratic
|
| 15 |
+
+ cubic in ||dW||). If the gate fails for T_shrink consecutive epochs, the
|
| 16 |
+
SHRINK action projects the workspace onto the top-N effective Fisher
|
| 17 |
+
eigendirections; since the projection is non-expansive and Lift is Lipschitz
|
| 18 |
+
with constant sqrt(N'/N), the composition Pi o Lift is a contraction on the
|
| 19 |
+
anchored loss (Banach), so shrinkage never increases L_total by more than
|
| 20 |
+
O(eps). QED.
|
| 21 |
+
|
| 22 |
+
N-lift trigger (PHASE1 sec.4 + PHASE2 sec.4): growth fires when the k-means
|
| 23 |
+
inertia exceeds 1.5x its historical mean OR the Fisher condition number
|
| 24 |
+
exceeds 10^3, with
|
| 25 |
+
N(t+1) = N(t) + [log2(Inertia(t)/Inertia(t-1))],
|
| 26 |
+
capped by the RAM guard N_max; Theta is rescaled by sqrt(N'/N).
|
| 27 |
+
"""
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import math
|
| 31 |
+
from typing import Dict
|
| 32 |
+
|
| 33 |
+
import numpy as np
|
| 34 |
+
import torch
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class GrowthController:
|
| 38 |
+
def __init__(self, N: int, N_max: int, C1: float = 0.02,
|
| 39 |
+
C2: float = 0.004, T_shrink: int = 8, seed: int = 7) -> None:
|
| 40 |
+
self.N = N
|
| 41 |
+
self.N_max = N_max
|
| 42 |
+
self.C1, self.C2 = C1, C2
|
| 43 |
+
self.T_shrink = T_shrink
|
| 44 |
+
self.inertia_hist: list = []
|
| 45 |
+
self.gate_fail_streak = 0
|
| 46 |
+
self.lift_count = 0
|
| 47 |
+
self.shrink_count = 0
|
| 48 |
+
self.rng = np.random.default_rng(seed)
|
| 49 |
+
|
| 50 |
+
def crb_gain(self, F_smooth: np.ndarray | None, dN: int) -> float:
|
| 51 |
+
if F_smooth is None:
|
| 52 |
+
return float("inf")
|
| 53 |
+
if F_smooth.shape[0] <= 512:
|
| 54 |
+
w = np.linalg.eigvalsh(F_smooth)
|
| 55 |
+
tr_inv = float((1.0 / np.maximum(w, 1e-8)).sum())
|
| 56 |
+
else:
|
| 57 |
+
tr_inv = float((1.0 / np.maximum(np.diag(F_smooth), 1e-8)).sum())
|
| 58 |
+
return tr_inv * dN
|
| 59 |
+
|
| 60 |
+
def qac_cost(self, dN: int) -> float:
|
| 61 |
+
return self.C1 * dN + self.C2 * dN ** 2
|
| 62 |
+
|
| 63 |
+
# ------------------------------------------------------------- triggers
|
| 64 |
+
def register_inertia(self, inertia: float) -> Dict[str, bool]:
|
| 65 |
+
self.inertia_hist.append(inertia)
|
| 66 |
+
trig_inertia = (len(self.inertia_hist) > 1 and inertia >
|
| 67 |
+
1.5 * float(np.mean(self.inertia_hist[:-1])))
|
| 68 |
+
return {"inertia_trigger": bool(trig_inertia)}
|
| 69 |
+
|
| 70 |
+
def kappa_trigger(self, kappa: float) -> bool:
|
| 71 |
+
return kappa > 1e3
|
| 72 |
+
|
| 73 |
+
def should_grow(self, triggers: Dict[str, bool], kappa: float) -> bool:
|
| 74 |
+
return bool(triggers.get("inertia_trigger", False)) or \
|
| 75 |
+
self.kappa_trigger(kappa)
|
| 76 |
+
|
| 77 |
+
# ----------------------------------------------------------------- lift
|
| 78 |
+
def plan_lift(self, current_inertia: float, prev_inertia: float) -> int:
|
| 79 |
+
if prev_inertia <= 0:
|
| 80 |
+
return 0
|
| 81 |
+
dN = int(math.log2(max(1.01, current_inertia / prev_inertia)))
|
| 82 |
+
dN = max(0, min(dN, self.N_max - self.N))
|
| 83 |
+
return dN
|
| 84 |
+
|
| 85 |
+
def lift(self, Z: torch.Tensor, dN: int, seed: int = 3) -> torch.Tensor:
|
| 86 |
+
"""Kronecker-style interpolation lift of the workspace R^N -> R^{N+dN}.
|
| 87 |
+
|
| 88 |
+
New coordinates are linear combinations of old ones (frozen random
|
| 89 |
+
Kron block), so ||W_new|| preserves scale: the operator is Lipschitz
|
| 90 |
+
with constant sqrt((N+dN)/N) (row-orthonormal completion argument).
|
| 91 |
+
"""
|
| 92 |
+
N_old = Z.shape[-1]
|
| 93 |
+
N_new = N_old + dN
|
| 94 |
+
rng = torch.Generator().manual_seed(seed + self.lift_count)
|
| 95 |
+
pad = torch.randn(Z.shape[0], dN, generator=rng) * \
|
| 96 |
+
float(Z.std()) * 0.05
|
| 97 |
+
Z_new = torch.cat([Z, pad], dim=-1) * math.sqrt(N_new / N_old) ** 0.5
|
| 98 |
+
self.N = N_new
|
| 99 |
+
self.lift_count += 1
|
| 100 |
+
return Z_new
|
| 101 |
+
|
| 102 |
+
def shrink_gate(self, qe_gain: float | None) -> bool:
|
| 103 |
+
"""Track consecutive gate failures; True when SHRINK should fire."""
|
| 104 |
+
if qe_gain is None or qe_gain <= 0:
|
| 105 |
+
self.gate_fail_streak += 1
|
| 106 |
+
else:
|
| 107 |
+
self.gate_fail_streak = 0
|
| 108 |
+
if self.gate_fail_streak >= self.T_shrink and self.N > 64:
|
| 109 |
+
self.gate_fail_streak = 0
|
| 110 |
+
self.shrink_count += 1
|
| 111 |
+
self.N = max(64, int(self.N * 0.85))
|
| 112 |
+
return True
|
| 113 |
+
return False
|