Skip to content

Commit c6f1361

Browse files
committed
unify BTA_pd_spd like kernels
1 parent 9c563ba commit c6f1361

4 files changed

Lines changed: 204 additions & 72 deletions

File tree

include/utils/stacked_pd_linalg.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ matrix *BTA_spd_pd_alloc(const stacked_pd *B, const permuted_dense *A);
5959
void BTDA_spd_pd_fill_values(const stacked_pd *B, const double *d,
6060
const permuted_dense *A, stacked_pd *C);
6161

62+
/* Allocate sparsity for C = B^T @ A where B is stacked_pd and A is CSC.
63+
The output C is stacked_pd. */
64+
matrix *BTA_spd_csc_alloc(const stacked_pd *B, const CSC_matrix *A);
65+
66+
/* Fill values of C = B^T @ diag(d) @ A where B is stacked_pd, A is CSC,
67+
d is a global vector of length B->m, and C is stacked_pd. */
68+
void BTDA_spd_csc_fill_values(const stacked_pd *B, const double *d,
69+
const CSC_matrix *A, stacked_pd *C);
70+
6271
/* Allocate sparsity for a new stacked_pd C = A^T A. */
6372
matrix *ATA_spd_alloc(const stacked_pd *A);
6473

src/utils/stacked_pd_linalg.c

Lines changed: 119 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,60 @@ void transpose_spd_fill_values(const stacked_pd *A, stacked_pd *C)
9494
coalesce_spd_fill_values(raw, C);
9595
}
9696

97+
// ====================================================================================
98+
// Shared "spd blockwise + coalesce-accumulate" skeleton.
99+
//
100+
// Used by kernels of the shape C = f(B, X) where B is a stacked_pd and each
101+
// per-block partial f(B_k, X) is a PD. The partials' row_perms (and cells)
102+
// may overlap across k, so we collect them via new_stacked_pd_unchecked,
103+
// coalesce with the _unchecked variant, and use the accumulating scatter
104+
// at fill time. The raw spd is stashed on out->pre_coalesce so fill can
105+
// refresh per-block values without reallocating structure.
106+
//
107+
// Three kernel pairs build on this skeleton: BTA_spd_pd / BTDA_spd_pd,
108+
// BTA_spd_csc / BTDA_spd_csc, and ATA_spd / ATDA_spd. Each variant supplies
109+
// per-block alloc and fill kernels via callback + context.
110+
// ====================================================================================
111+
typedef matrix *(*spd_per_block_alloc_fn)(const permuted_dense *Bk, const void *ctx);
112+
113+
typedef void (*spd_per_block_fill_fn)(const permuted_dense *Bk, const double *d,
114+
const void *ctx, permuted_dense *Ck);
115+
116+
static matrix *spd_blockwise_alloc_coalesce(const stacked_pd *B, int Cn,
117+
spd_per_block_alloc_fn op,
118+
const void *ctx)
119+
{
120+
int n_blocks = B->n_blocks;
121+
permuted_dense **partials =
122+
(permuted_dense **) SP_MALLOC(n_blocks * sizeof(permuted_dense *));
123+
for (int k = 0; k < n_blocks; k++)
124+
{
125+
partials[k] = (permuted_dense *) op(B->blocks[k], ctx);
126+
}
127+
matrix *raw =
128+
new_stacked_pd_unchecked(B->base.n, Cn, n_blocks, partials, NULL, NULL);
129+
free(partials);
130+
matrix *C = coalesce_spd_alloc_unchecked((stacked_pd *) raw);
131+
((stacked_pd *) C)->pre_coalesce = (stacked_pd *) raw;
132+
return C;
133+
}
134+
135+
static void spd_blockwise_fill_coalesce_accumulate(const stacked_pd *B,
136+
const double *d, const void *ctx,
137+
stacked_pd *C,
138+
spd_per_block_fill_fn op)
139+
{
140+
if (C->base.nnz == 0) return;
141+
142+
stacked_pd *raw = C->pre_coalesce;
143+
for (int k = 0; k < B->n_blocks; k++)
144+
{
145+
op(B->blocks[k], d, ctx, raw->blocks[k]);
146+
}
147+
memset(C->base.x, 0, C->base.nnz * sizeof(double));
148+
coalesce_spd_fill_values_accumulate(raw, C);
149+
}
150+
97151
// ------------------------------------------------------------------------------------
98152
// C = ATDA for stacked_pd A. Let A = [A1; A2; A3] where Ai has n columns (the same
99153
// number as A). Then ATDA = A1^T D1 A1 + A2^T D2 A2 + A3^T D3 A3. Term i and j
@@ -108,50 +162,33 @@ share cells.The output groups cols of A by signature sig_C(c) = {k: c ∈ C_k};
108162
and col_perm = ⋃ C_k for k in the signature. No structural zeros.
109163
The output's `pre_coalesce` slot holds per-source scratch PDs (one symmetric
110164
PD per source block, row_perm = col_perm = C_k) that
111-
ATDA_spd_fill_values writes into. */
112-
matrix *ATA_spd_alloc(const stacked_pd *A)
165+
ATDA_spd_fill_values writes into.
166+
167+
Note on per-block kernel choice: we use ATA_pd_alloc (rather than
168+
new_permuted_dense directly) because it also pre-sizes
169+
A->blocks[k]->kernel_dwork — that scratch buffer is what
170+
ATDA_pd_fill_values reads from during the fill phase. */
171+
static matrix *wrapper_ATA_pd(const permuted_dense *Ak, const void *ctx)
113172
{
114-
int n = A->base.n;
115-
int n_blocks = A->n_blocks;
173+
(void) ctx;
174+
return ATA_pd_alloc(Ak);
175+
}
116176

