Skip to content

missing_second_order_ad warning fires even when an analytical Hessian is supplied via OptimizationFunction(f; hess = ...) #1240

Description

@gragusa

Description

The missing_second_order_ad verbosity message warns that a Hessian-requiring solver was selected without a SecondOrder AD type, and that a SecondOrder fallback "will be created". The check that emits it, however, only inspects prob.f.adtype and the solver's requirements — it never checks whether the OptimizationFunction already carries a user-supplied analytical Hessian:

https://github.com/SciML/Optimization.jl/blob/main/lib/OptimizationBase/src/cache.jl#L66-L76

if !(
        prob.f.adtype isa DifferentiationInterface.SecondOrder ||
            prob.f.adtype isa AutoZygote
    ) &&
        (
        SciMLBase.requireshessian(opt) || SciMLBase.requiresconshess(opt) ||
            SciMLBase.requireslagh(opt)
    )
    @SciMLMessage(
        lazy"The selected optimization algorithm requires second order derivatives, but `SecondOrder` ADtype was not provided. ...",
        processed_verbose, :missing_second_order_ad
    )

So the warning fires for the fully-supported pattern "analytical grad + analytical hess + NoAD", where it is a false positive on both counts:

  • Nothing is missing. With NoAD, instantiate_function simply wraps the user callbacks (lib/OptimizationBase/src/function.jl, the SciMLBase.NoAD methods), and on the DI path AD Hessians are only constructed when f.hess === nothing (lib/OptimizationBase/src/OptimizationDIExt.jl, hess = if h == true && f.hess === nothing ...). The user Hessian always takes precedence.
  • Nothing "will be created." For NoAD, adtype_to_soadtype maps soadtype = NoAD — no SecondOrder wrapper is built, contrary to the message text.

MWE

using Optimization, OptimizationOptimJL

rosenbrock(x, p) = (1 - x[1])^2 + 100 * (x[2] - x[1]^2)^2
function rosen_grad!(g, x, p)
    g[1] = -2 * (1 - x[1]) - 400 * x[1] * (x[2] - x[1]^2)
    g[2] = 200 * (x[2] - x[1]^2)
    return nothing
end
function rosen_hess!(H, x, p)
    H[1, 1] = 2 - 400 * (x[2] - 3 * x[1]^2)
    H[1, 2] = -400 * x[1]; H[2, 1] = -400 * x[1]; H[2, 2] = 200.0
    return nothing
end

f = OptimizationFunction(rosenbrock; grad = rosen_grad!, hess = rosen_hess!)
prob = OptimizationProblem(f, zeros(2))
sol = solve(prob, Newton())

emits

┌ Warning: Verbosity toggle: missing_second_order_ad
│  The selected optimization algorithm requires second order derivatives, but `SecondOrder` ADtype was not provided. So a `SecondOrder` with SciMLBase.NoAD() for both inner and outer will be created, this can be suboptimal and not work in some cases so an explicit `SecondOrder` ADtype is recommended.

while converging to (1.0, 1.0) with retcode: Success. Instrumenting rosen_hess! with a counter shows it is called 18 times during this solve — the analytical Hessian is what Newton consumes, exactly as intended.

Why it matters beyond noise

The message is emitted at cache creation, i.e. once per solve call. Workflows that re-solve a cached OptimizationProblem in a hot loop get one warning per solve. In my use case, where I solve a convex dual problem with analytical derivatives once per MCMC likelihood evaluation, this produces 25,011 copies of the warning (11 MB of logs) in a single chain.

Proposed fix

Gate each requirement on the corresponding user-supplied field being absent, e.g.

needs_hess      = SciMLBase.requireshessian(opt) && prob.f.hess === nothing &&
                  prob.f.fgh === nothing && prob.f.hv === nothing
needs_cons_hess = SciMLBase.requiresconshess(opt) && prob.f.cons_h === nothing
needs_lag_hess  = SciMLBase.requireslagh(opt) && prob.f.lag_h === nothing

if !(prob.f.adtype isa DifferentiationInterface.SecondOrder ||
     prob.f.adtype isa AutoZygote) &&
   (needs_hess || needs_cons_hess || needs_lag_hess)
    @SciMLMessage(...)
end

(and analogously for the AutoZygote branch). Happy to open a PR along these lines if the approach sounds right.

Workaround for anyone hitting this

solve(prob, Newton(); verbose = OptimizationVerbosity(missing_second_order_ad = SciMLLogging.Silent())) silences exactly this toggle while keeping other diagnostics.

Environment

  • Optimization v5.6.1, OptimizationBase v5.1.3 (same check present on current main)
  • OptimizationOptimJL v0.4.14
  • Julia 1.12.6, macOS (aarch64)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions