From f3ca8d4c9d9570cd0a313ece4769b4801ed45b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 28 Oct 2026 10:53:48 +0100 Subject: [PATCH 1/3] Use natural tangents in Matrix(::HermOrSym) rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frule and rrule for `Matrix`/`Array` of a `Symmetric`/`Hermitian` now project with `ProjectTo(A)`, giving the natural tangent `(Δ + Δ')/2` instead of the structural one that double-counted off-diagonals. This matches FiniteDifferences 0.12.35, whose `to_vec(::Symmetric/Hermitian)` now yields natural tangents. Re-enables the `prod` tests on `Symmetric` (#475). Co-Authored-By: Claude Opus 5.5 --- Project.toml | 4 ++-- src/rulesets/LinearAlgebra/symmetric.jl | 28 +++++------------------- test/Project.toml | 4 ++-- test/rulesets/Base/indexing.jl | 1 + test/rulesets/Base/mapreduce.jl | 5 ++--- test/rulesets/LinearAlgebra/symmetric.jl | 7 ++++++ 6 files changed, 19 insertions(+), 30 deletions(-) diff --git a/Project.toml b/Project.toml index 4bf00b042..42ccbfdc2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ChainRules" uuid = "082447d4-558c-5d27-93f4-14fc19e9eca2" -version = "1.74.6" +version = "1.74.7" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" @@ -18,7 +18,7 @@ StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" [compat] Adapt = "3.4.0, 4" -ChainRulesCore = "1.25" +ChainRulesCore = "1.26.6" Distributed = "1" GPUArraysCore = "0.1.0, 0.2" IrrationalConstants = "0.1.1, 0.2" diff --git a/src/rulesets/LinearAlgebra/symmetric.jl b/src/rulesets/LinearAlgebra/symmetric.jl index 23e4b2941..82215f465 100644 --- a/src/rulesets/LinearAlgebra/symmetric.jl +++ b/src/rulesets/LinearAlgebra/symmetric.jl @@ -16,36 +16,18 @@ function rrule(T::Type{<:LinearAlgebra.HermOrSym}, A::AbstractMatrix, uplo) end function frule((_, ΔA), TM::Type{<:Matrix}, A::LinearAlgebra.HermOrSym) - return TM(A), TM(_symherm_forward(A, ΔA)) -end -function frule((_, ΔA), ::Type{Array}, A::LinearAlgebra.HermOrSym) - return Array(A), Array(_symherm_forward(A, ΔA)) + ∂A = ProjectTo(A)(unthunk(ΔA)) + return TM(A), ∂A isa AbstractZero ? ∂A : TM(∂A) end +frule(Δargs, ::Type{Array}, A::LinearAlgebra.HermOrSym) = frule(Δargs, Matrix, A) function rrule(TM::Type{<:Matrix}, A::LinearAlgebra.HermOrSym) - function Matrix_pullback(Ω̄) - ΔΩ = unthunk(Ω̄) - TA = _symhermtype(A) - T∂A = TA{eltype(ΔΩ),typeof(ΔΩ)} - uplo = A.uplo - ∂A = T∂A(_symherm_back(typeof(A), ΔΩ, Symbol(uplo)), uplo) - return NoTangent(), ∂A - end + project_A = ProjectTo(A) + Matrix_pullback(ΔΩ) = (NoTangent(), project_A(ΔΩ)) return TM(A), Matrix_pullback end rrule(::Type{Array}, A::LinearAlgebra.HermOrSym) = rrule(Matrix, A) -# for Ω = Matrix(A::HermOrSym), push forward ΔA to get ∂Ω -function _symherm_forward(A, Ȧ) - ΔA = unthunk(Ȧ) - TA = _symhermtype(A) - return if typeof(ΔA) <: TA - ΔA - else - TA{eltype(ΔA),typeof(ΔA)}(ΔA, A.uplo) - end -end - # for Ω = HermOrSym(A, uplo), pull back ΔΩ to get ∂A @inline function _symherm_back(::Type{T}, Ω̄, uplo::Symbol) where {T} ΔΩ = unthunk(Ω̄) diff --git a/test/Project.toml b/test/Project.toml index a41d33187..ed8458d00 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -16,9 +16,9 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] Adapt = "3.4.0, 4" -ChainRulesCore = "1.25" +ChainRulesCore = "1.26.6" ChainRulesTestUtils = "1.5" -FiniteDifferences = "0.12.20" +FiniteDifferences = "0.12.35" GPUArraysCore = "0.1.0, 0.2" JLArrays = "0.1, 0.4" LinearAlgebra = "1" diff --git a/test/rulesets/Base/indexing.jl b/test/rulesets/Base/indexing.jl index a38dfc4d4..a88eb55a0 100644 --- a/test/rulesets/Base/indexing.jl +++ b/test/rulesets/Base/indexing.jl @@ -102,6 +102,7 @@ end @test unthunk(dgrad) ≈ Diagonal([0, 2, 0]) test_rrule(getindex, Symmetric(rand(3, 3)), 2, 2; check_inferred=false) # Infers to Any + test_rrule(getindex, Symmetric(rand(3, 3)), 2, 3; check_inferred=false) sgrad = rrule(getindex, Symmetric(rand(3, 3)), 2, 3)[2](1.0)[2] @test unthunk(sgrad) ≈ [0 0 0; 0 0 1/2; 0 1/2 0] end diff --git a/test/rulesets/Base/mapreduce.jl b/test/rulesets/Base/mapreduce.jl index 033e9f19e..4175fae2d 100644 --- a/test/rulesets/Base/mapreduce.jl +++ b/test/rulesets/Base/mapreduce.jl @@ -208,9 +208,8 @@ struct SumRuleConfig <: RuleConfig{Union{HasReverseMode}} end # Symmetric -- at least this doesn't have zeros, still an unlikely combination xs = Symmetric(rand(T,4,4)) @test unthunk(rrule(prod, Symmetric(T[1 2; -333 4]))[2](1.0)[2]) == [16 8; 8 4] - # TODO debug why these fail https://github.com/JuliaDiff/ChainRules.jl/issues/475 - @test_skip test_rrule(prod, xs) - @test_skip test_rrule(prod, xs, fkwargs=(dims=2,)) + test_rrule(prod, xs) + test_rrule(prod, xs, fkwargs=(dims=2,)) end end @testset "Array{Float32}, no zero entries" begin diff --git a/test/rulesets/LinearAlgebra/symmetric.jl b/test/rulesets/LinearAlgebra/symmetric.jl index 1292aa1d0..dee3d1b99 100644 --- a/test/rulesets/LinearAlgebra/symmetric.jl +++ b/test/rulesets/LinearAlgebra/symmetric.jl @@ -56,6 +56,13 @@ using ChainRulesTestUtils: _fdm # intentionally specifying tangents here to test both SymHerm (default) and Matrix test_frule(f, x) test_frule(f, x ⊢ randn(T, 3, 3)) + @test frule((NoTangent(), ZeroTangent()), f, x) == (f(x), ZeroTangent()) + + Δx = SymHerm(randn(T, 3, 3), uplo === :U ? :L : :U) + @test frule((NoTangent(), Δx), f, x)[2] == Δx + ∂x = rrule(f, x)[2](Δx)[2] + @test ∂x == Δx + @test ∂x.uplo == x.uplo end # symmetric/hermitian eigendecomposition follows the sign convention From 361583ab486a020b44a5297add080c0493039b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Thu, 29 Oct 2026 00:04:58 +0100 Subject: [PATCH 2/3] Require ChainRulesCore 1.26.7 and test Diagonal cotangents ChainRulesCore 1.26.7 (JuliaDiff/ChainRulesCore.jl#723) makes the projection of a Diagonal tangent onto Hermitian real. Without it, the Matrix(::Hermitian) rrule would keep the imaginary part of a Diagonal cotangent, which the old hand-written rule dropped. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 +- test/Project.toml | 2 +- test/rulesets/LinearAlgebra/symmetric.jl | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 42ccbfdc2..ffa365eb2 100644 --- a/Project.toml +++ b/Project.toml @@ -18,7 +18,7 @@ StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" [compat] Adapt = "3.4.0, 4" -ChainRulesCore = "1.26.6" +ChainRulesCore = "1.26.7" Distributed = "1" GPUArraysCore = "0.1.0, 0.2" IrrationalConstants = "0.1.1, 0.2" diff --git a/test/Project.toml b/test/Project.toml index ed8458d00..5525ab719 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -16,7 +16,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] Adapt = "3.4.0, 4" -ChainRulesCore = "1.26.6" +ChainRulesCore = "1.26.7" ChainRulesTestUtils = "1.5" FiniteDifferences = "0.12.35" GPUArraysCore = "0.1.0, 0.2" diff --git a/test/rulesets/LinearAlgebra/symmetric.jl b/test/rulesets/LinearAlgebra/symmetric.jl index dee3d1b99..34e4ad507 100644 --- a/test/rulesets/LinearAlgebra/symmetric.jl +++ b/test/rulesets/LinearAlgebra/symmetric.jl @@ -63,6 +63,12 @@ using ChainRulesTestUtils: _fdm ∂x = rrule(f, x)[2](Δx)[2] @test ∂x == Δx @test ∂x.uplo == x.uplo + + # a `Diagonal` cotangent stays `Diagonal` and agrees with the dense one + ΔΩ = Diagonal(randn(ComplexF64, 3)) + ∂x = rrule(f, x)[2](ΔΩ)[2] + @test ∂x isa Diagonal + @test ∂x == rrule(f, x)[2](Matrix(ΔΩ))[2] end # symmetric/hermitian eigendecomposition follows the sign convention From 32371edaea8320b4337b02870574810053a2a10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Thu, 29 Oct 2026 00:06:47 +0100 Subject: [PATCH 3/3] Rename result of Diagonal cotangent test Co-Authored-By: Claude Opus 5.5 --- test/rulesets/LinearAlgebra/symmetric.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/rulesets/LinearAlgebra/symmetric.jl b/test/rulesets/LinearAlgebra/symmetric.jl index 34e4ad507..e78b52616 100644 --- a/test/rulesets/LinearAlgebra/symmetric.jl +++ b/test/rulesets/LinearAlgebra/symmetric.jl @@ -66,9 +66,9 @@ using ChainRulesTestUtils: _fdm # a `Diagonal` cotangent stays `Diagonal` and agrees with the dense one ΔΩ = Diagonal(randn(ComplexF64, 3)) - ∂x = rrule(f, x)[2](ΔΩ)[2] - @test ∂x isa Diagonal - @test ∂x == rrule(f, x)[2](Matrix(ΔΩ))[2] + ∂x_diag = rrule(f, x)[2](ΔΩ)[2] + @test ∂x_diag isa Diagonal + @test ∂x_diag == rrule(f, x)[2](Matrix(ΔΩ))[2] end # symmetric/hermitian eigendecomposition follows the sign convention