From b84de5c3af82defb75fc61c96dcf4d2b6113c241 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:45:00 +0100 Subject: [PATCH 1/2] Stabilize singular empirical covariance precision Signed-off-by: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> --- .../cuml/covariance/empirical_covariance.py | 10 ++++++++-- .../cuml/tests/test_empirical_covariance.py | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/python/cuml/cuml/covariance/empirical_covariance.py b/python/cuml/cuml/covariance/empirical_covariance.py index 6a248ecd94..29ed9ddb75 100644 --- a/python/cuml/cuml/covariance/empirical_covariance.py +++ b/python/cuml/cuml/covariance/empirical_covariance.py @@ -23,6 +23,12 @@ def _empirical_covariance(X, assume_centered=False): return cp.dot(X_centered.T, X_centered) / X.shape[0] +def _pinv(covariance): + """Compute a dtype-aware Moore-Penrose pseudoinverse.""" + rcond = max(covariance.shape) * cp.finfo(covariance.dtype).eps + return cp.linalg.pinv(covariance, rcond=rcond) + + def _log_likelihood(emp_cov, precision): """Compute the sample mean log-likelihood under a covariance model.""" sign, log_det_precision = cp.linalg.slogdet(precision) @@ -188,7 +194,7 @@ def fit(self, X, y=None) -> "EmpiricalCovariance": self.covariance_ = covariance if self.store_precision: - self.precision_ = cp.linalg.pinv(covariance) + self.precision_ = _pinv(covariance) else: self.precision_ = None @@ -207,7 +213,7 @@ def get_precision(self): if self.store_precision: return self.precision_ - return cp.linalg.pinv(self.covariance_) + return _pinv(self.covariance_) @mlfunc(convert_output=False) def score(self, X_test, y=None) -> float: diff --git a/python/cuml/tests/test_empirical_covariance.py b/python/cuml/tests/test_empirical_covariance.py index 04d1b2f28a..d7a028bb25 100644 --- a/python/cuml/tests/test_empirical_covariance.py +++ b/python/cuml/tests/test_empirical_covariance.py @@ -244,6 +244,26 @@ def test_mahalanobis_matches_sklearn(): ) +@pytest.mark.parametrize("store_precision", [True, False]) +def test_singular_covariance_matches_sklearn(store_precision): + X = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float32) + cu_cov = EmpiricalCovariance(store_precision=store_precision).fit(X) + sk_cov = SklearnEmpiricalCovariance(store_precision=store_precision).fit(X) + + np.testing.assert_allclose( + np.asarray(cu_cov.get_precision()), + sk_cov.get_precision(), + rtol=1e-5, + atol=1e-6, + ) + np.testing.assert_allclose( + np.asarray(cu_cov.mahalanobis(X[:2])), + sk_cov.mahalanobis(X[:2]), + rtol=1e-5, + atol=1e-6, + ) + + def test_error_norm_frobenius_matches_sklearn(): X = _make_random_data() cu_cov = EmpiricalCovariance().fit(X) From 26116129ded39d28a20561bbdd30686bed61340f Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Thu, 10 Sep 2026 05:24:54 -0500 Subject: [PATCH 2/2] Fix copyright notice Signed-off-by: Simon Adorf --- python/cuml/tests/test_empirical_covariance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuml/tests/test_empirical_covariance.py b/python/cuml/tests/test_empirical_covariance.py index d7a028bb25..5c4d851cb9 100644 --- a/python/cuml/tests/test_empirical_covariance.py +++ b/python/cuml/tests/test_empirical_covariance.py @@ -1,5 +1,5 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 #