Skip to content

Don't embed initial-guess object types in collocation caches (fix 20x compile-time regression)#511

Merged
ChrisRackauckas merged 3 commits into
SciML:masterfrom
ChrisRackauckas-Claude:fix-issue-500-solution-u0-specialization
Jun 10, 2026
Merged

Don't embed initial-guess object types in collocation caches (fix 20x compile-time regression)#511
ChrisRackauckas merged 3 commits into
SciML:masterfrom
ChrisRackauckas-Claude:fix-issue-500-solution-u0-specialization

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor

Note

This PR should be ignored until reviewed by @ChrisRackauckas.

Fixes #500.

Problem

Since the SciMLBase v3 / RecursiveArrayTools v4 ecosystem update (BoundaryValueDiffEq v5.23.0), solving a BVP whose u0 is a previous solution (the standard warm-start pattern via remake(prob, u0 = sol)) became ~20x slower, with @time reporting ~100% compilation on every fresh session.

Root cause

All collocation __init methods stored the problem in the cache via

prob_ = !(prob.u0 isa AbstractArray) ? remake(prob; u0 = u0) : prob

Under RAT v3 / SciMLBase v2, ODESolution was not an AbstractArray, so the cache got a problem remade with the extracted plain-vector u0. Under RAT v4, AbstractVectorOfArray <: AbstractArray (and ODESolution <: AbstractVectorOfArray), so the gate flipped: the full solution type stayed embedded in cache.prob. Measured with the issue's MRE, typeof(cache.prob.u0) went from Vector{Float64} (15 chars) to the entire previous solution type (~79,000 chars, which itself nests the previous solve's whole cache type). Every loss closure, DI jacobian preparation, and NonlinearSolve specialization then compiles against that type.

Fix

Gate on AbstractVectorOfArray explicitly in MIRK, FIRK (expand + nested), and MIRKN, so initial-guess objects (previous solutions, VectorOfArray/DiffEqArray, functions) are always stripped to the extracted u0 vector — i.e. restore the exact RAT v3-era semantics. (Ascher never had the remake and is unchanged; it predates this regression.)

Timings (issue MRE, Julia 1.12.6, second solve with solution-object u0, fresh session)

configuration second solve
BoundaryValueDiffEq v5.21.0 (last good) 20.2 s
BoundaryValueDiffEqMIRK v1.17.0 released 272.9 s (100% compilation)
this patch 18.0 s

Identical numerical results in all configurations (sol2.u[1] = [100.0, 3.6e-17, 0.000428...]).

Tests

  • New regression testsets in MIRK and FIRK basic tests assert init(prob, alg, ...).prob.u0 isa Vector{Float64} when u0 is an ODESolution or VectorOfArray, plus successful solves. These are deterministic type checks, not timing tests.
  • Includes a separate one-line commit fixing const g = 9.81 inside the MIRK "Compatibility with StaticArrays" @testset — a local-scope const, which is a lowering error that aborted inclusion of mirk_basic_tests.jl at that expression, silently skipping that testset and everything after it. Latent on master; surfaced because the new regression test (appended after it) never ran until it was fixed. (Sublibrary CI has been path-filtering around these tests.)

Local verification (Julia 1.12.6)

  • Pkg.test("BoundaryValueDiffEqMIRK"): all green — Basic 200/200 (incl. previously-dark testsets after the const fix), NLLS 72/72, Ensemble 10/10, AD 9/9, Singular 8/8, VectorOfVector 2/2, DynOpt 2/2, QA 11/11.
  • Pkg.test("BoundaryValueDiffEqMIRKN"): all green (25/25 + QA 11/11).
  • Pkg.test("BoundaryValueDiffEqFIRK"): new regression testset 4/4; suite shows 2 fail + 3 "Unexpected Pass" in pre-existing "Convergence on Linear" stage-4/5 checks for problems 9/10 — dependency-stack drift unrelated to this PR (these run a plain Vector{Float64} u0 path this PR provably does not touch); documented in Misc test group red on master: BigFloat BVP solves stall — regression from LinearSolve v3.85 nonstructural-zeros reduction #509 (comment).
  • Runic run on all changed files.

Process notes

Investigation: reproduced the issue MRE against pinned v5.21.0 / v5.23.0 environments, measured typeof(cache.prob.u0) string lengths to locate the type-nesting blowup, confirmed ODESolution <: AbstractArray flipped false→true across the RAT v3→v4 boundary while <: AbstractVectorOfArray is true on both, then patched the four gate sites and re-verified the MRE end-to-end. Pre-existing master CI failures found along the way (Misc/Wrappers/format/Downgrade groups) were handled separately: PRs #507, #508, #510 and issues #509 + SciML/LinearSolve.jl#1027.

🤖 Generated with Claude Code

