Skip to content

Commit c412a33

Browse files
dance858claude
andauthored
Version-guard the BA_pd_spd transpose cache (#115)
BA_pd_spd_fill_values refreshed B's cached transpose on every call. Now it refreshes only when B's values_version has moved since the last fill; the seen counter lives on the cache pd itself. To make the guard sound, the two parameter-refresh paths that wrote pd values silently (left_matmul refresh_dense_left, quad_form) now call matrix_values_changed after their memcpys, per the matrix.h contract. BA_spd_spd_fill_values passes spd blocks, which have no values_version of their own (writers bump the owning spd), so it bypasses the guard and refreshes unconditionally via BTA_pd_spd_fill_values. Also fixes the stale comment on test_BA_pd_kron_spd_no_cache_staleness (the cache it says was removed is back; the kron path is correct because it bypasses the wrapper) and adds a refresh-on-bump regression test. Claude-Session: https://claude.ai/code/session_016rQ3LPdi31kNLmTy9F2oEa Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 942df2d commit c412a33

8 files changed

Lines changed: 99 additions & 8 deletions

File tree

‎include/utils/permuted_dense.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ typedef struct permuted_dense
6767
size_t kernel_iwork_size;
6868

6969
/* Cached transpose of this PD as another permuted_dense, allocated lazily
70-
on first call to permuted_dense_ensure_transpose_cache. */
70+
on first call to permuted_dense_ensure_transpose_cache. On the cache PD
71+
itself, transpose_seen records the source's base.values_version whose
72+
values the cache holds; consumers refill iff it is stale. */
7173
struct permuted_dense *transpose_cache;
74+
uint64_t transpose_seen;
7275
} permuted_dense;
7376

7477
/* Constructor. row_perm and col_perm must be strictly increasing in their

‎src/atoms/affine/left_matmul.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ static void refresh_dense_left(left_matmul_expr *lnode)
231231
actually corresponds to the transpose of A, and we transpose AT to get A. */
232232
memcpy(lnode->AT->x, lnode->param_source->value, m * n * sizeof(double));
233233
A_transpose(lnode->A->x, lnode->AT->x, n, m);
234+
matrix_values_changed(lnode->AT);
235+
matrix_values_changed(lnode->A);
234236
}
235237

236238
/* We expect u->d1 == A->n. However, numpy's broadcasting rules allow users to

‎src/atoms/other/quad_form.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ static void refresh_param_values_qf(quad_form_expr *qnode)
4545
qnode->base.needs_parameter_refresh = false;
4646
memcpy(qnode->Q->x, qnode->param_source->value,
4747
(size_t) qnode->n * qnode->n * sizeof(double));
48+
matrix_values_changed(qnode->Q);
4849
}
4950

5051
static void forward(expr *node, const double *u)

‎src/utils/permuted_dense_linalg.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ permuted_dense *permuted_dense_ensure_transpose_cache(const permuted_dense *B_co
5757
}
5858
permuted_dense *BT = (permuted_dense *) transpose_pd_alloc(B);
5959
B->transpose_cache = BT;
60+
/* Deliberately stale so the first fill always refreshes the cache. */
61+
BT->transpose_seen = B->base.values_version - 1;
6062
return BT;
6163
}
6264

