|
| 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 | +import torch |
| 16 | +from torch import Tensor |
| 17 | + |
| 18 | +from emerging_optimizers import utils |
| 19 | +from emerging_optimizers.utils import FP32MatmulPrecT |
| 20 | + |
| 21 | + |
| 22 | +__all__ = ["scaled_cans_coupled_ns"] |
| 23 | + |
| 24 | +_CANS_COEFFS = ( |
| 25 | + (5.182503604966906, -5.126830178299687), |
| 26 | + (2.586120737395915, -0.641538812403133), |
| 27 | + (2.567364126726186, -0.6391058222170474), |
| 28 | + (2.520560084348265, -0.6330225823828756), |
| 29 | + (2.410759275435182, -0.6186815444268036), |
| 30 | + (2.1883348130094173, -0.5893091162177136), |
| 31 | + (1.8595760874873613, -0.5449991062102938), |
| 32 | + (1.589020160467417, -0.5075811685214573), |
| 33 | + (1.5051653981684994, -0.4957799077972079), |
| 34 | + (1.4925557853149838, -0.49259266842078675), |
| 35 | +) |
| 36 | + |
| 37 | + |
| 38 | +def scaled_cans_coupled_ns( |
| 39 | + x: Tensor, |
| 40 | + eps: float = 1e-12, |
| 41 | + fp32_matmul_prec: FP32MatmulPrecT = "highest", |
| 42 | +) -> Tensor: |
| 43 | + """Compute inverse square roots with scaled coupled CANS Newton-Schulz. |
| 44 | +
|
| 45 | + CANS polynomial-based inverse-root computation from https://arxiv.org/abs/2506.10935. |
| 46 | +
|
| 47 | + This implementation applies the CANS orthogonalization polynomials to the coupled |
| 48 | + Newton-Schulz iteration for a symmetric positive-definite matrix. It uses a fixed |
| 49 | + ten-step schedule and normalizes with the matrix infinity norm rather than the exact |
| 50 | + spectral norm. The infinity norm is an inexpensive upper bound, but can conservatively |
| 51 | + scale matrices whose rows contain substantial cancellation and consequently slow |
| 52 | + convergence for their smallest eigenvalues. |
| 53 | +
|
| 54 | + The tabulated coefficients fold a 1% spectral safety margin into the polynomial. Starting |
| 55 | + from the unscaled CANS pairs ``(beta, alpha)``, steps zero through eight keep ``beta`` and |
| 56 | + use ``alpha / 1.01``. The final pair additionally absorbs the output normalization and is |
| 57 | + computed as ``(beta / sqrt(1.01), alpha / 1.01**1.5)``. This is algebraically equivalent |
| 58 | + in exact arithmetic to normalizing by ``1.01 * scale`` and applying the original |
| 59 | + coefficients. The literals were generated with Python IEEE-754 binary64 arithmetic and |
| 60 | + rounded to the nearest representable binary64 value. In particular, |
| 61 | + ``1.5 / sqrt(1.01)`` evaluates to ``1.4925557853149838``; the decimal |
| 62 | + ``1.492555785314984`` is one binary64 ULP higher. At runtime these Python scalars are |
| 63 | + applied to FP32 tensors, while the matrix multiplications use the selected BF16, TF32, or |
| 64 | + FP32 mode. |
| 65 | +
|
| 66 | + In practice, the result is approximate because the iteration is truncated after ten steps, |
| 67 | + the normalization may overestimate the largest eigenvalue, matrix multiplications may use |
| 68 | + reduced precision, and the returned inverse root is explicitly symmetrized. ``eps`` also |
| 69 | + clamps the normalization for degenerate inputs. |
| 70 | +
|
| 71 | + Args: |
| 72 | + x: A 2D symmetric positive-definite matrix or 3D batch of matrices. |
| 73 | + eps: Lower bound used when normalizing the matrices. |
| 74 | + fp32_matmul_prec: Precision used for FP32 matrix multiplications: ``"medium"`` for BF16, |
| 75 | + ``"high"`` for TF32, or ``"highest"`` for FP32. |
| 76 | +
|
| 77 | + Returns: |
| 78 | + The approximate inverse square root with the same shape as ``x``. |
| 79 | + """ |
| 80 | + if x.dim() not in (2, 3) or x.shape[-2] != x.shape[-1]: |
| 81 | + raise TypeError(f"x must be a square matrix or batch of square matrices, got shape {tuple(x.shape)}") |
| 82 | + if not x.is_cuda: |
| 83 | + raise TypeError("scaled_cans_coupled_ns only supports CUDA tensors") |
| 84 | + |
| 85 | + with utils.fp32_matmul_precision(fp32_matmul_prec): |
| 86 | + input_dtype = x.dtype |
| 87 | + x = x.float() |
| 88 | + scale = torch.linalg.matrix_norm(x, ord=float("inf"), dim=(-2, -1), keepdim=True).clamp_min_(eps) |
| 89 | + y = x / scale |
| 90 | + z = torch.eye(x.shape[-1], device=x.device, dtype=x.dtype).expand_as(x) |
| 91 | + cans_addmm = torch.addmm if x.dim() == 2 else torch.baddbmm |
| 92 | + for beta, alpha in _CANS_COEFFS: |
| 93 | + p = z @ y |
| 94 | + z, y = ( |
| 95 | + cans_addmm(z, p, z, beta=beta, alpha=alpha), |
| 96 | + cans_addmm(y, y, p, beta=beta, alpha=alpha), |
| 97 | + ) |
| 98 | + |
| 99 | + z.mul_(torch.rsqrt(scale)) |
| 100 | + return ((z + z.mT) / 2.0).to(input_dtype) |
0 commit comments