From 9fbc3ae927a77191d86cd45b022b5a8af884132c Mon Sep 17 00:00:00 2001 From: dance858 Date: Mon, 9 Mar 2026 16:53:36 -0700 Subject: [PATCH 1/4] add some tests and sketch new converter --- .../nlp_solvers/diff_engine/converters.py | 62 ++++++++++++------- .../test_matmul_sparse.py | 40 ++++++++++++ 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py b/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py index 348d80c4b5..5b9bffd524 100644 --- a/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py +++ b/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py @@ -48,31 +48,47 @@ def _convert_matmul(expr, children): if left_arg.is_constant(): A = left_arg.value - if not isinstance(A, sparse.csr_matrix): - A = sparse.csr_matrix(A) - - return _diffengine.make_left_matmul( - children[1], - A.data.astype(np.float64), - A.indices.astype(np.int32), - A.indptr.astype(np.int32), - A.shape[0], - A.shape[1], - ) + if sparse.issparse(A) or True: + if not isinstance(A, sparse.csr_matrix): + A = sparse.csr_matrix(A) + + return _diffengine.make_sparse_left_matmul( + children[1], + A.data.astype(np.float64, copy=False), + A.indices.astype(np.int32, copy=False), + A.indptr.astype(np.int32, copy=False), + A.shape[0], + A.shape[1], + ) + else: + return _diffengine.make_dense_left_matmul( + children[1], + A.flatten(order='F'), + A.shape[0], + A.shape[1], + ) elif right_arg.is_constant(): A = right_arg.value - - if not isinstance(A, sparse.csr_matrix): - A = sparse.csr_matrix(A) - - return _diffengine.make_right_matmul( - children[0], - A.data.astype(np.float64), - A.indices.astype(np.int32), - A.indptr.astype(np.int32), - A.shape[0], - A.shape[1], - ) + + if sparse.issparse(A) or True: + if not isinstance(A, sparse.csr_matrix): + A = sparse.csr_matrix(A) + + return _diffengine.make_sparse_right_matmul( + children[0], + A.data.astype(np.float64, copy=False), + A.indices.astype(np.int32, copy=False), + A.indptr.astype(np.int32, copy=False), + A.shape[0], + A.shape[1], + ) + else: + return _diffengine.make_dense_right_matmul( + children[0], + A.flatten(order='F'), + A.shape[0], + A.shape[1], + ) else: return _diffengine.make_matmul(children[0], children[1]) diff --git a/cvxpy/tests/nlp_tests/stress_tests_diff_engine/test_matmul_sparse.py b/cvxpy/tests/nlp_tests/stress_tests_diff_engine/test_matmul_sparse.py index 729550f1fa..cd15aa5c6b 100644 --- a/cvxpy/tests/nlp_tests/stress_tests_diff_engine/test_matmul_sparse.py +++ b/cvxpy/tests/nlp_tests/stress_tests_diff_engine/test_matmul_sparse.py @@ -73,3 +73,43 @@ def test_dense_sparse_sparse(self): assert np.allclose(dense_val, csc_val) assert np.allclose(dense_sol, sparse_sol) assert np.allclose(dense_sol, csc_sol) + + def test_dense_left_matmul(self): + np.random.seed(0) + m, n = 4, 4 + A = np.random.rand(m, n) + X = cp.Variable((n, n), nonneg=True) + B = np.random.rand(m, n) + obj = cp.Minimize(cp.sum_squares(A @ X - B)) + constraints = [] + problem = cp.Problem(obj, constraints) + problem.solve(nlp=True, verbose=True) + checker = DerivativeChecker(problem) + checker.run_and_assert() + + def test_dense_right_matmul(self): + np.random.seed(0) + m, n = 4, 4 + A = np.random.rand(m, n) + X = cp.Variable((n, n), nonneg=True) + B = np.random.rand(m, n) + obj = cp.Minimize(cp.sum_squares(X @ A - B)) + constraints = [] + problem = cp.Problem(obj, constraints) + problem.solve(nlp=True, verbose=True) + checker = DerivativeChecker(problem) + checker.run_and_assert() + + def test_sparse_and_dense_matmul(self): + np.random.seed(0) + m, n = 4, 4 + A = np.random.rand(m, n) + C = sp.random(m, n, density=0.5) + X = cp.Variable((n, n), nonneg=True) + B = np.random.rand(m, n) + obj = cp.Minimize(cp.sum_squares(A @ X @ C - B)) + constraints = [] + problem = cp.Problem(obj, constraints) + problem.solve(nlp=True, verbose=True) + checker = DerivativeChecker(problem) + checker.run_and_assert() From 98fc20202c488fbff6fcf3b37fe0915b19740d1a Mon Sep 17 00:00:00 2001 From: dance858 Date: Mon, 9 Mar 2026 17:14:58 -0700 Subject: [PATCH 2/4] flattening --- .../solvers/nlp_solvers/diff_engine/converters.py | 8 ++++---- cvxpy/tests/nlp_tests/derivative_checker.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py b/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py index 5b9bffd524..daeea9eb68 100644 --- a/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py +++ b/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py @@ -48,7 +48,7 @@ def _convert_matmul(expr, children): if left_arg.is_constant(): A = left_arg.value - if sparse.issparse(A) or True: + if sparse.issparse(A): if not isinstance(A, sparse.csr_matrix): A = sparse.csr_matrix(A) @@ -63,14 +63,14 @@ def _convert_matmul(expr, children): else: return _diffengine.make_dense_left_matmul( children[1], - A.flatten(order='F'), + A.flatten(order='C'), A.shape[0], A.shape[1], ) elif right_arg.is_constant(): A = right_arg.value - if sparse.issparse(A) or True: + if sparse.issparse(A): if not isinstance(A, sparse.csr_matrix): A = sparse.csr_matrix(A) @@ -85,7 +85,7 @@ def _convert_matmul(expr, children): else: return _diffengine.make_dense_right_matmul( children[0], - A.flatten(order='F'), + A.flatten(order='C'), A.shape[0], A.shape[1], ) diff --git a/cvxpy/tests/nlp_tests/derivative_checker.py b/cvxpy/tests/nlp_tests/derivative_checker.py index 033bc82786..c4f7eab953 100644 --- a/cvxpy/tests/nlp_tests/derivative_checker.py +++ b/cvxpy/tests/nlp_tests/derivative_checker.py @@ -89,7 +89,6 @@ def check_constraint_values(self, x=None): python_values.append(constr_val) python_values = np.hstack(python_values) if python_values else np.array([]) - match = np.allclose(c_values, python_values, rtol=1e-10, atol=1e-10) return match From c65e54343e2be4d74adbcb6c794c0e021cd2aa3b Mon Sep 17 00:00:00 2001 From: dance858 Date: Mon, 9 Mar 2026 17:40:00 -0700 Subject: [PATCH 3/4] 2d A in converter --- .../solvers/nlp_solvers/diff_engine/converters.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py b/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py index daeea9eb68..3127263423 100644 --- a/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py +++ b/cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py @@ -32,7 +32,6 @@ def normalize_shape(shape): shape = tuple(shape) return (1,) * (2 - len(shape)) + shape - def _chain_add(children): """Chain multiple children with binary adds: a + b + c -> add(add(a, b), c).""" result = children[0] @@ -61,11 +60,12 @@ def _convert_matmul(expr, children): A.shape[1], ) else: + m, n = normalize_shape(A.shape) return _diffengine.make_dense_left_matmul( children[1], A.flatten(order='C'), - A.shape[0], - A.shape[1], + m, + n, ) elif right_arg.is_constant(): A = right_arg.value @@ -83,11 +83,12 @@ def _convert_matmul(expr, children): A.shape[1], ) else: + m, n = normalize_shape(A.shape) return _diffengine.make_dense_right_matmul( children[0], A.flatten(order='C'), - A.shape[0], - A.shape[1], + m, + n, ) else: return _diffengine.make_matmul(children[0], children[1]) From 44c1d122c1cf3b0143f34c86ed5a48c63bd49980 Mon Sep 17 00:00:00 2001 From: dance858 Date: Mon, 9 Mar 2026 17:46:53 -0700 Subject: [PATCH 4/4] one more test --- .../stress_tests_diff_engine/test_matmul_sparse.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cvxpy/tests/nlp_tests/stress_tests_diff_engine/test_matmul_sparse.py b/cvxpy/tests/nlp_tests/stress_tests_diff_engine/test_matmul_sparse.py index cd15aa5c6b..bcb49e1d9b 100644 --- a/cvxpy/tests/nlp_tests/stress_tests_diff_engine/test_matmul_sparse.py +++ b/cvxpy/tests/nlp_tests/stress_tests_diff_engine/test_matmul_sparse.py @@ -113,3 +113,17 @@ def test_sparse_and_dense_matmul(self): problem.solve(nlp=True, verbose=True) checker = DerivativeChecker(problem) checker.run_and_assert() + + def test_sparse_and_dense_matmul2(self): + np.random.seed(0) + m, n = 4, 3 + A = np.random.rand(n, m) + C = sp.random(m, n, density=0.5) + X = cp.Variable((n, n), nonneg=True) + B = np.random.rand(m, m) + obj = cp.Minimize(cp.sum_squares(C @ X @ A - B)) + constraints = [] + problem = cp.Problem(obj, constraints) + problem.solve(nlp=True, verbose=True) + checker = DerivativeChecker(problem) + checker.run_and_assert()