@@ -17,42 +17,37 @@ using OptimizationBase.FastClosures
1717
1818# --- Dual-tolerant gradient dispatch ------------------------------------------------------
1919# A DI preparation is monomorphic: built once at the construction types (`x`, `p`), its
20- # internal buffers are concrete (e.g. `Float64`) and reject inputs of any other type. That is
21- # correct and fast for the optimization solve, but a downstream sensitivity layer (e.g.
22- # SciMLSensitivity's `OptimizationAdjoint`) differentiates the KKT stationarity conditions
23- # w.r.t. the parameters by pushing `ForwardDiff.Dual`s through the gradient: a dual `p` (and a
24- # dual output buffer) evaluated at a real `θ = x*`. Those duals hit the prepared buffers and
25- # throw `DifferentiationInterface.PreparationMismatchError`.
20+ # internal buffers are concrete (e.g. `Float64`) and reject `ForwardDiff.Dual`s. That is correct
21+ # and fast for the solve, but a downstream sensitivity layer (e.g. SciMLSensitivity's
22+ # `OptimizationAdjoint`) differentiates the KKT conditions w.r.t. the parameters by pushing duals
23+ # through `grad`/`cons_j` — a dual `p` at a real `θ = x*`. Those duals hit the prepared buffers
24+ # and throw `DifferentiationInterface.PreparationMismatchError`.
2625#
27- # To support that without slowing the solve, the gradient closures keep the prepared fast path
28- # for the prepared type and fall back to a prep-free DI call (which prepares at the actual
29- # argument types each call) for anything else. The fallback cost is irrelevant off the
30- # optimization hot loop — sensitivity does one such call per solve, not per iteration.
31- # Scalar eltype of `p`, or `nothing` when `p` has no scalar to match: NullParameters/nothing,
32- # and structured `p` (tuple/array-of-arrays) whose `eltype` is not a `Number`. Only a numeric
33- # `p` returns a type that can veto the fast path.
34- function _grad_param_eltype (p)
35- p isa Union{SciMLBase. NullParameters, Nothing} && return nothing
36- pe = eltype (p)
37- return pe <: Number ? pe : nothing
26+ # So the closures keep the prepared fast path whenever the call carries no dual, and fall back to
27+ # a prep-free DI call (which prepares at the actual argument types) when one does. Dual-detection
28+ # reuses SciMLBase's `anyeltypedual` (`== Any` ⇒ no dual, the same convention `promote_u0` uses),
29+ # which recurses into a structured `p`, so a dual nested in a tuple/ComponentArray is caught too.
30+ # A *real* `p` of any eltype (Int, Float32, …) keeps the fast path: `p` enters as a Constant, so
31+ # its eltype never invalidates the prepared gradient.
32+ @inline function _use_prep (θ, p)
33+ return SciMLBase. anyeltypedual (θ) == Any && SciMLBase. anyeltypedual (p) == Any
3834end
3935
40- # True when every float-bearing input matches the prepared element type `T0`, so the prepared
41- # path is valid. A dual `θ` or dual numeric `p` (the sensitivity case) flunks this and routes to
42- # the prep-free fallback; `nothing`-eltype `p` never vetoes.
43- @inline function _grad_use_prep (:: Type{T0} , θ, p) where {T0}
44- pe = _grad_param_eltype (p)
45- return eltype (θ) === T0 && (pe === nothing || pe === T0)
46- end
47-
48- # Output-buffer eltype for the `p`-accepting constraint wrapper. Promote against `eltype(p)` only
49- # when it is a `Number` (so a dual `p` propagates in the sensitivity case); a structured `p` has
50- # a non-`Number` eltype that would promote to `Union{}`, so fall back to `eltype(x)` there.
36+ # Output-buffer eltype for the `p`-accepting constraint wrapper: the type `f.cons` produces,
37+ # including the *nested* dual when both `x` (DI's seeds) and `p` (the sensitivity layer) carry
38+ # duals with different tags (`promote_op(+, …)` nests them). Deliberately uses plain `eltype(p)`
39+ # rather than `SciMLBase.anyeltypedual`: this runs *inside* the DI-differentiated wrapper, and
40+ # Enzyme's forward mode corrupts the derivative shadow (DataType-valued entries) when the
41+ # allocation type flows through `anyeltypedual` — in either its value or its type-level form,
42+ # and even with an `EnzymeRules.inactive` mark on this helper. A structured `p` (non-`Number`
43+ # eltype) therefore falls back to `eltype(x)`; duals nested inside such a `p` are the one
44+ # unsupported case. (`_use_prep` above is free to use `anyeltypedual`: it runs in the outer
45+ # closure, outside anything a backend differentiates.)
5146@inline function _cons_out_eltype (x, p)
52- p isa Union{SciMLBase . NullParameters, Nothing} && return eltype (x)
53- pe = eltype (p)
54- T = pe <: Number ? Base . promote_op ( + , eltype (x), pe) : eltype (x )
55- return T === Union{} ? eltype (x ) : T
47+ Tu = eltype (x)
48+ p isa Union{SciMLBase . NullParameters, Nothing} && return Tu
49+ Tp = eltype (p )
50+ return Tp <: Number ? Base . promote_op ( + , Tu, Tp ) : Tu
5651end
5752
5853function instantiate_function (
@@ -80,25 +75,24 @@ function instantiate_function(
8075 adtype, soadtype = generate_adtype (adtype)
8176
8277 # Create gradient closures with proper type stability using let blocks.
83- # `T0 = eltype(x)` is the prepared element type; `_grad_use_prep` gates the prepared fast
84- # path vs the prep-free fallback (see the note above the imports).
78+ # `_use_prep` gates the prepared fast path vs the prep-free dual fallback (see the note
79+ # above the imports).
8580 grad = if g == true && f. grad === nothing
8681 _prep_grad = prepare_gradient (f. f, adtype, x, Constant (p))
87- T0 = eltype (x)
8882 if p != = SciMLBase. NullParameters () && p != = nothing
89- let _prep_grad = _prep_grad, f = f, adtype = adtype, T0 = T0
83+ let _prep_grad = _prep_grad, f = f, adtype = adtype
9084 function (res, θ, p = p)
91- return if _grad_use_prep (T0, θ, p) && eltype (res) === T0
85+ return if _use_prep ( θ, p)
9286 gradient! (f. f, res, _prep_grad, adtype, θ, Constant (p))
9387 else
9488 gradient! (f. f, res, adtype, θ, Constant (p))
9589 end
9690 end
9791 end
9892 else
99- let _prep_grad = _prep_grad, f = f, adtype = adtype, p = p, T0 = T0
93+ let _prep_grad = _prep_grad, f = f, adtype = adtype, p = p
10094 function (res, θ, p = p)
101- return if _grad_use_prep (T0, θ, p) && eltype (res) === T0
95+ return if _use_prep ( θ, p)
10296 gradient! (f. f, res, _prep_grad, adtype, θ, Constant (p))
10397 else
10498 gradient! (f. f, res, adtype, θ, Constant (p))
@@ -263,12 +257,11 @@ function instantiate_function(
263257 end
264258 end
265259 _prep_jac = prepare_jacobian (_cons_oop_p, adtype, x, Constant (p))
266- T0 = eltype (x)
267- let _cons_oop_p = _cons_oop_p, _prep_jac = _prep_jac, adtype = adtype, p = p, T0 = T0
260+ let _cons_oop_p = _cons_oop_p, _prep_jac = _prep_jac, adtype = adtype, p = p
268261 function (J, θ, p = p)
269- # Prepared fast path when types match the prep ; prep-free fallback for duals
270- # (see the `_grad_use_prep ` note above the imports).
271- if _grad_use_prep (T0, θ, p) && eltype (J) === T0
262+ # Prepared fast path unless a dual is present ; prep-free fallback for duals
263+ # (see the `_use_prep ` note above the imports).
264+ if _use_prep ( θ, p)
272265 jacobian! (_cons_oop_p, J, _prep_jac, adtype, θ, Constant (p))
273266 else
274267 jacobian! (_cons_oop_p, J, adtype, θ, Constant (p))
@@ -504,23 +497,22 @@ function instantiate_function(
504497 adtype, soadtype = generate_adtype (adtype)
505498
506499 # Create gradient closures with proper type stability using let blocks.
507- # `T0 = eltype(x)` is the prepared element type; `_grad_use_prep` gates the prepared fast
508- # path vs the prep-free fallback (see the note above the imports).
500+ # `_use_prep` gates the prepared fast path vs the prep-free dual fallback (see the note
501+ # above the imports).
509502 grad = if g == true && f. grad === nothing
510503 _prep_grad = prepare_gradient (f. f, adtype, x, Constant (p))
511- T0 = eltype (x)
512504 if p != = SciMLBase. NullParameters () && p != = nothing
513- let _prep_grad = _prep_grad, f = f, adtype = adtype, T0 = T0
505+ let _prep_grad = _prep_grad, f = f, adtype = adtype
514506 function (θ, p = p)
515- return _grad_use_prep (T0, θ, p) ?
507+ return _use_prep ( θ, p) ?
516508 gradient (f. f, _prep_grad, adtype, θ, Constant (p)) :
517509 gradient (f. f, adtype, θ, Constant (p))
518510 end
519511 end
520512 else
521- let _prep_grad = _prep_grad, f = f, adtype = adtype, p = p, T0 = T0
513+ let _prep_grad = _prep_grad, f = f, adtype = adtype, p = p
522514 function (θ, p = p)
523- return _grad_use_prep (T0, θ, p) ?
515+ return _use_prep ( θ, p) ?
524516 gradient (f. f, _prep_grad, adtype, θ, Constant (p)) :
525517 gradient (f. f, adtype, θ, Constant (p))
526518 end
@@ -648,13 +640,12 @@ function instantiate_function(
648640 cons_j! = if f. cons != = nothing && cons_j == true && f. cons_j === nothing
649641 # `f.cons` is out-of-place here and the prep already takes `Constant(p)`, so this
650642 # only needs to expose the parameter argument and add the dual-tolerant fallback
651- # (see the `_grad_use_prep ` note above the imports) — unlike the in-place method,
643+ # (see the `_use_prep ` note above the imports) — unlike the in-place method,
652644 # whose prepared wrapper bakes `p` in.
653645 _prep_jac = prepare_jacobian (f. cons, adtype, x, Constant (p))
654- T0 = eltype (x)
655- let f = f, _prep_jac = _prep_jac, adtype = adtype, p = p, T0 = T0
646+ let f = f, _prep_jac = _prep_jac, adtype = adtype, p = p
656647 function (θ, p = p)
657- J = _grad_use_prep (T0, θ, p) ?
648+ J = _use_prep ( θ, p) ?
658649 jacobian (f. cons, _prep_jac, adtype, θ, Constant (p)) :
659650 jacobian (f. cons, adtype, θ, Constant (p))
660651 if size (J, 1 ) == 1
0 commit comments