DiffEqBase: de-specialize p via OpaqueVoid in AutoSpecialize#3692
Draft
ChrisRackauckas-Claude wants to merge 2 commits into
Draft
DiffEqBase: de-specialize p via OpaqueVoid in AutoSpecialize#3692ChrisRackauckas-Claude wants to merge 2 commits into
ChrisRackauckas-Claude wants to merge 2 commits into
Conversation
For in-place ODEFunctions under `AutoSpecialize` / `FunctionWrapperSpecialize`,
when `p` is an `isbits` non-`NullParameters` payload the wrapping path now:
1. Builds a `DiffEqBase.OpaqueVoid{P, F}` that carries the concrete
parameter type `P` alongside the user's RHS.
2. Uses `RespecializeParams.OpaqueParams` in place of `typeof(p)` in the
`FunctionWrappersWrapper` signature(s) — same substitution applied to
the wrapped `tgrad` and Jacobian signatures when present.
3. Packs `p` into an `OpaqueParams` and returns it alongside the wrapped
`f` from `promote_f`, so `get_concrete_problem` sets `prob.p` to the
opaque container.
At call time the `OpaqueVoid` invokes a single `unsafe_load` (via
`RespecializeParams.unsafe_unpack`) to recover the concrete `P` value
before calling the user's `f`. The unpack is type-stable and allocation-free.
Net effect: problems whose underlying parameter struct types differ
(`LorenzP`, `LorenzP_f32`, …) share the same precompiled
`FunctionWrappersWrapper` / `Void` / integrator-specialization, while the
user's RHS stays fully specialized on the original concrete `P`.
`NullParameters` and non-`isbits` payloads keep the existing `Void`
wrapping path unchanged.
The ForwardDiff extension gains a matching multi-variant
`wrapfun_iip_opaque(ff, ::Type{P}, inputs, Val(CS))` so stiff/implicit
algorithms that call the wrapped RHS with `Dual` `u` (Jacobian) and/or
`Dual` `t` (tgrad) still find a matching FunctionWrapper variant. The
helper reuses the existing `_make_fww` inference shim.
`promote_f` now returns `(f, p)` rather than just `f`. The two call sites
in `get_concrete_problem` are updated accordingly; `promote_f` is
DiffEqBase-internal (no callers in extensions), so this is a private
refactor.
Tests added under `lib/DiffEqBase/test/opaque_p_test.jl` exercise the new
path via `get_concrete_problem` directly so the test stays
DiffEqBase-internal (no new integrator dep). Verified locally end-to-end
against OrdinaryDiffEqTsit5 (Val{false} path) and OrdinaryDiffEqRosenbrock
(Val{true} / FD path); wrapped RHS calls through the
`FunctionWrappersWrapper → OpaqueVoid → unsafe_unpack → user f` chain
are allocation-free.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
…king
The opaque-AutoSpecialize path packs `p` into a `RespecializeParams.OpaqueParams`
inside `get_concrete_problem` so the integrator's hot loop sees a uniform
`typeof(prob.p)` (and the wrapped `f` therefore has a uniform type across
parameter struct variants). The cost was that `sol.prob.p` then showed the
`OpaqueParams` wrapper to user-level code, breaking anything that reads
`sol.prob.p` directly (SymbolicIndexingInterface, ModelingToolkit, manual
`sol.prob.f(du, u, sol.prob.p, t)` calls, etc.).
Fix: keep the pack-internally behaviour exactly as-is (compile sharing is
preserved — integrator and wrapper still see `OpaqueParams`), but at the
end of `solve_up`, swap `sol.prob` back to the user's original problem
(or `remake(prob; u0 = u0, p = p)` when the user provided overrides).
After this change, `sol.prob.p === user_p` (identity preserved). The
opaque-wrapped `f` lives on in the integrator's machinery during the solve,
but does not leak out into `sol.prob.f`. `solve(sol.prob, alg)` round-trips
correctly (re-triggers concretization with fresh opaque packing).
Skipped when:
- sol is not an `AbstractSciMLSolution` (e.g. null-solution paths).
- The concretized `sol.prob.p` isn't an `OpaqueParams` to begin with
(non-isbits payload, FullSpecialize, etc.) — no restoration needed.
- The user explicitly passed an `OpaqueParams` as `p` — respect their
intent.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Member
|
I'm fairly skeptical of this being a good idea. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Draft PR — please ignore until reviewed by @ChrisRackauckas.
Important
Depends on SciML/RespecializeParams.jl being registered in the General registry. The new
lib/DiffEqBase/Project.toml[deps]entry points atRespecializeParams 0.1. Once registered, this PR's CI should pick it up.Summary
Inside
lib/DiffEqBase, whenpromote_fdecides to wrap an in-place ODE rhs underAutoSpecialize/FunctionWrapperSpecialize, andpis anisbitsnon-NullParameterspayload:Void(f.f)carrier is replaced with a newDiffEqBase.OpaqueVoid{P, F}that holds the concrete parameter typePas a type parameter alongside the user's RHS.FunctionWrappersWrappersignature(s) — forf.f,f.tgrad, andf.jac— getRespecializeParams.OpaqueParamssubstituted in place oftypeof(p).pis packed into anOpaqueParamsand returned alongside the wrappedffrompromote_f.get_concrete_problemthen setsprob.pto the opaque container.At call time
OpaqueVoid{P}(args...)does a singleRespecializeParams.unsafe_unpack(op, P)and forwards(du, u, p::P, t)(or the 3-arg form) to the user'sf. The unpack is type-stable and allocation-free.Net effect: two
ODEProblems whose parameter struct types differ (LorenzP_v1,LorenzP_v2, ...) now share the same precompiledFunctionWrappersWrapper/Void/ integrator specialization, sincetypeof(prob.p) === OpaqueParamsfor both. The user'sfis still fully specialized on the original concretePsincePflows throughOpaqueVoid's type parameter.NullParametersand non-isbitspayloads keep the existingVoidwrapping path unchanged.What changed
lib/DiffEqBase/Project.toml— addsRespecializeParams = 0.1to[deps]+[compat].lib/DiffEqBase/src/opaque_void.jl— new file:OpaqueVoid,should_opaque_p,pack_p_for_opaque,wrap_void_opaque,wrapfun_iip_opaque(single-variant; the FD multi-variant version lives in the extension).lib/DiffEqBase/src/DiffEqBase.jl—import RespecializeParamsandinclude("opaque_void.jl").lib/DiffEqBase/src/solve.jl— refactors bothpromote_foverloads (Val{true} = FD path, Val{false} = no-FD path) to return(f, p)instead of justf, threads the opaque branch through, and updates the twoget_concrete_problemcallers to unpack the tuple and remake withp_promote. TheSplitFunctionoverloads also return(f, p).promote_fis DiffEqBase-internal — no extensions override it — so the signature change is private.lib/DiffEqBase/ext/DiffEqBaseForwardDiffExt.jl— addswrapfun_iip_opaque(ff, ::Type{P}, inputs, Val(CS))mirroring the existing 4-variantwrapfun_iip(plain / Jacobian-Dual / time-Dual / both), reusing the_make_fwwinference shim. Stiff algorithms that call the wrapped RHS withDualufor Jacobian and/orDualtfor tgrad find a matching variant.lib/DiffEqBase/test/opaque_p_test.jl— new test, exercises the path throughget_concrete_problemdirectly (no integrator dep): isbits → opaque, FullSpecialize → no-op, NullParameters → no-op, non-isbits → no-op, two problems with differentLinearPvalues sharetypeof(prob.f)andtypeof(prob.p),unsafe_unpack(cp.p, LinearP)roundtrips.lib/DiffEqBase/test/runtests.jl— registers the new safetestset in theCoregroup.Test plan
GROUP=Core Pkg.test()onlib/DiffEqBaseis clean (8/8 new opaque tests + the rest of the Core suite unchanged).Val{false}wrapping path). Lorenz withLorenzP→ numerical result matchesFullSpecializebaseline to 1e-9;sol.prob.p isa OpaqueParams; two LorenzP problems sharetypeof(sol.prob.f).Val{true}/ ForwardDiff path). Rosenbrock23 on the same Lorenz — converges, matches baseline to 1e-8,sol.prob.p isa OpaqueParams.sol.prob.f.f(du, u, sol.prob.p, t)(i.e.FunctionWrappersWrapper → OpaqueVoid → unsafe_unpack → user f) measures@allocated == 0.Tests.ymlgreen across the matrix.Downstream.ymlgreen (this is the one I'm most curious about — it's where any breakage fromtypeof(prob.p)changing underAutoSpecializewould surface).FormatCheck.yml— Runic was applied locally on the modified files.Behavioral change worth flagging
This is on by default under AutoSpecialize (the user explicitly chose that variant in design). After
get_concrete_problemruns, the integrator seesprob.p :: RespecializeParams.OpaqueParamsinstead of the user's original struct. Downstream code that introspectssol.prob.pand expects the original concrete type will need to callRespecializeParams.unpack(sol.prob.p, P)to recover it. SymbolicIndexingInterface lookups, ModelingToolkit, sensitivity adjoints, etc. may be affected. The risk surface is real, which is why this is opening as draft for a careful review before merge.Out-of-scope for this PR (happy to add in follow-ups):
OpaqueRef-backed path for non-isbitspayloads (e.g.Vector{Float64}parameters). RespecializeParams already exposes the container; just needs the correspondingOpaqueRef-aware wrapfun variants here.pfromprobwithout explicitly knowing the payload type (e.g. an accessor that uses theobjectidtypeid for safety).promote_fis touched.