`const g = 9.81` inside a `@testset` block is a local-scope `const`,
which is a lowering error on all supported Julia versions. The error
aborts inclusion of mirk_basic_tests.jl at that expression, so this
testset and everything after it never ran (the sublibrary CI has been
path-filtered around it since the test reorganization).

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Fixes the compile-time regression reported in SciML#500: a solve whose u0 is
a previous solution (or any AbstractVectorOfArray) took ~20x longer than
v5.21.0, with @time reporting ~100% compilation on every fresh session.

Under RecursiveArrayTools v4 (SciMLBase v3 stack),
`AbstractVectorOfArray <: AbstractArray`, so the
`!(prob.u0 isa AbstractArray)` gate in `SciMLBase.__init` stopped
remaking the problem with the extracted plain-vector u0. The full
ODESolution type (~80k characters, including the previous solve's
entire cache type) stayed embedded in `cache.prob`, and every closure,
DI jacobian preparation, and NonlinearSolve specialization compiled
against it.

Gate on `AbstractVectorOfArray` explicitly in MIRK, FIRK (expand +
nested), and MIRKN so initial-guess objects are always stripped to the
extracted u0 vector, restoring the RAT v3-era behavior.

Timings for the issue's MRE (Julia 1.12.6, second solve with
solution-object u0, fresh session):
- BoundaryValueDiffEqMIRK 1.17.0 released: 272.9 s (100% compilation)
- with this patch:                          18.0 s
- BoundaryValueDiffEq 5.21.0 baseline:      20.2 s

Adds regression tests asserting `cache.prob.u0 isa Vector{Float64}`
when u0 is an ODESolution or VectorOfArray.

Fixes SciML#500

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1.11)

Time benchmarks
master 445146b... master / 445146b...
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK2() 0.538 ± 0.0055 s 0.544 ± 0.0092 s 0.99 ± 0.02
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK3() 11.2 ± 0.24 ms 11.4 ± 0.28 ms 0.982 ± 0.032
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK4() 2.42 ± 0.064 ms 2.45 ± 0.061 ms 0.99 ± 0.036
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK5() 2.93 ± 0.068 ms 2.98 ± 0.099 ms 0.985 ± 0.04
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK6() 1.43 ± 0.034 ms 1.45 ± 0.05 ms 0.987 ± 0.042
Simple Pendulum/IIP/MultipleShooting(10, Tsit5; grid_coarsening = false) 1.41 ± 0.037 ms 1.41 ± 0.036 ms 1 ± 0.037
Simple Pendulum/IIP/MultipleShooting(10, Tsit5; grid_coarsening = true) 2.87 ± 0.056 ms 2.88 ± 0.068 ms 0.995 ± 0.03
Simple Pendulum/IIP/MultipleShooting(100, Tsit5; grid_coarsening = false) 0.0351 ± 0.002 s 0.036 ± 0.0018 s 0.974 ± 0.075
Simple Pendulum/IIP/MultipleShooting(100, Tsit5; grid_coarsening = true) 0.0539 ± 0.00073 s 0.0549 ± 0.0019 s 0.981 ± 0.036
Simple Pendulum/IIP/Shooting(Tsit5()) 0.159 ± 0.085 ms 0.161 ± 0.085 ms 0.992 ± 0.75
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK2() 0.666 ± 0.041 s 0.695 ± 0.052 s 0.958 ± 0.092
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK3() 16.7 ± 3.1 ms 15.3 ± 3.2 ms 1.09 ± 0.31
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK4() 2.94 ± 0.15 ms 2.99 ± 0.13 ms 0.981 ± 0.067
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK5() 3.52 ± 0.13 ms 3.56 ± 0.12 ms 0.988 ± 0.05
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK6() 1.69 ± 0.072 ms 1.69 ± 0.079 ms 1 ± 0.063
Simple Pendulum/OOP/MultipleShooting(10, Tsit5; grid_coarsening = false) 3.02 ± 0.64 ms 2.95 ± 0.28 ms 1.03 ± 0.24
Simple Pendulum/OOP/MultipleShooting(10, Tsit5; grid_coarsening = true) 5.81 ± 4.6 ms 6.03 ± 5.1 ms 0.964 ± 1.1
Simple Pendulum/OOP/MultipleShooting(100, Tsit5; grid_coarsening = false) 0.0749 ± 0.0015 s 0.0776 ± 0.0031 s 0.966 ± 0.043
Simple Pendulum/OOP/MultipleShooting(100, Tsit5; grid_coarsening = true) 0.114 ± 0.0037 s 0.12 ± 0.0052 s 0.95 ± 0.051
Simple Pendulum/OOP/Shooting(Tsit5()) 0.543 ± 0.016 ms 0.555 ± 0.028 ms 0.978 ± 0.058
time_to_load 6.26 ± 0.071 s 6.4 ± 0.032 s 0.978 ± 0.012
Memory benchmarks
master 445146b... master / 445146b...
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK2() 0.389 M allocs: 0.0442 GB 0.389 M allocs: 0.0442 GB 1
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK3() 0.0436 M allocs: 4.85 MB 0.0436 M allocs: 4.85 MB 1
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK4() 15.7 k allocs: 1.63 MB 15.7 k allocs: 1.63 MB 1
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK5() 22.1 k allocs: 2.02 MB 22.1 k allocs: 2.02 MB 1
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK6() 12.8 k allocs: 1.04 MB 12.8 k allocs: 1.04 MB 1
Simple Pendulum/IIP/MultipleShooting(10, Tsit5; grid_coarsening = false) 25.5 k allocs: 1.82 MB 25.5 k allocs: 1.82 MB 1
Simple Pendulum/IIP/MultipleShooting(10, Tsit5; grid_coarsening = true) 0.049 M allocs: 3.38 MB 0.049 M allocs: 3.38 MB 1
Simple Pendulum/IIP/MultipleShooting(100, Tsit5; grid_coarsening = false) 0.553 M allocs: 0.0535 GB 0.553 M allocs: 0.0535 GB 1
Simple Pendulum/IIP/MultipleShooting(100, Tsit5; grid_coarsening = true) 0.833 M allocs: 0.0778 GB 0.833 M allocs: 0.0778 GB 1
Simple Pendulum/IIP/Shooting(Tsit5()) 4.62 k allocs: 0.223 MB 4.62 k allocs: 0.223 MB 1
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK2() 0.89 M allocs: 0.984 GB 0.89 M allocs: 0.984 GB 1
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK3() 0.0932 M allocs: 24.7 MB 0.0932 M allocs: 24.7 MB 1
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK4() 0.0324 M allocs: 3.94 MB 0.0324 M allocs: 3.94 MB 1
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK5() 0.045 M allocs: 4.97 MB 0.045 M allocs: 4.97 MB 1
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK6() 25.3 k allocs: 2.16 MB 25.3 k allocs: 2.16 MB 1
Simple Pendulum/OOP/MultipleShooting(10, Tsit5; grid_coarsening = false) 0.142 M allocs: 10.2 MB 0.142 M allocs: 10.2 MB 1
Simple Pendulum/OOP/MultipleShooting(10, Tsit5; grid_coarsening = true) 0.266 M allocs: 18.7 MB 0.266 M allocs: 18.7 MB 1
Simple Pendulum/OOP/MultipleShooting(100, Tsit5; grid_coarsening = false) 2.52 M allocs: 0.279 GB 2.52 M allocs: 0.279 GB 1
Simple Pendulum/OOP/MultipleShooting(100, Tsit5; grid_coarsening = true) 3.82 M allocs: 0.404 GB 3.82 M allocs: 0.404 GB 1
Simple Pendulum/OOP/Shooting(Tsit5()) 0.0373 M allocs: 1.69 MB 0.0373 M allocs: 1.69 MB 1
time_to_load 0.161 k allocs: 11.4 kB 0.161 k allocs: 11.4 kB 1

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

