Skip to content

Commit ea7adea

Browse files
dance858claude
andcommitted
Version-guard the BA_pd_spd transpose cache
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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016rQ3LPdi31kNLmTy9F2oEa
1 parent 5cf9d7e commit ea7adea

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
@@ -515,14 +515,18 @@ void BTDA_csc_spd_fill_values(const CSC_matrix *B, const double *d,
515515
// BA_pd_spd: C = B @ A where B is permuted_dense and A is stacked_pd. Thin
516516
// wrapper over the canonical BTA_pd_spd_* kernel: use B's lazily-cached
517517
// transpose and call BTA. The cache is populated on first call (in alloc)
518-
// and reused across subsequent fills.
518+
// and reused across subsequent fills; its values are refreshed only when
519+
// B's values_version has moved since the last fill (transpose_seen).
519520
//
520521
// Contract: B's perms must be immutable between alloc and fill (the cache
521522
// records B's perms at alloc time and is not re-validated at fill). For
522523
// callers where B's perms change between calls — notably the kron-spd path
523524
// that reuses a mutating scratch — bypass this wrapper and call
524525
// BTA_pd_spd_* directly. BA_dense_kron_spd does exactly that
525-
// (stacked_pd_kron_linalg.c) and is the only such caller today.
526+
// (stacked_pd_kron_linalg.c). The values_version guard additionally
527+
// requires B to be the owner of its value buffer; BA_spd_spd_fill_values
528+
// passes spd blocks (no version of their own) and therefore also bypasses
529+
// the wrapper for its fills.
526530
// ---------------------------------------------------------------------------------
527531
matrix *BA_pd_spd_alloc(const permuted_dense *B, const stacked_pd *A)
528532
{
@@ -534,7 +538,11 @@ void BA_pd_spd_fill_values(const permuted_dense *B, const stacked_pd *A,
534538
permuted_dense *C)
535539
{
536540
permuted_dense *BT = B->transpose_cache;
537-
transpose_pd_fill_values(B, BT);
541+
if (BT->transpose_seen != B->base.values_version)
542+
{
543+
transpose_pd_fill_values(B, BT);
544+
BT->transpose_seen = B->base.values_version;
545+
}
538546
BTA_pd_spd_fill_values(BT, A, C);
539547
}
540548

@@ -609,6 +617,11 @@ void BA_spd_spd_fill_values(const stacked_pd *B, const stacked_pd *A, stacked_pd
609617
{
610618
int q = C->src_block_idx[C->src_block_idx_p[k]];
611619
const permuted_dense *Bq = B->blocks[q];
612-
BA_pd_spd_fill_values(Bq, A, C->blocks[k]);
620+
/* Bypass BA_pd_spd_fill_values' version guard: spd blocks have no
621+
values_version of their own (writers bump the owning spd), so the
622+
cached transpose must be refreshed unconditionally here. */
623+
permuted_dense *BqT = Bq->transpose_cache;
624+
transpose_pd_fill_values(Bq, BqT);
625+
BTA_pd_spd_fill_values(BqT, A, C->blocks[k]);
613626
}
614627
}

tests/all_tests.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ int main(void)
490490
mu_run_test(test_BTA_sparse_matrices_csc_A, tests_run);
491491
mu_run_test(test_BTA_sparse_matrices_spd_A, tests_run);
492492
mu_run_test(test_BA_pd_kron_spd_no_cache_staleness, tests_run);
493+
mu_run_test(test_BA_pd_spd_transpose_cache_refresh, tests_run);
493494
mu_run_test(test_stacked_pd_construct_and_free, tests_run);
494495
mu_run_test(test_coalesce_no_overlap, tests_run);
495496
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
@@ -1328,8 +1328,10 @@ const char *test_BTA_sparse_matrices_spd_A(void)
13281328
its row_perm / col_perm in place. A prior implementation of
13291329
BA_pd_spd_fill_values cached B's transpose on B->transpose_cache,
13301330
which (a) was never populated for the fill-path scratch, and (b)
1331-
would have held stale perms even if populated. Now BA_pd_spd does
1332-
per-call transpose alloc/free so this scenario is correct. The
1331+
would have held stale perms even if populated. BA_pd_spd still uses
1332+
that cache, but the kron path stays correct because
1333+
BA_dense_kron_spd bypasses the wrapper and calls BTA_pd_spd_*
1334+
directly on its mutating scratch (see stacked_pd_linalg.c). The
13331335
reference path runs the same dispatch with J flattened to
13341336
sparse_matrix (which routes via BA_dense_kron_csc, no transpose-cache
13351337
involvement); we compare via to_csr. */
@@ -1393,4 +1395,68 @@ const char *test_BA_pd_kron_spd_no_cache_staleness(void)
13931395
return 0;
13941396
}
13951397

1398+
/* BA_pd_spd transpose cache: the fill refreshes B's cached transpose iff
1399+
B's values_version moved since the last fill. Fill once, mutate B's
1400+
values + bump, refill, and compare against a fresh computation with the
1401+
mutated values. */
1402+
const char *test_BA_pd_spd_transpose_cache_refresh(void)
1403+
{
1404+
/* B: 3x4 pd with a non-square 2x3 block. */
1405+
int B_rp[2] = {0, 2};
1406+
int B_cp[3] = {0, 1, 3};
1407+
double BX[6] = {1, 2, 3, 4, 5, 6};
1408+
matrix *B_m = new_permuted_dense(3, 4, 2, 3, B_rp, B_cp, BX);
1409+
permuted_dense *B = (permuted_dense *) B_m;
1410+
1411+
/* A: 4x5 spd with two disjoint-row blocks. */
1412+
int A0_rp[2] = {0, 1};
1413+
int A0_cp[2] = {0, 2};
1414+
double A0X[4] = {1, 2, 3, 4};
1415+
matrix *Ablk0 = new_permuted_dense(4, 5, 2, 2, A0_rp, A0_cp, A0X);
1416+
int A1_rp[2] = {2, 3};
1417+
int A1_cp[2] = {1, 4};
1418+
double A1X[4] = {5, 6, 7, 8};
1419+
matrix *Ablk1 = new_permuted_dense(4, 5, 2, 2, A1_rp, A1_cp, A1X);
1420+
permuted_dense *A_blocks[2] = {(permuted_dense *) Ablk0,
1421+
(permuted_dense *) Ablk1};
1422+
matrix *A_spd = new_stacked_pd(4, 5, 2, A_blocks, NULL, NULL);
1423+
stacked_pd *A = (stacked_pd *) A_spd;
1424+
1425+
matrix *C = BA_pd_spd_alloc(B, A);
1426+
BA_pd_spd_fill_values(B, A, (permuted_dense *) C);
1427+
mu_assert("seen must match after fill",
1428+
B->transpose_cache->transpose_seen == B->base.values_version);
1429+
1430+
/* Mutate B's values and bump; the guarded refill must refresh the
1431+
cached transpose. */
1432+
double BX2[6] = {-1, 7, 0.5, 2, -3, 6};
1433+
memcpy(B->X, BX2, 6 * sizeof(double));
1434+
matrix_values_changed(B_m);
1435+
BA_pd_spd_fill_values(B, A, (permuted_dense *) C);
1436+
mu_assert("seen must catch up after bump + refill",
1437+
B->transpose_cache->transpose_seen == B->base.values_version);
1438+
1439+
/* Reference: fresh B with the mutated values, fresh cache. */
1440+
matrix *B2_m = new_permuted_dense(3, 4, 2, 3, B_rp, B_cp, BX2);
1441+
permuted_dense *B2 = (permuted_dense *) B2_m;
1442+
matrix *C_ref = BA_pd_spd_alloc(B2, A);
1443+
BA_pd_spd_fill_values(B2, A, (permuted_dense *) C_ref);
1444+
1445+
CSR_matrix *csr_ours = C->to_csr(C);
1446+
CSR_matrix *csr_ref = C_ref->to_csr(C_ref);
1447+
mu_assert("m", csr_ours->m == csr_ref->m);
1448+
mu_assert("n", csr_ours->n == csr_ref->n);
1449+
mu_assert("nnz", csr_ours->nnz == csr_ref->nnz);
1450+
mu_assert("p", cmp_int_array(csr_ours->p, csr_ref->p, csr_ours->m + 1));
1451+
mu_assert("i", cmp_int_array(csr_ours->i, csr_ref->i, csr_ours->nnz));
1452+
mu_assert("x", cmp_double_array(csr_ours->x, csr_ref->x, csr_ours->nnz));
1453+
1454+
free_matrix(C_ref);
1455+
free_matrix(B2_m);
1456+
free_matrix(C);
1457+
free_matrix(A_spd);
1458+
free_matrix(B_m);
1459+
return 0;
1460+
}
1461+
13961462
#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)