Skip to content

Refresh composite param_source subtrees on parameter updates#107

Merged
dance858 merged 1 commit into
mainfrom
param-source-mark-refresh
Jul 13, 2026
Merged

Refresh composite param_source subtrees on parameter updates#107
dance858 merged 1 commit into
mainfrom
param-source-mark-refresh

Conversation

@Transurgeon

Copy link
Copy Markdown
Member

Problem

A param_source subtree is evaluated by the owning atom's gated recursion, not the main forward walk, so expr_set_needs_refresh never reaches nodes inside a composite source. Gated nodes there — promote, nested mults, cached matmul coefficients — keep serving values cached at construction after problem_update_params.

The failing shapes are common, not exotic (each verified with a standalone repro):

lowered shape example cvxpy origin result before this PR
vector_mult(promote(p), A) feeding left_matmul (p * A) @ x stale A on every re-solve
vector_mult(promote(g), Sig) feeding quad_form quad_form(x, g * Sig) stale Q
division/scaling chains feeding quad_form quad_over_lin(x, p) (canonicalized to I/p) stale Q
two nested gated levels ((2*p) * A) @ x stale A

Shapes that were already correct and stay untouched: bare-Parameter sources (refreshed by problem_update_params' memcpy), purely additive composites (p1+p2 — ungated nodes recurse unconditionally), and computed multipliers of variables ((2*p)*x — the gated node sits in the main tree, which the walker reaches).

Fix

One line per source-owning atom (scalar_mult, vector_mult, left_matmul — which also serves right_matmulconvolve, kron, quad_form): mark the side subtree dirty at the moment the atom is about to re-evaluate it,

if (node->param_source != NULL && node->needs_parameter_refresh)
{
    expr_set_needs_refresh(node->param_source);   /* new */
    node->param_source->forward(node->param_source, NULL);
}

The owner's flag is already set by the existing walker (it lives in the main tree), so the mark happens exactly once per parameter update; nested gated levels recurse for free because each level applies the same line to its own source. No struct changes, no walker changes, no behavior change for one-shot solves or bare-parameter sources.

Tests

tests/problem/test_param_source_refresh.h pins the three failing shapes (composite matmul coefficient, composite quad matrix, two nested gated levels), asserting constraint values and Jacobian/gradient values across an update. Each fails without the fix (e.g. stale 10.0 where 50.0 is expected) and passes with it. Full suite: 417/417.

Downstream validation (cvxpy pr-a/b/c stack, cached re-solve paths): the cvxpy-side canaries (quad_over_lin symbolic quad matrix, scaled parameter coefficient) and all four repro shapes above go from stale to fresh on this branch with no other changes; full cvxpy test suite green (modulo pre-existing, unrelated failures verified identical on other builds).

Relation to #104

This supersedes the first commit of #104 (param-source refresh propagation) with a smaller, atom-local change: no param_source pointer hoisted into the base expr struct. #104's second commit (retaining registered parameter nodes) is deliberately not included — it is an independent API-soundness hardening that can be its own PR if wanted.

🤖 Generated with Claude Code

@Transurgeon
Transurgeon force-pushed the param-source-mark-refresh branch from c93a83c to 4248f7d Compare July 12, 2026 22:16
…them

A param_source subtree is evaluated by the owning atom's gated recursion,
not the main forward walk, so expr_set_needs_refresh never reaches nodes
inside a composite source. Gated nodes there (promote, nested mults,
cached matmul coefficients) kept serving values cached at build time
after problem_update_params: p*A @ x and quad_form(x, g*Sig) both solved
with the original parameter values on every re-solve.

Fix: each source-owning atom marks its subtree dirty at the moment it is
about to re-evaluate it (one expr_set_needs_refresh call per atom, gated
on the flag the main walker already sets). Nested gated levels recurse
for free. No struct or walker changes; no behavior change for one-shot
solves or bare-parameter sources.

Tests: tests/problem/test_param_source_refresh.h pins the three failing
shapes (composite matmul coefficient, composite quad matrix, two nested
gated levels); each fails without the fix and passes with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dance858

Copy link
Copy Markdown
Collaborator

Beautiful solution. I could stare at it for hours.

@dance858
dance858 merged commit 4172c5e into main Jul 13, 2026
11 checks passed
@Transurgeon
Transurgeon deleted the param-source-mark-refresh branch July 13, 2026 03:30
Transurgeon added a commit that referenced this pull request Jul 13, 2026
…them (#107)

A param_source subtree is evaluated by the owning atom's gated recursion,
not the main forward walk, so expr_set_needs_refresh never reaches nodes
inside a composite source. Gated nodes there (promote, nested mults,
cached matmul coefficients) kept serving values cached at build time
after problem_update_params: p*A @ x and quad_form(x, g*Sig) both solved
with the original parameter values on every re-solve.

Fix: each source-owning atom marks its subtree dirty at the moment it is
about to re-evaluate it (one expr_set_needs_refresh call per atom, gated
on the flag the main walker already sets). Nested gated levels recurse
for free. No struct or walker changes; no behavior change for one-shot
solves or bare-parameter sources.

Tests: tests/problem/test_param_source_refresh.h pins the three failing
shapes (composite matmul coefficient, composite quad matrix, two nested
gated levels); each fails without the fix and passes with it.

Co-authored-by: Transurgeon <peter.zijie@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants