Skip to content

Commit 2b5be9e

Browse files
Transurgeonclaude
andcommitted
Fix quad_form segfault by initializing derivatives before forward pass
Init derivatives (which allocates dwork scratch space) must happen before forward evaluation, since quad_form's forward() uses dwork for csr_matvec. Also reorder constraint init_jacobian before constraint forward for consistency, and remove redundant problem_init_hessian call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 67ba4d8 commit 2b5be9e

2 files changed

Lines changed: 286 additions & 3 deletions

File tree

cvxpy/reductions/dcp2cone/diffengine_cone_program.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,15 @@ def build_diffengine_cone_program(problem, ordered_cons, inverse_data, quad_obj)
172172
x0 = np.zeros(n_vars, dtype=np.float64)
173173

174174
# --- Objective ---
175-
d = float(de.problem_objective_forward(capsule, x0))
175+
# Init derivatives first: allocates dwork needed by forward() (e.g. quad_form)
176176
de.problem_init_derivatives(capsule)
177+
d = float(de.problem_objective_forward(capsule, x0))
177178
q = de.problem_gradient(capsule).copy()
178179

179180
# --- Constraints ---
180181
if c_constraints:
181-
b_vec = de.problem_constraint_forward(capsule, x0)
182182
de.problem_init_jacobian(capsule)
183+
b_vec = de.problem_constraint_forward(capsule, x0)
183184

