|
13 | 13 | # See the License for the specific language governing permissions and |
14 | 14 | # limitations under the License. |
15 | 15 |
|
16 | | -from typing import TYPE_CHECKING, Callable, override |
17 | | - |
18 | | -import torch |
19 | | -from torch import distributed as dist |
20 | | -from torch import optim |
21 | 16 | from torch.optim.optimizer import ParamsT |
22 | 17 |
|
23 | | - |
24 | | -if TYPE_CHECKING: |
25 | | - from typing import overload |
26 | | - |
27 | 18 | from emerging_optimizers import mixin as opt_mixin |
28 | | -from emerging_optimizers import registry, utils |
29 | | -from emerging_optimizers.scalar_optimizers import update_functions |
30 | | -from emerging_optimizers.soap import soap, soap_utils, tp_utils |
| 19 | +from emerging_optimizers import registry |
31 | 20 | from emerging_optimizers.soap.soap import SOAP |
32 | | -from emerging_optimizers.utils import FP32MatmulPrecT, get_pg_rank, get_pg_size |
33 | 21 |
|
34 | 22 |
|
35 | | -__all__ = ["REKLS", "TpRekls"] |
| 23 | +__all__ = ["REKLS"] |
36 | 24 |
|
37 | 25 |
|
38 | 26 | @registry.register_optimizer("rekls") |
@@ -68,218 +56,3 @@ def __init__( |
68 | 56 | use_eigh=True, |
69 | 57 | use_kl_shampoo=True, |
70 | 58 | ) |
71 | | - |
72 | | - |
73 | | -@registry.register_optimizer("tp_rekls") |
74 | | -class TpRekls(opt_mixin.WeightDecayMixin, optim.Optimizer): |
75 | | - """Tensor-parallel variant of :class:`REKLS`. |
76 | | -
|
77 | | - Reimplemented from scratch (not inheriting from :class:`~emerging_optimizers.soap.soap.SOAP`) so the |
78 | | - tensor-parallel bookkeeping stays isolated. Eigenbases are not stored in optimizer state; they are |
79 | | - recomputed via :func:`~emerging_optimizers.soap.soap_utils.get_eigenbasis_eigh` from the kronecker |
80 | | - factors. Each step calls eigh twice — once on the pre-update L, R for the |
81 | | - :func:`~emerging_optimizers.soap.soap.update_kronecker_factors_kl_shampoo` correction, and once on |
82 | | - the post-update L, R for the gradient projection. |
83 | | -
|
84 | | - State per parameter (one entry per rank): |
85 | | - - ``step`` |
86 | | - - ``exp_avg``, ``exp_avg_sq``: full-size tensors duplicated across ``tp_group`` ranks. ``exp_avg`` |
87 | | - is rotated through the basis change between steps (project back via the pre-update eigenbasis, |
88 | | - then forward via the post-update eigenbasis), matching SOAP's |
89 | | - :func:`~emerging_optimizers.soap.soap.update_eigenbasis_and_exp_avgs`. ``exp_avg_sq`` is not |
90 | | - rotated, matching SOAP's eigh path. |
91 | | - - ``L``, ``R``: kronecker factor matrices, sharded along dimension 0 across ``tp_group``. |
92 | | -
|
93 | | - Args: |
94 | | - params: Iterable of parameters to optimize or dicts defining parameter groups. |
95 | | - lr: Learning rate. |
96 | | - betas: Inner Adam betas ``(b1, b2)``. |
97 | | - shampoo_beta: Beta for the kronecker factor moving average. |
98 | | - eps: Inner Adam epsilon. |
99 | | - weight_decay: Weight decay coefficient. |
100 | | - weight_decay_method: See :class:`~emerging_optimizers.mixin.WeightDecayMixin`. |
101 | | - tp_group: Process group across which parameters and gradients are sharded. |
102 | | - fp32_matmul_prec: Precision for the optimizer-state GEMM operations. |
103 | | -
|
104 | | - Note: |
105 | | - Sharding is configured per-parameter-group via ``partition_dim`` (an int in ``{0, 1}``, |
106 | | - or ``None`` for replicated parameters). Mixed-layout models should use one group per |
107 | | - distinct ``partition_dim``:: |
108 | | -
|
109 | | - optimizer = TpRekls([ |
110 | | - {"params": column_parallel_params, "partition_dim": 0}, |
111 | | - {"params": row_parallel_params, "partition_dim": 1}, |
112 | | - {"params": replicated_params, "partition_dim": None}, |
113 | | - ], lr=1e-3, tp_group=tp_group) |
114 | | -
|
115 | | - Groups without ``partition_dim`` use the default (``None`` → replicated, plain non-TP REKLS |
116 | | - step on each rank, no collectives, full-size ``L``/``R``). |
117 | | - """ |
118 | | - |
119 | | - def __init__( |
120 | | - self, |
121 | | - params: ParamsT, |
122 | | - lr: float, |
123 | | - betas: tuple[float, float] = (0.9, 0.95), |
124 | | - shampoo_beta: float = 0.95, |
125 | | - eps: float = 1e-8, |
126 | | - weight_decay: float = 0.01, |
127 | | - *, |
128 | | - weight_decay_method: opt_mixin.WeightDecayT = "decoupled", |
129 | | - tp_group: dist.ProcessGroup, |
130 | | - fp32_matmul_prec: FP32MatmulPrecT = "high", |
131 | | - ) -> None: |
132 | | - self.tp_group = tp_group |
133 | | - self.tp_size = get_pg_size(tp_group) |
134 | | - self.tp_rank = get_pg_rank(tp_group) |
135 | | - |
136 | | - self.weight_decay_method = weight_decay_method |
137 | | - self.fp32_matmul_prec = fp32_matmul_prec |
138 | | - |
139 | | - defaults = { |
140 | | - "lr": lr, |
141 | | - "betas": betas, |
142 | | - "shampoo_beta": shampoo_beta, |
143 | | - "eps": eps, |
144 | | - "weight_decay": weight_decay, |
145 | | - "partition_dim": None, |
146 | | - } |
147 | | - super().__init__(params, defaults) |
148 | | - |
149 | | - @staticmethod |
150 | | - def _validate_partition_dim(partition_dim: int | None) -> int | None: |
151 | | - if partition_dim is not None and partition_dim not in (0, 1): |
152 | | - raise ValueError(f"partition_dim must be 0, 1, or None, got {partition_dim}") |
153 | | - return partition_dim |
154 | | - |
155 | | - @torch.no_grad() # type: ignore[misc] |
156 | | - def _init_group(self, group: dict, skip_non_grad_params: bool = True) -> None: |
157 | | - partition_dim = self._validate_partition_dim(group["partition_dim"]) |
158 | | - for p in group["params"]: |
159 | | - if skip_non_grad_params and p.grad is None: |
160 | | - continue |
161 | | - if p.dim() != 2: |
162 | | - raise TypeError("TpRekls is only supported for 2D tensors") |
163 | | - state = self.state[p] |
164 | | - if len(state) == 0: |
165 | | - m, n = p.shape |
166 | | - # Get full size of m, n if the parameter is tensor-parallel. |
167 | | - if partition_dim == 0: |
168 | | - m *= self.tp_size |
169 | | - elif partition_dim == 1: |
170 | | - n *= self.tp_size |
171 | | - |
172 | | - # Both dimensions must be divisible by tp_size for the L/R shards (each sharded |
173 | | - # along dim 0) to gather back to the full square shape via torch.cat. |
174 | | - if partition_dim is not None and (m % self.tp_size or n % self.tp_size): |
175 | | - raise ValueError( |
176 | | - f"TpRekls requires both dimensions to be divisible by tp_size={self.tp_size}; " |
177 | | - f"got full shape ({m}, {n}) for a parameter with partition_dim={partition_dim}." |
178 | | - ) |
179 | | - |
180 | | - state["step"] = 0 |
181 | | - state["exp_avg"] = torch.zeros((m, n), dtype=torch.float32, device=p.device) |
182 | | - state["exp_avg_sq"] = torch.zeros((m, n), dtype=torch.float32, device=p.device) |
183 | | - # Match init_kronecker_factors in soap.py: default dtype (typically float32). |
184 | | - # L, R are sharded along dim 0 only when the param is tensor-parallel. |
185 | | - shard = self.tp_size if partition_dim is not None else 1 |
186 | | - state["L"] = torch.zeros((m // shard, m), device=p.device) |
187 | | - state["R"] = torch.zeros((n // shard, n), device=p.device) |
188 | | - |
189 | | - if TYPE_CHECKING: |
190 | | - |
191 | | - @overload |
192 | | - def step(self, closure: None = ...) -> None: ... |
193 | | - |
194 | | - @overload |
195 | | - def step(self, closure: Callable[[], float]) -> float: ... |
196 | | - |
197 | | - @torch.no_grad() # type: ignore[misc] |
198 | | - @override |
199 | | - def step(self, closure: None = None) -> None: |
200 | | - assert closure is None, "No support for closure" |
201 | | - for group in self.param_groups: |
202 | | - self._init_group(group) |
203 | | - |
204 | | - for group in self.param_groups: |
205 | | - partition_dim = self._validate_partition_dim(group["partition_dim"]) |
206 | | - for p in group["params"]: |
207 | | - if p.grad is None: |
208 | | - continue # pragma: no cover |
209 | | - |
210 | | - local_grad = p.grad.to(torch.float32) |
211 | | - state = self.state[p] |
212 | | - curr_iter_1_based = state["step"] + 1 |
213 | | - |
214 | | - # Apply weight decay before the gather so l2 mode propagates into full_grad. |
215 | | - self._apply_weight_decay_inplace(p, local_grad, group["lr"], group["weight_decay"]) |
216 | | - |
217 | | - if partition_dim is None: |
218 | | - # Replicated parameter: no all-gather, state is already full-size. |
219 | | - full_grad = local_grad |
220 | | - kronecker_factor_list = [state["L"], state["R"]] |
221 | | - else: |
222 | | - full_grad, kronecker_factor_list = tp_utils.all_gather_grad_and_kronecker_factors_tp( |
223 | | - kronecker_factor_list=[state["L"], state["R"]], |
224 | | - grad=local_grad, |
225 | | - partition_dim=partition_dim, |
226 | | - tp_group=self.tp_group, |
227 | | - ) |
228 | | - |
229 | | - # Apply shampoo beta bias correction. |
230 | | - shampoo_beta = group["shampoo_beta"] |
231 | | - shampoo_beta = 1 - (1 - shampoo_beta) / (1 - shampoo_beta**curr_iter_1_based) |
232 | | - |
233 | | - # KL-Shampoo correction needs the eigenbasis of the *pre-update* L, R; recompute it |
234 | | - # via eigh since we do not persist eigenbases across steps. |
235 | | - with utils.fp32_matmul_precision(self.fp32_matmul_prec): |
236 | | - pre_eigenbasis_list = soap_utils.get_eigenbasis_eigh(kronecker_factor_list) |
237 | | - soap.update_kronecker_factors_kl_shampoo( |
238 | | - kronecker_factor_list, |
239 | | - full_grad, |
240 | | - shampoo_beta=shampoo_beta, |
241 | | - eigenbasis_list=pre_eigenbasis_list, |
242 | | - eps=group["eps"], |
243 | | - ) |
244 | | - |
245 | | - # Persist the updated local shard back into state — only needed for the TP path, |
246 | | - # since the replicated path updated state["L"], state["R"] in place via the alias. |
247 | | - if partition_dim is not None: |
248 | | - state["L"].copy_(kronecker_factor_list[0].chunk(self.tp_size, dim=0)[self.tp_rank]) |
249 | | - state["R"].copy_(kronecker_factor_list[1].chunk(self.tp_size, dim=0)[self.tp_rank]) |
250 | | - |
251 | | - with utils.fp32_matmul_precision(self.fp32_matmul_prec): |
252 | | - # Rotate exp_avg from the pre-update eigenbasis to the post-update eigenbasis, |
253 | | - # and recompute the post-update eigenbasis via eigh. |
254 | | - eigenbasis_list, state["exp_avg"], state["exp_avg_sq"] = soap.update_eigenbasis_and_exp_avgs( |
255 | | - kronecker_factor_list=kronecker_factor_list, |
256 | | - eigenbasis_list=pre_eigenbasis_list, |
257 | | - exp_avg_sq=state["exp_avg_sq"], |
258 | | - exp_avg=state["exp_avg"], |
259 | | - use_eigh=True, |
260 | | - ) |
261 | | - |
262 | | - full_grad_projected = soap.precondition(full_grad, eigenbasis_list, dims=[[0], [0]]) |
263 | | - |
264 | | - # No matmul inside adam update. Put it under fp32_matmul_precision for code simplicity. |
265 | | - full_adam_update = update_functions.calculate_laprop_update( |
266 | | - full_grad_projected, |
267 | | - state["exp_avg"], |
268 | | - state["exp_avg_sq"], |
269 | | - True, # correct_bias |
270 | | - group["betas"], |
271 | | - curr_iter_1_based, |
272 | | - group["eps"], |
273 | | - ) |
274 | | - |
275 | | - full_precond_update = soap.precondition(full_adam_update, eigenbasis_list, dims=[[0], [1]]) |
276 | | - |
277 | | - if partition_dim is None: |
278 | | - p.add_(full_precond_update, alpha=-group["lr"]) |
279 | | - else: |
280 | | - local_precond_update = full_precond_update.chunk(self.tp_size, dim=partition_dim)[self.tp_rank] |
281 | | - p.add_(local_precond_update, alpha=-group["lr"]) |
282 | | - |
283 | | - state["step"] += 1 |
284 | | - |
285 | | - return None |
0 commit comments