Skip to content

Commit 2ae5a56

Browse files
TransurgeonTransurgeonclaude
authored
Add native kron (Kronecker product) affine atom (#101)
* Add native kron (Kronecker product) affine atom cvxpy's kron(A, B) always has one variable-free operand, so every output entry depends on a single child entry: Z[OUT] = coeff[OUT] * child[child_row[OUT]]. The output Jacobian is therefore the child Jacobian's rows gathered (with repetition) and scaled by the variable-free operand -- no coefficient matrix, no matmul, no CSC conversion; O(nnz(result)). child_row[] and coeff_idx[] depend only on the operand shapes and are precomputed once in new_kron. Handles kron(param/const, var) and kron(var, param/const), parametric or constant, with column-major (Fortran) flattening, and re-evaluates the variable-free operand each solve. forward, Jacobian and the affine Hessian backprop are all scaled gathers. Adds forward/Jacobian/wsum_hess unit tests (both forms, scalar operand, and numerical Jacobian/Hessian checks on a composite arg); all_tests now 405. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Format kron atom and mirror left_matmul structure Apply clang-format and tidy the native kron atom: factor parameter refresh into a helper, null freed pointers, and trim doc duplicated by the kron_expr definition in subexpr.h. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Rework kron into sparse-only left/right constructors with active blocks new_kron materialized every output row, including the structurally-zero blocks of a sparse constant operand — kron(I_p, X) built p^2 blocks of Jacobian rows with p(p-1) of them zero. Replace it with new_left_kron / new_right_kron taking the constant operand's active (nonzero) block indices, column-major; inactive output rows keep child_row == -1 and contribute a zero value and an empty Jacobian row. cvxpy passes the constant's nonzeros (all blocks for an updatable parameter, whose zeros are not permanent). Constructors assert active block indices are in range. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Run clang-format over the kron changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Condense kron comments and remove unused struct fields Drop the never-read p/q/r/s fields from kron_expr, tighten the header and inline comment blocks, brace single-statement bodies to match the surrounding atoms, and remove unused includes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Use memcpy to gather child row indices in kron jacobian_init Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Transurgeon <peter.zijie@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f4071dd commit 2ae5a56

7 files changed

Lines changed: 582 additions & 0 deletions

File tree

include/atoms/affine.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,16 @@ expr *new_vector_mult(expr *param_node, expr *child);
8080
kernel and may either represent a constant or an updatable parameter */
8181
expr *new_convolve(expr *param_node, expr *child);
8282

83+
/* Kronecker product Z = kron(A, B), where A is p x q and B is r x s. param_node
84+
holds the variable-free constant/parameter operand and child the variable one.
85+
active_blocks lists the column-major indices of the constant operand's nonzero
86+
entries; only the output rows they cover are materialized.
87+
88+
left_kron: A = param_node, B = child; active_blocks index into A.
89+
right_kron: A = child, B = param_node; active_blocks index into B. */
90+
expr *new_left_kron(expr *param_node, expr *child, int p, int q, int r, int s,
91+
const int *active_blocks, int n_active);
92+
expr *new_right_kron(expr *param_node, expr *child, int p, int q, int r, int s,
93+
const int *active_blocks, int n_active);
94+
8395
#endif /* AFFINE_H */

include/subexpr.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,19 @@ typedef struct convolve_expr
173173
CSC_matrix *Jchild_CSC;
174174
} convolve_expr;
175175

176+
/* Kronecker product Z = kron(A, B) where one operand is variable-free (held by
177+
* param_source) and the other (child = node->left) carries the variables. Each
178+
* output entry gathers a single child entry scaled by an entry of the constant
179+
* operand; rows not covered by the constant's active blocks are inactive
180+
* (child_row == -1) and stay structurally zero. */
181+
typedef struct kron_expr
182+
{
183+
expr base;
184+
expr *param_source; /* the constant/parameter operand */
185+
int *child_row; /* per output row: child entry gathered, -1 if inactive */
186+
int *coeff_idx; /* per output row: index into param_source->value */
187+
} kron_expr;
188+
176189
/* Bivariate matrix multiplication: Z = f(u) @ g(u) where both children
177190
* may be composite expressions. */
178191
typedef struct matmul_expr

