Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cvxpy/tests/test_linalg_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,22 @@ def test_indefinite_with_zero_diagonal(self):
with self.assertRaises(ValueError, msg=lau.SparseCholeskyMessages.INDEFINITE):
lau.sparse_cholesky(A, 0.0)

def test_rank_deficient_full_diagonal(self):
# [[1,1],[1,1]] is rank-1 PSD with no zero diagonal entries; QDLDL
# alone cannot factor it, so this exercises the eigh fallback.
A = sp.csc_array(np.array([[1.0, 1.0], [1.0, 1.0]]))
sign, L, p = lau.sparse_cholesky(A)
self.assertEqual(sign, 1.0)
self.assertEqual(L.shape, (2, 1))
self.check_gram(L[p, :], A)

def test_nsd_rank_deficient_full_diagonal(self):
A = sp.csc_array(-np.array([[1.0, 1.0], [1.0, 1.0]]))
sign, L, p = lau.sparse_cholesky(A)
self.assertEqual(sign, -1.0)
self.assertEqual(L.shape, (2, 1))
self.check_gram(L[p, :], -A)

def test_nonsingular_indefinite(self):
np.random.seed(0)
n = 5
Expand Down
20 changes: 16 additions & 4 deletions cvxpy/utilities/linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,19 @@ def sparse_cholesky(A, sym_tol=settings.CHOL_SYM_TOL, assume_psd=False):

except ValueError:
raise
except Exception as e:
raise ValueError(
f"{SparseCholeskyMessages.FACTORIZATION_FAILED}: {e}"
) from e
except Exception:
# QDLDL has no pivoting and fails when a zero pivot appears
# mid-elimination on rank-deficient PSD/NSD inputs (e.g. [[1,1],[1,1]]).
# Fall back to a dense symmetric eigendecomposition.
w, V = np.linalg.eigh(A.toarray())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The except Exception clause (unchanged from before) now triggers an O(n³) dense eigendecomposition + O(n²) memory allocation via A.toarray(), rather than just re-raising as a ValueError. If a large sparse matrix hits this path for an unexpected reason (not rank-deficiency), the dense fallback could OOM or hang.

Consider either:

  1. Narrowing the catch to the specific exception type QDLDL raises on zero-pivot (likely a C-level RuntimeError), or
  2. Adding a size guard before densifying, e.g.:
if n > 5000:
    raise ValueError(f"{SparseCholeskyMessages.FACTORIZATION_FAILED}: matrix too large for dense fallback")

This way truly unexpected failures on large matrices still surface immediately rather than silently attempting dense computation.

scale = np.max(np.abs(w))
if scale == 0:
return 1.0, sp.csr_array((n, 0)), np.arange(n)
is_psd = np.all(w >= -tol * scale)
is_nsd = np.all(w <= tol * scale)
if not (is_psd or is_nsd):
raise ValueError(SparseCholeskyMessages.INDEFINITE)
sign = 1.0 if is_psd else -1.0
mask = np.abs(w) > tol * scale
L = V[:, mask] * np.sqrt(np.abs(w[mask]))
return sign, sp.csr_array(L), np.arange(n)
Loading