Skip to content

[WIP – ignore until reviewed by @ChrisRackauckas] Experimental: constant-propagation-aware recursion (opt-in)#63

Closed
ChrisRackauckas-Claude wants to merge 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:constprop-aware-recursion
Closed

[WIP – ignore until reviewed by @ChrisRackauckas] Experimental: constant-propagation-aware recursion (opt-in)#63
ChrisRackauckas-Claude wants to merge 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:constprop-aware-recursion

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Please ignore until reviewed by @ChrisRackauckas. Draft opened by an agent. Stacked on #62 — branched off it, so until #62 merges the diff here also shows #62's commit. Review after #62; the constprop commits are the delta.

What & why

The is_leaf_sig mechanism (#62) papers over one false-positive class: a branch decided by a constant argument — e.g. selecting a parameter buffer by a literal index (getindex(::MTKParameters, ::Int)) — looks value-dependent because the recursion widens the index to Int, even though every real call site passes a literal that folds the branch.

This PR fixes that generically. When enabled, a call carrying Core.Const arguments is re-inferred with the constants preserved, so the constant-decided branch folds to a Core.Const condition that #62's _is_const_gotoifnot already skips.

Key point (why this works where optimize=true doesn't): inference runs without the optimizer, so the constant-decided branch folds but Base's structural branches are not inlined into view. A whole-function optimize=true scan was measured to turn a branch-free linear RHS from 0 → 8 GotoIfNot and a broadcast RHS 0 → 45; this avoids all of that.

It generalizes is_leaf_sig: verified end-to-end that a split-mode MTK MTKParameters RHS becomes branch-free with no MTK-specific override (retiring the need for SciML/ModelingToolkit.jl#4701), while relu stays detected and a dynamic (non-constant) index is still conservatively reported.

Mechanism

  • _arg_lattice — keep an argument as Core.Const(v) when it's a compile-time constant, instead of widening.
  • _recurse_sig — if const-prop is active and a call has any Core.Const argument (and isn't library code), take the constant path; otherwise the existing type path, byte-for-byte unchanged.
  • _const_infer_src / _hasbranching_const — re-infer via Base.Compiler (InferenceResult + explicit-source InferenceState + typeinf), scanning the resulting unoptimized CodeInfo.

Safety / experimental framing

Depends on Base.Compiler internals whose API churns across Julia versions (the InferenceState construction and inferred-source location already differ between 1.12 and 1.13). So it is:

  • Functionally capability-gated — at first use, a real constant-decided-branch fixture is run through the const inference; the feature only activates if the constant-index call comes back branch-free and the widened-index call does not. If the internals ever shift shape, the probe returns false and the feature stays inert (behaviour identical to the plain type recursion). This is what keeps CI green on every channel, now and on future prereleases.
  • OFF by default — enable with enable_const_prop!().

Cross-version status (verified locally by running the suite on each):

channel version const-prop tests
lts 1.10.11 inert (capable=false) 22/22
1 1.12.6 active 26/26
pre 1.13.0-rc1 active 26/26

Open questions for review (why it's opt-in, not default): reentrancy / inference-cache side-effects of running typeinf from within hasbranching; the extra inference per constant-carrying call; and the ongoing maintenance burden of tracking compiler internals vs. the stable is_leaf_sig.

Relationship to the stable stack

A follow-up spike, not a replacement. Recommended: land #62 + SciML/SciMLBase.jl#1413 + SciML/ModelingToolkit.jl#4701 (stable), then evaluate flipping this on (and dropping #4701) once the reentrancy/cost questions are settled.

Semver

Additive, opt-in: 0.1.70.1.8.

Ordinary recursion widens every argument to its type, so a branch decided by a
*constant* argument — e.g. selecting a buffer by a literal index inside a
parameter container — looks value-dependent even though every real call site
folds it. This is the false positive the `is_leaf_sig` overrides paper over.

When enabled, a call carrying `Core.Const` arguments is re-inferred with those
constants preserved (`Base.Compiler` `InferenceResult`/`InferenceState`/
`typeinf`), *without the optimizer*, so the constant-decided branch folds to a
`Core.Const` condition that `_is_const_gotoifnot` already skips — while Base's
structural branches are not inlined into view (which rules out an
`optimize=true` whole-function scan). It stays conservative: a genuinely
value-dependent branch, or a dynamic (non-constant) index, is still reported.

This generalizes `is_leaf_sig`: it removes the same false positives without any
per-container knowledge (e.g. it makes a split-mode MTK `MTKParameters` RHS
branch-free with no MTK-specific override).

Because it depends on `Base.Compiler` internals whose API churns across Julia
versions, it is:
  - capability-gated (`_CONST_PROP_CAPABLE`, requires the 1.12 `InferenceResult`
    shape) and
  - OFF by default; enable with `enable_const_prop!()`.
On unsupported versions, or whenever the const inference fails, it falls back to
the existing type-signature recursion, so behavior is unchanged unless opted in.

Added regression tests (default-off unchanged; enabled: constant index folds,
genuine branch kept, dynamic index still reported).

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
…1/pre)

The first cut gated on `hasmethod(InferenceResult, ...)` and read the inferred
body from `frame.result.src` -- both 1.12-shaped. On 1.13 the `hasmethod` check
still passes but the `InferenceState`/source-extraction dance changed, so the
fold silently did not happen and the enabled test failed on the `pre` channel.

Two fixes:

  - Cross-version inference: pass the uninferred source explicitly
    (`retrieve_code_info`) into a 4-arg `InferenceState` with the non-caching
    `:volatile` mode, and read the inferred `CodeInfo` from whichever of
    `frame.src` / `result.src` is populated. This folds on both 1.12 and 1.13.

  - Functional capability gate: instead of probing for a method signature, run a
    real constant-decided-branch fixture through the const inference at first use
    and require that the constant-index call comes back branch-free while the
    widened-index call does not. If the compiler internals ever shift shape, the
    probe returns false and the feature stays inert -- so behaviour is identical
    to the plain type recursion and CI stays green on unsupported versions.

Verified: lts 1.10.11 capable=false 22/22 (feature inert); 1.12.6 capable=true
26/26; pre 1.13.0-rc1 capable=true 26/26.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title [WIP – ignore until reviewed by @ChrisRackauckas] Experimental: constant-propagation-aware recursion (opt-in, 1.12+) [WIP – ignore until reviewed by @ChrisRackauckas] Experimental: constant-propagation-aware recursion (opt-in) Jul 3, 2026
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

Folded into #62. That PR now makes the constant-propagation-aware analysis the default and only false-positive-folding mechanism (no opt-in toggle, no is_leaf_sig), verified green on lts/1/pre and correct on the MTK RHS table. Closing this in favor of #62.

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