Repository navigation
NaNs in jacobian of a function that uses a StaticArray and norm #547
Description
Hi! I am having trouble differentiating a function with ForwardDiff.jl. I don't know why, but the resulting jacobian contains NaNs. Below is a MWE to kickstart the discussion.
Consider the following functions:
using BenchmarkTools, ForwardDiff, LinearAlgebra, StaticArrays
foo2(A) = SVector{4}(norm(r, 2) for r = eachrow(A))
function foo1!(out, x; μ = 0.8 / √2)
λ = SVector{3}(@view x[1:3])
K = SMatrix{3,3}(@view x[4:12])
A_λ = @SMatrix [ 1 0 -1 0 ;
0 1 0 -1 ;
μ μ μ μ ]
out[1:4] = A_λ' * λ + foo2(A_λ' * K)
out
endLet's try out foo1!:
julia> x = rand(12);
julia> out = zeros(4);
julia> foo1!(out, x)
4-element Vector{Float64}:
2.403435298832313
2.0030472808350077
0.7216049166673891
0.6293600802591698foo1! is type-stable and does not perform dynamic allocations:
julia> @btime $foo1!($out, $x)
21.684 ns (0 allocations: 0 bytes)
4-element Vector{Float64}:
2.403435298832313
2.0030472808350077
0.7216049166673891
0.6293600802591698We can use ForwardDiff.jl to compute the jacobian of foo1!:
julia> ForwardDiff.jacobian(foo1!, out, x)
4×12 Matrix{Float64}:
1.0 0.0 0.565685 0.606882 0.0 0.343304 … 0.353444 0.491234 0.0 0.277884
0.0 1.0 0.565685 0.0 0.744664 0.421245 0.22503 0.0 0.535939 0.303173
-1.0 0.0 0.565685 0.722007 0.0 -0.408429 -0.362283 0.261825 0.0 -0.14811
0.0 -1.0 0.565685 0.0 0.930926 -0.526611 -0.154069 0.0 0.243306 -0.137634However, if the inputs are all zeros, the jacobian will contain NaNs:
julia> x = zeros(12);
julia> ForwardDiff.jacobian(foo1!, out, x)
4×12 Matrix{Float64}:
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaNBut if we change the type of the matrix A_λ (in foo1!) from an SMatrix to a normal Matrix, the jacobian will be evaluated properly:
julia> function foo1!(out, x; μ = 0.8 / √2)
λ = SVector{3}(@view x[1:3])
K = SMatrix{3,3}(@view x[4:12])
A_λ = [ 1 0 -1 0 ;
0 1 0 -1 ;
μ μ μ μ ]
out[1:4] = A_λ' * λ + foo2(A_λ' * K)
out
end
foo1! (generic function with 1 method)
julia> ForwardDiff.jacobian(foo1!, out, x)
4×12 Matrix{Float64}:
1.0 0.0 0.565685 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.565685
0.0 1.0 0.565685 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.565685
-1.0 0.0 0.565685 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.565685
0.0 -1.0 0.565685 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.565685Moreover, I have also observed that the jacobian will not contain NaNs if we use the 1-norm or the Inf-norm, even if we keep A_λ as an SMatrix, i.e.,
foo2(A) = SVector{4}(norm(r, 1) for r = eachrow(A))or
foo2(A) = SVector{4}(norm(r, Inf) for r = eachrow(A))I am not sure if this is a bug or if I am doing something wrong... Can someone help me figure it out? Thank you in advance!
Activity
ferrolho commentedon Oct 2, 2021
I have read the docs section Fixing NaN/Inf Issues under User Documentation > Advanced Usage Guide. Indeed, if I set the
NANSAFE_MODE_ENABLEDconstant totrue, the jacobian no longer containsNaNs. However, the evaluated jacobian is different compared to the version offoo1!which definesA_λas a normalMatrix.If
A_λis aMatrix{Float64}:If
A_λis aSMatrix{3, 4, Float64, 12}:I would appreciate some guidance on what I should do. I.e., should I define
NANSAFE_MODE_ENABLEDastruefrom now on (and take the ~5%-10% performance hit)? Or is there something else I can do to fix this issue, without having to toggleNANSAFE_MODE_ENABLED? Thank you!KristofferC commentedon Oct 2, 2021
Regardless of
NaNs, it would be good to figure out whyMatrixandSMatrixdiffer. I guess just peppering some print statements throughout the code should reveal where things diverge.ferrolho commentedon Oct 2, 2021
Hi, @KristofferC. Thank you for your reply. You mean in ForwardDiff.jl? Where should I start? 😅
KristofferC commentedon Oct 2, 2021
With StaticArrays 0.12.4 I get the same result between static matrix and matrix running the example in the first post. With StaticArrays 1.2.13 I get your result. So something happened there.
Bisecting points to JuliaArrays/StaticArrays.jl#908 as the offending PR.
mcabbott commentedon Oct 7, 2021
A more minimal example is
With a hand-written
norm, we are evaluating atsqrt(0)where the gradient is infinite. The non-NaN answer is something like a choice of subgradient:Note also that because it's caused by an explicit branch (at least according to JuliaArrays/StaticArrays.jl#964, didn't check LinearAlgebra's source) it will be changed by #481, to never take the measure-zero branch, and hence always give
NaNs.Zygote's behaviour (using a rule for
normfrom ChainRules, here) does not pick one of these, BTW:That could choose to pick a particular "sub-gradient". But I'm not sure that doing that with DiffRules would work, since the function takes a vector not a scalar.
sethaxen commentedon Jan 19, 2022
It seems this particular issue is due to the derivative for
sqrtassuming the input is nonzero.I think the right fix here is to bypass DiffRules' rules for
^,sqrt, andcbrtto return 0 in these cases, since it can't take into account the value of the input derivative.See also JuliaDiff/ChainRules.jl#576
mcabbott commentedon Jan 19, 2022
Oh right, well spotted. The dual part is indeed zero:
Catching that and replacing it by zero can avoid the NaN, but seems slightly weird in that it doesn't pick a subgradient (if that's the term). While
[0, eps()]gives you a vector of length 1, telling you that moving away from zero will change the function, this would imply that the function is flat there.We can picture this (for a 2-vector) as being the tip of a cone, a rolled up piece of paper cone. The current behaviour is to say that the tip is a singularity, hence has no gradient. The sqrt proposal would smooth the tip off, so that microscopic angels can treat it as a dance floor. If you could always pick
[0, eps()], then instead you are always informed that you can go downhill, and a random direction is provided, but the rate at which you will go downhill is correct.Maybe that's not so strange then. It's a bit like
derivative(abs, 0) == 0, rounding off the singularity. While[0, eps()]is likederivative(abs, 0) == 1. At present CR picks the former, ForwardDiff the latter.sethaxen commentedon Jan 19, 2022
FWIW, finite differences finds that the gradient of
normat a zero vector is zero:So I would be inclined to side with CR here.
But I think either way, the
sqrt,cbrt,^issue is bad behavior that needs to be addressed.mcabbott commentedon Jan 19, 2022
Finite differencing just averages the two sides of
abs, right? If it were doing anything smarter I'd give it less weight...devmotion commentedon Jan 19, 2022
As a side remark: As noted above, everything works if one uses the NaN-safe setting (https://juliadiff.org/ForwardDiff.jl/dev/user/advanced/#Fixing-NaN/Inf-Issues). I think one should benchmark if and to what extent performance is affected by making NaN-safe the default. I think it's weird to report incorrect results with the default setting.
mcabbott commentedon Jan 19, 2022
Great. I see that the implementation is here:
ForwardDiff.jl/src/partials.jl
Lines 93 to 121 in 43ef860
Is it obvious why this test
!isfinite(x) && iszero(partials)not justiszero(partials)? And if both are needed, why&¬&? Strange to put a branch inside anifelse. Maybe the idea is that with 12 partials & one x, it's quicker to check one & branch?