Skip to content

Commit 361bfa3

Browse files
Transurgeonclaude
andcommitted
Add kron, vstack, and dense-to-sparse matmul support to diffengine converters
Adds native kron_left node for kron(C, X) and left_matmul fallback for kron(X, C). Converts dense matmul args to sparse CSR for better performance. Adds vstack via transpose(hstack(transpose(args))) and fixes 1D right matmul reshape handling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8b736dc commit 361bfa3

1 file changed

Lines changed: 151 additions & 8 deletions

File tree

cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,63 @@ def normalize_shape(shape):
3232
shape = tuple(shape)
3333
return (1,) * (2 - len(shape)) + shape
3434

35+
36+
def _dense_to_csr_args(A):
37+
"""Convert dense matrix to CSR data/indices/indptr arrays for C engine."""
38+
A_csr = sparse.csr_matrix(np.asarray(A, dtype=np.float64))
39+
return (
40+
A_csr.data.astype(np.float64, copy=False),
41+
A_csr.indices.astype(np.int32, copy=False),
42+
A_csr.indptr.astype(np.int32, copy=False),
43+
)
44+
45+
46+
def _build_kron_left_csr(C, p, q):
47+
"""Build sparse CSR matrix M such that vec(kron(C, X)) = M @ vec(X).
48+
49+
C is m x n constant, X is p x q variable. Uses Fortran (column-major) order.
50+
M has shape (m*p*n*q, p*q) with at most nnz(C)*p*q nonzeros.
51+
"""
52+
m, n = C.shape
53+
C = np.asarray(C, dtype=np.float64)
54+
55+
i_arr, j_arr = np.arange(m), np.arange(n)
56+
k_arr, l_arr = np.arange(p), np.arange(q)
57+
58+
ii, jj, kk, ll = np.meshgrid(i_arr, j_arr, k_arr, l_arr, indexing='ij')
59+
rows = (jj * q + ll) * (m * p) + ii * p + kk
60+
cols = ll * p + kk
61+
vals = C[ii, jj]
62+
63+
rows, cols, vals = rows.ravel(), cols.ravel(), vals.ravel()
64+
mask = vals != 0
65+
return sparse.csr_matrix(
66+
(vals[mask], (rows[mask], cols[mask])), shape=(m * p * n * q, p * q)
67+
)
68+
69+
70+
def _build_kron_right_csr(C, mx, nx):
71+
"""Build sparse CSR matrix M such that vec(kron(X, C)) = M @ vec(X).
72+
73+
X is mx x nx variable, C is p x q constant. Uses Fortran (column-major) order.
74+
"""
75+
p, q = C.shape
76+
C = np.asarray(C, dtype=np.float64)
77+
78+
i_arr, j_arr = np.arange(mx), np.arange(nx)
79+
k_arr, l_arr = np.arange(p), np.arange(q)
80+
81+
ii, jj, kk, ll = np.meshgrid(i_arr, j_arr, k_arr, l_arr, indexing='ij')
82+
rows = (jj * q + ll) * (mx * p) + ii * p + kk
83+
cols = jj * mx + ii
84+
vals = C[kk, ll]
85+
86+
rows, cols, vals = rows.ravel(), cols.ravel(), vals.ravel()
87+
mask = vals != 0
88+
return sparse.csr_matrix(
89+
(vals[mask], (rows[mask], cols[mask])), shape=(mx * p * nx * q, mx * nx)
90+
)
91+
3592
def _chain_add(children):
3693
"""Chain multiple children with binary adds: a + b + c -> add(add(a, b), c)."""
3794
result = children[0]
@@ -46,7 +103,7 @@ def _convert_matmul(expr, children):
46103

