|
12 | 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | 13 | # See the License for the specific language governing permissions and |
14 | 14 | # limitations under the License. |
| 15 | +import math |
15 | 16 | from typing import TYPE_CHECKING, Callable, ClassVar, override |
16 | 17 |
|
17 | 18 |
|
|
23 | 24 | from torch.optim.optimizer import ParamsT |
24 | 25 |
|
25 | 26 | from emerging_optimizers import mixin as opt_mixin |
26 | | -from emerging_optimizers import registry |
| 27 | +from emerging_optimizers import registry, utils |
| 28 | +from emerging_optimizers.legacy_soap import soap |
27 | 29 | from emerging_optimizers.shampoo import precond_base |
28 | 30 | from emerging_optimizers.utils import eig as eig_utils |
29 | 31 |
|
30 | 32 |
|
31 | 33 | __all__ = [ |
| 34 | + "KlShampoo", |
| 35 | + "KlShampooPreconditioner", |
32 | 36 | "Shampoo", |
33 | 37 | "ShampooBase", |
34 | 38 | "ShampooPreconditioner", |
35 | 39 | ] |
36 | 40 |
|
37 | 41 |
|
| 42 | +def _get_root_inverse_from_eigens( |
| 43 | + eigvals: torch.Tensor, |
| 44 | + eigvecs: torch.Tensor, |
| 45 | + p_root_inv: float, |
| 46 | + eps: float, |
| 47 | +) -> torch.Tensor: |
| 48 | + # Eigh can sometime return negative values for numerical 0; clamping to 0 removes them |
| 49 | + eigvals = eigvals.clamp_min(0) |
| 50 | + |
| 51 | + # Tikhonov regularization |
| 52 | + exp = 1.0 / p_root_inv |
| 53 | + inv_root_scale = eigvals**exp / (eigvals ** (2 * exp) + eps ** (2 * exp)) |
| 54 | + return (eigvecs * inv_root_scale) @ eigvecs.mT |
| 55 | + |
| 56 | + |
38 | 57 | class ShampooPreconditioner: |
39 | 58 | """Per-parameter Shampoo preconditioner holding the Kronecker factors of one 2D parameter. |
40 | 59 |
|
@@ -128,38 +147,106 @@ def step(self, grad: torch.Tensor, shampoo_beta: float) -> None: |
128 | 147 | """ |
129 | 148 | self.update_kronecker_factors(grad, shampoo_beta) |
130 | 149 |
|
131 | | - def _get_root_inverse(self, kronecker_factor: torch.Tensor) -> torch.Tensor: |
132 | | - """Computes ``kronecker_factor^(-1/p_root_inv)`` from its eigendecomposition. |
| 150 | + def precondition(self, x: torch.Tensor) -> torch.Tensor: |
| 151 | + """Applies both root inverse to a matrix in the parameter basis. |
133 | 152 |
|
134 | 153 | Args: |
135 | | - kronecker_factor: left or right kronecker factor |
| 154 | + x: Matrix in the parameter basis. |
136 | 155 |
|
137 | 156 | Returns: |
138 | | - The inverse root of the factor. |
| 157 | + The preconditioned matrix, in the parameter basis. |
139 | 158 | """ |
140 | | - eigvals, eigvecs = eig_utils.eigh_with_fallback(kronecker_factor) |
| 159 | + eigvals_L, eigvecs_L = eig_utils.eigh_with_fallback(self.kronecker_factor_pair.L) |
| 160 | + eigvals_R, eigvecs_R = eig_utils.eigh_with_fallback(self.kronecker_factor_pair.R) |
141 | 161 |
|
142 | | - # Eigh can sometime return negative values for numerical 0; clamping to 0 removes them |
143 | | - eigvals = eigvals.clamp_min(0) |
| 162 | + root_inv_L = _get_root_inverse_from_eigens(eigvals_L, eigvecs_L, self.p_root_inv, self.eps) |
| 163 | + root_inv_R = _get_root_inverse_from_eigens(eigvals_R, eigvecs_R, self.p_root_inv, self.eps) |
144 | 164 |
|
145 | | - # Tikhonov regularization |
146 | | - exp = 1.0 / self.p_root_inv |
147 | | - inv_root_scale = eigvals**exp / (eigvals ** (2 * exp) + self.eps ** (2 * exp)) |
148 | | - return (eigvecs * inv_root_scale) @ eigvecs.mT |
| 165 | + return root_inv_L @ x @ root_inv_R |
149 | 166 |
|
150 | | - def precondition(self, x: torch.Tensor) -> torch.Tensor: |
151 | | - """Applies both root inverse to a matrix in the parameter basis. |
152 | 167 |
|
153 | | - Args: |
154 | | - x: Matrix in the parameter basis. |
| 168 | +class KlShampooPreconditioner(ShampooPreconditioner): |
| 169 | + """Shampoo preconditioner with the KL-corrected Kronecker factor update.""" |
155 | 170 |
|
156 | | - Returns: |
157 | | - The preconditioned matrix, in the parameter basis. |
| 171 | + def __init__( |
| 172 | + self, |
| 173 | + state: dict, |
| 174 | + p_root_inv: float, |
| 175 | + eps: float, |
| 176 | + ) -> None: |
| 177 | + super().__init__(state, p_root_inv, eps) |
| 178 | + self.eigenbasis_pair = precond_base.TensorPair(state["Q_L"], state["Q_R"]) |
| 179 | + self.eigvals_pair = precond_base.TensorPair(state["eigvals_L"], state["eigvals_R"]) |
| 180 | + |
| 181 | + @staticmethod |
| 182 | + @override |
| 183 | + def init_state( |
| 184 | + shape: tuple[int, ...], |
| 185 | + device: torch.device, |
| 186 | + ) -> dict[str, torch.Tensor]: |
| 187 | + state = ShampooPreconditioner.init_state(shape, device) |
| 188 | + m, n = shape |
| 189 | + state.update( |
| 190 | + { |
| 191 | + "Q_L": torch.eye(m, device=device), |
| 192 | + "Q_R": torch.eye(n, device=device), |
| 193 | + "eigvals_L": torch.zeros(m, device=device), |
| 194 | + "eigvals_R": torch.zeros(n, device=device), |
| 195 | + } |
| 196 | + ) |
| 197 | + return state |
| 198 | + |
| 199 | + @override |
| 200 | + def rebind_state(self, state: dict) -> None: |
| 201 | + updates = { |
| 202 | + "L": self.kronecker_factor_pair.L, |
| 203 | + "R": self.kronecker_factor_pair.R, |
| 204 | + "Q_L": self.eigenbasis_pair.L, |
| 205 | + "Q_R": self.eigenbasis_pair.R, |
| 206 | + "eigvals_L": self.eigvals_pair.L, |
| 207 | + "eigvals_R": self.eigvals_pair.R, |
| 208 | + } |
| 209 | + missing = updates.keys() - state.keys() |
| 210 | + if missing: |
| 211 | + raise KeyError(f"rebind_state: state missing keys {sorted(missing)}") |
| 212 | + state.update(updates) |
| 213 | + |
| 214 | + @override |
| 215 | + def init_step(self, grad: torch.Tensor, shampoo_beta: float) -> None: |
| 216 | + """Performs the first step's factor update, before any history exists. |
| 217 | +
|
| 218 | + Args: |
| 219 | + grad: Gradient of the parameter on the first step. |
| 220 | + shampoo_beta: EMA coefficient for the Kronecker factor update. |
158 | 221 | """ |
159 | | - root_inv_L = self._get_root_inverse(self.kronecker_factor_pair.L) |
160 | | - root_inv_R = self._get_root_inverse(self.kronecker_factor_pair.R) |
| 222 | + self.update_kronecker_factors(grad, shampoo_beta) |
161 | 223 |
|
162 | | - return root_inv_L @ x @ root_inv_R |
| 224 | + @override |
| 225 | + def update_kronecker_factors(self, grad: torch.Tensor, shampoo_beta: float) -> None: |
| 226 | + with utils.fp32_matmul_precision("highest"): |
| 227 | + soap.update_kronecker_factors_kl_shampoo( |
| 228 | + self.kronecker_factor_pair, |
| 229 | + grad, |
| 230 | + shampoo_beta, |
| 231 | + self.eigenbasis_pair, |
| 232 | + self.eigvals_pair, |
| 233 | + self.eps, |
| 234 | + ) |
| 235 | + |
| 236 | + @override |
| 237 | + def precondition(self, x: torch.Tensor) -> torch.Tensor: |
| 238 | + eigvals_L, eigvecs_L = eig_utils.eigh_with_fallback(self.kronecker_factor_pair.L) |
| 239 | + eigvals_R, eigvecs_R = eig_utils.eigh_with_fallback(self.kronecker_factor_pair.R) |
| 240 | + |
| 241 | + self.eigenbasis_pair = precond_base.TensorPair(eigvecs_L, eigvecs_R) |
| 242 | + self.eigvals_pair = precond_base.TensorPair(eigvals_L, eigvals_R) |
| 243 | + |
| 244 | + root_inv_L = _get_root_inverse_from_eigens(eigvals_L, eigvecs_L, self.p_root_inv, self.eps) |
| 245 | + root_inv_R = _get_root_inverse_from_eigens(eigvals_R, eigvecs_R, self.p_root_inv, self.eps) |
| 246 | + |
| 247 | + m, n = x.shape |
| 248 | + shape_scale = math.sqrt(m / n) / (math.sqrt(m) + math.sqrt(n)) |
| 249 | + return (root_inv_L @ x @ root_inv_R) * shape_scale |
163 | 250 |
|
164 | 251 |
|
165 | 252 | class ShampooBase(optim.Optimizer, opt_mixin.WeightDecayMixin): |
@@ -286,7 +373,7 @@ def step(self, closure: Callable[[], float] | None = None) -> float | None: |
286 | 373 | Args: |
287 | 374 | closure: Unsupported; must be ``None``. |
288 | 375 |
|
289 | | - Raises:f |
| 376 | + Raises: |
290 | 377 | ValueError: If ``closure`` is not ``None``. |
291 | 378 | """ |
292 | 379 | if closure is not None: |
@@ -359,3 +446,31 @@ def _scalar_update( |
359 | 446 | """ |
360 | 447 | exp_avg.lerp_(grad, 1 - momentum) |
361 | 448 | return exp_avg |
| 449 | + |
| 450 | + |
| 451 | +@registry.register_optimizer("kl_shampoo") |
| 452 | +class KlShampoo(Shampoo): |
| 453 | + """Shampoo with KL-corrected Kronecker factors and EMA momentum.""" |
| 454 | + |
| 455 | + PreconditionerCls: ClassVar[type[precond_base.ShampooPreconditionerProtocol]] = KlShampooPreconditioner |
| 456 | + |
| 457 | + def __init__( |
| 458 | + self, |
| 459 | + params: ParamsT, |
| 460 | + lr: float, |
| 461 | + momentum: float = 0.9, |
| 462 | + shampoo_beta: float = 0.95, |
| 463 | + eps: float = 1e-8, |
| 464 | + weight_decay: float = 0.01, |
| 465 | + *, |
| 466 | + p_root_inv: float = 4, |
| 467 | + ) -> None: |
| 468 | + super().__init__( |
| 469 | + params, |
| 470 | + lr, |
| 471 | + momentum, |
| 472 | + shampoo_beta, |
| 473 | + eps, |
| 474 | + weight_decay, |
| 475 | + p_root_inv=p_root_inv, |
| 476 | + ) |
0 commit comments