Skip to content

Fix MultipleShooting caches for tagged AD states - #541

Closed
ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-shooting-dual-odecache
Closed

Fix MultipleShooting caches for tagged AD states#541
ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-shooting-dual-odecache

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Jul 10, 2026

Copy link
Copy Markdown
Member

Ignore this PR until it has been reviewed by @ChrisRackauckas.

Summary

  • lazily initialize and reuse MultipleShooting ODE integrator caches for each nonlinear-state element type and shooting-grid size
  • keep the BVP time type unchanged when ForwardDiff introduces tagged state values
  • cover the previously untested serial execution path and bump BoundaryValueDiffEqShooting to 1.17.4

Cause

MultipleShooting hoists ODE integrator allocation out of residual evaluations. The default nonlinear polyalgorithm can evaluate that residual with a new tagged ForwardDiff.Dual element type when it falls back to its line-search method. Reinitializing the original Float64 ODE cache with those states throws Float64(::ForwardDiff.Dual).

The existing DifferentiationInterface Jacobian caches continue using their preallocated matching ODE caches. The new fallback is only used when another caller introduces a different state element type, and it reuses the resulting cache after the first evaluation.

This is separate from #571, which only adjusts the LinearSolve test compatibility range.

Verification

  • Exact downgrade reproducer before the fix:
    • EnsembleThreads: Float64(::ForwardDiff.Dual{ForwardDiff.Tag{NonlinearFunction...}})
    • EnsembleSerial: the same conversion error
  • Exact downgrade after the fix, Julia 1.10.11:
    • BOUNDARYVALUEDIFFEQ_TEST_GROUP=Core: Basic 84/84, NLLS 56/56, Orbital 40/40
  • Latest compatible dependencies, Julia 1.12.6:
    • BOUNDARYVALUEDIFFEQ_TEST_GROUP=All: Basic 84/84, NLLS 56/56, Orbital 40/40
    • BOUNDARYVALUEDIFFEQ_TEST_GROUP=QA: 19/19
  • The earlier implementation's benchmark run found unchanged allocation counts and statistically neutral MultipleShooting timings; this narrower implementation will be rechecked by fresh CI.
  • whole-repository Runic check: passed

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1.11)

Time benchmarks
master 5ca641c... master / 5ca641c...
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK2() 0.652 ± 0.016 s 0.656 ± 0.011 s 0.994 ± 0.029
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK3() 13.2 ± 0.37 ms 13.2 ± 0.33 ms 0.996 ± 0.037
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK4() 2.82 ± 0.11 ms 2.82 ± 0.1 ms 1 ± 0.053
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK5() 3.39 ± 0.094 ms 3.41 ± 0.11 ms 0.993 ± 0.043
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK6() 1.62 ± 0.047 ms 1.63 ± 0.067 ms 0.998 ± 0.05
Simple Pendulum/IIP/MultipleShooting(10, Tsit5; grid_coarsening = false) 1.57 ± 0.078 ms 1.58 ± 0.088 ms 0.993 ± 0.074
Simple Pendulum/IIP/MultipleShooting(10, Tsit5; grid_coarsening = true) 3.19 ± 0.17 ms 3.22 ± 0.16 ms 0.99 ± 0.074
Simple Pendulum/IIP/MultipleShooting(100, Tsit5; grid_coarsening = false) 0.0417 ± 0.004 s 0.0416 ± 0.0038 s 1 ± 0.13
Simple Pendulum/IIP/MultipleShooting(100, Tsit5; grid_coarsening = true) 0.0657 ± 0.004 s 0.0646 ± 0.0013 s 1.02 ± 0.066
Simple Pendulum/IIP/Shooting(Tsit5()) 0.183 ± 0.084 ms 0.186 ± 0.086 ms 0.983 ± 0.64
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK2() 0.861 ± 0.0051 s 0.859 ± 0.0069 s 1 ± 0.01
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK3() 17.2 ± 6.2 ms 17.4 ± 6.1 ms 0.987 ± 0.5
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK4() 3.47 ± 0.16 ms 3.51 ± 0.14 ms 0.99 ± 0.061
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK5() 4.27 ± 0.23 ms 4.23 ± 0.26 ms 1.01 ± 0.084
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK6() 2 ± 0.14 ms 1.99 ± 0.15 ms 1.01 ± 0.1
Simple Pendulum/OOP/MultipleShooting(10, Tsit5; grid_coarsening = false) 3.66 ± 1.1 ms 3.6 ± 0.6 ms 1.02 ± 0.35
Simple Pendulum/OOP/MultipleShooting(10, Tsit5; grid_coarsening = true) 7.02 ± 6.3 ms 6.9 ± 6.3 ms 1.02 ± 1.3
Simple Pendulum/OOP/MultipleShooting(100, Tsit5; grid_coarsening = false) 0.0976 ± 0.0033 s 0.0991 ± 0.0039 s 0.985 ± 0.051
Simple Pendulum/OOP/MultipleShooting(100, Tsit5; grid_coarsening = true) 0.16 ± 0.01 s 0.153 ± 0.0073 s 1.04 ± 0.082
Simple Pendulum/OOP/Shooting(Tsit5()) 0.636 ± 0.061 ms 0.637 ± 0.08 ms 0.999 ± 0.16
time_to_load 7.72 ± 0.022 s 7.67 ± 0.026 s 1.01 ± 0.0044
Memory benchmarks
master 5ca641c... master / 5ca641c...
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK2() 0.389 M allocs: 0.0443 GB 0.389 M allocs: 0.0443 GB 1
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK3() 0.0436 M allocs: 4.88 MB 0.0436 M allocs: 4.88 MB 1
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK4() 15.7 k allocs: 1.65 MB 15.7 k allocs: 1.65 MB 1
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK5() 22.1 k allocs: 2.04 MB 22.1 k allocs: 2.04 MB 1
Simple Pendulum/IIP/BoundaryValueDiffEqMIRK.MIRK6() 12.8 k allocs: 1.05 MB 12.8 k allocs: 1.05 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.39 MB 0.049 M allocs: 3.39 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.64 k allocs: 0.224 MB 4.64 k allocs: 0.224 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.8 MB 0.0932 M allocs: 24.8 MB 1
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK4() 0.0324 M allocs: 3.96 MB 0.0324 M allocs: 3.96 MB 1
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK5() 0.045 M allocs: 4.99 MB 0.045 M allocs: 4.99 MB 1
Simple Pendulum/OOP/BoundaryValueDiffEqMIRK.MIRK6() 25.3 k allocs: 2.17 MB 25.3 k allocs: 2.17 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.159 k allocs: 11.2 kB 0.159 k allocs: 11.2 kB 1

