Skip to content

Supply the _costabs Dual overload SupernodalLU's matching documents - #1118

Merged
ChrisRackauckas merged 1 commit into
SciML:mainfrom
ChrisRackauckas-Claude:snlu-dual-matching
Jul 27, 2026
Merged

Supply the _costabs Dual overload SupernodalLU's matching documents#1118
ChrisRackauckas merged 1 commit into
SciML:mainfrom
ChrisRackauckas-Claude:snlu-dual-matching

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

Please ignore until reviewed by @ChrisRackauckas.

Follow-up to the SupernodalLU series, and the first half of the dual-number question from #1064.

The bug

src/SupernodalLU/matching.jl:203-206 documents an overload that does not exist:

# Float64 view of |x| for the matching's combinatorial side.  Value types
# with no exact Float64 conversion (e.g. ForwardDiff.Dual) get an overload in
# the package extension that extracts the primal value.
@inline _costabs(x::Number) = Float64(abs(x))

grep -rn _costabs src ext lib returns only the definition and its two call sites — the extension overload was left behind when the solver was vendored from PurePardiso.jl. So the comment describes behaviour the package does not have.

MC64 matching runs its max-product assignment in Float64, so it needs a real magnitude per entry. Factoring a Dual matrix whose diagonal is missing or weak — exactly when matching = :auto engages — therefore throws:

MethodError: no method matching Float64(::ForwardDiff.Dual{Nothing, Float64, 1})
  _costabs at matching.jl:206 [inlined]
  mc64_matching(::SparseMatrixCSC{Dual{Nothing, Float64, 1}, Int64})
  snlu(::SparseMatrixCSC{Dual{...}, Int64}; ...)

Measured before/after on an anti-diagonal matrix (needs_matching = true):

before:  snlu THREW: MethodError: no method matching Float64(::Dual{Nothing, Float64, 1})
         methods(_costabs) = 1
after:   snlu OK, matched = true
         methods(_costabs) = 2

Scope

Only the ordering of candidate pivots is decided from these numbers; the factorization stays in Dual arithmetic, so taking the primal loses nothing.

This is reachable today through LinearSolve.__init. The public path is unaffected__dual_init strips duals before any algorithm sees them, so solve/init on a Dual sparse problem factor in Float64 (verified: inner A eltype Float64, derivative correct to 5.7e-15 relative). This PR does not change how duals are routed, and deliberately does not add SupernodalLUFactorization to _use_direct_dual_solve: I measured native Dual factorization at 1.7× / 3.9× / 15.3× the primal cost at N = 1 / 4 / 8 partials, which is the same shape as the RFLU regression in #1052.

_scalarval needs no equivalent fix — returning NaN for a Dual is intended and documented there (anorm is informational; thresholds stay in native arithmetic).

Test

test/Core/forwarddiff_overloads.jl gains a case on an anti-diagonal matrix — no structural diagonal at all, so matching always engages — asserting both the value and the derivative against the closed form. It fails on the unfixed code at the snlu call.

GROUP=Core passes locally (rc=0); the new testset passes 4/4.

🤖 Generated with Claude Code

https://claude.ai/code/session_017rr42T1vFRRT8D7DMAmJzf

`src/SupernodalLU/matching.jl` says of `_costabs`:

    Value types with no exact Float64 conversion (e.g. ForwardDiff.Dual) get
    an overload in the package extension that extracts the primal value.

No such overload exists. It was left behind when the solver was vendored from
PurePardiso.jl, so the comment documents behaviour the package does not have.

MC64 matching decides its max-product assignment in Float64, so `_costabs`
needs a real magnitude per entry. Without the overload, factoring a `Dual`
matrix whose diagonal is missing or weak - exactly when `matching = :auto`
engages - throws:

    MethodError: no method matching Float64(::ForwardDiff.Dual{Nothing, Float64, 1})
      _costabs at matching.jl:206 [inlined]
      mc64_matching(::SparseMatrixCSC{Dual{Nothing, Float64, 1}, Int64})

Only the *ordering* of candidate pivots comes from these numbers; the
factorization itself stays in Dual arithmetic, so taking the primal loses
nothing.

Reachable today through `LinearSolve.__init`. The public path is unaffected:
`__dual_init` strips duals before any algorithm sees them, so `solve` and
`init` on a Dual sparse problem factor in Float64 and never reach this code.

`_scalarval` needs no such fix - returning `NaN` for a Dual is intended and
documented there (`anorm` is informational; thresholds stay in native
arithmetic).

Test: an anti-diagonal matrix, which has no structural diagonal at all so
matching always engages, checked in both value and derivative against the
closed form. It fails on the unfixed code at the `snlu` call.

GROUP=Core passes.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rr42T1vFRRT8D7DMAmJzf
@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review July 27, 2026 08:42
@ChrisRackauckas
ChrisRackauckas merged commit e9d545e into SciML:main Jul 27, 2026
59 of 62 checks passed
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