Skip to content

Commit 27fe09b

Browse files
committed
ai fixes. DONOT merge yet
Signed-off-by: Hao Wu <skyw@nvidia.com>
1 parent 41b1711 commit 27fe09b

6 files changed

Lines changed: 131 additions & 42 deletions

File tree

emerging_optimizers/soap/moso.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,20 @@ def _update_eigenbasis_and_adam_exp_avgs(
237237
if use_eigh:
238238
_, (updated_eigenbasis,) = soap_utils.get_eigenbasis_eigh([momentum_factor])
239239
else:
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.
240+
# permute_eigenbasis_and_exp_avg_sq permutes exp_avg_sq along the factor-list axes (dim 0 for
241+
# the single factor here), so transpose the right-preconditioned case in and out.
242242
x = exp_avg_sq if left_preconditioned else exp_avg_sq.mT
243-
_, (updated_eigenbasis,), x = soap_utils.get_eigenbasis_qr(
243+
(eigenbasis,), x = soap_utils.permute_eigenbasis_and_exp_avg_sq(
244244
[momentum_factor],
245245
[eigenbasis],
246246
x,
247-
power_iter_steps=power_iter_steps,
248247
)
249248
exp_avg_sq = x if left_preconditioned else x.mT
249+
_, (updated_eigenbasis,) = soap_utils.get_eigenbasis_qr(
250+
[momentum_factor],
251+
[eigenbasis],
252+
power_iter_steps=power_iter_steps,
253+
)
250254

251255
exp_avg = _project_to_one_sided_eigenbasis(
252256
x=exp_avg,

emerging_optimizers/soap/soap.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,9 @@ def update_eigenbasis_and_exp_avgs(
455455
1. Projects exp_avg back to the original basis
456456
2. Updates the eigenbases (via eigh, or QR decomposition and power iteration (orthogonal iteration))
457457
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)
458+
on the QR path the eigenbases and exp_avg_sq slots are first permuted by descending approximate
459+
eigenvalues of the updated factors (not needed for the eigh path, which rebuilds the basis
460+
from scratch)
460461
3. Projects exp_avg back to the new eigenbasis
461462
462463
Args:
@@ -505,10 +506,16 @@ def update_eigenbasis_and_exp_avgs(
505506
kronecker_factor_list,
506507
)
507508
else:
508-
updated_eigvals_list, updated_eigenbasis_list, exp_avg_sq = soap_utils.get_eigenbasis_qr(
509+
# Orthogonal iteration is column-order sensitive, so first permute the eigenbases (and the
510+
# matching exp_avg_sq slots) by descending approximate eigenvalues of the updated factors.
511+
eigenbasis_list, exp_avg_sq = soap_utils.permute_eigenbasis_and_exp_avg_sq(
509512
kronecker_factor_list,
510513
eigenbasis_list,
511514
exp_avg_sq,
515+
)
516+
updated_eigvals_list, updated_eigenbasis_list = soap_utils.get_eigenbasis_qr(
517+
kronecker_factor_list,
518+
eigenbasis_list,
512519
power_iter_steps,
513520
)
514521

emerging_optimizers/soap/soap_utils.py

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"get_eigenbasis_eigh",
2828
"get_eigenbasis_qr",
2929
"get_eigenbasis_svd",
30+
"permute_eigenbasis_and_exp_avg_sq",
3031
]
3132

3233

@@ -78,47 +79,70 @@ def get_eigenbasis_svd(
7879
return updated_eigenbasis_list
7980

8081

81-
def get_eigenbasis_qr(
82+
def permute_eigenbasis_and_exp_avg_sq(
8283
kronecker_factor_list: TensorList,
8384
eigenbasis_list: TensorList,
8485
exp_avg_sq: torch.Tensor,
86+
) -> tuple[TensorList, torch.Tensor]:
87+
"""Permute each eigenbasis and the matching ``exp_avg_sq`` axis by descending approximate eigenvalues.
88+
89+
Computes the approximate eigenvalues of each eigenbasis against its kronecker factor (the Rayleigh
90+
quotients :math:`\\mathrm{diag}(Q^{\\top} K Q)`) and permutes the eigenbasis columns into descending
91+
order, permuting ``exp_avg_sq`` along the corresponding axis so its per-slot statistics stay
92+
aligned. Used before :func:`get_eigenbasis_qr`, whose orthogonal iteration is column-order
93+
sensitive; the eigh path does not need it since it rebuilds the eigenbasis from scratch.
94+
95+
Args:
96+
kronecker_factor_list: List of preconditioner matrices (L and R).
97+
eigenbasis_list: List of current eigenbases (QL and QR).
98+
exp_avg_sq: Inner Adam second moment tensor, permuted along each kronecker-factor axis to
99+
match the new descending-eigenvalue column ordering.
100+
101+
Returns:
102+
``(permuted_eigenbasis_list, permuted_exp_avg_sq)``.
103+
"""
104+
permuted_eigenbasis_list: TensorList = []
105+
for ind, (kronecker_factor, eigenbasis) in enumerate(zip(kronecker_factor_list, eigenbasis_list, strict=True)):
106+
approx_eigvals = eig_utils.conjugate(kronecker_factor, eigenbasis, diag=True)
107+
sort_idx = torch.argsort(approx_eigvals, descending=True)
108+
permuted_eigenbasis_list.append(eigenbasis[:, sort_idx])
109+
exp_avg_sq = exp_avg_sq.index_select(ind, sort_idx)
110+
return permuted_eigenbasis_list, exp_avg_sq
111+
112+
113+
def get_eigenbasis_qr(
114+
kronecker_factor_list: TensorList,
115+
eigenbasis_list: TensorList,
85116
power_iter_steps: int = 1,
86-
) -> tuple[TensorList, TensorList, torch.Tensor]:
117+
) -> tuple[TensorList, TensorList]:
87118
"""Updates the eigenbases of the preconditioner using power iteration and QR.
88119
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.
120+
Computes using multiple rounds of power iteration followed by QR decomposition (orthogonal
121+
iteration). ``eigenbasis_list`` is expected to be already sorted by descending approximate
122+
eigenvalues (see :func:`permute_eigenbasis_and_exp_avg_sq`). The returned approximate eigenvalues
123+
(the Rayleigh quotients :math:`\\mathrm{diag}(Q^{\\top} K Q)`) are aligned with the updated
124+
eigenbasis columns.
95125
96126
Args:
97127
kronecker_factor_list: List of preconditioner matrices (L and R).
98128
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.
101129
power_iter_steps: Number of power iteration steps to perform before QR decomposition.
102130
More steps can lead to better convergence but increased computation time.
103131
104132
Returns:
105133
Tuple of (list of approximate eigenvalues of each kronecker factor in its updated eigenbasis,
106-
in descending order, updated list of orthonormal eigenbases (QL and QR) with columns ordered to
107-
match, ``exp_avg_sq`` permuted to match).
134+
updated list of orthonormal eigenbases (QL and QR)).
108135
"""
109136
updated_eigenbasis_list: TensorList = []
110137
updated_eigvals_list: TensorList = []
111-
for ind, (kronecker_factor, eigenbasis) in enumerate(zip(kronecker_factor_list, eigenbasis_list, strict=True)):
138+
for kronecker_factor, eigenbasis in zip(kronecker_factor_list, eigenbasis_list, strict=True):
112139
Q = eig_utils.orthogonal_iteration(
113140
kronecker_factor=kronecker_factor,
114141
eigenbasis=eigenbasis,
115142
power_iter_steps=power_iter_steps,
116143
)
117144
with utils.fp32_matmul_precision("highest"):
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)
145+
updated_eigvals_list.append(eig_utils.conjugate(kronecker_factor, Q, diag=True))
146+
updated_eigenbasis_list.append(Q)
123147

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

emerging_optimizers/utils/eig.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,9 @@ 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 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).
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.permute_eigenbasis_and_exp_avg_sq`).
8382
8483
Args:
8584
kronecker_factor: Kronecker factor matrix (symmetric, used as the projector).

tests/test_soap.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -582,13 +582,26 @@ def test_eigenbasis_matches_reference(self, shape: tuple, num_steps: int):
582582
rtol=1e-5,
583583
)
584584

585-
for eigenbasis_test, eigenbasis_ref in zip([test_state["Q_L"], test_state["Q_R"]], ref_state["Q"]):
585+
# SOAP returns eigenbases sorted by descending approximate eigenvalues while the reference
586+
# keeps its power-iteration column order, so sort both by their approximate eigenvalues
587+
# (against the shared kronecker factor, asserted equal above) before comparing.
588+
for kronecker_factor, eigenbasis_test, eigenbasis_ref in zip(
589+
[test_state["L"], test_state["R"]],
590+
[test_state["Q_L"], test_state["Q_R"]],
591+
ref_state["Q"],
592+
strict=True,
593+
):
594+
sorted_eigenbasis_list = []
595+
for eigenbasis in (eigenbasis_test, eigenbasis_ref):
596+
approx_eigvals = torch.diag(eigenbasis.T @ kronecker_factor @ eigenbasis)
597+
sort_idx = torch.argsort(approx_eigvals, descending=True)
598+
sorted_eigenbasis_list.append(eigenbasis[:, sort_idx])
586599
torch.testing.assert_close(
587-
eigenbasis_test,
588-
eigenbasis_ref,
600+
sorted_eigenbasis_list[0],
601+
sorted_eigenbasis_list[1],
589602
atol=1e-4,
590603
rtol=1e-4,
591-
msg=lambda msg: f"Eigenbasis mismatch at step {step}:\n{msg}",
604+
msg=lambda msg, step=step: f"Eigenbasis mismatch at step {step}:\n{msg}",
592605
)
593606

594607
# Compare step counters

tests/test_soap_utils.py

Lines changed: 50 additions & 8 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
16+
from _comparison import assert_close_to_identity, assert_equal
1717
from absl import flags, logging
1818
from absl.testing import absltest, parameterized
1919

@@ -61,12 +61,9 @@ 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))
65-
66-
eigvals_list, Q_new_list, permuted_exp_avg_sq = soap_utils.get_eigenbasis_qr(
64+
eigvals_list, Q_new_list = soap_utils.get_eigenbasis_qr(
6765
kronecker_factor_list=kronecker_factor_list,
6866
eigenbasis_list=eigenbasis_list,
69-
exp_avg_sq=exp_avg_sq,
7067
power_iter_steps=1,
7168
)
7269

@@ -85,8 +82,6 @@ def test_get_eigenbasis_qr(self, N: int, M: int) -> None:
8582
self.assertEqual(len(eigvals_list), 2)
8683
for eigvals, kronecker_factor, Q_new in zip(eigvals_list, kronecker_factor_list, Q_new_list, strict=True):
8784
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:]))
9085
torch.testing.assert_close(
9186
eigvals,
9287
torch.diag(Q_new.t() @ kronecker_factor @ Q_new),
@@ -95,7 +90,54 @@ def test_get_eigenbasis_qr(self, N: int, M: int) -> None:
9590
msg=lambda msg: f"eigvals do not match the Rayleigh quotients diag(Q^T K Q)\n\n{msg}",
9691
)
9792

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

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

0 commit comments

Comments
 (0)