Copy link
Copy Markdown
Member Author

Rebased the branch onto current origin/master (5e9eb4ef) after auditing the fork branch and confirming its sole remote commit was ours. The lease-protected update rewrote the head to cc238124.

Post-rebase local verification:

  • BOUNDARYVALUEDIFFEQ_TEST_GROUP=Core julia +1.10 --project=lib/BoundaryValueDiffEqShooting -e 'using Pkg; Pkg.test(; allow_reresolve=false)'
  • Shooting Basic Problems: 82/82 passed
  • Shooting NLLS: 56/56 passed
  • Shooting Orbital: 40/40 passed
  • Exact manifest SHA-256 remained 8288d7401aa75eef68222d71b4a2ddc843f47d6115ee2a4f4b2602fb877b63e4
  • Repository-wide Runic check: passed

No test was skipped, disabled, or weakened.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Post-timer CI classification:

  • The targeted BoundaryValueDiffEqShooting downgrade job passed in 17m25s.
  • Shooting Core passed on current, LTS, and pre-release Julia; Shooting QA also passed.
  • The MIRK downgrade failure is present on clean master in run 29706074896, job 88243213843. The warning/error signatures were also reproduced locally on clean master and are tracked upstream in Optimization.jl #1269 and #1270.
  • The FIRK job and ModelingToolkit downstream job are runner losses: each workflow run is marked complete while its test step remains in_progress. FIRK passed on the cited clean-master downgrade run.
  • The benchmark job failed immediately inside the AirspeedVelocity action before package benchmarks ran.

The Shooting compat change has no failing targeted lane.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Fresh validation (2026-07-26): clean master f41609e reproduces the original Basic Shooting Float64(::ForwardDiff.Dual) error (54 pass, 1 error). Applying this exact source diff on that clean base with the exact all-deps resolver passes the full Shooting Core group: Basic 82/82, NLLS 56/56, Orbital 40/40. Runic --check . and git diff --check also pass. The regression bisects to fe630c8.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude
ChrisRackauckas-Claude force-pushed the fix-shooting-dual-odecache branch from cc23812 to 5ca641c Compare July 27, 2026 22:48
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Cache Shooting ODE integrators by eltype Fix MultipleShooting caches for tagged AD states Jul 27, 2026

Copy link
Copy Markdown
Member Author

Refreshed this existing draft rather than opening another duplicate.

Before the lease-protected update, I fetched the fork and audited the PR branch. Its only branch-only commit remained cc238124, authored by ChrisRackauckas-Claude; there were no new commits or contributors. I then replaced it with 5ca641c9 using an exact SHA-bound force-with-lease.

The revised implementation removes the earlier Dual conversion of prob.tspan and the related solution-time container changes. Dual state values do not require Dual time values, and retaining the BVP's original time type is safer for dimensionful or otherwise distinct time types. It also adds a serial out-of-place regression test and the Shooting patch-version bump.

Fresh local verification on 5ca641c9:

  • exact downgraded dependencies, Julia 1.10.11, Shooting Core: Basic 84/84, NLLS 56/56, Orbital 40/40
  • latest compatible dependencies, Julia 1.12.6, Shooting All/Core: Basic 84/84, NLLS 56/56, Orbital 40/40
  • latest compatible dependencies, Julia 1.12.6, Shooting QA: 19/19
  • whole-repository Runic check and git diff --check: passed

Bisect correction: the earlier comment's fe630c86 shorthand was not a valid first-bad result. In a formal bisect with the exact downgrade resolver, fe630c86, 906ff..., 74d108..., and dcfdd... are good; 086c4ccc is bad. Its predecessor a5af442d cannot reach the target test because its own compat resolves BoundaryValueDiffEqCore 2.3.0 and fails earlier during precompilation. Git therefore reports a5af442d or 086c4ccc as the possible first bad commit, with 086c4ccc the first directly observable tagged-cache failure. The latent Float64-cache reuse itself dates to the earlier cache-hoisting implementation (b07b168c).

Copy link
Copy Markdown
Member Author

Fresh-CI audit for head 5ca641c90ef57f7a6586aa2fac3425066bf70c46 (2026-07-27 18:52 EDT):

Relevant active runs:

The 1-hour CI timer is set for 19:49:50 EDT; monitoring is being handed back to the root task.

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.

2 participants