Skip to content

Commit 1ce78e8

Browse files
committed
feat: add scaled CANS inverse root utility
Signed-off-by: mkhona <mkhona@nvidia.com>
1 parent bfa7cf3 commit 1ce78e8

2 files changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
import torch
16+
from absl import flags, logging
17+
from absl.testing import absltest, parameterized
18+
19+
from emerging_optimizers.soap.matrix_root_inverse_utils import scaled_cans_coupled_ns
20+
from emerging_optimizers.utils import FP32MatmulPrecT
21+
22+
23+
flags.DEFINE_enum("device", "cpu", ["cpu", "cuda"], "Device to run tests on")
24+
flags.DEFINE_integer("seed", None, "Random seed for reproducible tests")
25+
FLAGS = flags.FLAGS
26+
27+
28+
def setUpModule() -> None:
29+
if FLAGS.seed is not None:
30+
logging.info("Setting random seed to %d", FLAGS.seed)
31+
torch.manual_seed(FLAGS.seed)
32+
if torch.cuda.is_available():
33+
torch.cuda.manual_seed_all(FLAGS.seed)
34+
35+
36+
class CANSUtilsTest(parameterized.TestCase):
37+
@parameterized.parameters((4, 4), (2, 4, 4)) # type: ignore[misc]
38+
def test_scaled_cans_supports_optional_batch_dimension(self, shape: tuple[int, ...]) -> None:
39+
if FLAGS.device != "cuda":
40+
self.skipTest("CANS requires CUDA")
41+
42+
x = torch.randn(*shape, device=FLAGS.device)
43+
matrix = x @ x.mT + 0.1 * torch.eye(shape[-1], device=FLAGS.device)
44+
45+
inverse_root = scaled_cans_coupled_ns(matrix)
46+
47+
self.assertEqual(inverse_root.shape, matrix.shape)
48+
self.assertTrue(torch.isfinite(inverse_root).all())
49+
50+
def test_scaled_cans_rejects_cpu_tensor(self) -> None:
51+
with self.assertRaisesRegex(TypeError, "only supports CUDA"):
52+
scaled_cans_coupled_ns(torch.eye(4))
53+
54+
@parameterized.parameters("medium", "high", "highest") # type: ignore[misc]
55+
def test_scaled_cans_supports_fp32_matmul_precision(self, fp32_matmul_prec: FP32MatmulPrecT) -> None:
56+
if FLAGS.device != "cuda":
57+
self.skipTest("CANS requires CUDA")
58+
59+
x = torch.randn(4, 4, device=FLAGS.device)
60+
matrix = x @ x.T + 0.1 * torch.eye(4, device=FLAGS.device)
61+
previous_precision = torch.get_float32_matmul_precision()
62+
63+
inverse_root = scaled_cans_coupled_ns(matrix, fp32_matmul_prec=fp32_matmul_prec)
64+
65+
self.assertTrue(torch.isfinite(inverse_root).all())
66+
self.assertEqual(torch.get_float32_matmul_precision(), previous_precision)
67+
68+
69+
if __name__ == "__main__":
70+
absltest.main()

0 commit comments

Comments
 (0)