Skip to content

Commit 41b1711

Browse files
committed
update exp_avg_sq sorting
Signed-off-by: Hao Wu <skyw@nvidia.com>
1 parent 63fce27 commit 41b1711

5 files changed

Lines changed: 45 additions & 188 deletions

File tree

emerging_optimizers/soap/moso.py

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -234,21 +234,19 @@ def _update_eigenbasis_and_adam_exp_avgs(
234234
left_preconditioned=left_preconditioned,
235235
)
236236

237-
eigenbasis, exp_avg_sq = _sort_one_sided_eigenbasis_and_exp_avg_sq(
238-
momentum_factor=momentum_factor,
239-
eigenbasis=eigenbasis,
240-
exp_avg_sq=exp_avg_sq,
241-
left_preconditioned=left_preconditioned,
242-
)
243-
244237
if use_eigh:
245238
_, (updated_eigenbasis,) = soap_utils.get_eigenbasis_eigh([momentum_factor])
246239
else:
247-
_, (updated_eigenbasis,) = soap_utils.get_eigenbasis_qr(
240+
# get_eigenbasis_qr permutes exp_avg_sq along the factor-list axes (dim 0 for the single
241+
# factor here), so transpose the right-preconditioned case in and out.
242+
x = exp_avg_sq if left_preconditioned else exp_avg_sq.mT
243+
_, (updated_eigenbasis,), x = soap_utils.get_eigenbasis_qr(
248244
[momentum_factor],
249245
[eigenbasis],
246+
x,
250247
power_iter_steps=power_iter_steps,
251248
)
249+
exp_avg_sq = x if left_preconditioned else x.mT
252250

253251
exp_avg = _project_to_one_sided_eigenbasis(
254252
x=exp_avg,
@@ -258,21 +256,6 @@ def _update_eigenbasis_and_adam_exp_avgs(
258256
return updated_eigenbasis, exp_avg, exp_avg_sq
259257

260258

261-
@torch.no_grad() # type: ignore[misc]
262-
def _sort_one_sided_eigenbasis_and_exp_avg_sq(
263-
momentum_factor: torch.Tensor,
264-
eigenbasis: torch.Tensor,
265-
exp_avg_sq: torch.Tensor,
266-
left_preconditioned: bool,
267-
) -> tuple[torch.Tensor, torch.Tensor]:
268-
"""Sort eigenbasis slots by approximate eigenvalue and permute Adam second moments."""
269-
approx_eigvals = utils.eig.conjugate(momentum_factor, eigenbasis, diag=True)
270-
sort_idx = torch.argsort(approx_eigvals, descending=True, stable=True)
271-
sorted_eigenbasis = eigenbasis[:, sort_idx]
272-
exp_avg_sq_dim = 0 if left_preconditioned else 1
273-
return sorted_eigenbasis, exp_avg_sq.index_select(exp_avg_sq_dim, sort_idx)
274-
275-
276259
@torch.no_grad() # type: ignore[misc]
277260
def _project_to_one_sided_eigenbasis(
278261
x: torch.Tensor,

emerging_optimizers/soap/soap.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,10 @@ def update_eigenbasis_and_exp_avgs(
453453
used for preconditioning. It follows these steps:
454454
455455
1. Projects exp_avg back to the original basis
456-
2. Updates the eigenbases (via eigh, or QR decomposition and power iteration (orthogonal iteration)),
457-
along with the (approximate) eigenvalues of the kronecker factors in the updated eigenbases
456+
2. Updates the eigenbases (via eigh, or QR decomposition and power iteration (orthogonal iteration))
457+
along with the (approximate) eigenvalues of the kronecker factors in the updated eigenbases;
458+
on the QR path exp_avg_sq is permuted to match the sorted eigenbasis columns (not needed for
459+
the eigh path, which rebuilds the basis from scratch)
458460
3. Projects exp_avg back to the new eigenbasis
459461
460462
Args:
@@ -463,7 +465,8 @@ def update_eigenbasis_and_exp_avgs(
463465
eigenbasis_list: List of current eigenbases (QL and QR)
464466
used for preconditioning. These will be updated by this function.
465467
exp_avg_sq: Inner Adam's second moment tensor, used for scaling the preconditioner updates.
466-
This tensor is modified in-place.
468+
Permuted along each kronecker-factor axis on the QR path to track the sorted eigenbasis
469+
columns; returned unchanged on the eigh path.
467470
exp_avg: Inner Adam's first moment tensor, used for tracking gradient momentum.
468471
This tensor is modified in-place.
469472
use_eigh: Whether to use full symmetric eigendecomposition (eigh) to compute the eigenbasis.
@@ -496,28 +499,16 @@ def update_eigenbasis_and_exp_avgs(
496499
dims=[[0], [1]],
497500
)
498501

499-
# Step 2a: Sort current eigenbases by descending approximate eigenvalues of the updated kronecker
500-
# factors, and permute exp_avg_sq.
501-
# Shared by both eigh and QR paths so the new eigh-path approximation matches the QR-path slot semantics
502-
# under small per-step drift.
503-
# Sorting eigenbases is not necessary for eigh path technically, but decided to keep API simple.
504-
eigenbasis_list, exp_avg_sq = soap_utils.sort_eigenbasis_by_approx_eigvals(
505-
kronecker_factor_list,
506-
eigenbasis_list,
507-
exp_avg_sq,
508-
)
509-
510-
# Step 2b: Update eigenbases and compute the eigenvalues in the updated eigenbases
502+
# Step 2: Update eigenbases
511503
if use_eigh:
512504
updated_eigvals_list, updated_eigenbasis_list = soap_utils.get_eigenbasis_eigh(
513505
kronecker_factor_list,
514506
)
515507
else:
516-
# Use QR decomposition and power iteration (orthogonal iteration) starting from the
517-
# pre-sorted eigenbases.
518-
updated_eigvals_list, updated_eigenbasis_list = soap_utils.get_eigenbasis_qr(
508+
updated_eigvals_list, updated_eigenbasis_list, exp_avg_sq = soap_utils.get_eigenbasis_qr(
519509
kronecker_factor_list,
520510
eigenbasis_list,
511+
exp_avg_sq,
521512
power_iter_steps,
522513
)
523514

emerging_optimizers/soap/soap_utils.py

Lines changed: 19 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -26,42 +26,10 @@
2626
__all__ = [
2727
"get_eigenbasis_eigh",
2828
"get_eigenbasis_qr",
29-
"sort_eigenbasis_by_approx_eigvals",
3029
"get_eigenbasis_svd",
3130
]
3231

3332

34-
def sort_eigenbasis_by_approx_eigvals(
35-
kronecker_factor_list: TensorList,
36-
eigenbasis_list: TensorList,
37-
exp_avg_sq: torch.Tensor,
38-
) -> tuple[TensorList, torch.Tensor]:
39-
"""Permute each eigenbasis and matching ``exp_avg_sq`` axis by descending approximate eigenvalues.
40-
41-
Both the eigh and QR eigenbasis-update paths consume the sorted output: the eigh path discards
42-
the permuted eigenbasis but uses the permuted ``exp_avg_sq`` (the sort_idx best approximates the
43-
permutation component of the eigh-vs-old basis change under small drift); the QR path power-
44-
iterates from the pre-sorted basis.
45-
46-
Args:
47-
kronecker_factor_list: List of preconditioner matrices (L and R).
48-
eigenbasis_list: List of current eigenbases (QL and QR).
49-
exp_avg_sq: Inner Adam second moment tensor permuted along each Kronecker-factor
50-
axis to match the new descending-eigenvalue column ordering.
51-
52-
Returns:
53-
``(sorted_eigenbasis_list, sorted_exp_avg_sq)``.
54-
"""
55-
sorted_eigenbasis_list: TensorList = []
56-
sorted_exp_avg_sq = exp_avg_sq
57-
for ind, (kronecker_factor, eigenbasis) in enumerate(zip(kronecker_factor_list, eigenbasis_list, strict=True)):
58-
approx_eigvals = eig_utils.conjugate(kronecker_factor, eigenbasis, diag=True)
59-
sort_idx = torch.argsort(approx_eigvals, descending=True)
60-
sorted_eigenbasis_list.append(eigenbasis[:, sort_idx])
61-
sorted_exp_avg_sq = sorted_exp_avg_sq.index_select(ind, sort_idx)
62-
return sorted_eigenbasis_list, sorted_exp_avg_sq
63-
64-
6533
def get_eigenbasis_eigh(
6634
kronecker_factor_list: TensorList,
6735
) -> tuple[TensorList, TensorList]:
@@ -73,17 +41,6 @@ def get_eigenbasis_eigh(
7341
Returns:
7442
Tuple of (list of eigenvalues in descending order, list of orthonormal kronecker factor
7543
eigenbases matrices).
76-
77-
Example:
78-
.. code-block:: python
79-
80-
# Create sample Kronecker factors (symmetric positive definite matrices)
81-
k_factor1 = torch.randn(4, 4)
82-
k_factor1 = k_factor1 @ k_factor1.T # Make symmetric positive definite
83-
k_factor2 = torch.randn(5, 5)
84-
k_factor2 = k_factor2 @ k_factor2.T # Make symmetric positive definite
85-
86-
eigvals_list, ortho_matrices = get_eigenbasis_eigh([k_factor1, k_factor2])
8744
"""
8845
updated_eigenbasis_list: TensorList = []
8946
updated_eigvals_list: TensorList = []
@@ -111,19 +68,6 @@ def get_eigenbasis_svd(
11168
11269
Returns:
11370
List of orthonormal kronecker factor eigenbases matrices
114-
115-
Example:
116-
.. code-block:: python
117-
118-
# Create sample Kronecker factors (symmetric positive definite matrices)
119-
k_factor1 = torch.randn(4, 4)
120-
k_factor1 = k_factor1 @ k_factor1.T # Make symmetric positive definite
121-
k_factor2 = torch.randn(5, 5)
122-
k_factor2 = k_factor2 @ k_factor2.T # Make symmetric positive definite
123-
124-
# Get orthogonal matrices for these factors
125-
ortho_matrices = get_eigenbasis_svd([k_factor1, k_factor2])
126-
# ortho_matrices[0] has shape [4, 4] and ortho_matrices[1] has shape [5, 5]
12771
"""
12872
updated_eigenbasis_list: TensorList = []
12973

@@ -137,63 +81,44 @@ def get_eigenbasis_svd(
13781
def get_eigenbasis_qr(
13882
kronecker_factor_list: TensorList,
13983
eigenbasis_list: TensorList,
84+
exp_avg_sq: torch.Tensor,
14085
power_iter_steps: int = 1,
141-
) -> tuple[TensorList, TensorList]:
86+
) -> tuple[TensorList, TensorList, torch.Tensor]:
14287
"""Updates the eigenbases of the preconditioner using power iteration and QR.
14388
144-
Computes using multiple rounds of power iteration followed by QR decomposition (orthogonal iteration).
145-
``eigenbasis_list`` is expected to be already sorted by descending approximate eigenvalues (see
146-
:func:`sort_eigenbasis_by_approx_eigvals`).
147-
148-
The approximate eigenvalues of each kronecker factor in its updated eigenbasis (the Rayleigh
149-
quotients :math:`\\mathrm{diag}(Q^{\\top} K Q)`) are also computed.
89+
Each eigenbasis is refined with multiple rounds of power iteration followed by QR decomposition
90+
(orthogonal iteration), then its columns are sorted by descending approximate eigenvalues of the
91+
kronecker factor (the Rayleigh quotients :math:`\\mathrm{diag}(Q^{\\top} K Q)`), so the returned
92+
eigenvalues and eigenbases are in descending order like :func:`get_eigenbasis_eigh`.
93+
``exp_avg_sq`` is permuted along each kronecker-factor axis by the same sort, so its per-slot
94+
statistics stay aligned with the eigenbasis columns.
15095
15196
Args:
15297
kronecker_factor_list: List of preconditioner matrices (L and R).
15398
eigenbasis_list: List of current eigenbases (QL and QR).
99+
exp_avg_sq: Inner Adam second moment tensor, permuted along each kronecker-factor axis to
100+
match the sorted eigenbasis columns.
154101
power_iter_steps: Number of power iteration steps to perform before QR decomposition.
155102
More steps can lead to better convergence but increased computation time.
156103
157104
Returns:
158105
Tuple of (list of approximate eigenvalues of each kronecker factor in its updated eigenbasis,
159-
updated list of orthonormal eigenbases (QL and QR)).
160-
161-
Example:
162-
.. code-block:: python
163-
164-
# Create sample Kronecker factors (symmetric positive definite matrices)
165-
n, m = 10, 20
166-
k_factor1 = torch.randn(n, n)
167-
k_factor1 = k_factor1 @ k_factor1.T # Make symmetric positive definite
168-
k_factor2 = torch.randn(m, m)
169-
k_factor2 = k_factor2 @ k_factor2.T # Make symmetric positive definite
170-
171-
# Get orthogonal matrices for these kronecker factors
172-
kronecker_factor_list = [k_factor1, k_factor2]
173-
_, eigenbasis_list = get_eigenbasis_eigh(kronecker_factor_list)
174-
175-
# Perturb the kronecker factor matrices, simulating the effect of gradient updates
176-
perturbation = 1e-2*torch.randn(n, m)
177-
perturbed_kronecker_factor_list = [None, None]
178-
perturbed_kronecker_factor_list[0] = k_factor1 + perturbation@perturbation.T
179-
perturbed_kronecker_factor_list[1] = k_factor2 + perturbation.T@perturbation
180-
181-
# Refine the orthogonal matrices using QR (eigenbasis_list already sorted)
182-
eigvals_list, updated_ortho_matrices = get_eigenbasis_qr(
183-
perturbed_kronecker_factor_list,
184-
eigenbasis_list,
185-
)
106+
in descending order, updated list of orthonormal eigenbases (QL and QR) with columns ordered to
107+
match, ``exp_avg_sq`` permuted to match).
186108
"""
187109
updated_eigenbasis_list: TensorList = []
188110
updated_eigvals_list: TensorList = []
189-
for kronecker_factor, eigenbasis in zip(kronecker_factor_list, eigenbasis_list, strict=True):
111+
for ind, (kronecker_factor, eigenbasis) in enumerate(zip(kronecker_factor_list, eigenbasis_list, strict=True)):
190112
Q = eig_utils.orthogonal_iteration(
191113
kronecker_factor=kronecker_factor,
192114
eigenbasis=eigenbasis,
193115
power_iter_steps=power_iter_steps,
194116
)
195-
updated_eigenbasis_list.append(Q)
196117
with utils.fp32_matmul_precision("highest"):
197-
updated_eigvals_list.append(eig_utils.conjugate(kronecker_factor, Q, diag=True))
118+
approx_eigvals = eig_utils.conjugate(kronecker_factor, Q, diag=True)
119+
approx_eigvals, sort_idx = torch.sort(approx_eigvals, descending=True)
120+
updated_eigvals_list.append(approx_eigvals)
121+
updated_eigenbasis_list.append(Q[:, sort_idx])
122+
exp_avg_sq = exp_avg_sq.index_select(ind, sort_idx)
198123

199-
return updated_eigvals_list, updated_eigenbasis_list
124+
return updated_eigvals_list, updated_eigenbasis_list, exp_avg_sq

emerging_optimizers/utils/eig.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ def orthogonal_iteration(
7676
"""Refines an eigenbasis via power iteration with QR re-orthogonalization.
7777
7878
Performs ``power_iter_steps`` rounds of ``Q = QR(kronecker_factor @ Q)`` starting from
79-
``eigenbasis``. The columns of ``eigenbasis`` are expected to already be aligned with the
80-
intended descending-eigenvalue ordering of ``kronecker_factor`` (see
81-
:func:`emerging_optimizers.soap.soap_utils.sort_eigenbasis_by_approx_eigvals`).
79+
``eigenbasis``. The columns of ``eigenbasis`` are expected to already be approximately aligned
80+
with the descending-eigenvalue ordering of ``kronecker_factor`` (maintained in the SOAP pipeline
81+
by :func:`emerging_optimizers.soap.soap_utils.get_eigenbasis_qr`, which returns its eigenbases in
82+
descending order).
8283
8384
Args:
8485
kronecker_factor: Kronecker factor matrix (symmetric, used as the projector).

tests/test_soap_utils.py

Lines changed: 7 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
import torch
16-
from _comparison import assert_close_to_identity, assert_equal
16+
from _comparison import assert_close_to_identity
1717
from absl import flags, logging
1818
from absl.testing import absltest, parameterized
1919

@@ -61,10 +61,12 @@ def test_get_eigenbasis_qr(self, N: int, M: int) -> None:
6161
torch.randn(M, M, device=self.device),
6262
torch.randn(N, N, device=self.device),
6363
]
64+
exp_avg_sq = torch.abs(torch.randn(M, N, device=self.device))
6465

65-
eigvals_list, Q_new_list = soap_utils.get_eigenbasis_qr(
66+
eigvals_list, Q_new_list, permuted_exp_avg_sq = soap_utils.get_eigenbasis_qr(
6667
kronecker_factor_list=kronecker_factor_list,
6768
eigenbasis_list=eigenbasis_list,
69+
exp_avg_sq=exp_avg_sq,
6870
power_iter_steps=1,
6971
)
7072

@@ -83,6 +85,8 @@ def test_get_eigenbasis_qr(self, N: int, M: int) -> None:
8385
self.assertEqual(len(eigvals_list), 2)
8486
for eigvals, kronecker_factor, Q_new in zip(eigvals_list, kronecker_factor_list, Q_new_list, strict=True):
8587
self.assertEqual(eigvals.shape, (kronecker_factor.shape[0],))
88+
# Eigenvalues (and matching eigenbasis columns) are returned in descending order
89+
self.assertTrue(torch.all(eigvals[:-1] >= eigvals[1:]))
8690
torch.testing.assert_close(
8791
eigvals,
8892
torch.diag(Q_new.t() @ kronecker_factor @ Q_new),
@@ -91,54 +95,7 @@ def test_get_eigenbasis_qr(self, N: int, M: int) -> None:
9195
msg=lambda msg: f"eigvals do not match the Rayleigh quotients diag(Q^T K Q)\n\n{msg}",
9296
)
9397

94-
@parameterized.parameters( # type: ignore[misc]
95-
{"N": 4, "M": 8},
96-
{"N": 16, "M": 8},
97-
)
98-
def test_sort_eigenbasis_by_approx_eigvals(self, N: int, M: int) -> None:
99-
"""Sort function permutes eigenbasis columns and exp_avg_sq slots consistently."""
100-
g = torch.randint(-5, 6, (M, N), device=self.device) / 16.0
101-
# Add a small ridge so K is full-rank (g @ g.t() is rank-deficient when M > N, etc.),
102-
# which keeps the approximate eigenvalues away from numerical zero where ordering becomes
103-
# ambiguous under float rounding.
104-
eps_eye = lambda n: 1e-3 * torch.eye(n, device=self.device)
105-
kronecker_factor_list = [g @ g.t() + eps_eye(M), g.t() @ g + eps_eye(N)]
106-
eigenbasis_list = [torch.linalg.qr(Q).Q for Q in kronecker_factor_list]
107-
exp_avg_sq = torch.abs(torch.randint(-5, 6, (M, N), device=self.device) / 16.0)
108-
109-
sorted_eigenbasis_list, sorted_exp_avg_sq = soap_utils.sort_eigenbasis_by_approx_eigvals(
110-
kronecker_factor_list,
111-
eigenbasis_list,
112-
exp_avg_sq,
113-
)
114-
115-
# Compute the expected per-axis permutations from the originals.
116-
sort_idx_list = []
117-
for K, Q in zip(kronecker_factor_list, eigenbasis_list, strict=True):
118-
sort_idx_list.append(torch.argsort(torch.diag(Q.t() @ K @ Q), descending=True))
119-
120-
# Each eigenbasis is column-permuted by its own sort_idx.
121-
for i, (Q_old, Q_sorted) in enumerate(zip(eigenbasis_list, sorted_eigenbasis_list, strict=True)):
122-
assert_equal(
123-
Q_sorted,
124-
Q_old[:, sort_idx_list[i]],
125-
msg=lambda m, i=i: f"eigenbasis i={i} not permuted by sort_idx\n\n{m}",
126-
)
127-
128-
# exp_avg_sq is permuted along every axis cumulatively.
129-
expected_sq = exp_avg_sq
130-
for i, sort_idx in enumerate(sort_idx_list):
131-
expected_sq = expected_sq.index_select(i, sort_idx)
132-
assert_equal(
133-
sorted_exp_avg_sq,
134-
expected_sq,
135-
msg=lambda m: f"exp_avg_sq not permuted to match sorted eigenbases\n\n{m}",
136-
)
137-
138-
# Sorted eigenbases yield descending approximate eigenvalues.
139-
for K, Q in zip(kronecker_factor_list, sorted_eigenbasis_list, strict=True):
140-
sorted_eigvals = torch.diag(Q.t() @ K @ Q)
141-
self.assertTrue(torch.all(sorted_eigvals[:-1] >= sorted_eigvals[1:]))
98+
self.assertEqual(permuted_exp_avg_sq.shape, (M, N))
14299

143100
@parameterized.parameters( # type: ignore[misc]
144101
{"dims": [128, 512]},

0 commit comments

Comments
 (0)