‎src/utils/stacked_pd_linalg.c‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -578,14 +578,18 @@ void BTDA_csc_spd_fill_values(const CSC_matrix *B, const double *d,
578578
// BA_pd_spd: C = B @ A where B is permuted_dense and A is stacked_pd. Thin
579579
// wrapper over the canonical BTA_pd_spd_* kernel: use B's lazily-cached
580580
// transpose and call BTA. The cache is populated on first call (in alloc)
581-
// and reused across subsequent fills.
581+
// and reused across subsequent fills; its values are refreshed only when
582+
// B's values_version has moved since the last fill (transpose_seen).
582583
//
583584
// Contract: B's perms must be immutable between alloc and fill (the cache
584585
// records B's perms at alloc time and is not re-validated at fill). For
585586
// callers where B's perms change between calls — notably the kron-spd path
586587
// that reuses a mutating scratch — bypass this wrapper and call
587588
// BTA_pd_spd_* directly. BA_dense_kron_spd does exactly that
588-
// (stacked_pd_kron_linalg.c) and is the only such caller today.
589+
// (stacked_pd_kron_linalg.c). The values_version guard additionally
590+
// requires B to be the owner of its value buffer; BA_spd_spd_fill_values
591+
// passes spd blocks (no version of their own) and therefore also bypasses
592+
// the wrapper for its fills.
589593
// ---------------------------------------------------------------------------------
590594
matrix *BA_pd_spd_alloc(const permuted_dense *B, const stacked_pd *A)
591595
{
@@ -597,7 +601,11 @@ void BA_pd_spd_fill_values(const permuted_dense *B, const stacked_pd *A,
597601
permuted_dense *C)
598602
{
599603
permuted_dense *BT = B->transpose_cache;
600-
transpose_pd_fill_values(B, BT);
604+
if (BT->transpose_seen != B->base.values_version)
605+
{
606+
transpose_pd_fill_values(B, BT);
607+
BT->transpose_seen = B->base.values_version;
608+
}
601609
BTA_pd_spd_fill_values(BT, A, C);
602610
}
603611

@@ -672,6 +680,11 @@ void BA_spd_spd_fill_values(const stacked_pd *B, const stacked_pd *A, stacked_pd
672680
{
673681
int q = C->src_block_idx[C->src_block_idx_p[k]];
674682
const permuted_dense *Bq = B->blocks[q];
675-
BA_pd_spd_fill_values(Bq, A, C->blocks[k]);
683+
/* Bypass BA_pd_spd_fill_values' version guard: spd blocks have no
684+
values_version of their own (writers bump the owning spd), so the
685+
cached transpose must be refreshed unconditionally here. */
686+
permuted_dense *BqT = Bq->transpose_cache;
687+
transpose_pd_fill_values(Bq, BqT);
688+
BTA_pd_spd_fill_values(BqT, A, C->blocks[k]);
676689
}
677690
}

‎tests/all_tests.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ int main(void)
510510
mu_run_test(test_BTA_matrices_fill_csc_pd, tests_run);
511511
mu_run_test(test_BTA_matrices_fill_csc_csc, tests_run);
512512
mu_run_test(test_BA_pd_kron_spd_no_cache_staleness, tests_run);
513+
mu_run_test(test_BA_pd_spd_transpose_cache_refresh, tests_run);
513514
mu_run_test(test_stacked_pd_construct_and_free, tests_run);
514515
mu_run_test(test_coalesce_no_overlap, tests_run);
515516
mu_run_test(test_coalesce_three_signatures, tests_run);

‎tests/utils/test_matmul_dispatchers.h‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2369,8 +2369,10 @@ const char *test_BTA_matrices_fill_csc_csc(void)
23692369
its row_perm / col_perm in place. A prior implementation of
23702370
BA_pd_spd_fill_values cached B's transpose on B->transpose_cache,
23712371
which (a) was never populated for the fill-path scratch, and (b)
2372-
would have held stale perms even if populated. Now BA_pd_spd does
2373-
per-call transpose alloc/free so this scenario is correct. The
2372+
would have held stale perms even if populated. BA_pd_spd still uses
2373+
that cache, but the kron path stays correct because
2374+
BA_dense_kron_spd bypasses the wrapper and calls BTA_pd_spd_*
2375+
directly on its mutating scratch (see stacked_pd_linalg.c). The
23742376
reference path runs the same dispatch with J flattened to
23752377
sparse_matrix (which routes via BA_dense_kron_csc, no transpose-cache
23762378
involvement); we compare via to_csr. */
@@ -2434,4 +2436,68 @@ const char *test_BA_pd_kron_spd_no_cache_staleness(void)
24342436
return 0;
24352437
}
24362438

2439+
/* BA_pd_spd transpose cache: the fill refreshes B's cached transpose iff
2440+
B's values_version moved since the last fill. Fill once, mutate B's
2441+
values + bump, refill, and compare against a fresh computation with the
2442+
mutated values. */
2443+
const char *test_BA_pd_spd_transpose_cache_refresh(void)
2444+
{
2445+
/* B: 3x4 pd with a non-square 2x3 block. */
2446+
int B_rp[2] = {0, 2};
2447+
int B_cp[3] = {0, 1, 3};
2448+
double BX[6] = {1, 2, 3, 4, 5, 6};
2449+
matrix *B_m = new_permuted_dense(3, 4, 2, 3, B_rp, B_cp, BX);
2450+
permuted_dense *B = (permuted_dense *) B_m;
2451+
2452+
/* A: 4x5 spd with two disjoint-row blocks. */
2453+
int A0_rp[2] = {0, 1};
2454+
int A0_cp[2] = {0, 2};
2455+
double A0X[4] = {1, 2, 3, 4};
2456+
matrix *Ablk0 = new_permuted_dense(4, 5, 2, 2, A0_rp, A0_cp, A0X);
2457+
int A1_rp[2] = {2, 3};
2458+
int A1_cp[2] = {1, 4};
2459+
double A1X[4] = {5, 6, 7, 8};
2460+
matrix *Ablk1 = new_permuted_dense(4, 5, 2, 2, A1_rp, A1_cp, A1X);
2461+
permuted_dense *A_blocks[2] = {(permuted_dense *) Ablk0,
2462+
(permuted_dense *) Ablk1};
2463+
matrix *A_spd = new_stacked_pd(4, 5, 2, A_blocks, NULL, NULL);
2464+
stacked_pd *A = (stacked_pd *) A_spd;
2465+
2466+
matrix *C = BA_pd_spd_alloc(B, A);
2467+
BA_pd_spd_fill_values(B, A, (permuted_dense *) C);
2468+
mu_assert("seen must match after fill",
2469+
B->transpose_cache->transpose_seen == B->base.values_version);
2470+
2471+
/* Mutate B's values and bump; the guarded refill must refresh the
2472+
cached transpose. */
2473+
double BX2[6] = {-1, 7, 0.5, 2, -3, 6};
2474+
memcpy(B->X, BX2, 6 * sizeof(double));
2475+
matrix_values_changed(B_m);
2476+
BA_pd_spd_fill_values(B, A, (permuted_dense *) C);
2477+
mu_assert("seen must catch up after bump + refill",
2478+
B->transpose_cache->transpose_seen == B->base.values_version);
2479+
2480+
/* Reference: fresh B with the mutated values, fresh cache. */
2481+
matrix *B2_m = new_permuted_dense(3, 4, 2, 3, B_rp, B_cp, BX2);
2482+
permuted_dense *B2 = (permuted_dense *) B2_m;
2483+
matrix *C_ref = BA_pd_spd_alloc(B2, A);
2484+
BA_pd_spd_fill_values(B2, A, (permuted_dense *) C_ref);
2485+
2486+
CSR_matrix *csr_ours = C->to_csr(C);
2487+
CSR_matrix *csr_ref = C_ref->to_csr(C_ref);
2488+
mu_assert("m", csr_ours->m == csr_ref->m);
2489+
mu_assert("n", csr_ours->n == csr_ref->n);
2490+
mu_assert("nnz", csr_ours->nnz == csr_ref->nnz);
2491+
mu_assert("p", cmp_int_array(csr_ours->p, csr_ref->p, csr_ours->m + 1));
2492+
mu_assert("i", cmp_int_array(csr_ours->i, csr_ref->i, csr_ours->nnz));
2493+
mu_assert("x", cmp_double_array(csr_ours->x, csr_ref->x, csr_ours->nnz));
2494+
2495+
free_matrix(C_ref);
2496+
free_matrix(B2_m);
2497+
free_matrix(C);
2498+
free_matrix(A_spd);
2499+
free_matrix(B_m);
2500+
return 0;
2501+
}
2502+
24372503
#endif /* TEST_MATMUL_DISPATCHERS_H */

‎tests/utils/test_stacked_pd.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1118,12 +1118,15 @@ const char *test_BA_pd_spd_alloc_then_fill_values(void)
11181118
BA_pd_spd_fill_values((permuted_dense *) B, (stacked_pd *) A,
11191119
(permuted_dense *) C_m);
11201120

1121-
/* Mutate B and A_0 values. */
1121+
/* Mutate B and A_0 values. B is an owner pd, so the write must be
1122+
announced (matrix.h contract) — BA_pd_spd_fill_values refreshes its
1123+
cached transpose of B iff B's values_version moved. */
11221124
permuted_dense *B_pd = (permuted_dense *) B;
11231125
B_pd->X[0] = 5;
11241126
B_pd->X[1] = 6;
11251127
B_pd->X[2] = 7;
11261128
B_pd->X[3] = 8;
1129+
matrix_values_changed(B);
11271130
permuted_dense *A0_pd = (permuted_dense *) A0;
11281131
A0_pd->X[0] = 100;
11291132
A0_pd->X[1] = 200;

0 commit comments

Comments
 (0)