src/atoms/affine/kron.c

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/*
2+
* Copyright 2026 Daniel Cederberg and William Zhang
3+
*
4+
* This file is part of the SparseDiffEngine project.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
#include "atoms/affine.h"
19+
#include "subexpr.h"
20+
#include "utils/CSR_matrix.h"
21+
#include "utils/sparse_matrix.h"
22+
#include "utils/tracked_alloc.h"
23+
#include <assert.h>
24+
#include <string.h>
25+
26+
/* Kronecker product Z = kron(A, B), where one operand is variable-free (held by
27+
* param_source) and the other (child = node->left) carries the variables.
28+
*
29+
* With column-major flattening, output entry OUT = (i*r + k) + (j*s + l)*(p*r)
30+
* equals A[i,j] * B[k,l], so each output entry depends on a single child entry:
31+
*
32+
* Z[OUT] = coeff[OUT] * vec(child)[child_row[OUT]]
33+
* J[OUT, :] = coeff[OUT] * J_child[child_row[OUT], :]
34+
*
35+
* where coeff[OUT] = param_source->value[coeff_idx[OUT]]. The constructors fill
36+
* child_row/coeff_idx only for the constant operand's active (nonzero) blocks;
37+
* the remaining rows keep child_row == -1 and are structurally zero. */
38+
39+
/* Pull current parameter values through any broadcast/promote wrappers. */
40+
static void refresh_param_values(kron_expr *knode)
41+
{
42+
if (!knode->base.needs_parameter_refresh)
43+
{
44+
return;
45+
}
46+
47+
knode->param_source->forward(knode->param_source, NULL);
48+
knode->base.needs_parameter_refresh = false;
49+
}
50+
51+
static void forward(expr *node, const double *u)
52+
{
53+
expr *child = node->left;
54+
kron_expr *knode = (kron_expr *) node;
55+
56+
refresh_param_values(knode);
57+
child->forward(child, u);
58+
59+
const double *a = knode->param_source->value;
60+
const double *x = child->value;
61+
double *y = node->value;
62+
for (int out = 0; out < node->size; out++)
63+
{
64+
int cr = knode->child_row[out];
65+
y[out] = (cr < 0) ? 0.0 : a[knode->coeff_idx[out]] * x[cr];
66+
}
67+
}
68+
69+
static void jacobian_init_impl(expr *node)
70+
{
71+
expr *child = node->left;
72+
kron_expr *knode = (kron_expr *) node;
73+
74+
jacobian_init(child);
75+
76+
/* Row OUT of the result copies the sparsity of child row child_row[OUT]
77+
(with repetition); inactive rows are empty. */
78+
CSR_matrix *Jc = child->jacobian->to_csr(child->jacobian);
79+
80+
int total = 0;
81+
for (int out = 0; out < node->size; out++)
82+
{
83+
int cr = knode->child_row[out];
84+
if (cr >= 0)
85+
{
86+
total += Jc->p[cr + 1] - Jc->p[cr];
87+
}
88+
}
89+
90+
CSR_matrix *Jk = new_CSR_matrix(node->size, node->n_vars, total);
91+
int idx = 0;
92+
Jk->p[0] = 0;
93+
for (int out = 0; out < node->size; out++)
94+
{
95+
int cr = knode->child_row[out];
96+
if (cr >= 0)
97+
{
98+
int row_nnz = Jc->p[cr + 1] - Jc->p[cr];
99+
memcpy(Jk->i + idx, Jc->i + Jc->p[cr], row_nnz * sizeof(int));
100+
idx += row_nnz;
101+
}
102+
Jk->p[out + 1] = idx;
103+
}
104+
node->jacobian = new_sparse_matrix(Jk);
105+
}
106+
107+
static void eval_jacobian(expr *node)
108+
{
109+
expr *child = node->left;
110+
kron_expr *knode = (kron_expr *) node;
111+
112+
child->eval_jacobian(child);
113+
114+
/* Sparsity is fixed after jacobian_init, so the row offsets still align;
115+
refill active rows as scale * child-row-values. */
116+
CSR_matrix *Jc = child->jacobian->to_csr(child->jacobian);
117+
CSR_matrix *Jk = node->jacobian->to_csr(node->jacobian);
118+
const double *a = knode->param_source->value;
119+
120+
int idx = 0;
121+
for (int out = 0; out < node->size; out++)
122+
{
123+
int cr = knode->child_row[out];
124+
if (cr < 0)
125+
{
126+
continue;
127+
}
128+
double scale = a[knode->coeff_idx[out]];
129+
for (int t = Jc->p[cr]; t < Jc->p[cr + 1]; t++)
130+
{
131+
Jk->x[idx++] = scale * Jc->x[t];
132+
}
133+
}
134+
}
135+
136+
static void wsum_hess_init_impl(expr *node)
137+
{
138+
expr *child = node->left;
139+
140+
wsum_hess_init(child);
141+
node->wsum_hess = child->wsum_hess->copy_sparsity(child->wsum_hess);
142+
/* backprop workspace: one weight per child entry */
143+
node->work->dwork = (double *) sp_malloc(child->size * sizeof(double));
144+
}
145+
146+
static void eval_wsum_hess(expr *node, const double *w)
147+
{
148+
expr *child = node->left;
149+
kron_expr *knode = (kron_expr *) node;
150+
const double *a = knode->param_source->value;
151+
double *w_prime = node->work->dwork;
152+
153+
/* kron is affine in the child, so we only push the weights back through the
154+
gather: w'[child_row[OUT]] += coeff[OUT] * w[OUT]. Many output rows map to
155+
one child entry, hence the accumulation. */
156+
memset(w_prime, 0, child->size * sizeof(double));
157+
for (int out = 0; out < node->size; out++)
158+
{
159+
int cr = knode->child_row[out];
160+
if (cr >= 0)
161+
{
162+
w_prime[cr] += a[knode->coeff_idx[out]] * w[out];
163+
}
164+
}
165+
166+
child->eval_wsum_hess(child, w_prime);
167+
memcpy(node->wsum_hess->x, child->wsum_hess->x,
168+
node->wsum_hess->nnz * sizeof(double));
169+
}
170+
171+
static bool is_affine(const expr *node)
172+
{
173+
return node->left->is_affine(node->left);
174+
}
175+
176+
static void free_type_data(expr *node)
177+
{
178+
kron_expr *knode = (kron_expr *) node;
179+
sp_free(knode->child_row);
180+
sp_free(knode->coeff_idx);
181+
free_expr(knode->param_source);
182+
183+
knode->child_row = NULL;
184+
knode->coeff_idx = NULL;
185+
knode->param_source = NULL;
186+
}
187+
188+
/* Allocate a kron node and its (all-inactive) index arrays. The left/right
189+
constructors then fill the active rows. */
190+
static kron_expr *new_kron_common(expr *param_node, expr *child, int p, int q, int r,
191+
int s)
192+
{
193+
int size_out = (p * r) * (q * s);
194+
195+
kron_expr *knode = (kron_expr *) sp_calloc(1, sizeof(kron_expr));
196+
expr *node = &knode->base;
197+
init_expr(node, p * r, q * s, child->n_vars, forward, jacobian_init_impl,
198+
eval_jacobian, is_affine, wsum_hess_init_impl, eval_wsum_hess,
199+
free_type_data);
200+
node->left = child;
201+
expr_retain(child);
202+
203+
knode->param_source = param_node;
204+
expr_retain(param_node);
205+
206+
knode->child_row = (int *) sp_malloc(size_out * sizeof(int));
207+
knode->coeff_idx = (int *) sp_malloc(size_out * sizeof(int));
208+
for (int out = 0; out < size_out; out++)
209+
{
210+
knode->child_row[out] = -1; /* inactive until an active block fills it */
211+
}
212+
213+
knode->base.needs_parameter_refresh = true;
214+
return knode;
215+
}
216+
217+
/* Z = kron(A, B) with A = param_node (p x q) the constant, B = child (r x s) the
218+
variable. active_blocks holds column-major indices i + j*p of A's nonzeros. */
219+
expr *new_left_kron(expr *param_node, expr *child, int p, int q, int r, int s,
220+
const int *active_blocks, int n_active)
221+
{
222+
kron_expr *knode = new_kron_common(param_node, child, p, q, r, s);
223+
int n_rows = p * r;
224+
for (int b = 0; b < n_active; b++)
225+
{
226+
int bidx = active_blocks[b]; /* = i + j*p into A */
227+
assert(0 <= bidx && bidx < p * q);
228+
int i = bidx % p;
229+
int j = bidx / p;
230+
for (int l = 0; l < s; l++)
231+
{
232+
for (int k = 0; k < r; k++)
233+
{
234+
int out = (i * r + k) + (j * s + l) * n_rows;
235+
knode->child_row[out] = k + l * r; /* col-major into B */
236+
knode->coeff_idx[out] = bidx; /* col-major into A */
237+
}
238+
}
239+
}
240+
return &knode->base;
241+
}
242+
243+
/* Z = kron(A, B) with A = child (p x q) the variable, B = param_node (r x s) the
244+
constant. active_blocks holds column-major indices k + l*r of B's nonzeros. */
245+
expr *new_right_kron(expr *param_node, expr *child, int p, int q, int r, int s,
246+
const int *active_blocks, int n_active)
247+
{
248+
kron_expr *knode = new_kron_common(param_node, child, p, q, r, s);
249+
int n_rows = p * r;
250+
for (int b = 0; b < n_active; b++)
251+
{
252+
int bidx = active_blocks[b]; /* = k + l*r into B */
253+
assert(0 <= bidx && bidx < r * s);
254+
int k = bidx % r;
255+
int l = bidx / r;
256+
for (int j = 0; j < q; j++)
257+
{
258+
for (int i = 0; i < p; i++)
259+
{
260+
int out = (i * r + k) + (j * s + l) * n_rows;
261+
knode->child_row[out] = i + j * p; /* col-major into A */
262+
knode->coeff_idx[out] = bidx; /* col-major into B */
263+
}
264+
}
265+
}
266+
return &knode->base;
267+
}

