|
| 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() |
0 commit comments