Default MTK problems to AutoDespecialize - #4919
Conversation
|
Follow-up commit dc26a82 addresses the Julia 1.10 allocation-sensitive test exposed by the first CI run.
Prior failing CI job: https://github.com/SciML/ModelingToolkit.jl/actions/runs/31363657429/job/93377585293 |
|
Architecture update: this draft will be refactored before review. The generic parameter container and function barrier are moving to SciMLBase as a reusable public API; ModelingToolkit will retain only the AutoSpecialize selection and MTK-specific interface hooks. The current MTK-owned |
|
The SciMLBase naming prerequisite is now visible as draft SciML/SciMLBase.jl#1510. The next stacked SciMLBase change will add the general |
|
Pushed the SciMLBase-backed refactor in commit |
|
Additional local validation: |
|
The first CI failures are the expected stacked-dependency boundary, not test failures: each inspected job exits during resolution because this PR requires Local validations explicitly develop that SciMLBase branch and therefore exercise the intended stack. I am leaving the compat floor intact rather than weakening it or adding a temporary non-release source to the shipping project. |
AayushSabharwal
left a comment
There was a problem hiding this comment.
Apart from these, why does this need to live in MTK? Why can't SciMLBase/DiffEqBase/whatever automatically wrap prob.p in DespecializedParams the same way it adds the FWWs?
| ::AbstractSystem, ps::Union{MTKParameters, SciMLBase.DespecializedParameters}, | ||
| args::Pair{<:Any, <:NestedGetIndex}... | ||
| ) | ||
| unwrapped_ps = _unwrap_mtk_parameters(ps) |
There was a problem hiding this comment.
This should recurse and dispatch on unwrapped_ps, otherwise the loop body is slow.
| ic = get_index_cache(sys) # this exists because the parameters are `MTKParameters` | ||
| isempty(ps.discrete) && return nothing | ||
| num_discretes = only(blocksize(ps.discrete[1])) | ||
| unwrapped_ps = _unwrap_mtk_parameters(ps) |
There was a problem hiding this comment.
Similarly, recurse on unwrapped_ps
| src_ps = parameter_values(src) | ||
| dst_ps = parameter_values(dst) | ||
| oldcache = dst_ps.caches | ||
| unwrapped_dst_ps = _unwrap_mtk_parameters(dst_ps) |
There was a problem hiding this comment.
Recurse on unwrapped_dst_ps
|
It is pretty much just in SciMLBase now. It's defined there, and all of the solver interactions are defined there. What's defined here are just things that assume you have an So if you go through it's like https://github.com/SciML/ModelingToolkit.jl/pull/4919/changes#diff-03e65df124406336ab53d3df653e5f77efb7e6dc09c13462e4d4e66f8dd61cffR194 unwrapping, https://github.com/SciML/ModelingToolkit.jl/pull/4919/changes#diff-af7ea5152025e34e41b4af797ced76c1c311b16bc1714c659050ebb3f73c6a0fR720 unwrapping, etc. Now yeah, one last thing is, is it possible for this to work with a DiffEqBase autowrap? Maybe. It would still need some unwrapping in routines here so that it's aware of |
|
Pushed the corrected architecture as a normal fast-forward in commit 2849525c78.
Post-rebase focused validation is |
|
The first completed CI failures are confirmed registry-propagation failures, not test assertions. Representative job https://github.com/SciML/ModelingToolkit.jl/actions/runs/31481133357/job/93746089848 stops in SciMLBase v3.46.0 is registered in General at JuliaRegistries/General#164118, but the fresh runner's package-server registry snapshot still ends at 3.45.0. Catalyst, NeuralPDE, and SciMLBase downstream jobs fail at this same build step. I am not weakening the required 3.46 floor. DiffEqBase 7.14 is the next stacked release prerequisite once the OrdinaryDiffEq draft is reviewed and merged. |
|
The generic solver path requested in review is now published as SciML/OrdinaryDiffEq.jl#4216. The ownership boundary on the current stack is:
The identical generic barrier test fails 3/15 assertions before the DiffEqBase implementation and passes 15/15 after it. The complete MTK discriminator passes 50/50, and the full ModelingToolkitBase SymbolicIndexingInterface group passes 64 symbolic-indexing, 1,820 SciML Problem Input, 117 MTKParameters, and 50 despecialized-parameter assertions. I have updated the PR body with these results and the final compile/runtime benchmarks. I have not resolved the existing review threads so their disposition remains explicit for the reviewer. |
|
Package-server propagation update (2026-08-11): A fresh local probe using the package server eager registry preference now resolves and installs The same probe with the default registry preference, after The default registry tarball contains versions only through 3.44, whereas the eager tarball contains 3.46. I am therefore not retrying the failed standard CI jobs yet; they would still resolve against the stale default snapshot. Registration: JuliaRegistries/General#164118 |
2849525 to
0596aa9
Compare
|
Rebase and registered-release update:
No review thread was resolved by this update. |
|
Full local docs result: the exact command completed all examples and exited 1 on the independently reproduced current-master categories Feature discrimination on the full log:
The clean-master comparison is https://github.com/SciML/ModelingToolkit.jl/actions/runs/31478304896/job/93752799681. A separate clean-master audit is reducing those failures; no baseline docs change is included here. |
|
Pushed two follow-up commits after validating against the fully registered release graph (
Local verification on the pushed tree: The sole Root QA reported 19 pass / 4 fail / 2 error: the current-master JET, ExplicitImports, public-docstring, and reexport failures tracked in #4670, plus Aqua's persistent-task deadline while its cold wrapper cache was still compiling. Aqua's identical persistent-task probe passed in a warm standalone released environment. Piracy and all public-owner checks pass. Runic, typos over added lines, and The corrected same-system Commit links: |
|
|
||
| if p isa MTKParameters | ||
| buffer = p.initials | ||
| unwrapped_p = _unwrap_mtk_parameters(p) |
There was a problem hiding this comment.
This should recurse on unwrapped_p
Compile timings: default / Rodas5P / FBDF (before vs after MTK-shaped precompile)Isolated first-solve compile, one fresh Julia 1.12.6 process per cell,
Second-solve compile is 0 in every cell. Isolated first
|
| Solve | Problem | Before | After | Ratio |
|---|---|---|---|---|
solve(prob) |
2-state ODE | 1.25 s | 0.76 s | 1.7× |
Rodas5P() |
2-state ODE | 1.14 s | 0.81 s | 1.4× |
FBDF() |
2-state ODE | 1.04 s | 0.77 s | 1.3× |
solve(prob) |
index-1 mass-matrix DAE (ODEProblem) |
18.50 s | 1.49 s | 12.4× |
Rodas5P() |
index-1 mass-matrix DAE | 9.73 s | 1.67 s | 5.8× |
FBDF() |
index-1 mass-matrix DAE | 5.88 s | 1.63 s | 3.6× |
solve(prob) |
DAEProblem |
7.45 s | 5.33 s | 1.4× |
ODEProblem / DAEProblem construction is unchanged (~15 s ODE / ~19 s DAE) and is generated f + initialization, not the solver.
Breakout: wrap / first f / init / solve!
Same systems, still isolated. wrap is DiffEqBase.get_concrete_problem. solve! is the stepper after init.
| wrap | first f |
init |
solve! |
|
|---|---|---|---|---|
after solve(ode) |
0.67 | 0.05 | 0.50 | 0.00 |
after Rodas5P(ode) |
0.63 | 0.05 | 0.18 | 0.05 |
after FBDF(ode) |
0.67 | 0.05 | 0.26 | 0.03 |
after solve(mmdae) |
1.00 | 0.05 | 0.91 | 0.00 |
after Rodas5P(mmdae) |
1.01 | 0.05 | 0.63 | 0.05 |
after FBDF(mmdae) |
1.00 | 0.08 | 0.64 | 0.03 |
after solve(dae) |
0.74 | 0.09 | 2.47 | 2.68 |
after solve(dae) + NoInit |
0.72 | 0.08 | 1.88 | 2.83 |
after DFBDF()(dae) |
0.71 | 0.08 | 2.63 | 2.98 |
after DFBDF(AutoFiniteDiff())(dae) |
0.70 | 0.08 | 2.34 | 2.78 |
before solve(ode) |
0.63 | 0.05 | 1.06 | 0.00 |
before Rodas5P(ode) |
0.63 | 0.05 | 1.00 | 0.05 |
before FBDF(ode) |
0.66 | 0.05 | 0.93 | 0.02 |
before solve(mmdae) |
1.22 | 0.05 | 4.74 | 11.45 |
before solve(dae) |
0.80 | 0.11 | 3.11 | 4.50 |
before solve(dae) + NoInit |
0.80 | 0.10 | 2.61 | 3.93 |
ODE / mass-matrix solve! is ~0. The leftover first-solve time there is this system's generated-function wrap plus OverrideInit, not the stepper.
Why DAEProblem does not compile down
It is the solver. After wrap, dae.f.f is still GeneratedFunctionWrapper{…, RGF hash, …}. DiffEqBase.promote_f only wraps ODEFunction:
wrap_path = f isa ODEFunction && isinplace(f) && !(f.f isa AbstractSciMLOperator) && …DAEFunction misses that gate, so AutoDespecialize never installs FunctionWrappersWrappers + DespecializedParameters on the residual. DFBDF's integrator / cache / solve! therefore specialize on this system's RGF hash and recompile for every new DAE.
That is also why the mass-matrix DAE improved 12× and the residual-form DAEProblem did not: mmdae is an ODEProblem, so it takes the wrap path. NoInit only drops ~0.6 s of DAE init (OverrideInit); solve! stays ~2.7 s.
solve(dae) is DFBDF(autodiff=AutoFiniteDiff()). The BDF precompile used DFBDF() (AutoForwardDiff). Both were timed; both keep the unwrapped GFW, so the autodiff mismatch is secondary.
The next compile win for residual DAEs is wrapping DAEFunction in promote_f (signature (out, du, u, p, t)), not more MTK precompile statements.
Ignore until reviewed by @ChrisRackauckas.
Why DAE
|
| Problem | spec | f after wrap |
p after wrap |
|---|---|---|---|
Handwritten DAEProblem |
AutoSpecialize |
same closure, not FWW | still Vector{Float64} |
MTK DAEProblem |
AutoDespecialize |
same GeneratedFunctionWrapper{hash} |
DespecializedParameters |
So the Newton cache built in init is still specialized on this system's residual type.
That cache is what init is compiling
DFBDF alg_cache calls build_nlsolver, which builds NLNewton + DAEResidualJacobianWrapper + FiniteDiff Jacobian prep, all parameterized by f. OrdinaryDiffEqBDF 2.4.1's own @compile_workload is FBDF on Lorenz plus a mass-matrix ODEProblem. There is no DFBDF and no DAEProblem in that workload.
Isolated init compile, DFBDF(autodiff=AutoFiniteDiff()), Julia 1.12.6, JULIA_NUM_THREADS=1:
| Problem | initializealg |
init compile |
solve! compile |
|---|---|---|---|
| Handwritten DAE | default (no initialization_data) |
1.85 s | 3.82 s |
| Handwritten DAE | NoInit |
1.76 s | 3.68 s |
| MTK DAE | default (OverrideInit) |
2.33 s | 2.69 s |
| MTK DAE | NoInit |
1.74 s | 2.72 s |
Handwritten and MTK NoInit are the same ~1.8 s. That number is DFBDF cache construction, not ModelingToolkit. The extra ~0.6 s on default MTK init is OverrideInit of the NonlinearLeastSquaresProblem (another unwrapped GFW).
Our MTK solve(dae) / DFBDF() precompile only caches one GFW hash, so the next system's alg_cache still misses. More solve(dae) statements in ModelingToolkit will not move this.
What would move it
- Wrap
DAEFunctioninDiffEqBase.promote_fwith signature(out, du, u, p, t)(same FWW +DespecializedParameterspath ODE already takes). - Add a
DFBDF/DAEProblemprecompile in OrdinaryDiffEqBDF so the Newton/Jacobian cache exists for the wrapped type.
Ignore until reviewed by @ChrisRackauckas.
Full compile-time benches (ODE / mmdae / DAE, default / Rodas5P / FBDF)Collecting the isolated numbers and the wrap /
Second-solve compile is 0 in every isolated cell. Problem construction is unchanged and is not in the solve numbers: Isolated first
|
| Solve | Problem | Before | After | Ratio |
|---|---|---|---|---|
solve(prob) |
2-state ODE | 1.25 s | 0.76 s | 1.7× |
Rodas5P() |
2-state ODE | 1.14 s | 0.81 s | 1.4× |
FBDF() |
2-state ODE | 1.04 s | 0.77 s | 1.3× |
solve(prob) |
index-1 mass-matrix DAE (ODEProblem) |
18.50 s | 1.49 s | 12.4× |
Rodas5P() |
index-1 mass-matrix DAE | 9.73 s | 1.67 s | 5.8× |
FBDF() |
index-1 mass-matrix DAE | 5.88 s | 1.63 s | 3.6× |
solve(prob) |
DAEProblem |
7.45 s | 5.33 s | 1.4× |
ODE first-solves only drop ~0.3 s because before already precompiled that stepper. The win is the mass-matrix DAE path, which before did not precompile.
Breakout: wrap / first f / init / solve!
Same systems, still isolated. wrap = DiffEqBase.get_concrete_problem. solve! is the stepper after init.
| wrap | first f |
init |
solve! |
|
|---|---|---|---|---|
after solve(ode) |
0.67 | 0.05 | 0.50 | 0.00 |
after Rodas5P(ode) |
0.63 | 0.05 | 0.18 | 0.05 |
after FBDF(ode) |
0.67 | 0.05 | 0.26 | 0.03 |
after solve(mmdae) |
1.00 | 0.05 | 0.91 | 0.00 |
after Rodas5P(mmdae) |
1.01 | 0.05 | 0.63 | 0.05 |
after FBDF(mmdae) |
1.00 | 0.08 | 0.64 | 0.03 |
after solve(dae) |
0.74 | 0.09 | 2.47 | 2.68 |
after solve(dae) + NoInit |
0.72 | 0.08 | 1.88 | 2.83 |
after DFBDF()(dae) |
0.71 | 0.08 | 2.63 | 2.98 |
after DFBDF(AutoFiniteDiff())(dae) |
0.70 | 0.08 | 2.34 | 2.78 |
before solve(ode) |
0.63 | 0.05 | 1.06 | 0.00 |
before Rodas5P(ode) |
0.63 | 0.05 | 1.00 | 0.05 |
before FBDF(ode) |
0.66 | 0.05 | 0.93 | 0.02 |
before solve(mmdae) |
1.22 | 0.05 | 4.74 | 11.45 |
before solve(dae) |
0.80 | 0.11 | 3.11 | 4.50 |
before solve(dae) + NoInit |
0.80 | 0.10 | 2.61 | 3.93 |
ODE / mass-matrix: f is wrapped to FunctionWrappersWrapper + DespecializedParameters, so solve! is ~0. Leftover first-solve time is this system's generated-function wrap plus OverrideInit, not the stepper.
DAEProblem: f is not wrapped. solve! is 2.7–4.5 s because DFBDF specializes on this system's GeneratedFunctionWrapper hash.
Same-process after (warmed; not isolated)
One process, after loading ModelingToolkitBase + the solver extensions. Later rows benefit from earlier solves. Useful as a “second system in the same session” check, not as a first-solve number.
| first compile | second compile | |
|---|---|---|
| Tsit5 ODE | 0.97 s | 0 |
| Rodas5P ODE | 0.30 s | 0 |
| Rodas5P mmdae | 1.14 s | 0 |
| FBDF ODE | 0.067 s | 0 |
| FBDF mmdae | 0.094 s | 0 |
DFBDF() DAE |
4.79 s | 0 |
solve(ode) |
0.059 s | 0 |
solve(mmdae) |
0.087 s | 0 |
solve(dae) |
2.79 s | 0 |
Split on a new system in that same process (wrap already paid): Tsit5 init 0.16 s / solve! 0; Rodas5P init 0.09 s / solve! 0; FBDF init 0.04 s / solve! 0; DFBDF() init 0.39 s / solve! 0.
Why DAE init is still ~1.8–2.3 s
Two facts, both measured:
- DiffEqBase does not wrap the DAE residual.
promote_f’s wrap gate isf isa ODEFunction. AutoDespecialize still wrapsp(DespecializedParameters).f.fis returned unchanged (wrap_f_same=true). - DFBDF
initis not precompiled in a way that transfers. OrdinaryDiffEqBDF 2.4.1’s workload is FBDF on Lorenz plus a mass-matrixODEProblem. NoDFBDF, noDAEProblem. Our MTKsolve(dae)/DFBDF()precompile only caches one GFW hash.
Isolated init of DFBDF(autodiff=AutoFiniteDiff()):
| Problem | initializealg |
init |
solve! |
|---|---|---|---|
| Handwritten DAE | default (no initialization_data) |
1.85 s | 3.82 s |
| Handwritten DAE | NoInit |
1.76 s | 3.68 s |
| MTK DAE | default (OverrideInit) |
2.33 s | 2.69 s |
| MTK DAE | NoInit |
1.74 s | 2.72 s |
Handwritten and MTK NoInit are the same ~1.8 s. That is DFBDFCache → build_nlsolver → NLNewton + DAEResidualJacobianWrapper + FiniteDiff jac prep, parameterized by this f. The extra ~0.6 s on default MTK init is OverrideInit of the NonlinearLeastSquaresProblem (another unwrapped GFW).
solve(dae) is DFBDF(autodiff=AutoFiniteDiff()). The BDF precompile used DFBDF() (AutoForwardDiff). Both were timed; both keep the unwrapped GFW, so the autodiff mismatch is secondary.
What would move the remaining numbers
- ODE / mmdae leftover (~0.75–1.7 s first solve): this system's wrap + OverrideInit. Not the stepper.
DAEProblem: wrapDAEFunctioninDiffEqBase.promote_fwith signature(out, du, u, p, t), and add aDFBDF/DAEProblemprecompile in OrdinaryDiffEqBDF. More MTKsolve(dae)statements will not moveinitorsolve!.
Ignore until reviewed by @ChrisRackauckas.
|
DAE coverage is now split into two focused OrdinaryDiffEq PRs:
I ran a two-system ModelingToolkit discriminator against this PR head with both changes developed locally. Before metadata widening it failed The test verifies that the source The separate BDF precompile benchmark reduced fresh DFBDF initialization compilation from 2.426 s to 0.394 s (about 84%). |
8e5c44a to
f16f92b
Compare
|
Review update: f16f92b now recursively processes Fail-before discriminator, using the identical new nested-wrapper test on the unfixed source: Passing-after on the rebased head: The full root initialization group also passes on the final code: Runic over every changed Julia file, typos over the full diff, and |
| merge!(defaults, Dict(v => p[k] for (k, v) in pairs(paramobj))) | ||
| elseif p isa MTKParameters | ||
| elseif p isa Union{MTKParameters, SciMLBase.DespecializedParameters} | ||
| p = _unwrap_mtk_parameters(p) |
There was a problem hiding this comment.
Should recurse on the unwrapped p
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
f16f92b to
0678b4c
Compare
|
Updated the branch for the latest review in commit 0678b4c. The earlier recursion in Failing-before / passing-after discriminator:
Local verification on Julia 1.12: ModelingToolkitBase's concrete JET test passed 54/54. Its package-wide QA reproduced the current-master result (13 passed, 2 failed, 6 errored: existing JET/ExplicitImports/reexport debt), documented at #4670 (comment) and #4958 (comment). Runic, added-line typos, and |
Ignore this draft until it has been reviewed by @ChrisRackauckas.
What changed and why
ModelingToolkit-generated problems now default to the new dynamic parameter policy,
AutoDespecialize.AutoSpecializeis unchanged. DiffEqBase performs the generic solve-time promotion toDespecializedParameters; MTK constructs ordinary concreteMTKParametersand only supplies the unwrapping needed at generated-function, symbolic-indexing, reconstruction, initialization, parameter time-series, jump, linearization, and cache boundaries.The distinction between the policies is:
AutoSpecialize: existing function-specialization behavior, unchanged by this work.AutoDespecialize: dynamicp; one solver-facing outer type can hold changing concrete parameter layouts, with a function barrier restoring the concrete value immediately before the SciMLFunction call.AutoRespecialize: the constrained, non-dynamic opaque-closure policy formerly namedAutoDePSpecialize; the old name remains a deprecated alias.FullSpecialize: fully concrete opt-out.SciMLBase owns the public marker,
DespecializedParameters, generic unwrapping, and barriers for the primary call of every built-in SciMLFunction family. DiffEqBase owns generic solve-time promotion and its precompile entry point; OrdinaryDiffEq packages precompile representative solves. MTK contains no solver wrapping or solve precompile implementation.The prerequisite implementations are merged and registered:
Failing before / passing after
The same focused test was run against the unfixed and fixed implementations. With registered SciMLBase 3.46 but without the generic DiffEqBase implementation, 3 of 15 assertions fail because the second concrete parameter layout remains solver-facing and changes problem/function types:
With the generic DiffEqBase implementation, the identical test passes:
The complete MTK discriminator passes 53/53. It covers default and opt-out policies, stable solver-facing problem types across different concrete MTK parameter-buffer layouts, generated RHS/Jacobian calls, symbolic
getp/setp, SciMLStructures canonicalize/replace/replace!, reconstruction, initialization, remake, solving, observed values, jumps, ForwardDiff, Zygote, and SciMLSensitivity.The SCC initialization path exposed a separate constructor-parameter mismatch on the
first registered-stack run. Before the fix, constructing the existing SCC test problem
errored with
MethodError: no method matching SCCNonlinearProblem{true, AutoDespecialize}(...). The identical construction against registered SciMLBase 3.46.0and DiffEqBase 7.14.0 passes after the fix and prints
REGISTERED_SCC_AUTODESPECIALIZE_PASS.The first full root
InterfaceIIrun then exposed the custom semilinear constructormarker as the only remaining unsupported specialization query. The exact added assertion
failed before with
MethodError: no method matching specialization(::Type{SemilinearODEFunction{true, AutoDespecialize}})and passes afteradding the local interface method, printing
REGISTERED_SEMILINEAR_SPECIALIZATION_PASS.The released-stack InterfaceI run exposed one non-split initialization path that rebuilt
pas a raw vector after DiffEqBase had promoted it. Before the fix, the symbolic-eventproblem had a
DespecializedParametersfunction wrapper but aVector{Float64}concreteparameter value and failed with
No matching function wrapper was found!. The sameproblem after the fix solves successfully, keeps
DespecializedParametersin the concreteproblem and solution, and locates the event at
t = 1to floating-point precision.The Julia 1.10 initialization group also showed that two existing
@test_broken @inferredchecks now succeed. On the pushed head they therefore error with
Expression evaluated to non-Boolean(660 pass / 2 error / 12 broken). Testing the inference directly instead gives660 pass / 12 broken, followed by 65/65 initial-value tests.
Verification completed
Root QA was also run on the exact stack. It reported 19 pass / 4 fail / 2 error:
the current-master JET, ExplicitImports, public-docstring, and reexport failures tracked at
#4670, plus one Aqua persistent-task
deadline while the cold wrapper cache was still compiling. Aqua's identical persistent-task
probe passed in a warm standalone released environment. Piracy and all public-owner checks
pass. No test was skipped or silenced.
The registered-release
InterfaceIgroup reached 1500 pass / 5 pre-existing broken withall 366 symbolic-event assertions passing. Its sole failure was the independently known
load-sensitive
full_equationssubprocess exceeding its fixed 180-second deadline duringconcurrent compilation. The identical subprocess passed in isolation in 10.74 seconds.
The exact full documentation command completed all examples. It contains zero SCC
AutoDespecializeconstructor errors and zero unresolvedAutoDespecializeorDespecializedParametersreferences. It exits 1 on the independently reproducedcurrent-master
docs_block,missing_docs,cross_references, andlinkcheckfailures;the clean-master comparison is
https://github.com/SciML/ModelingToolkit.jl/actions/runs/31478304896/job/93752799681.
Performance
A fresh-process, one-thread OrdinaryDiffEq benchmark used a second concrete parameter layout and a fixed 1,000-step solve:
AutoDespecializeAutoSpecializeFullSpecializeAutoDespecializereduced second-layout compile time by 94.3% versusAutoSpecialize. The deliberately tiny RHS exposes the fixed barrier overhead: warm execution was 1.88×AutoSpecialize, an additional 0.259 ms per 1,000-step solve.Generated MTK systems show the intended larger-workload behavior:
AutoDespecializeAutoSpecializeFullSpecializeAutoDespecializeAutoSpecializeFullSpecializeFor generated MTK functions, compile time is substantially lower than
FullSpecializeand approximately tied withAutoSpecialize, because generated-function compilation dominates this benchmark. Warm performance is workload-dependent; at 512 states the wrapper costs about 0.55 ms versusFullSpecialize, while the measuredAutoSpecializemedian was noisier and slower. These results support a compile-time default, not a claim that the dynamic path is runtime-free.Review points
Prior art