117-
/* Pseudo-spd: per source block k, a square PD with
118-
row_perm = col_perm = C_k and uninitialized X. We use the
119-
PD-level ATA_pd_alloc here (rather than new_permuted_dense
120-
directly) because it also pre-sizes A->blocks[k]->kernel_dwork — that
121-
scratch buffer is what ATDA_pd_fill_values reads from during the
122-
fill phase. */
123-
permuted_dense **scratch_blocks =
124-
(permuted_dense **) SP_MALLOC(n_blocks * sizeof(permuted_dense *));
125-
for (int k = 0; k < n_blocks; k++)
126-
{
127-
scratch_blocks[k] = (permuted_dense *) ATA_pd_alloc(A->blocks[k]);
128-
}
129-
matrix *scratch =
130-
new_stacked_pd_unchecked(n, n, n_blocks, scratch_blocks, NULL, NULL);
131-
free(scratch_blocks);
177+
static void wrapper_ATDA_pd(const permuted_dense *Ak, const double *d,
178+
const void *ctx, permuted_dense *Ck)
179+
{
180+
(void) ctx;
181+
ATDA_pd_fill_values(Ak, d, Ck);
182+
}
132183

133-
/* prepare coalescing of overlapping scratch blocks */
134-
matrix *C = coalesce_spd_alloc_unchecked((stacked_pd *) scratch);
135-
((stacked_pd *) C)->pre_coalesce = (stacked_pd *) scratch;
136-
return C;
184+
matrix *ATA_spd_alloc(const stacked_pd *A)
185+
{
186+
return spd_blockwise_alloc_coalesce(A, A->base.n, wrapper_ATA_pd, NULL);
137187
}
138188

139189
void ATDA_spd_fill_values(const stacked_pd *A, const double *d, stacked_pd *C)
140190
{
141-
stacked_pd *scratch = C->pre_coalesce;
142-
143-
/* compute Ai^T @ Di @ Ai into the scratch PDs. */
144-
for (int k = 0; k < A->n_blocks; k++)
145-
{
146-
ATDA_pd_fill_values(A->blocks[k], d, scratch->blocks[k]);
147-
}
148-
149-
/* zero C (one memset is sufficient since, by design, the values of
150-
different blocks are stored consecutively in memory). */
151-
memset(C->base.x, 0, C->base.nnz * sizeof(double));
152-
153-
/* scatter + add each Ai^T Di Ai into its destination C block. */
154-
coalesce_spd_fill_values_accumulate(scratch, C);
191+
spd_blockwise_fill_coalesce_accumulate(A, d, NULL, C, wrapper_ATDA_pd);
155192
}
156193