tests/all_tests.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "forward_pass/affine/test_convolve.h"
1111
#include "forward_pass/affine/test_diag_mat.h"
1212
#include "forward_pass/affine/test_hstack.h"
13+
#include "forward_pass/affine/test_kron.h"
1314
#include "forward_pass/affine/test_left_matmul_dense.h"
1415
#include "forward_pass/affine/test_linear_op.h"
1516
#include "forward_pass/affine/test_neg.h"
@@ -30,6 +31,7 @@
3031
#include "jacobian_tests/affine/test_diag_mat.h"
3132
#include "jacobian_tests/affine/test_hstack.h"
3233
#include "jacobian_tests/affine/test_index.h"
34+
#include "jacobian_tests/affine/test_kron.h"
3335
#include "jacobian_tests/affine/test_left_matmul.h"
3436
#include "jacobian_tests/affine/test_neg.h"
3537
#include "jacobian_tests/affine/test_promote.h"
@@ -76,6 +78,7 @@
7678
#include "wsum_hess/affine/test_diag_mat.h"
7779
#include "wsum_hess/affine/test_hstack.h"
7880
#include "wsum_hess/affine/test_index.h"
81+
#include "wsum_hess/affine/test_kron.h"
7982
#include "wsum_hess/affine/test_left_matmul.h"
8083
#include "wsum_hess/affine/test_right_matmul.h"
8184
#include "wsum_hess/affine/test_scalar_mult.h"
@@ -151,6 +154,10 @@ int main(void)
151154
mu_run_test(test_convolve_forward, tests_run);
152155
mu_run_test(test_convolve_forward_row, tests_run);
153156
mu_run_test(test_convolve_forward_param, tests_run);
157+
mu_run_test(test_kron_forward_const_left, tests_run);
158+
mu_run_test(test_kron_forward_const_right, tests_run);
159+
mu_run_test(test_kron_forward_scalar, tests_run);
160+
mu_run_test(test_kron_forward_sparse, tests_run);
154161
mu_run_test(test_diag_mat_forward, tests_run);
155162
mu_run_test(test_upper_tri_forward_4x4, tests_run);
156163

