Skip to content

Commit 0c5cdcd

Browse files
authored
Add klshampoo (#302)
Signed-off-by: Hao Wu <skyw@nvidia.com>
1 parent 6117b1b commit 0c5cdcd

4 files changed

Lines changed: 270 additions & 38 deletions

File tree

‎emerging_optimizers/shampoo/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
from emerging_optimizers.shampoo.shampoo import Shampoo
16+
from emerging_optimizers.shampoo.shampoo import KlShampoo, Shampoo
1717
from emerging_optimizers.shampoo.soap_v3 import KlMSoap, KlSoapV3, ReklsV3

‎emerging_optimizers/shampoo/shampoo.py‎

Lines changed: 137 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15+
import math
1516
from typing import TYPE_CHECKING, Callable, ClassVar, override
1617

1718

@@ -23,18 +24,36 @@
2324
from torch.optim.optimizer import ParamsT
2425

2526
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
2729
from emerging_optimizers.shampoo import precond_base
2830
from emerging_optimizers.utils import eig as eig_utils
2931

3032

3133
__all__ = [
34+
"KlShampoo",
35+
"KlShampooPreconditioner",
3236
"Shampoo",
3337
"ShampooBase",
3438
"ShampooPreconditioner",
3539
]
3640

3741

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+
3857
class ShampooPreconditioner:
3958
"""Per-parameter Shampoo preconditioner holding the Kronecker factors of one 2D parameter.
4059
@@ -128,38 +147,106 @@ def step(self, grad: torch.Tensor, shampoo_beta: float) -> None:
128147
"""
129148
self.update_kronecker_factors(grad, shampoo_beta)
130149

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.
133152
134153
Args:
135-
kronecker_factor: left or right kronecker factor
154+
x: Matrix in the parameter basis.
136155
137156
Returns:
138-
The inverse root of the factor.
157+
The preconditioned matrix, in the parameter basis.
139158
"""
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)
141161

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)
144164

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
149166

150-
def precondition(self, x: torch.Tensor) -> torch.Tensor:
151-
"""Applies both root inverse to a matrix in the parameter basis.
152167

153-
Args:
154-
x: Matrix in the parameter basis.
168+
class KlShampooPreconditioner(ShampooPreconditioner):
169+
"""Shampoo preconditioner with the KL-corrected Kronecker factor update."""
155170

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.
158221
"""
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)
161223

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
163250

164251

165252
class ShampooBase(optim.Optimizer, opt_mixin.WeightDecayMixin):
@@ -286,7 +373,7 @@ def step(self, closure: Callable[[], float] | None = None) -> float | None:
286373
Args:
287374
closure: Unsupported; must be ``None``.
288375
289-
Raises:f
376+
Raises:
290377
ValueError: If ``closure`` is not ``None``.
291378
"""
292379
if closure is not None:
@@ -359,3 +446,31 @@ def _scalar_update(
359446
"""
360447
exp_avg.lerp_(grad, 1 - momentum)
361448
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

Comments
 (0)