CI triage (first run, 27257879703 et al.)

This PR is the first to exercise the sublibrary CI matrix since the grouped-tests reorganization (master pushes were path-filtered, sublibraries / detect → everything skipped), so it surfaced several issues that are not from this PR's diff:

1. Self-hosted runner outage (~07:15–07:40 UTC): Runic, Spell Check, MIRK Core (julia 1/pre), MIRKN Core (lts/1), FIRK QA (julia 1) etc. all failed with "The self-hosted runner lost communication with the server" (demeter4-*/arctic1-* fleet), uniform ~10 min duration, zero failed steps, no logs. Pure infrastructure; needs a re-run (I don't have rerun rights on this repo — will retrigger with an empty commit once the in-flight jobs finish).

2. Coverage step bug in the shared workflows (all sublibrary jobs): jobs that did run die at the julia-processcoverage step with directory "lib/BoundaryValueDiffEqMIRK/ext" not found! because sublibrary-project-tests.yml passes <lib>/src,<lib>/ext and none of the BVDE sublibraries have ext/. The QA (lts) job's test step passed before the coverage step killed it. Fix opened: SciML/.github#85 (needs the v1 floating tag moved after merge).

3. Real test results that did complete:

Local ground truth for this PR (Julia 1.12.6): full Pkg.test green for MIRK (200/200 basic + all groups + QA 11/11) and MIRKN; FIRK green except the documented pre-existing drift. MRE timing: 272.9 s → 18.0 s for the second solve.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review June 10, 2026 11:17
@ChrisRackauckas ChrisRackauckas merged commit 2a7aefb into SciML:master Jun 10, 2026
13 of 42 checks passed
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.

BoundaryValueDiffEq v5.23.0 ruins execution speed of collocation solvers

2 participants