Skip to content

Commit 5c1aa49

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

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 math
16+
17+
import torch
18+
from torch import Tensor
19+
20+
from emerging_optimizers import utils
21+
from emerging_optimizers.utils import FP32MatmulPrecT
22+
23+
24+
__all__ = ["scaled_cans_coupled_ns"]
25+
26+
_CANS_COEFFS = (
27+
(5.182503604966906, -5.178098480082684),
28+
(2.586120737395915, -0.6479542005271643),
29+
(2.567364126726186, -0.6454968804392178),
30+
(2.520560084348265, -0.6393528082067044),
31+
(2.410759275435182, -0.6248683598710716),
32+
(2.1883348130094173, -0.5952022073798908),
33+
(1.8595760874873613, -0.5504490972723968),
34+
(1.589020160467417, -0.5126569802066718),
35+
(1.5051653981684994, -0.5007377068751799),
36+
(1.5, -0.5),
37+
)
38+
_CANS_M = 16384.0
39+
_CANS_Z_MAX = (
40+
1.0,
41+
5.183,
42+
13.403,
43+
34.409,
44+
86.731,
45+
209.09,
46+
457.55,
47+
850.85,
48+
1352.0,
49+
2035.0,
50+
3052.5,
51+
)
52+
_CANS_Y_MAX = (1.0, 1.297, 1.726, 1.473, 1.545, 1.415, 1.273, 1.148, 1.0, 1.0, 1.0)
53+
_CANS_P_MAX = (1.0, 3.98, 3.95, 3.88, 3.71, 3.32, 2.60, 1.73, 1.15, 1.008, 1.0)
54+
_CANS_S_Z = tuple(_CANS_M / value for value in _CANS_Z_MAX)
55+
_CANS_S_Y = tuple(_CANS_M / value for value in _CANS_Y_MAX)
56+
_CANS_S_P = tuple(_CANS_M / value for value in _CANS_P_MAX)
57+
_CANS_ALPHA_Y_0 = _CANS_COEFFS[0][1] * _CANS_S_Y[1] / (_CANS_S_Y[0] ** 2)
58+
_CANS_BETA_Y_0 = _CANS_COEFFS[0][0] * _CANS_S_Y[1] / _CANS_S_Y[0]
59+
_CANS_Z_SCALE_0 = _CANS_COEFFS[0][1] * _CANS_S_Z[1] / _CANS_S_Y[0]
60+
_CANS_Z_DIAG_ADD_0 = _CANS_COEFFS[0][0] * _CANS_S_Z[1]
61+
_CANS_ALPHA_P = tuple(_CANS_S_P[k] / (_CANS_S_Z[k] * _CANS_S_Y[k]) for k in range(1, len(_CANS_COEFFS)))
62+
_CANS_ALPHA_Y = tuple(
63+
_CANS_COEFFS[k][1] * _CANS_S_Y[k + 1] / (_CANS_S_Y[k] * _CANS_S_P[k]) for k in range(1, len(_CANS_COEFFS))
64+
)
65+
_CANS_BETA_Y = tuple(_CANS_COEFFS[k][0] * _CANS_S_Y[k + 1] / _CANS_S_Y[k] for k in range(1, len(_CANS_COEFFS)))
66+
_CANS_ALPHA_Z_BASE = tuple(
67+
_CANS_COEFFS[k][1] * _CANS_S_Z[k + 1] / (_CANS_S_Z[k] * _CANS_S_P[k]) for k in range(1, len(_CANS_COEFFS))
68+
)
69+
_CANS_BETA_Z_BASE = tuple(_CANS_COEFFS[k][0] * _CANS_S_Z[k + 1] / _CANS_S_Z[k] for k in range(1, len(_CANS_COEFFS)))
70+
_CANS_ALPHA_Z = _CANS_ALPHA_Z_BASE[:-1] + (_CANS_ALPHA_Z_BASE[-1] / _CANS_S_Z[-1],)
71+
_CANS_BETA_Z = _CANS_BETA_Z_BASE[:-1] + (_CANS_BETA_Z_BASE[-1] / _CANS_S_Z[-1],)
72+
73+
74+
def _estimate_max_eigenvalue(x: Tensor, eps: float) -> Tensor:
75+
n = x.size(-1)
76+
diag = x.diagonal(dim1=-2, dim2=-1)
77+
mean = diag.sum(dim=-1) / n
78+
sq_norm = torch.sum(x**2, dim=(-2, -1))
79+
variance = torch.clamp((sq_norm / n) - (mean**2), min=0.0)
80+
ws_bound = mean + torch.sqrt(variance) * math.sqrt(n - 1)
81+
82+
abs_x = torch.abs(x)
83+
row_sum = torch.sum(abs_x, dim=-1).clamp_min_(eps)
84+
minc_bound = torch.max(torch.einsum("...ij,...j->...i", abs_x, row_sum) / row_sum, dim=-1).values
85+
return torch.minimum(ws_bound, minc_bound)
86+
87+
88+
def scaled_cans_coupled_ns(
89+
x: Tensor,
90+
eps: float = 1e-12,
91+
fp32_matmul_prec: FP32MatmulPrecT = "highest",
92+
) -> Tensor:
93+
"""Compute inverse square roots with scaled coupled CANS Newton-Schulz.
94+
95+
Args:
96+
x: A 2D symmetric positive-definite matrix or 3D batch of matrices.
97+
eps: Lower bound used when normalizing the matrices.
98+
fp32_matmul_prec: Precision used for FP32 matrix multiplications: ``"medium"`` for BF16,
99+
``"high"`` for TF32, or ``"highest"`` for FP32.
100+
101+
Returns:
102+
The approximate inverse square root with the same shape as ``x``.
103+
"""
104+
if x.dim() not in (2, 3) or x.shape[-2] != x.shape[-1]:
105+
raise TypeError(f"x must be a square matrix or batch of square matrices, got shape {tuple(x.shape)}")
106+
if not x.is_cuda:
107+
raise TypeError("scaled_cans_coupled_ns only supports CUDA tensors")
108+
109+
with utils.fp32_matmul_precision(fp32_matmul_prec):
110+
input_dtype = x.dtype
111+
x = x.float()
112+
max_eigval = (_estimate_max_eigenvalue(x, eps) * 1.01).clamp_min_(eps)
113+
max_eigval = max_eigval[..., None, None]
114+
cans_addmm = torch.addmm if x.dim() == 2 else torch.baddbmm
115+
y = x * (_CANS_S_Y[0] / max_eigval)
116+
117+
z = y.mul(_CANS_Z_SCALE_0)
118+
z.diagonal(dim1=-2, dim2=-1).add_(_CANS_Z_DIAG_ADD_0)
119+
y = cans_addmm(y, y, y, beta=_CANS_BETA_Y_0, alpha=_CANS_ALPHA_Y_0)
120+
121+
for k in range(8):
122+
p = cans_addmm(z, z, y, beta=0.0, alpha=_CANS_ALPHA_P[k])
123+
y, z = (
124+
cans_addmm(y, y, p, beta=_CANS_BETA_Y[k], alpha=_CANS_ALPHA_Y[k]),
125+
cans_addmm(z, p, z, beta=_CANS_BETA_Z[k], alpha=_CANS_ALPHA_Z[k]),
126+
)
127+
128+
p = cans_addmm(z, z, y, beta=0.0, alpha=_CANS_ALPHA_P[8])
129+
inverse_root = cans_addmm(z, p, z, beta=_CANS_BETA_Z[8], alpha=_CANS_ALPHA_Z[8])
130+
inverse_root.mul_(torch.rsqrt(max_eigval))
131+
inverse_root = (inverse_root + inverse_root.mT) / 2.0
132+
return inverse_root.to(input_dtype)

tests/test_cans_utils.py

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.cans_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)