157194
// ---------------------------------------------------------------------------------
@@ -344,52 +381,62 @@ void BTDA_pd_spd_fill_values(const permuted_dense *B, const double *d,
344381
//
345382
// B = B1 + B2 + B3 where each Bi is a global permuted dense. Then
346383
// C = B^T D A = C1 + C2 + C3 where Ci = Bi^T D A.
384+
//
385+
// TODO: each BTDA_pd_pd_fill_values call internally allocates a DA intermediate
386+
// (see permuted_dense_linalg.c BTDA_pd_pd_fill_values). That means the BTDA
387+
// variant here allocates n_blocks DA temps per fill, all on the hot Hessian
388+
// path. Must be fixed — same future remedy as the per-block BTDA: fold
389+
// diag(d) directly into BTA_pd_pd's gather step.
347390
// ---------------------------------------------------------------------------------
348-
matrix *BTA_spd_pd_alloc(const stacked_pd *B, const permuted_dense *A)
391+
static matrix *wrapper_BTA_pd_pd(const permuted_dense *Bk, const void *ctx)
349392
{
350-
int n_blocks = B->n_blocks;
351-
352-
/* Ck = B_k^T @ A */
353-
permuted_dense **Cks =
354-
(permuted_dense **) SP_MALLOC(n_blocks * sizeof(permuted_dense *));
355-
for (int k = 0; k < n_blocks; k++)
356-
{
357-
Cks[k] = (permuted_dense *) BTA_pd_pd_alloc(B->blocks[k], A);
358-
}
393+
return BTA_pd_pd_alloc(Bk, (const permuted_dense *) ctx);
394+
}
359395

360-
matrix *raw =
361-
new_stacked_pd_unchecked(B->base.n, A->base.n, n_blocks, Cks, NULL, NULL);
362-
free(Cks);
396+
static void wrapper_BTDA_pd_pd(const permuted_dense *Bk, const double *d,
397+
const void *ctx, permuted_dense *Ck)
398+
{
399+
BTDA_pd_pd_fill_values(Bk, d, (const permuted_dense *) ctx, Ck);
400+
}
363401

364-
/* coalesce */
365-
matrix *C = coalesce_spd_alloc_unchecked((stacked_pd *) raw);
366-
((stacked_pd *) C)->pre_coalesce = (stacked_pd *) raw;
367-
return C;
402+
matrix *BTA_spd_pd_alloc(const stacked_pd *B, const permuted_dense *A)
403+
{
404+
return spd_blockwise_alloc_coalesce(B, A->base.n, wrapper_BTA_pd_pd, A);
368405
}
369406

370407
void BTDA_spd_pd_fill_values(const stacked_pd *B, const double *d,
371408
const permuted_dense *A, stacked_pd *C)
372409
{
373-
if (C->base.nnz == 0)
374-
{
375-
return;
376-
}
410+
spd_blockwise_fill_coalesce_accumulate(B, d, A, C, wrapper_BTDA_pd_pd);
411+
}
377412

378-
/* TODO: each BTDA_pd_pd_fill_values call internally allocates a DA
379-
intermediate (see permuted_dense_linalg.c BTDA_pd_pd_fill_values).
380-
That means this function allocates n_blocks DA temps per fill,
381-
all on the hot Hessian path. Must be fixed — same future remedy
382-
as the per-block BTDA: fold diag(d) directly into BTA_pd_pd's
383-
gather step. */
384-
stacked_pd *raw = C->pre_coalesce;
385-
for (int k = 0; k < B->n_blocks; k++)
386-
{
387-
BTDA_pd_pd_fill_values(B->blocks[k], d, A, raw->blocks[k]);
388-
}
413+
// ---------------------------------------------------------------------------------
414+
// BTA_spd_csc / BTDA_spd_csc: C = B^T @ (diag(d) @) A where B is stacked_pd,
415+
// A is CSC. Output C is stacked_pd. Same two-stage skeleton as BTA_spd_pd /
416+
// BTDA_spd_pd, just with BTA_pd_csc / BTDA_pd_csc as the per-block kernel.
417+
// Unlike the pd-on-the-right path, BTDA_pd_csc reuses the per-block
418+
// B_k->kernel_dwork sized by BTA_pd_csc_alloc — no temp DA allocation.
419+
// ---------------------------------------------------------------------------------
420+
static matrix *wrapper_BTA_pd_csc(const permuted_dense *Bk, const void *ctx)
421+
{
422+
return BTA_pd_csc_alloc(Bk, (const CSC_matrix *) ctx);
423+
}
389424

390-
/* zero C before accumulating (matches ATDA_spd_fill_values). */
391-
memset(C->base.x, 0, C->base.nnz * sizeof(double));
392-
coalesce_spd_fill_values_accumulate(raw, C);
425+
static void wrapper_BTDA_pd_csc(const permuted_dense *Bk, const double *d,
426+
const void *ctx, permuted_dense *Ck)
427+
{
428+
BTDA_pd_csc_fill_values(Bk, d, (const CSC_matrix *) ctx, Ck);
429+
}
430+
431+
matrix *BTA_spd_csc_alloc(const stacked_pd *B, const CSC_matrix *A)
432+
{
433+
return spd_blockwise_alloc_coalesce(B, A->n, wrapper_BTA_pd_csc, A);
434+
}
435+
436+
void BTDA_spd_csc_fill_values(const stacked_pd *B, const double *d,
437+
const CSC_matrix *A, stacked_pd *C)
438+
{
439+
spd_blockwise_fill_coalesce_accumulate(B, d, A, C, wrapper_BTDA_pd_csc);
393440
}
394441

395442
// ---------------------------------------------------------------------------------

tests/all_tests.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ int main(void)
439439
mu_run_test(test_BTA_pd_spd_two_blocks_both_kept, tests_run);
440440
mu_run_test(test_BTDA_pd_spd_two_blocks_both_kept, tests_run);
441441
mu_run_test(test_BTDA_spd_pd_overlapping_cp, tests_run);
442+
mu_run_test(test_BTDA_spd_csc_overlapping_cp, tests_run);
442443
mu_run_test(test_stacked_pd_construct_and_free, tests_run);
443444
mu_run_test(test_coalesce_no_overlap, tests_run);
444445
mu_run_test(test_coalesce_three_signatures, tests_run);

tests/utils/test_matrix_BTA.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,4 +560,79 @@ const char *test_BTDA_spd_pd_overlapping_cp(void)
560560
return 0;
561561
}
562562

