Drop @fastmath from L2_NORM/Linf_NORM so isfinite guards survive - #1182
Draft
ChrisRackauckas-Claude wants to merge 1 commit into
Draft
Drop @fastmath from L2_NORM/Linf_NORM so isfinite guards survive#1182ChrisRackauckas-Claude wants to merge 1 commit into
ChrisRackauckas-Claude wants to merge 1 commit into
Conversation
`@fastmath` emits LLVM `nnan`/`ninf` on the reduction and the `sqrt`, which lets the optimizer assume the result is finite. Where the norm inlines into its caller that assumption deletes the check: the `!isfinite(objective)` protective break in `termination_conditions.jl` compiled to a constant `false` for `L2_NORM` on a `Vector`. Since the NLLS default termination mode standardizes `Base.Fix2(norm, 2)` to `L2_NORM`, a diverged least-squares solve fell through to the stall logic and `TrustRegion` returned `ReturnCode.StalledSuccess` at `‖F‖ = NaN`. `@simd` already supplies `reassoc` and `contract`, so the reduction still vectorizes identically (4x `<4 x double>` accumulators, `vfmadd231pd`); the only added work is `sqrt`'s domain check, ~1.2 ns per call independent of length. Dropping `@fastmath` from the `Complex` methods also restores `hypot` scaling, which `abs_fast` skips and overflows on. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
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.
Please ignore until reviewed by @ChrisRackauckas.
Released NonlinearSolve can return a successful retcode on a diverged solve. Reproduced against the registry, not this branch —
NonlinearSolve v4.27.0/NonlinearSolveBase v2.46.0:Cause
L2_NORMandLinf_NORMare defined with@fastmath(lib/NonlinearSolveBase/src/common_defaults.jl, the only@fastmathin the repo).@fastmath sqrtemitscall fast double @llvm.sqrt.f64, andfastimpliesnnan ninf, so LLVM proves anyisfiniteon the result is true and deletes it. With the released code,compiles to, in full:
The safe-mode protective break is written exactly that way (
termination_conditions.jl:256,if !isfinite(objective)), and the least-squares defaultinternalnormisBase.Fix2(norm, 2), whichstandardize_norms toL2_NORM. So for least-squares the guard is dead. SquareNonlinearProblemdefaults toBase.Fix1(maximum, abs)→Linf_NORM(maximum(abs, u), no@fastmathon the reduction) and is unaffected.From there: the guard misses → TrustRegion rejects every step (
ρ = NaN, soNaN > thresholdisfalse) →u == uprevexactly →L2_NORM(u - uprev) = 0 ≤ abstol→ the stall branch fires withleastsq == true→StalledSuccess, whichsuccessful_retcodereports astrue.Causal isolation — same mode, same input, only the norm differs:
internalnormdu = [NaN, 1.0][-Inf, Inf]Base.Fix2(norm, 2)→L2_NORMFailureFailureBase.Fix1(maximum, abs)→Linf_NORMUnstableUnstableL2_NORMbehind@noinlineUnstableUnstableThe
@noinlinerow rules out an ordinary logic bug: the guard's input is correct (L2_NORM([NaN, 1.0])isNaNat top level), and only inlining-plus-fold explains the miss.code_typedstill contains the check; it is an LLVM fold, and it happens at-O1and above on every Julia version tested (1.10, 1.11, 1.12, 1.13-rc2).It is broader than NaN, and broader than
isfiniteL2_NORMcomputes an unscaledsum(abs2), so any‖F‖ ≳ 1.34e154overflows toInfinternally. Starting fromu0 = [200.0, 0.0]withr(u0) = [-5.22e173, -1.0]— every entry finite — master returnsStalledSuccess/successful; this branch returnsUnstable.isnanis folded too, thoughisinfis not.max/minswallow NaN, because Julia builds them on an internalisnantest which is also folded: on mastermax(L2_NORM([NaN,1.0]), 1.0) == 1.0. That launders NaN into the initial trust radius (trust_region.jl:336),utils.jl:313,homotopy_sweep.jl:839and twoSimpleNonlinearSolvesites. Removing the taint at source fixes all of them — verifiedNaNafter the change.Ordered comparisons (
fcmp ole/ogt) are not affected and survive verbatim; every accept/reject site consuming a tainted norm was checked and behaves identically before and after, always in the reject direction.Fix
Drop
@fastmathfrom the two norms rather than rewriting the guards. Magnitude comparisons do not work here —NaN > 1e300isfalse, so they catchInfbut notNaN— and!(x <= c)formulations are themselves fast-math-dependent. Screening the raw residual costs an extra O(n) pass per iteration.The cost is nil, and the reason is that
@fastmathwas not doing the work.@simdalready supplies thereassoc+contractthat vectorizes the reduction. Codegen forL2_NORM(::Vector{Float64}), counted in-process so the shared machine cannot distort it:<4 x double>/fmul/faddvfmadd/vaddpd(native)fastreassoc,contractIdentical vector width, identical flops, identical allocations. The only added work is
sqrt's negative-domain check. Interleaved wall clock is +1.0% at n=8, +0.7% at n=65536 — and the norm is O(n) against an O(n²)–O(n³) linear solve per iteration.Effect
42 (problem × algorithm) cells: successful retcode on a non-finite objective 2 → 0. Both were
TrustRegionon least-squares. Separately, the same dead guard was turningUnstableintoMaxItersfor NewtonRaphson, GaussNewton, PseudoTransient, Broyden, Klement and the default polyalgorithm on least-squares — 7 algorithms × 4 problems now reportUnstableinstead of burning 100 iterations.Also fixed:
Base.FastMath.abs_fast(::Complex)skipshypotscaling, soL2_NORM(1e200 + 1e200im)returnedInfinstead of1.414e200.Tests
Testing this is awkward, because the fold depends on the optimizer. A naive
@test !isfinite(L2_NORM(x))passes on unfixed master — the assertion is itself folded. The tests therefore assert observable behaviour (cache.retcode == Unstable,!successful_retcode(sol)), never the predicate.Negative control,
src/common_defaults.jlalone reverted, fullGROUP=Core:GROUP=Corepasses for both packages on the branch. Runic clean.Known limitation, stated plainly: the test only bites when the optimizer folds. At
-O0, or if LLVM changes, it would pass on unfixed code. It can never false-pass on fixed code, which is the property that matters, but nothing here locks in "no@fastmathin these norms" at source level. A lint or a@code_llvmassertion would be a stronger guarantee if you want one.Related, not fixed here
DiffEqBase.ODE_DEFAULT_NORMhas the same hazard —isnan(ODE_DEFAULT_NORM(u, t))also compiles toret i8 0. It appears latent today (theinitdtpath still reportsUnstableby another route), but it is the same trap. Worth its own issue.🤖 Generated with Claude Code