184185
# Get Jacobian as CSR components (data, indices, indptr, shape)
185186
jac_data, jac_indices, jac_indptr, jac_shape = de.problem_jacobian(capsule)
@@ -195,7 +196,7 @@ def build_diffengine_cone_program(problem, ordered_cons, inverse_data, quad_obj)
195196
P = None
196197
if quad_obj:
197198
duals = np.zeros(b_vec.shape[0], dtype=np.float64)
198-
de.problem_init_hessian(capsule)
199+
# problem_init_hessian already called by problem_init_derivatives above
199200
h_data, h_indices, h_indptr, h_shape = de.problem_hessian(capsule, 1.0, duals)
200201
P_csr = sp.csr_matrix((h_data, h_indices, h_indptr), shape=h_shape)
201202
# Symmetrize: P = P_lower + P_lower^T - diag(P_lower)
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
"""
2+
Copyright, the CVXPY authors
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+
"""
16+
import unittest
17+
18+
import numpy as np
19+
import pytest
20+
21+
import cvxpy as cp
22+
from cvxpy.tests.base_test import BaseTest
23+
from cvxpy.tests.solver_test_helpers import StandardTestLPs, StandardTestSOCPs
24+
25+
try:
26+
from sparsediffpy import _sparsediffengine # noqa: F401
27+
HAS_DIFFENGINE = True
28+
except ImportError:
29+
HAS_DIFFENGINE = False
30+
31+
32+
@pytest.mark.skipif(not HAS_DIFFENGINE, reason="sparsediffpy not installed")
33+
class TestDiffengineConeProgram(BaseTest):
34+
"""Tests for the DIFFENGINE canonicalization backend."""
35+
36+
BACKEND = 'DIFFENGINE'
37+
38+
def _solve(self, prob, **kwargs):
39+
"""Solve with DIFFENGINE backend via Clarabel."""
40+
return prob.solve(solver=cp.CLARABEL, canon_backend=self.BACKEND, **kwargs)
41+
42+
def _solve_default(self, prob, **kwargs):
43+
"""Solve with default backend for comparison."""
44+
return prob.solve(solver=cp.CLARABEL, **kwargs)
45+
46+
def test_simple_lp(self) -> None:
47+
"""Test a simple LP: minimize c'x s.t. x >= 1."""
48+
x = cp.Variable(3)
49+
c = np.array([1.0, 2.0, 3.0])
50+
prob = cp.Problem(cp.Minimize(c @ x), [x >= 1])
51+
52+
val_de = self._solve(prob)
53+
self.assertEqual(prob.status, cp.OPTIMAL)
54+
x_de = x.value.copy()
55+
56+
val_default = self._solve_default(prob)
57+
self.assertAlmostEqual(val_de, val_default, places=4)
58+
self.assertItemsAlmostEqual(x_de, x.value, places=4)
59+
60+
def test_lp_with_equality(self) -> None:
61+
"""Test LP with equality constraints."""
62+
x = cp.Variable(2)
63+
prob = cp.Problem(
64+
cp.Minimize(x[0] + 2 * x[1]),
65+
[x[0] + x[1] == 1, x >= 0],
66+
)
67+
68+
val_de = self._solve(prob)
69+
self.assertEqual(prob.status, cp.OPTIMAL)
70+
x_de = x.value.copy()
71+
72+
val_default = self._solve_default(prob)
73+
self.assertAlmostEqual(val_de, val_default, places=4)
74+
self.assertItemsAlmostEqual(x_de, x.value, places=4)
75+
76+
def test_lp_matrix_constraint(self) -> None:
77+
"""Test LP with matrix variable."""
78+
X = cp.Variable((2, 2))
79+
prob = cp.Problem(
80+
cp.Minimize(cp.sum(X)),
81+
[X >= np.eye(2)],
82+
)
83+
84+
val_de = self._solve(prob)
85+
self.assertEqual(prob.status, cp.OPTIMAL)
86+
X_de = X.value.copy()
87+
88+
val_default = self._solve_default(prob)
89+
self.assertAlmostEqual(val_de, val_default, places=4)
90+
self.assertItemsAlmostEqual(X_de, X.value, places=4)
91+
92+
def test_symbolic_quad_form_conversion(self) -> None:
93+
"""Test that SymbolicQuadForm is converted by the diffengine backend."""
94+
from cvxpy.reductions.dcp2cone.dcp2cone import Dcp2Cone
95+
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.converters import (
96+
build_variable_dict,
97+
convert_expr,
98+
)
99+
100+
x = cp.Variable(2)
101+
P = np.array([[2.0, 0.0], [0.0, 2.0]])
102+
prob = cp.Problem(cp.Minimize(cp.quad_form(x, P)), [x >= 1])
103+
104+
# Dcp2Cone with quad_obj=True produces SymbolicQuadForm
105+
dcp2cone = Dcp2Cone(quad_obj=True)
106+
new_prob, _ = dcp2cone.apply(prob)
107+
obj_expr = new_prob.objective.expr
108+
self.assertEqual(type(obj_expr).__name__, "SymbolicQuadForm")
109+
110+
# Verify the diffengine converter handles it
111+
var_dict, n_vars = build_variable_dict(new_prob.variables())
112+
c_obj = convert_expr(obj_expr, var_dict, n_vars)
113+
self.assertIsNotNone(c_obj)
114+
115+
def test_qp(self) -> None:
116+
"""Test a simple QP: minimize x'x s.t. x >= 1."""
117+
x = cp.Variable(2)
118+
prob = cp.Problem(
119+
cp.Minimize(cp.sum_squares(x) + x[0]),
120+
[x >= 1],
121+
)
122+
123+
val_de = self._solve(prob)
124+
self.assertEqual(prob.status, cp.OPTIMAL)
125+
x_de = x.value.copy()
126+
127+
val_default = self._solve_default(prob)
128+
self.assertAlmostEqual(val_de, val_default, places=4)
129+
self.assertItemsAlmostEqual(x_de, x.value, places=4)
130+
131+
def test_soc_constraint(self) -> None:
132+
"""Test with second-order cone constraint."""
133+
x = cp.Variable(3)
134+
prob = cp.Problem(
135+
cp.Minimize(x[0]),
136+
[cp.norm(x[1:], 2) <= x[0], x[0] >= 0, x[1] == 1, x[2] == 1],
137+
)
138+
139+
val_de = self._solve(prob)
140+
self.assertEqual(prob.status, cp.OPTIMAL)
141+
x_de = x.value.copy()
142+
143+
val_default = self._solve_default(prob)
144+
self.assertAlmostEqual(val_de, val_default, places=4)
145+
self.assertItemsAlmostEqual(x_de, x.value, places=4)
146+
147+
def test_zero_and_nonneg(self) -> None:
148+
"""Test with mixed Zero and NonNeg constraints."""
149+
x = cp.Variable(3)
150+
prob = cp.Problem(
151+
cp.Minimize(cp.sum(x)),
152+
[x[0] == 2, x[1:] >= 0, x[1] + x[2] == 3],
153+
)
154+
155+
val_de = self._solve(prob)
156+
self.assertEqual(prob.status, cp.OPTIMAL)
157+
x_de = x.value.copy()
158+
159+
val_default = self._solve_default(prob)
160+
self.assertAlmostEqual(val_de, val_default, places=4)
161+
self.assertItemsAlmostEqual(x_de, x.value, places=4)
162+
163+
def test_infeasible(self) -> None:
164+
"""Test that infeasible problems are detected."""
165+
x = cp.Variable(2)
166+
prob = cp.Problem(
167+
cp.Minimize(cp.sum(x)),
168+
[x >= 1, x <= -1],
169+
)
170+
self._solve(prob)
171+
self.assertEqual(prob.status, cp.INFEASIBLE)
172+
173+
def test_unbounded(self) -> None:
174+
"""Test that unbounded problems are detected."""
175+
x = cp.Variable(2)
176+
prob = cp.Problem(cp.Minimize(cp.sum(x)))
177+
self._solve(prob)
178+
self.assertIn(prob.status, [cp.UNBOUNDED, "infeasible_or_unbounded"])
179+
180+
def test_multiple_variables(self) -> None:
181+
"""Test with multiple separate variables."""
182+
x = cp.Variable(2)
183+
y = cp.Variable(2)
184+
prob = cp.Problem(
185+
cp.Minimize(cp.sum(x) + 2 * cp.sum(y)),
186+
[x >= 1, y >= 2, x[0] + y[0] == 5],
187+
)
188+
189+
val_de = self._solve(prob)
190+
self.assertEqual(prob.status, cp.OPTIMAL)
191+
x_de, y_de = x.value.copy(), y.value.copy()
192+
193+
val_default = self._solve_default(prob)
194+
self.assertAlmostEqual(val_de, val_default, places=4)
195+
self.assertItemsAlmostEqual(x_de, x.value, places=4)
196+
self.assertItemsAlmostEqual(y_de, y.value, places=4)
197+
198+
def test_scalar_variable(self) -> None:
199+
"""Test with a scalar variable."""
200+
x = cp.Variable()
201+
prob = cp.Problem(cp.Minimize(x), [x >= 5])
202+
203+
val_de = self._solve(prob)
204+
self.assertEqual(prob.status, cp.OPTIMAL)
205+
self.assertAlmostEqual(val_de, 5.0, places=4)
206+
207+
def test_large_lp(self) -> None:
208+
"""Test a moderate-size LP."""
209+
n = 50
210+
np.random.seed(0)
211+
c = np.abs(np.random.randn(n))
212+
A = np.random.randn(20, n)
213+
b = A @ np.abs(np.random.randn(n)) + 1.0
214+
215+
x = cp.Variable(n)
216+
prob = cp.Problem(cp.Minimize(c @ x), [A @ x <= b, x >= 0])
217+
218+
val_de = self._solve(prob)
219+
self.assertEqual(prob.status, cp.OPTIMAL)
220+
x_de = x.value.copy()
221+
222+
val_default = self._solve_default(prob)
223+
self.assertAlmostEqual(val_de, val_default, places=3)
224+
self.assertItemsAlmostEqual(x_de, x.value, places=3)
225+
226+
227+
@pytest.mark.skipif(not HAS_DIFFENGINE, reason="sparsediffpy not installed")
228+
class TestDiffengineStandardLPs(BaseTest):
229+
"""Run StandardTestLPs with the DIFFENGINE backend."""
230+
231+
KWARGS = dict(solver=cp.CLARABEL, canon_backend='DIFFENGINE')
232+
233+
def test_lp_0(self) -> None:
234+
StandardTestLPs.test_lp_0(**self.KWARGS)
235+
236+
def test_lp_1(self) -> None:
237+
StandardTestLPs.test_lp_1(**self.KWARGS)
238+
239+
def test_lp_2(self) -> None:
240+
StandardTestLPs.test_lp_2(**self.KWARGS)
241+
242+
def test_lp_3(self) -> None:
243+
StandardTestLPs.test_lp_3(**self.KWARGS)
244+
245+
def test_lp_4(self) -> None:
246+
StandardTestLPs.test_lp_4(**self.KWARGS)
247+
248+
def test_lp_5(self) -> None:
249+
StandardTestLPs.test_lp_5(**self.KWARGS)
250+
251+
def test_lp_6(self) -> None:
252+
StandardTestLPs.test_lp_6(**self.KWARGS)
253+
254+
@pytest.mark.skip(reason="lp_7 requires sdpap module")
255+
def test_lp_7(self) -> None:
256+
StandardTestLPs.test_lp_7(**self.KWARGS)
257+
258+
259+
@pytest.mark.skipif(not HAS_DIFFENGINE, reason="sparsediffpy not installed")
260+
class TestDiffengineStandardSOCPs(BaseTest):
261+
"""Run StandardTestSOCPs with the DIFFENGINE backend."""
262+
263+
KWARGS = dict(solver=cp.CLARABEL, canon_backend='DIFFENGINE')
264+
265+
def test_socp_0(self) -> None:
266+
StandardTestSOCPs.test_socp_0(**self.KWARGS)
267+
268+
def test_socp_1(self) -> None:
269+
StandardTestSOCPs.test_socp_1(**self.KWARGS)
270+
271+
def test_socp_2(self) -> None:
272+
StandardTestSOCPs.test_socp_2(**self.KWARGS)
273+
274+
def test_socp_3ax0(self) -> None:
275+
StandardTestSOCPs.test_socp_3ax0(**self.KWARGS)
276+
277+
def test_socp_3ax1(self) -> None:
278+
StandardTestSOCPs.test_socp_3ax1(**self.KWARGS)
279+
280+
281+
if __name__ == '__main__':
282+
unittest.main()

0 commit comments

Comments
 (0)