@@ -246,6 +253,10 @@ int main(void)
246253
mu_run_test(test_jacobian_matmul, tests_run);
247254
mu_run_test(test_jacobian_convolve, tests_run);
248255
mu_run_test(test_jacobian_convolve_composite, tests_run);
256+
mu_run_test(test_jacobian_kron_const_left, tests_run);
257+
mu_run_test(test_jacobian_kron_const_right, tests_run);
258+
mu_run_test(test_jacobian_kron_sparse, tests_run);
259+
mu_run_test(test_jacobian_kron_composite, tests_run);
249260
mu_run_test(test_jacobian_transpose, tests_run);
250261
mu_run_test(test_jacobian_transpose_pd_preserved, tests_run);
251262
mu_run_test(test_diag_mat_jacobian_variable, tests_run);
@@ -320,6 +331,8 @@ int main(void)
320331
mu_run_test(test_wsum_hess_right_matmul_vector, tests_run);
321332
mu_run_test(test_wsum_hess_convolve, tests_run);
322333
mu_run_test(test_wsum_hess_convolve_composite, tests_run);
334+
mu_run_test(test_wsum_hess_kron, tests_run);
335+
mu_run_test(test_wsum_hess_kron_composite, tests_run);
323336
mu_run_test(test_wsum_hess_broadcast_row, tests_run);
324337
mu_run_test(test_wsum_hess_broadcast_col, tests_run);
325338
mu_run_test(test_wsum_hess_broadcast_scalar_to_matrix, tests_run);

0 commit comments

Comments
 (0)