Skip to content

Fix FullSpecialize initialization map buffer types - #5145

Draft
ChrisRackauckas-Claude wants to merge 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:agent/fullspecialize-map-buffer-type
Draft

Fix FullSpecialize initialization map buffer types#5145
ChrisRackauckas-Claude wants to merge 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:agent/fullspecialize-map-buffer-type

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

What changed and why

Two regressions from #5045, both from the
generated initialization maps rebuilding u0/p in different container types than the
problem carries. Neither is GPU-specific — the first breaks a plain host solve.

1. p (29981c03e2). _static_initialization_buffer promoted any mutable isbits
buffer to an MVector, including a plain Vector. Solve-time initialization assigns the
map's result straight back:

integrator.p = initializeprobpmap(prob, initsol)

which cannot convert. Any ODEProblem{iip, FullSpecialize} whose initialization is solved
at solve time rather than at construction threw before its first step.

2. u0 (80986a6532). The state map forced iip ? MVector : SVector, so
ODEProblem{true, FullSpecialize}(sys, …).u0 came back as an MVector where it had been a
Vector. That is a user-visible type change, and it breaks GPU ensembles outright, because
a device array cannot have a mutable element type:

JLArray only supports element types that are allocated inline.
MVector{1, Float64} is a mutable type

Both now produce static storage only where the caller asked for it — a static
u0_constructor/p_constructor, or DiffEqGPU's kernel path, which converts p before the
map runs. That is also where the allocation win was actually wanted; forcing it meant a
model with hundreds of tunables built an MVector that size on every initialization.

One subtlety worth flagging: the plain buffer is filled explicitly rather than with
collect, because StaticArrays overloads collect to preserve staticness — so
collect(T, ::SVector) hands back a SizedVector, not a Vector. The parameter path
passes a Tuple and was unaffected; the state map passes an SVector and was not.

Verification

Julia 1.12.7. Both regressions bisect cleanly to #5045 — same script against the merge
commit's parent 0bba967daf:

                     pre-5045        post-5045
AutoDespecialize     Success         Success
FullSpecialize       Success         MethodError: Cannot `convert` …

u0 type, same comparison:

pre-5045    u0::Vector{Float64}
post-5045   u0::MVector{1, Float64}
this PR     u0::Vector{Float64}

New regression test, failing before / passing after (fix stashed and restored in one
session):

before:  FullSpecialize initialization survives a solve-time initialization | 1 Error
         p match false → MethodError: Cannot `convert` …
after:   p match true → Success  u0=[1.0, 3.0]

GROUP=Initialization, on the rebased branch:

Guess Propagation         |  11 pass |  11 total | 31.4s
InitializationSystem Test | 735 pass | 12 broken | 747 total | 19m45.9s
Initial Values Test       |  65 pass |  65 total | 3m28.4s

Exit 0, zero errors. The 12 broken are pre-existing @test_brokens on master; none were
added, removed, skipped or loosened. Runic, typos over the diff and git diff --check
are clean.

Test assertions I changed, and why

Three assertions from #5045 required StaticVector buffers from a Vector-backed problem.
That is exactly the behaviour that breaks solve, so they now require the map's buffers to
match the problem's own types, which is the real contract. The static_constructor cases
still assert isbits results, so the opt-in path is still pinned. Please push back if you
would rather keep forced-static and fix the integrator side instead.

This does narrow #5045's stated allocation result: the "784 → 80 bytes" row was measured on
a plain host problem, and that configuration now keeps Vector buffers. The 0-byte
static-constructor row and the GPU path are unaffected.

What I did not verify

Only GROUP=Initialization was run. Extensions, FMI, QA, SymbolicIndexingInterface,
downstream and the docs build were not rerun for this change; CI is the arbiter. No GPU
hardware on this machine, so the MVector-as-device-array-eltype failure is reproduced
through JLArrays, not CUDA.

Ignore this draft until reviewed by @ChrisRackauckas.

🤖 Generated with Claude Code (model: claude-opus-5[1m])

https://claude.ai/code/session_01GdSpCLd7NBZuuePJmcDzU7

ChrisRackauckas and others added 2 commits September 13, 2026 09:58
`_static_initialization_buffer` promoted any mutable isbits buffer to an
`MVector`, including a plain `Vector`. The generated parameter map therefore
returned a different `MTKParameters` type than the problem carried, and
solve-time initialization assigns that straight back:

    integrator.p = initializeprobpmap(prob, initsol)

which cannot convert. Any `ODEProblem{iip, FullSpecialize}` whose
initialization is solved at `solve` time rather than at construction threw
before its first step, a regression from SciML#5045.

Only produce static storage where the prototype is already static. The
allocation win then lands exactly where the problem opted into static storage
(a static `u0`/`p_constructor`, and DiffEqGPU's kernel path, which converts `p`
before the map runs) instead of being forced on every problem — which also
avoids building an `MVector` sized by the parameter count on large models.

The two assertions requiring `StaticVector` buffers from a `Vector`-backed
problem encoded the behaviour that breaks `solve`; they now require the map's
buffers to match the problem's own types, which is the actual contract. The
`static_constructor` cases still assert `isbits` results.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code 2.0.14
Agent-Model: claude-opus-5[1m]
Agent-Session: https://claude.ai/code/session_01GdSpCLd7NBZuuePJmcDzU7
Claude-Session: https://claude.ai/code/session_01GdSpCLd7NBZuuePJmcDzU7
The state map forced `iip ? MVector : SVector`, so
`ODEProblem{true, FullSpecialize}(sys, ...).u0` came back as an `MVector`
where it had been a `Vector` before SciML#5045 — a user-visible type change, and
one that breaks GPU ensembles outright, since a device array cannot have a
mutable element type:

    JLArray only supports element types that are allocated inline.
    MVector{1, Float64} is a mutable type

Build the buffer the way the default map path does and let `u0_constructor`
decide, matching the `p` handling in the previous commit. A caller that wants
static storage asks for it, and then gets an `SArray` — which is what a GPU
kernel needs anyway.

`_static_initialization_buffer` also has to fill its plain buffer explicitly:
StaticArrays overloads `collect` to preserve staticness, so
`collect(T, ::SVector)` returns a `SizedVector`. The parameter path passes a
`Tuple` and was unaffected; the state map passes an `SVector` and was not.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code 2.0.14
Agent-Model: claude-opus-5[1m]
Agent-Session: https://claude.ai/code/session_01GdSpCLd7NBZuuePJmcDzU7
Claude-Session: https://claude.ai/code/session_01GdSpCLd7NBZuuePJmcDzU7
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