Add precompile extensions for common ODE solvers#4360
Add precompile extensions for common ODE solvers#4360ChrisRackauckas merged 2 commits intoSciML:masterfrom
Conversation
Add package extensions for OrdinaryDiffEqDefault, OrdinaryDiffEqRosenbrock, and OrdinaryDiffEqBDF that precompile the full solve() path for standard MTK ODE problems using PrecompileTools.@compile_workload. Each extension builds a simple 1-state ODE system via mtkcompile and solves it with the respective solver (default, Rodas5P, FBDF) during precompilation. This caches the compiled native code so that the first solve() call at runtime avoids the multi-second compilation cost. This follows the pattern suggested in PR SciML#4335, where the precompile workload in ModelingToolkitBase was limited to ODEProblem construction and wrapfun_iip because it has no solver dependency. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
The initial 1-state 0-parameter precompile system produced
MTKParameters{SVector{0, Float64}, ...} which doesn't match the
MTKParameters{Vector{Float64}, ...} type that any system with
parameters uses. This meant the FunctionWrappersWrapper compiled
for the precompile system couldn't be reused for real problems.
Switch to a 2-state system with parameters (D(x) ~ a*y,
D(y) ~ -b*x) so the precompiled code covers the common case.
Verified on Lotka-Volterra (different 2-state, 4-parameter system):
solve(prob) [default]: 12.1s -> 1.05s (11.5x)
solve(prob, Rodas5P()): 3.0s -> 0.68s (4.4x)
solve(prob, FBDF()): 2.8s -> 0.10s (28x)
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Clean TTFX Benchmark (separate Julia processes,
|
| Metric | Baseline | With extensions | Speedup |
|---|---|---|---|
| System build + ODEProblem | 94.2s | 26.7s | 3.5x |
solve(prob) default |
11.8s | 1.0s | 11.6x |
solve(prob, Rodas5P()) |
3.0s | 0.55s | 5.5x |
solve(prob, FBDF()) |
2.7s | 0.10s | 27x |
| 2nd solve (runtime) | 0.4ms | 0.4ms | identical |
This works because all parameterized MTK systems share the same MTKParameters{Vector{Float64}, Vector{Float64}, Tuple{}, Tuple{}, Tuple{}, Tuple{}} type, so the FunctionWrappersWrapper code compiled for the precompile system is reused for any other parameterized system.
|
Pretty cool. What's the script used for the benchmarks? I'm curious what exactly is in this "System build" step and why it is so much faster. Is the benchmark script using exactly the system in the precompile workload? |
|
Is there an effect on |
Nope, just if the solver packages are loaded
It's re-running precompilation after other invalidations. |
|
We really need to get tests green here 😅 |
|
A bunch of stuff started failing this morning 😅 The only persistent failure is one on InterfaceII which I'm unable to reproduce, and the BVP stuff |
|
And Downstream, for which I have #4226 but it runs into something suspiciously similar to the InterfaceII failure above |
Summary
Adds package extensions that precompile the full
solve()path for standard ModelingToolkit ODE problems usingPrecompileTools.@compile_workload. This follows the pattern suggested in #4335, where the precompile workload in ModelingToolkitBase was limited toODEProblemconstruction andwrapfun_iipbecause it has no solver dependency.Three extensions are added:
MTKOrdinaryDiffEqDefaultExt— precompilessolve(prob)(default solver selection)MTKOrdinaryDiffEqRosenbrockExt— precompilessolve(prob, Rodas5P())MTKOrdinaryDiffEqBDFExt— precompilessolve(prob, FBDF())Each extension builds a 2-state ODE system with parameters (
D(x) ~ a*y, D(y) ~ -b*x) in@setup_workloadand callssolve()in@compile_workload, so the compiled native code for the full solve dispatch path is cached in the package image.Why parameters matter
MTKParametersis parameterized: a 0-parameter system usesMTKParameters{SVector{0, Float64}, ...}while any system with parameters usesMTKParameters{Vector{Float64}, ...}. Since this type flows intoFunctionWrappersWrapperat solve time, a 0-parameter precompile system produces cached code that can't be reused for any real problem with parameters. Using a system with parameters ensures the precompiled code covers the common case.TTFX Benchmark Results
Julia 1.12, measured on a different problem (Lotka-Volterra, 2-state, 4-parameter, nonlinear) than what was precompiled (simple 2-state harmonic oscillator with 2 parameters):
solve(prob)(default)solve(prob, Rodas5P())solve(prob, FBDF())Second solve calls (pure runtime) are identical at ~0.9ms for both, confirming no runtime overhead.
One-time precompilation cost
The extensions add to the initial
Pkg.precompile():MTKOrdinaryDiffEqDefaultExt: ~95sMTKOrdinaryDiffEqRosenbrockExt: ~89sMTKOrdinaryDiffEqBDFExt: ~89sThis is a one-time cost that pays for itself in the first session.
Test plan
Base.get_extensionconfirms)solve()works correctly with all three solvers (retcode=Success)