From da4575c4e4d08b7be172d4f91e503367f42db3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Mon, 12 Oct 2026 21:29:00 +0200 Subject: [PATCH] Enable NaN-safe mode by default Fixes #857. The previous behaviour remains available by setting the `nansafe_mode` preference to `false`. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 +- docs/src/user/advanced.md | 53 +++++++++++++++++++-------------------- src/prelude.jl | 2 +- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Project.toml b/Project.toml index 801792c6..91eac238 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ForwardDiff" uuid = "f6369f11-7733-5829-9624-2563aa707210" -version = "1.4.6" +version = "1.5.0" [deps] CommonSubexpressions = "bbf7d656-a473-5ed7-a52c-81e309532950" diff --git a/docs/src/user/advanced.md b/docs/src/user/advanced.md index 69468751..9b74f341 100644 --- a/docs/src/user/advanced.md +++ b/docs/src/user/advanced.md @@ -117,41 +117,35 @@ Note that it is usually best to pick a chunk size which divides evenly into the dimension. Otherwise, ForwardDiff has to construct and utilize an extra "remainder" chunk to complete the calculation. -## Fixing NaN/Inf Issues +## NaN-Safe Mode -ForwardDiff's default behavior is to return `NaN` for undefined derivatives (or otherwise -mirror the behavior of the function in `Base`, if it would return an error). This is -usually the correct thing to do, but in some cases can erroneously "poison" values which -aren't sensitive to the input and thus cause ForwardDiff to incorrectly return `NaN` or -`Inf` derivatives. For example: +A dual number whose perturbation component is zero is unperturbed: it must not propagate +derivative information, regardless of the derivative of the function it is passed to. +Naively propagating derivatives by multiplying them with the perturbation component +violates this property whenever the derivative is not finite, since `0.0 * Inf` and +`0.0 * NaN` are `NaN`. ForwardDiff therefore checks whether each perturbation component is +zero before propagating derivative information ("`NaN`-safe mode"): ```julia-repl -# the dual number's perturbation component is zero, so this -# variable should not propagate derivative information julia> log(ForwardDiff.Dual{:tag}(0.0, 0.0)) -Dual{:tag}(-Inf,NaN) # oops, this NaN should be 0.0 -``` - -Here, ForwardDiff computes the derivative of `log(0.0)` as `NaN` and then propagates -this derivative by multiplying it by the perturbation component. Usually, ForwardDiff can -rely on the identity `x * 0.0 == 0.0` to prevent the derivatives from propagating when -the perturbation component is `0.0`. However, this identity doesn't hold if `isnan(y)` -or `isinf(y)`, in which case a `NaN` derivative will be propagated instead. +Dual{:tag}(-Inf,0.0) -It is possible to fix this behavior by checking that the perturbation component is zero -before attempting to propagate derivative information, but this check can noticeably -decrease performance (~5%-10% on our benchmarks). +julia> ForwardDiff.gradient(v -> sqrt(v[1]) + v[2], [0.0, 1.0]) +2-element Vector{Float64}: + Inf + 1.0 +``` -In order to preserve performance in the majority of use cases, ForwardDiff disables this -check by default. If your code is affected by this `NaN` behavior, you can enable -ForwardDiff's `NaN`-safe mode by using the +This check can noticeably decrease performance in code that is dominated by cheap +arithmetic operations (up to ~50% on some of our benchmarks). If you are sure that your +code is not affected, you can disable `NaN`-safe mode by using the [Preferences.jl](https://github.com/JuliaPackaging/Preferences.jl) API to set -the `nansafe_mode` preference to true, for example via: +the `nansafe_mode` preference to false, for example via: ```julia-repl julia> using ForwardDiff, Preferences -julia> set_preferences!(ForwardDiff, "nansafe_mode" => true) +julia> set_preferences!(ForwardDiff, "nansafe_mode" => false) ``` Note that Julia has to be restarted and ForwardDiff has to be reloaded after changing @@ -162,16 +156,21 @@ Alternatively, you can set the preference before loading ForwardDiff, for exampl ```julia-repl julia> using Preferences, UUIDs -julia> set_preferences!(UUID("f6369f11-7733-5829-9624-2563aa707210"), "nansafe_mode" => true) +julia> set_preferences!(UUID("f6369f11-7733-5829-9624-2563aa707210"), "nansafe_mode" => false) julia> using ForwardDiff julia> log(ForwardDiff.Dual{:tag}(0.0, 0.0)) -Dual{:tag}(-Inf,0.0) +Dual{:tag}(-Inf,NaN) + +julia> ForwardDiff.gradient(v -> sqrt(v[1]) + v[2], [0.0, 1.0]) +2-element Vector{Float64}: + Inf + NaN ``` In the future, we plan on allowing users and downstream library authors to dynamically -enable [`NaN`-safe mode via the `AbstractConfig` +toggle [`NaN`-safe mode via the `AbstractConfig` API](https://github.com/JuliaDiff/ForwardDiff.jl/issues/181). ## Hessian of a vector-valued function diff --git a/src/prelude.jl b/src/prelude.jl index 9e037afa..bbc3a494 100644 --- a/src/prelude.jl +++ b/src/prelude.jl @@ -1,4 +1,4 @@ -const NANSAFE_MODE_ENABLED = @load_preference("nansafe_mode", false) +const NANSAFE_MODE_ENABLED = @load_preference("nansafe_mode", true) const DEFAULT_CHUNK_THRESHOLD = @load_preference("default_chunk_threshold", 12) const AMBIGUOUS_TYPES = (AbstractFloat, Irrational, Integer, Rational, Real, RoundingMode)