Skip to content

[NOT FOR MERGE — archived prototype] Erase nonnumeric buffer element types from MTKParameters - #4833

Closed
ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:opaque-nonnumeric-buffer
Closed

[NOT FOR MERGE — archived prototype] Erase nonnumeric buffer element types from MTKParameters#4833
ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:opaque-nonnumeric-buffer

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

Do not merge. Do not review. Opened purely to preserve an investigation in a reviewable form, and closed immediately. Context and full analysis: #4831.

This prototype delivers a large first-solve win and carries a disqualifying runtime regression that cannot be fixed without also changing parameter codegen. It is archived so the measurements and the design constraint are not lost.

The problem

Models with a nonnumeric parameter (a DataInterpolations spline, a callable, any struct) put that declared type into the MTKParameters type. Solver specializations are keyed on the parameter object, so two models differing only in CubicSpline vs LinearInterpolation share no compiled code and each pays a full first-solve compilation (~16s with the default algorithm; see #4831 for that measurement).

What this does

Wraps the nonnumeric buffers in an OpaqueNonnumeric{K} — a Ref{Any}, with K (buffer count) kept in the type because the generated parameter-path dispatch needs it, and element types erased. The field stays named nonnumeric and getproperty unwraps, so the ~45 existing p.nonnumeric[i][j] sites are untouched. One file plus tests.

Compile time (two models, different nonnumeric parameter types, Tsit5)

baseline erased
MTKParameters type uniform false true
solve A (cold) 4.917s 4.782s
solve B (different nonnumeric type) 3.870s 0.015s
results identical identical

Runtime (same model; nonnumeric parameter read on every RHS evaluation)

baseline erased
RHS ns/call 1.37 666.33
RHS allocations/call 0 112 B
warm solve 0.108 ms 0.188 ms

So it trades ~3.9s of one-time compilation for a 486x slower RHS on exactly the models it targets. Net loss for any non-trivial integration — hence not mergeable.

Why, and what would be needed

Parameters are destructured via Symbolics DestructuredArgs over an argument typed Vector{Vector{Any}}, so the RHS reads buffers as p[i][j]. Baseline gets type stability for free from the concrete Vector{FA}; erasing the element type makes that read Any, so every nonnumeric access inside the time loop dispatches dynamically and boxes. This is inherent to erasing at the buffer level — storing Vector{Any} directly costs the same.

The only fix is for codegen to emit a typed access for nonnumeric buffers, e.g. (p[i]::Vector{FA})[j]. BufferTemplate already carries type::TypeT, so the index cache knows the concrete element types at codegen time. That is a change to the parameter destructuring in codegen_utils.jl (possibly needing Symbolics support for typed destructuring) and is not attempted here.

Tests

Adds test/opaque_nonnumeric.jl covering symbolic read/write of a nonnumeric parameter, the rewritten remake_buffer path (numeric and nonnumeric edits), copy/==/as_any_buffer, SciMLStructures canonicalize/replace, and the type-uniformity property. These passed 16/16 as a standalone script; the committed file swaps in the non-deprecated Dict problem constructor, and I am re-running it against the committed form — if that surfaces anything I will push a correction to the branch even though the PR is closed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UuAbA8hSnrirMrJjFqxwn5

NOT FOR MERGE — snapshot of an investigation. See SciML#4831.
Delivers a large first-solve win but carries a disqualifying runtime
regression that cannot be fixed without also changing parameter codegen.

Models whose parameters include a nonnumeric entry (a DataInterpolations
spline, a callable, any struct) put that declared type into the
`MTKParameters` type. Solver specializations are keyed on the parameter
object, so two models differing only in, say, `CubicSpline` vs
`LinearInterpolation` share no compiled code and each pays a full
first-solve compilation.

This wraps the nonnumeric buffers in an `OpaqueNonnumeric{K}` (a
`Ref{Any}`; `K` is the buffer count, which the generated parameter-path
dispatch needs, with element types erased). The field stays named
`nonnumeric` and `getproperty` unwraps it, so the ~45 existing
`p.nonnumeric[i][j]` sites are untouched. The generated `remake_buffer`
can no longer promote nonnumeric element types at the type level, so those
buffers are rebuilt dynamically with an `Any` eltype — invisible to the
`MTKParameters` type, and consistent with what `as_any_buffer` and
`__remake_buffer` already do.

Compile time, two models with different nonnumeric parameter types:

                                        baseline   erased
  MTKParameters type uniform            false      true
  solve A (cold)                        4.917s     4.782s
  solve B (different nonnumeric type)   3.870s     0.015s

Runtime, same model, nonnumeric parameter read every RHS evaluation:

                     baseline   erased
  RHS ns/call        1.37       666.33
  RHS allocations    0          112 B
  warm solve         0.108ms    0.188ms

The regression is inherent to erasing at the buffer level, not specific to
this mechanism: parameters are destructured via Symbolics `DestructuredArgs`
over an argument typed `Vector{Vector{Any}}`, so the RHS reads buffers as
`p[i][j]`. Baseline got type stability for free from the concrete
`Vector{FA}`; erasing the eltype makes that read `Any`, so every nonnumeric
access inside the time loop dispatches dynamically and boxes.

Making this viable requires codegen to emit a typed access for nonnumeric
buffers, e.g. `(p[i]::Vector{FA})[j]`. `BufferTemplate` already carries
`type::TypeT`, so the index cache knows the concrete element types at
codegen time — but that is a change to the parameter destructuring in
codegen_utils.jl, possibly needing Symbolics support, and is not attempted
here.

Adds test/opaque_nonnumeric.jl covering symbolic read/write of a nonnumeric
parameter, the rewritten remake_buffer path, copy/equality/as_any_buffer,
SciMLStructures canonicalize/replace, and the type-uniformity property.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Closing immediately as intended — this is an archive, not a proposal.

Kept for the record because the measurements answer a question that would otherwise have to be rediscovered: nonnumeric buffer erasure does deliver the first-solve win (3.870s → 0.015s for a second model with a different nonnumeric type), and it cannot be shipped without typed nonnumeric access in the parameter codegen (RHS 1.37ns / 0 alloc → 666ns / 112B without it).

Design discussion continues on #4831. The branch stays on the fork; anyone picking this up should start from the codegen half, since the parameter-buffer half is done and tested here.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Verification note (closing the caveat in the description): the committed test/opaque_nonnumeric.jl, with the non-deprecated Dict problem constructor, passes 16/16 against the committed parameter_buffer.jl. No correction to the branch was needed.

Test Summary:            | Pass  Total  Time
opaque nonnumeric buffer |   16     16  3.7s

So the parameter-buffer half of this is complete and green as archived. The blocker remains the codegen half only.

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