563+
/* Primitive BTDA_spd_csc kernel (ATA-style direct: per-block BTDA_pd_csc
564+
+ accumulating coalesce). C = B^T @ diag(d) @ A with B a 2-block
565+
stacked_pd whose col_perms share column 2, and A a sparse_matrix
566+
wrapping a small CSR. Reference is the production dispatcher with B
567+
flattened to a sparse_matrix; outputs differ in storage (stacked_pd
568+
vs sparse_matrix), so compare via to_csr. */
569+
const char *test_BTDA_spd_csc_overlapping_cp(void)
570+
{
571+
/* B: 4x3 spd, two blocks with overlapping col_perms (share col 2).
572+
Same layout as test_BTDA_spd_pd_overlapping_cp.
573+
blk0: rows {0,1}, cols {0,2}, X = [[1,2],[3,4]]
574+
blk1: rows {2,3}, cols {1,2}, X = [[5,6],[7,8]] */
575+
int B0_rp[2] = {0, 1};
576+
int B0_cp[2] = {0, 2};
577+
double B0X[4] = {1, 2, 3, 4};
578+
matrix *blk0 = new_permuted_dense(4, 3, 2, 2, B0_rp, B0_cp, B0X);
579+
int B1_rp[2] = {2, 3};
580+
int B1_cp[2] = {1, 2};
581+
double B1X[4] = {5, 6, 7, 8};
582+
matrix *blk1 = new_permuted_dense(4, 3, 2, 2, B1_rp, B1_cp, B1X);
583+
permuted_dense *B_blocks[2] = {(permuted_dense *) blk0, (permuted_dense *) blk1};
584+
matrix *B_spd = new_stacked_pd(4, 3, 2, B_blocks, NULL, NULL);
585+
586+
/* A: 4x3 sparse_matrix wrapping a CSR with arbitrary nonzeros.
587+
Row 0: col 0 = 1, col 2 = 2
588+
Row 1: col 1 = 3
589+
Row 2: col 0 = 4, col 1 = 5, col 2 = 6
590+
Row 3: col 2 = 7
591+
p = [0, 2, 3, 6, 7], i = [0,2, 1, 0,1,2, 2], x = [1,2, 3, 4,5,6, 7]. */
592+
CSR_matrix *A_csr = new_CSR_matrix(4, 3, 7);
593+
int Ap[5] = {0, 2, 3, 6, 7};
594+
int Ai[7] = {0, 2, 1, 0, 1, 2, 2};
595+
double Ax[7] = {1, 2, 3, 4, 5, 6, 7};
596+
memcpy(A_csr->p, Ap, sizeof(Ap));
597+
memcpy(A_csr->i, Ai, sizeof(Ai));
598+
memcpy(A_csr->x, Ax, sizeof(Ax));
599+
matrix *A_sm = new_sparse_matrix(A_csr);
600+
601+
/* Non-trivial d so BTDA != BTA. */
602+
double d[4] = {2.0, -1.5, 0.5, 1.25};
603+
604+
/* Route 1: our new BTDA_spd_csc_fill_values. Need A's csc_cache. */
605+
sparse_matrix_ensure_csc_cache((sparse_matrix *) A_sm);
606+
A_sm->refresh_csc_values(A_sm);
607+
matrix *C_ours =
608+
BTA_spd_csc_alloc((stacked_pd *) B_spd, ((sparse_matrix *) A_sm)->csc_cache);
609+
BTDA_spd_csc_fill_values((stacked_pd *) B_spd, d,
610+
((sparse_matrix *) A_sm)->csc_cache,
611+
(stacked_pd *) C_ours);
612+
613+
/* Route 2: dispatcher with B flattened to sparse_matrix. Note
614+
BTA_matrices_alloc(A, B) computes B^T @ A, so A goes first. */
615+
matrix *B_sparse = spd_to_sparse_matrix_copy(B_spd);
616+
matrix *C_ref = BTA_matrices_alloc(A_sm, B_sparse);
617+
B_sparse->refresh_csc_values(B_sparse);
618+
BTDA_matrices_fill_values(A_sm, d, B_sparse, C_ref);
619+
620+
/* C_ours is stacked_pd, C_ref is sparse_matrix — compare via to_csr. */
621+
CSR_matrix *csr_ours = C_ours->to_csr(C_ours);
622+
CSR_matrix *csr_ref = C_ref->to_csr(C_ref);
623+
mu_assert("m", csr_ours->m == csr_ref->m);
624+
mu_assert("n", csr_ours->n == csr_ref->n);
625+
mu_assert("nnz", csr_ours->nnz == csr_ref->nnz);
626+
mu_assert("p", cmp_int_array(csr_ours->p, csr_ref->p, csr_ours->m + 1));
627+
mu_assert("i", cmp_int_array(csr_ours->i, csr_ref->i, csr_ours->nnz));
628+
mu_assert("x", cmp_double_array(csr_ours->x, csr_ref->x, csr_ours->nnz));
629+
630+
free_matrix(C_ref);
631+
free_matrix(B_sparse);
632+
free_matrix(C_ours);
633+
free_matrix(A_sm);
634+
free_matrix(B_spd);
635+
return 0;
636+
}
637+
563638
#endif /* TEST_MATRIX_BTA_H */

0 commit comments

Comments
 (0)