Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #851 +/- ##
==========================================
+ Coverage 91.23% 91.33% +0.10%
==========================================
Files 11 11
Lines 1072 1073 +1
==========================================
+ Hits 978 980 +2
+ Misses 94 93 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
devmotion
left a comment
There was a problem hiding this comment.
It seems indexing A instead of parent(A) is noticeably slower: it adds a branch and a strided read per element, and on top of that broadcast over a Matrix seems ~1.4x slower than map over it. For _structured_value of Symmetric/Hermitian that's a factor of ~2.2, and for _structured_partials of Hermitian{Complex} ~1.66.
I think we can avoid this regression by specializing on isbitstype(V). Something like
_maptri(f, A::Union{Symmetric,Hermitian}) = _maptri(f, A, parent(A))
_maptri(f, A, P::AbstractArray{V}) where {V} = isbitstype(V) ? map(f, P) : broadcast(f, A)
_structured_value(A::Symmetric{Dual{T,V,N}}) where {T,V,N} = Symmetric(_maptri(value, A), A.uplo === 'U' ? :U : :L)
_structured_value(A::Hermitian{Dual{T,V,N}}) where {T,V,N} = Hermitian(_maptri(value, A), A.uplo === 'U' ? :U : :L)
_structured_value(A::Hermitian{Complex{Dual{T,V,N}}}) where {T,V,N} = Hermitian(_maptri(z -> complex(value(real(z)), value(imag(z))), A), A.uplo === 'U' ? :U : :L)
_structured_partials(A::Symmetric{Dual{T,V,N}}, j::Int) where {T,V,N} = Symmetric(_maptri(a -> partials(a, j), A), A.uplo === 'U' ? :U : :L)
_structured_partials(A::Hermitian{Dual{T,V,N}}, j::Int) where {T,V,N} = Hermitian(_maptri(a -> partials(a, j), A), A.uplo === 'U' ? :U : :L)
function _structured_partials(A::Hermitian{Complex{Dual{T,V,N}}}, j::Int) where {T,V,N}
return Hermitian(_maptri(z -> complex(partials(real(z), j), partials(imag(z), j)), A), A.uplo === 'U' ? :U : :L)
endMy impression was also that the two complex methods have to be fused into a single closure, otherwise A[i, j] is evaluated twice per element.
Regarding the tests: eigvals(::Hermitian{Complex{BigFloat}}) returns complex eigenvalues. This only works because Calculus.finite_difference_jacobian writes into a Matrix{Float64} and the imaginary parts happen to be exactly zero - with a rounding-level imaginary part the test errors with InexactError instead of failing. I suspect this is also what #workaround for a bug in julia 1.10 is about: the error it avoids is a MethodError in _to_duals caused by these complex eigenvalues, and it occurs on 1.12 as well. If eigvals returns real values the analytic eigen works on both versions, and then Hermitian(real(Matrix(M))) can be dropped as well - it discards imag(M), so currently the imaginary parts in _structured_value/_structured_partials are only exercised with zeros.
It also seems the #undef tests cover only uplo = :U - check uplo = :L as well?
|
The julia-1.10 bug was that if you tried to take I also added the specialisation for |
|
Thanks, the performance regression, the fused closures, the complex eigenvalues and the missing A few remaining points:
|
|
Importing I don't think there's a point fixing it for Everything else I did as requested. |
devmotion
left a comment
There was a problem hiding this comment.
- The PR deletes the existing
uplo = :$uplotestset. That's the only test that checks values and partials are read from theuplotriangle when the storage isn't symmetric, so it should be restored. - I'd still prefer to drop the mocks. They only work for 2x2: for any other size
eigvalssilently returns wrong values (e.g.M[4]is entry (1, 2) of a 3x3 matrix), andeigendivides byM[1, 2]. They also stay defined for all test files included afterJacobianTest.jl. The fix can be tested directly without any eigensolver, e.g. inside the restoreduplotestset (passes with this PR,UndefRefErroron master):@testset "#undef outside of the `uplo` triangle" begin x = ForwardDiff.Dual{Nothing}.(BigFloat[1, 2, 3], BigFloat[1, 0, 0], BigFloat[0, 1, 0]) k = uplo === :U ? 3 : 2 # linear index of the stored off-diagonal element M = similar(x, 2, 2) M[1, 1], M[k], M[2, 2] = x[1], x[2], x[3] Mc = similar(x, Complex{eltype(x)}, 2, 2) Mc[1, 1], Mc[k], Mc[2, 2] = x[1], x[2] + im * x[1], x[3] s = uplo === :U ? 1 : -1 # sign of imag(A[1, 2]) @testset "$name" for (name, A, value, ∂1, ∂2) in ( ("Symmetric{<:Real}", Symmetric(M, uplo), [1 2; 2 3], [1 0; 0 0], [0 1; 1 0]), ("Hermitian{<:Real}", Hermitian(M, uplo), [1 2; 2 3], [1 0; 0 0], [0 1; 1 0]), ("Hermitian{<:Complex}", Hermitian(Mc, uplo), [1 2+s*im; 2-s*im 3], [1 s*im; -s*im 0], [0 1; 1 0]), ) @test ForwardDiff._structured_value(A) == value @test ForwardDiff._structured_partials(A, 1) == ∂1 @test ForwardDiff._structured_partials(A, 2) == ∂2 end end
- The
Float64inputs in the new testset don't produce#undefelements, only uninitialized memory. So these tests run on garbage values that happen to be ignored. length(ev) == length(dv)is removed in 1.14, but ForwardDiff still supports 1.10–1.13, where it's accepted. The fix is simple (only map over the firstlength(dv) - 1elements ofev), so I think it would be good to include it here.
|
This is Kafkaesque. You asked me to merge the uplo test with the undef test, I did so, and then you complain that I deleted the uplo test. Fine, I deleted everything, added the test you wanted, and now fixed SymTridiagonal as well. |
The functions
_structured_valueand_structured_partialsaccess theparentof the matrix, which causes a crash when it contains #undef elements, as is the case if we only partially initialize a BigFloat matrix. Here I'm switchingmapto broadcasting, which can deal with it correctly.For testing I assume you don't want
GenericLinearAlgebraas a test dependency, so I wrote some mockeigvalsandeigenfunctions instead.The MWE to get the crash is to run
test_gradient(BigFloat)below: