[NOT FOR MERGE — archived prototype] Erase nonnumeric buffer element types from MTKParameters - #4833
Conversation
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>
|
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. |
|
Verification note (closing the caveat in the description): the committed So the parameter-buffer half of this is complete and green as archived. The blocker remains the codegen half only. |
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
MTKParameterstype. Solver specializations are keyed on the parameter object, so two models differing only inCubicSplinevsLinearInterpolationshare 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}— aRef{Any}, withK(buffer count) kept in the type because the generated parameter-path dispatch needs it, and element types erased. The field stays namednonnumericandgetpropertyunwraps, so the ~45 existingp.nonnumeric[i][j]sites are untouched. One file plus tests.Compile time (two models, different nonnumeric parameter types, Tsit5)
MTKParameterstype uniformRuntime (same model; nonnumeric parameter read on every RHS evaluation)
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
DestructuredArgsover an argument typedVector{Vector{Any}}, so the RHS reads buffers asp[i][j]. Baseline gets type stability for free from the concreteVector{FA}; erasing the element type makes that readAny, so every nonnumeric access inside the time loop dispatches dynamically and boxes. This is inherent to erasing at the buffer level — storingVector{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].BufferTemplatealready carriestype::TypeT, so the index cache knows the concrete element types at codegen time. That is a change to the parameter destructuring incodegen_utils.jl(possibly needing Symbolics support for typed destructuring) and is not attempted here.Tests
Adds
test/opaque_nonnumeric.jlcovering symbolic read/write of a nonnumeric parameter, the rewrittenremake_bufferpath (numeric and nonnumeric edits),copy/==/as_any_buffer,SciMLStructurescanonicalize/replace, and the type-uniformity property. These passed 16/16 as a standalone script; the committed file swaps in the non-deprecatedDictproblem 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