Skip to content

Add precompile extensions for common ODE solvers#4360

Merged
ChrisRackauckas merged 2 commits intoSciML:masterfrom
ChrisRackauckas-Claude:precompile-solver-extensions
Feb 27, 2026
Merged

Add precompile extensions for common ODE solvers#4360
ChrisRackauckas merged 2 commits intoSciML:masterfrom
ChrisRackauckas-Claude:precompile-solver-extensions

Conversation

@ChrisRackauckas-Claude
Copy link
Copy Markdown

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Feb 27, 2026

Summary

Adds package extensions that precompile the full solve() path for standard ModelingToolkit ODE problems using PrecompileTools.@compile_workload. This follows the pattern suggested in #4335, where the precompile workload in ModelingToolkitBase was limited to ODEProblem construction and wrapfun_iip because it has no solver dependency.

Three extensions are added:

  • MTKOrdinaryDiffEqDefaultExt — precompiles solve(prob) (default solver selection)
  • MTKOrdinaryDiffEqRosenbrockExt — precompiles solve(prob, Rodas5P())
  • MTKOrdinaryDiffEqBDFExt — precompiles solve(prob, FBDF())

Each extension builds a 2-state ODE system with parameters (D(x) ~ a*y, D(y) ~ -b*x) in @setup_workload and calls solve() in @compile_workload, so the compiled native code for the full solve dispatch path is cached in the package image.

Why parameters matter

MTKParameters is parameterized: a 0-parameter system uses MTKParameters{SVector{0, Float64}, ...} while any system with parameters uses MTKParameters{Vector{Float64}, ...}. Since this type flows into FunctionWrappersWrapper at 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):

Metric Baseline (no extensions) With extensions Speedup
Package load 12.1s 12.9s ~same
System build + ODEProblem 95.6s 27.3s 3.5x
solve(prob) (default) 12.1s 1.05s 11.5x
solve(prob, Rodas5P()) 2.96s 0.68s 4.4x
solve(prob, FBDF()) 2.83s 0.10s 28x
Total TTFX (load+build+solve) 119.8s 41.3s 2.9x

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: ~95s
  • MTKOrdinaryDiffEqRosenbrockExt: ~89s
  • MTKOrdinaryDiffEqBDFExt: ~89s

This is a one-time cost that pays for itself in the first session.

Test plan

  • All three extensions precompile successfully
  • Extensions load correctly (Base.get_extension confirms)
  • solve() works correctly with all three solvers (retcode=Success)
  • TTFX verified on a different problem than precompiled (Lotka-Volterra)
  • CI tests pass

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>
@ChrisRackauckas-Claude
Copy link
Copy Markdown
Author

Clean TTFX Benchmark (separate Julia processes, --startup-file=no)

Benchmark problem: Lotka-Volterra (2-state, 4 parameters, nonlinear) — structurally different from the precompiled harmonic oscillator (2-state, 2 parameters, linear).

Each measured in a separate Julia process (PIDs 1207000 vs 1207387), Julia 1.12.4, --startup-file=no:

=== BASELINE (registered MTK, no extensions) — PID 1207387 ===
Package load:           11.17s
System build:           94.168s
TTFX (default solver):  11.797s
TTFX (Rodas5P):         3.019s
TTFX (FBDF):            2.719s

=== WITH EXTENSIONS (dev MTK) — PID 1207000 ===
Package load:           12.67s
System build:           26.727s
TTFX (default solver):  1.017s
TTFX (Rodas5P):         0.55s
TTFX (FBDF):            0.102s
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.

@AayushSabharwal
Copy link
Copy Markdown
Member

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?

@AayushSabharwal
Copy link
Copy Markdown
Member

Is there an effect on using ModelingToolkit time if none of the solver packages are loaded?

@ChrisRackauckas
Copy link
Copy Markdown
Member

Is there an effect on using ModelingToolkit time if none of the solver packages are loaded?

Nope, just if the solver packages are loaded

why it is so much faster

It's re-running precompilation after other invalidations.

@ChrisRackauckas
Copy link
Copy Markdown
Member

We really need to get tests green here 😅

@ChrisRackauckas ChrisRackauckas merged commit dadbce4 into SciML:master Feb 27, 2026
27 of 68 checks passed
@AayushSabharwal
Copy link
Copy Markdown
Member

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

@AayushSabharwal
Copy link
Copy Markdown
Member

And Downstream, for which I have #4226 but it runs into something suspiciously similar to the InterfaceII failure above

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.

3 participants