Repository navigation
Fix double counting of Symmetric/Hermitian off-diagonals - #252
Conversation
to_vec(::HermOrSym) rebuilt the matrix from one triangle of the dense vector, so cotangents computed by j′vp and grad had their off-diagonals doubled. Rebuilding from the symmetric/hermitian part instead gives the natural cotangent and still round-trips primals exactly. Fixes #164. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I am tossing up, is this the right way or should we be actually slicing out just one triangle. (and flip rather than |
|
That was my first thought as well (see also #164 and #132). The problem is that Example: for a 2×2 For
That's what this PR does. |
…n tests (#3896) FiniteDifferences v0.12.35 (JuliaDiff/FiniteDifferences.jl#252) changed to_vec(::HermOrSym) to project cotangents onto the symmetric/hermitian subspace via (X + X') / 2, halving off-diagonal entries relative to FiniteDifferences <= v0.12.34. Enzyme.gradient computes structure gradients with respect to the backing storage parent(x) (accumulating 2 * G[i, j] in the active triangle and leaving the unused triangle zero), causing Wishart and InverseWishart in test/integration/Distributions/runtests.jl to fail by a factor of 2 on off-diagonal entries. Compute the FiniteDifferences reference gradient with respect to parent(x) and compare parent(grad) for Symmetric/Hermitian inputs, matching Enzyme's structure-gradient semantics across both older and newer versions of FiniteDifferences.
Fixes #164.
to_vec(::HermOrSym)vectorizes the dense matrix, but itsfrom_vecrebuilt the result from only one triangle. For primals that's fine, because both triangles agree. For cotangents (j′vp,grad) each off-diagonal entry collects contributions from both triangles, so the result had its off-diagonals doubled:from_vecnow rebuilds from the symmetric part(X + transpose(X)) / 2, or the hermitian part(X + X') / 2. That gives the natural cotangent, i.e. all ones above. It also matchesChainRulesCore.ProjectTo(::Symmetric), and primals still round-trip exactly.Behaviour change:
j′vp,gradandjacobianwith respect toSymmetric/Hermitianinputs return different (correct) values. For example, the rawjacobian(fdm, sum, Symmetric(A))changes from[1 0 2 1]to[1 1 1 1], with the two triangles now sharing the derivative. ChainRules.jl has rules and tests that compensate for the old double counting, for examplerrule(::Type{<:Matrix}, ::HermOrSym). They will fail once this is released, so I'll open a ChainRules.jl PR that switches them to natural tangents and requires FiniteDifferences 0.12.35. Please coordinate the release with that PR. This change came up via JuliaDiff/ChainRules.jl#783.Tests: new
jvp/j′vptestset forSymmetric/Hermitian×Float64/ComplexF64× bothuplo. It fails on main (off-diagonals 2×) and passes here. Bumps the version to 0.12.35.🤖 Generated with Claude Code