Skip to content

Commit d4d374a

Browse files
skywclaude
andcommitted
Move TpRekls and tp_utils into tp_rekls.py
Contain the experimental tensor-parallel REKLS code in its own module: move the TpRekls class and the all_gather_grad_and_kronecker_factors_tp helper out of rekls.py / tp_utils.py into tp_rekls.py, and update tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Hao Wu <skyw@nvidia.com>
1 parent 67d3786 commit d4d374a

5 files changed

Lines changed: 299 additions & 295 deletions

File tree

emerging_optimizers/soap/rekls.py

Lines changed: 2 additions & 229 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,14 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
from typing import TYPE_CHECKING, Callable, override
17-
18-
import torch
19-
from torch import distributed as dist
20-
from torch import optim
2116
from torch.optim.optimizer import ParamsT
2217

23-
24-
if TYPE_CHECKING:
25-
from typing import overload
26-
2718
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
3120
from emerging_optimizers.soap.soap import SOAP
32-
from emerging_optimizers.utils import FP32MatmulPrecT, get_pg_rank, get_pg_size
3321

3422

35-
__all__ = ["REKLS", "TpRekls"]
23+
__all__ = ["REKLS"]
3624

3725

3826
@registry.register_optimizer("rekls")
@@ -68,218 +56,3 @@ def __init__(
6856
use_eigh=True,
6957
use_kl_shampoo=True,
7058
)
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

Comments
 (0)