47104
if left_arg.is_constant():
48105
A = left_arg.value
49-
106+
50107
if sparse.issparse(A):
51108
if not isinstance(A, sparse.csr_matrix):
52109
A = sparse.csr_matrix(A)
@@ -60,21 +117,31 @@ def _convert_matmul(expr, children):
60117
A.shape[1],
61118
)
62119
else:
120+
A = np.asarray(A, dtype=np.float64)
63121
m, n = normalize_shape(A.shape)
64-
return _diffengine.make_dense_left_matmul(
122+
return _diffengine.make_sparse_left_matmul(
65123
children[1],
66-
A.flatten(order='C'),
124+
*_dense_to_csr_args(A.reshape(m, n)),
67125
m,
68126
n,
69127
)
70128
elif right_arg.is_constant():
71129
A = right_arg.value
130+
if not sparse.issparse(A):
131+
A = np.asarray(A, dtype=np.float64)
132+
133+
# For right matmul f(x) @ A: if A is 1D (n,), treat as column vector (n, 1)
134+
# C engine produces (d1, 1), but CVXPY expects (1, d1) for 1D results,
135+
# so we reshape after.
136+
is_1d = (A.ndim == 1)
137+
if is_1d:
138+
A = A.reshape(-1, 1)
72139

73140
if sparse.issparse(A):
74141
if not isinstance(A, sparse.csr_matrix):
75142
A = sparse.csr_matrix(A)
76143

77-
return _diffengine.make_sparse_right_matmul(
144+
result = _diffengine.make_sparse_right_matmul(
78145
children[0],
79146
A.data.astype(np.float64, copy=False),
80147
A.indices.astype(np.int32, copy=False),
@@ -83,20 +150,38 @@ def _convert_matmul(expr, children):
83150
A.shape[1],
84151
)
85152
else:
86-
m, n = normalize_shape(A.shape)
87-
return _diffengine.make_dense_right_matmul(
153+
A = np.asarray(A, dtype=np.float64)
154+
m, n = A.shape
155+
result = _diffengine.make_sparse_right_matmul(
88156
children[0],
89-
A.flatten(order='C'),
157+
*_dense_to_csr_args(A),
90158
m,
91159
n,
92160
)
161+
162+
# Reshape (d1, 1) -> (1, d1) to match CVXPY's convention for 1D results
163+
if is_1d:
164+
d1, _ = _diffengine.get_expr_dimensions(result)
165+
result = _diffengine.make_reshape(result, 1, d1)
166+
return result
93167
else:
94168
return _diffengine.make_matmul(children[0], children[1])
95169

96170
def _convert_hstack(expr, children):
97171
"""Convert horizontal stack (hstack) of expressions."""
98172
return _diffengine.make_hstack(children)
99173

174+
175+
def _convert_vstack(expr, children):
176+
"""Convert vertical stack (vstack) via transpose(hstack(transpose(args))).
177+
178+
Reuses existing hstack and transpose — matches the approach in
179+
SparseDiffEngine PR #52.
180+
"""
181+
transposed = [_diffengine.make_transpose(c) for c in children]
182+
hstacked = _diffengine.make_hstack(transposed)
183+
return _diffengine.make_transpose(hstacked)
184+
100185
def _convert_multiply(expr, children):
101186
"""Convert multiplication based on argument types."""
102187
left_arg, right_arg = expr.args
@@ -288,6 +373,61 @@ def _convert_diag_vec(expr, children):
288373
raise NotImplementedError("diag_vec with k != 0 not supported in diff engine")
289374
return _diffengine.make_diag_vec(children[0])
290375

376+
377+
def _convert_kron(expr, children):
378+
"""Convert kron(C, X) or kron(X, C) where one argument is constant.
379+
380+
Uses native kron_left node for kron(C, X) — computes Jacobian via index
381+
remapping without materializing the full kron coefficient matrix.
382+
Falls back to left_matmul approach for kron(X, C).
383+
"""
384+
left_arg, right_arg = expr.args
385+
386+
if left_arg.is_constant():
387+
C = np.asarray(left_arg.value, dtype=np.float64)
388+
child = children[1]
389+
p, q = normalize_shape(right_arg.shape)
390+
391+
# Reshape child to (p, q) for the C engine
392+
child_pq = _diffengine.make_reshape(child, p, q)
393+
394+
C_csr = sparse.csr_matrix(C)
395+
result = _diffengine.make_kron_left(
396+
child_pq,
397+
C_csr.data.astype(np.float64, copy=False),
398+
C_csr.indices.astype(np.int32, copy=False),
399+
C_csr.indptr.astype(np.int32, copy=False),
400+
C_csr.shape[0], C_csr.shape[1], p, q,
401+
)
402+
403+
# Reshape to CVXPY's expected output shape
404+
d1, d2 = normalize_shape(expr.shape)
405+
return _diffengine.make_reshape(result, d1, d2)
406+
else:
407+
# kron(X, C): fall back to left_matmul approach
408+
C = np.asarray(right_arg.value, dtype=np.float64)
409+
child = children[0]
410+
mx, nx = normalize_shape(left_arg.shape)
411+
p, q = C.shape
412+
m, n = mx, nx
413+
M = _build_kron_right_csr(C, mx, nx)
414+
415+
child_size = M.shape[1]
416+
out_size = M.shape[0]
417+
418+
child_flat = _diffengine.make_reshape(child, 1, child_size)
419+
result = _diffengine.make_sparse_left_matmul(
420+
child_flat,
421+
M.data.astype(np.float64, copy=False),
422+
M.indices.astype(np.int32, copy=False),
423+
M.indptr.astype(np.int32, copy=False),
424+
M.shape[0],
425+
M.shape[1],
426+
)
427+
428+
d1, d2 = normalize_shape(expr.shape)
429+
return _diffengine.make_reshape(result, d1, d2)
430+
291431
# Mapping from CVXPY atom names to C diff engine functions
292432
# Converters receive (expr, children) where expr is the CVXPY expression
293433
ATOM_CONVERTERS = {
@@ -334,11 +474,14 @@ def _convert_diag_vec(expr, children):
334474
# Reductions returning scalar
335475
"Prod": _convert_prod,
336476
"transpose": _convert_transpose,
337-
# Horizontal stack
477+
# Horizontal / vertical stack
338478
"Hstack": _convert_hstack,
479+
"Vstack": _convert_vstack,
339480
"Trace": _convert_trace,
340481
# Diagonal
341482
"diag_vec": _convert_diag_vec,
483+
# Kronecker product
484+
"kron": _convert_kron,
342485
}
343486

344487

0 commit comments

Comments
 (0)