Skip to content

Commit 181f01e

Browse files
committed
use anyeltypedual and make more robust
1 parent 39ee61f commit 181f01e

2 files changed

Lines changed: 62 additions & 68 deletions

File tree

lib/OptimizationBase/src/OptimizationDIExt.jl

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -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
3834
end
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
5651
end
5752

5853
function 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

lib/OptimizationBase/test/dual_tolerant_tests.jl

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# derivative closures added on the `dual-tolerant-grad` branch.
33
#
44
# The behaviors under test (all previously uncovered):
5-
# 1. `_grad_use_prep` gating logic in isolation.
5+
# 1. `_use_prep` dual-detection gating logic in isolation.
66
# 2. `grad`/`cons_j` still hit the prepared fast path for the construction types
77
# and stay numerically correct.
88
# 3. `grad`/`cons_j` accept an explicit `p` different from the construction `p`.
@@ -13,7 +13,7 @@
1313
using OptimizationBase, Test, ForwardDiff, FiniteDiff
1414
using ADTypes, Enzyme
1515
import SciMLBase
16-
using OptimizationBase: _grad_use_prep
16+
using OptimizationBase: _use_prep
1717

1818
# Parametrized objective and constraint whose derivatives genuinely depend on `p`,
1919
# so a dual `p` produces a nonzero, checkable sensitivity.
@@ -29,19 +29,22 @@ p1 = [5.0, 7.0] # a different parameter value
2929
∇xf(x, p) = ForwardDiff.gradient(xx -> objp(xx, p), x)
3030
consjac(x, p) = ForwardDiff.jacobian(xx -> consp(xx, p), x)
3131

32-
@testset "_grad_use_prep gating" begin
32+
@testset "_use_prep dual gating" begin
3333
d = ForwardDiff.Dual{Nothing}(1.0, 1.0)
34-
# Matching real inputs -> prepared fast path.
35-
@test _grad_use_prep(Float64, [1.0, 2.0], [3.0, 4.0])
36-
# NullParameters / nothing carry no differentiable params, never veto.
37-
@test _grad_use_prep(Float64, [1.0, 2.0], SciMLBase.NullParameters())
38-
@test _grad_use_prep(Float64, [1.0, 2.0], nothing)
34+
# No duals -> prepared fast path, regardless of real parameter eltype.
35+
@test _use_prep([1.0, 2.0], [3.0, 4.0])
36+
@test _use_prep([1.0, 2.0], SciMLBase.NullParameters())
37+
@test _use_prep([1.0, 2.0], nothing)
38+
@test _use_prep([1.0, 2.0], [1, 100]) # Int p keeps the fast path
39+
@test _use_prep([1.0, 2.0], Float32[1, 2]) # mixed-precision p keeps it too
40+
@test _use_prep([1.0, 2.0], (rand(2, 2), rand(2))) # structured (tuple) p keeps it
41+
@test _use_prep(Float32[1.0, 2.0], [3.0, 4.0]) # off-construction real eltype: still fast
3942
# Dual θ -> fallback.
40-
@test !_grad_use_prep(Float64, [d, d], [3.0, 4.0])
43+
@test !_use_prep([d, d], [3.0, 4.0])
4144
# Real θ but dual p (the sensitivity case) -> fallback.
42-
@test !_grad_use_prep(Float64, [1.0, 2.0], [d, d])
43-
# Mismatched θ element type -> fallback.
44-
@test !_grad_use_prep(Float64, Float32[1.0, 2.0], [3.0, 4.0])
45+
@test !_use_prep([1.0, 2.0], [d, d])
46+
# Dual nested inside a structured p -> fallback (anyeltypedual recurses).
47+
@test !_use_prep([1.0, 2.0], ([d, d], [1.0]))
4548
end
4649

4750
# Runs the full matrix of assertions against an already-instantiated problem.
@@ -130,7 +133,7 @@ end
130133
optprob.grad(g, xt)
131134
@test g ForwardDiff.gradient(xx -> losst(xx, pt), xt) rtol = 1.0e-6
132135
# A structured `p` must still route through the prepared fast path.
133-
@test OptimizationBase._grad_use_prep(Float64, xt, pt)
136+
@test _use_prep(xt, pt)
134137
end
135138

136139
@testset "parametrized cons_j (Enzyme)" begin

0 commit comments

Comments
 (0)