Skip to content

Commit 2491dc1

Browse files
authored
Add Newton Schultz via Polar Express as a retraction for Iso optimizer (#294)
* Add Newton Schultz via Polar Express as a retraction for Iso optimizer Signed-off-by: Konstantin Golobokov <kogolobo@uw.edu>
1 parent 02b6ec8 commit 2491dc1

5 files changed

Lines changed: 359 additions & 42 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Benchmark Stiefel retractions in Iso: latency, throughput, and numerical precision."""
16+
17+
from collections.abc import Callable
18+
from typing import Any
19+
20+
import torch
21+
import triton
22+
23+
from emerging_optimizers import utils
24+
from emerging_optimizers.riemannian_optimizers.retractions.stiefel import (
25+
cayley_retraction,
26+
newton_schulz_retraction,
27+
polar_retraction,
28+
qr_retraction,
29+
)
30+
31+
32+
def bench(fn: Callable[[], Any], warmup: int = 25, rep: int = 100) -> float:
33+
"""Time fn with triton.testing.do_bench (returns execution time in ms)."""
34+
return triton.testing.do_bench(fn, warmup=warmup, rep=rep)
35+
36+
37+
def test_numerical_accuracy(
38+
ref: torch.Tensor,
39+
qr: torch.Tensor,
40+
cayley: torch.Tensor,
41+
ns5: torch.Tensor,
42+
ns8: torch.Tensor,
43+
) -> dict[str, float]:
44+
"""Verify factor orthogonality and error vs exact polar factor in a single batched D2H sync."""
45+
k = ref.shape[-1]
46+
eye = torch.eye(k, device=ref.device, dtype=ref.dtype)
47+
48+
# Orthogonality deviations on GPU: ||Q^T Q - I||_max
49+
orth_polar = (ref.mT @ ref - eye).abs().max()
50+
orth_qr = (qr.mT @ qr - eye).abs().max()
51+
orth_cayley = (cayley.mT @ cayley - eye).abs().max()
52+
orth_ns5 = (ns5.mT @ ns5 - eye).abs().max()
53+
orth_ns8 = (ns8.mT @ ns8 - eye).abs().max()
54+
55+
# Approximation error vs analytical polar factor on GPU
56+
ref_f = ref.float()
57+
diff_ns5 = (ns5.float() - ref_f).abs()
58+
err_ns5_abs = diff_ns5.max()
59+
err_ns5_rel = (diff_ns5 / ref_f.abs().clamp_min(1e-8)).max()
60+
61+
diff_ns8 = (ns8.float() - ref_f).abs()
62+
err_ns8_abs = diff_ns8.max()
63+
err_ns8_rel = (diff_ns8 / ref_f.abs().clamp_min(1e-8)).max()
64+
65+
# Batch all 9 metrics into a single device-to-host synchronization
66+
metrics = torch.stack(
67+
[
68+
orth_polar,
69+
orth_qr,
70+
orth_cayley,
71+
orth_ns5,
72+
orth_ns8,
73+
err_ns5_abs,
74+
err_ns5_rel,
75+
err_ns8_abs,
76+
err_ns8_rel,
77+
]
78+
).tolist()
79+
80+
return {
81+
"orth_polar": metrics[0],
82+
"orth_qr": metrics[1],
83+
"orth_cayley": metrics[2],
84+
"orth_ns5": metrics[3],
85+
"orth_ns8": metrics[4],
86+
"err_ns5_abs": metrics[5],
87+
"err_ns5_rel": metrics[6],
88+
"err_ns8_abs": metrics[7],
89+
"err_ns8_rel": metrics[8],
90+
}
91+
92+
93+
def run_case(
94+
m: int,
95+
n: int,
96+
lr: float = 1e-3,
97+
device: str = "cuda",
98+
fp32_prec: utils.FP32MatmulPrecT = "high",
99+
) -> None:
100+
"""Benchmark retractions for a single parameter tensor of shape (M, N)."""
101+
torch.manual_seed(0)
102+
k = min(m, n)
103+
104+
# Initialize factor matrices U (M, K) and V (N, K) on the Stiefel manifold
105+
raw_u = torch.randn(m, k, device=device, dtype=torch.float32)
106+
raw_v = torch.randn(n, k, device=device, dtype=torch.float32)
107+
u_init, _ = torch.linalg.qr(raw_u, mode="reduced")
108+
v_init, _ = torch.linalg.qr(raw_v, mode="reduced")
109+
110+
# Synthetic momentum updates
111+
mom_u = torch.randn_like(u_init)
112+
mom_v = torch.randn_like(v_init)
113+
114+
# --- Correctness & Numerical Precision ---
115+
ref_u = polar_retraction(u_init, mom_u, lr)
116+
117+
qr_u = qr_retraction(u_init, mom_u, lr)
118+
cayley_u = cayley_retraction(u_init, mom_u, lr)
119+
ns5_u = newton_schulz_retraction(u_init, mom_u, lr, num_ns_steps=5)
120+
ns8_u = newton_schulz_retraction(u_init, mom_u, lr, num_ns_steps=8)
121+
122+
acc = test_numerical_accuracy(ref_u, qr_u, cayley_u, ns5_u, ns8_u)
123+
124+
# --- Latency Benchmarking (Factor U + Factor V) ---
125+
with utils.fp32_matmul_precision(fp32_prec):
126+
t_polar = bench(lambda: (polar_retraction(u_init, mom_u, lr), polar_retraction(v_init, mom_v, lr)))
127+
t_qr = bench(lambda: (qr_retraction(u_init, mom_u, lr), qr_retraction(v_init, mom_v, lr)))
128+
t_cayley = bench(
129+
lambda: (
130+
cayley_retraction(u_init, mom_u, lr),
131+
cayley_retraction(v_init, mom_v, lr),
132+
)
133+
)
134+
t_ns5 = bench(
135+
lambda: (
136+
newton_schulz_retraction(u_init, mom_u, lr, num_ns_steps=5),
137+
newton_schulz_retraction(v_init, mom_v, lr, num_ns_steps=5),
138+
)
139+
)
140+
t_ns8 = bench(
141+
lambda: (
142+
newton_schulz_retraction(u_init, mom_u, lr, num_ns_steps=8),
143+
newton_schulz_retraction(v_init, mom_v, lr, num_ns_steps=8),
144+
)
145+
)
146+
147+
tag = f"M={m:<5d} N={n:<5d} K={k:<5d}"
148+
speedup_ns8_vs_polar = t_polar / t_ns8
149+
150+
print(
151+
f"{tag} | polar(svd) {t_polar:8.3f} ms | qr {t_qr:7.3f} ms | cayley {t_cayley:7.3f} ms | "
152+
f"ns(steps=5) {t_ns5:7.3f} ms | ns(steps=8) {t_ns8:7.3f} ms | "
153+
f"ns8 speedup vs svd: {speedup_ns8_vs_polar:6.2f}x"
154+
)
155+
print(
156+
f"{'':>4}orth drift ||Q^T Q - I||: svd={acc['orth_polar']:.2e} | qr={acc['orth_qr']:.2e} | "
157+
f"cayley={acc['orth_cayley']:.2e} | ns5={acc['orth_ns5']:.2e} | ns8={acc['orth_ns8']:.2e}"
158+
)
159+
print(
160+
f"{'':>4}ns vs polar factor: ns5 abs={acc['err_ns5_abs']:.2e} rel={acc['err_ns5_rel']:.2e} | "
161+
f"ns8 abs={acc['err_ns8_abs']:.2e} rel={acc['err_ns8_rel']:.2e}\n"
162+
)
163+
164+
165+
def main() -> None:
166+
"""Run benchmark across square and rectangular transformer projection dimensions."""
167+
torch.cuda.init()
168+
device_name = torch.cuda.get_device_name(0)
169+
print(f"Device: {device_name}")
170+
171+
cases = [
172+
(1024, 1024),
173+
(2048, 2048),
174+
(4096, 4096),
175+
(4096, 2048),
176+
(8192, 2048),
177+
]
178+
179+
print("=== Iso Stiefel Retraction Benchmark Suite ===")
180+
for m, n in cases:
181+
run_case(m, n)
182+
183+
184+
if __name__ == "__main__":
185+
main()

‎emerging_optimizers/riemannian_optimizers/isospectral.py‎

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@
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-
from typing import TYPE_CHECKING, Callable, Literal, override
15+
from typing import TYPE_CHECKING, Callable, override
16+
17+
from emerging_optimizers.riemannian_optimizers.retractions.stiefel import (
18+
RetractionT,
19+
cayley_retraction,
20+
newton_schulz_retraction,
21+
polar_retraction,
22+
qr_retraction,
23+
)
1624

1725

1826
if TYPE_CHECKING:
@@ -26,43 +34,6 @@
2634

2735
__all__ = ["Iso"]
2836

29-
RetractionT = Literal["qr", "polar", "cayley"]
30-
31-
32-
def _qr_retraction(
33-
point: torch.Tensor,
34-
momentum: torch.Tensor,
35-
step_size: float,
36-
) -> torch.Tensor:
37-
matrix = point - step_size * momentum
38-
q, r = torch.linalg.qr(matrix, mode="reduced")
39-
signs = torch.diagonal(r).sign()
40-
signs.masked_fill_(signs == 0, 1)
41-
return q * signs
42-
43-
44-
def _polar_retraction(
45-
point: torch.Tensor,
46-
momentum: torch.Tensor,
47-
step_size: float,
48-
) -> torch.Tensor:
49-
matrix = point - step_size * momentum
50-
u, _, vh = torch.linalg.svd(matrix, full_matrices=False)
51-
return u @ vh
52-
53-
54-
def _cayley_retraction(
55-
point: torch.Tensor,
56-
momentum: torch.Tensor,
57-
step_size: float,
58-
) -> torch.Tensor:
59-
direction = -momentum
60-
skew = direction @ point.mT - point @ direction.mT
61-
identity = torch.eye(point.shape[0], dtype=point.dtype, device=point.device)
62-
lhs = identity - 0.5 * step_size * skew
63-
rhs = (identity + 0.5 * step_size * skew) @ point
64-
return torch.linalg.solve(lhs, rhs)
65-
6637

6738
def _retract_factors(
6839
u: torch.Tensor,
@@ -89,11 +60,13 @@ def _retract_factors(
8960
ValueError: If the retraction method is unsupported.
9061
"""
9162
if retraction == "qr":
92-
retract = _qr_retraction
63+
retract = qr_retraction
9364
elif retraction == "polar":
94-
retract = _polar_retraction
65+
retract = polar_retraction
9566
elif retraction == "cayley":
96-
retract = _cayley_retraction
67+
retract = cayley_retraction
68+
elif retraction == "newton_schulz":
69+
retract = newton_schulz_retraction
9770
else:
9871
raise ValueError(f"Invalid retraction: {retraction}")
9972

@@ -138,7 +111,7 @@ def __init__(
138111
raise ValueError(f"Invalid learning rate: {lr}")
139112
if not 0.0 <= momentum < 1.0:
140113
raise ValueError(f"Invalid momentum value: {momentum}")
141-
if retraction not in ("qr", "polar", "cayley"):
114+
if retraction not in ("qr", "polar", "cayley", "newton_schulz"):
142115
raise ValueError(f"Invalid retraction: {retraction}")
143116

144117
defaults = {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
from emerging_optimizers.riemannian_optimizers.retractions.stiefel import *

0 commit comments

Comments
 (0)