From 2652fa445ac69d3f28c87442cae51016ece8f03e Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 29 Oct 2026 14:59:30 -0400 Subject: [PATCH 1/5] Keep tuple structure of broadcast cotangents The cotangent of a tuple-valued broadcast arrives as a `Tangent`, which Zygote constructs as `Tangent{Any}`. Broadcasting over it falls back to `collect`, so the pullbacks produced `Vector`s with abstract element types instead of tuples. Broadcast over the backing tuple instead. Also make `unzip(::Tuple)` inferable by dispatching to the `Val{N}` method, as is already done for arrays. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 +- src/rulesets/Base/broadcast.jl | 13 +++++++++---- src/unzipped.jl | 2 +- test/rulesets/Base/broadcast.jl | 14 ++++++++++++++ test/unzipped.jl | 5 +++++ 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 2a59656eb..a18c5c4b0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ChainRules" uuid = "082447d4-558c-5d27-93f4-14fc19e9eca2" -version = "1.74.8" +version = "1.74.9" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" diff --git a/src/rulesets/Base/broadcast.jl b/src/rulesets/Base/broadcast.jl index ae389b84f..726c1e031 100644 --- a/src/rulesets/Base/broadcast.jl +++ b/src/rulesets/Base/broadcast.jl @@ -2,6 +2,11 @@ using Base.Broadcast: Broadcast, broadcasted, Broadcasted, BroadcastStyle const RCR = RuleConfig{>:HasReverseMode} const TRI_NO = (NoTangent(), NoTangent(), NoTangent()) +# The cotangent of a tuple-valued broadcast is a `Tangent` (`Tangent{Any}` from Zygote). Broadcasting +# over that would `collect` it into a `Vector`, losing the element types, so use its backing tuple. +_bc_cotangent(dys::Tangent{<:Any, <:Tuple}) = ChainRulesCore.backing(dys) +_bc_cotangent(dys) = dys + function rrule(::typeof(copy), bc::Broadcasted) uncopy(Δ) = (NoTangent(), Δ) return copy(bc), uncopy @@ -59,7 +64,7 @@ function split_bc_derivatives(f::F, arg) where {F} @debug("split broadcasting derivative", f) ys = f.(arg) function bc_one_back(dys) # For f.(x) we do not need StructArrays / unzip at all - delta = broadcast(unthunk(dys), ys, arg) do dy, y, a + delta = broadcast(_bc_cotangent(unthunk(dys)), ys, arg) do dy, y, a das = only(derivatives_given_output(y, f, a)) dy * conj(only(das)) # possibly this * should be made nan-safe. end @@ -72,7 +77,7 @@ function split_bc_derivatives(f::F, args::Vararg{Any,N}) where {F,N} @debug("split broadcasting derivatives", f, N) ys = f.(args...) function bc_many_back(dys) - deltas = unzip_broadcast(unthunk(dys), ys, args...) do dy, y, as... + deltas = unzip_broadcast(_bc_cotangent(unthunk(dys)), ys, args...) do dy, y, as... das = only(derivatives_given_output(y, f, as...)) map(da -> dy * conj(da), das) # possibly this * should be made nan-safe. end @@ -110,7 +115,7 @@ function split_bc_inner(frule_fun::R, cfg::RuleConfig, f::F, arg) where {R,F} frule_fun(cfg, (NoTangent(), one(a)), f, a) end function back_forwards(dys) - delta = broadcast(ydots, unthunk(dys), arg) do ydot, dy, a + delta = broadcast(ydots, _bc_cotangent(unthunk(dys)), arg) do ydot, dy, a ProjectTo(a)(conj(ydot) * dy) # possibly this * should be made nan-safe. end return (TRI_NO..., ProjectTo(arg)(delta)) @@ -129,7 +134,7 @@ function split_bc_pullbacks(cfg::RCR, f::F, args::Vararg{Any,N}) where {F,N} rrule_via_ad(cfg, f, a...) end function back_generic(dys) - deltas = unzip_broadcast(backs, dys) do back, dy # (could be map, sizes match) + deltas = unzip_broadcast(backs, _bc_cotangent(dys)) do back, dy # (could be map, sizes match) map(unthunk, back(dy)) end dargs = map(unbroadcast, args, Base.tail(deltas)) diff --git a/src/unzipped.jl b/src/unzipped.jl index fe5875e6f..4bd9e6d63 100644 --- a/src/unzipped.jl +++ b/src/unzipped.jl @@ -132,7 +132,7 @@ julia> unzip(((1,2), (30,40), (500,600))) function unzip(xs::Tuple) x1 = first(xs) x1 isa Tuple || throw(ArgumentError("unzip only accepts arrays or tuples of tuples")) - return ntuple(i -> map(Get(i), xs),length(x1)) + return unzip(xs, Val(length(x1))) end struct Get{i} end diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index ea9bac8d0..ad37cd5c5 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -197,6 +197,20 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @test y3 == () @test bk3(Tangent{Tuple{}}()) == (NoTangent(), NoTangent(), NoTangent(), ZeroTangent(), ZeroTangent()) end + @testset "tuple broadcast with Tangent{Any} cotangent" begin + # Zygote passes the cotangent of a tuple as `Tangent{Any}`, which broadcasting used to + # `collect` into a `Vector`, losing the element types. + x = (1.0, 2.0f0) + y2, bk2 = rrule(CFG, copy∘broadcasted, BT1, log, x) # split 2 + dx2 = @inferred bk2(Tangent{Any}(1.0, 1.0f0)) + @test dx2[4] isa Tangent{typeof(x), typeof(x)} + @test all(ChainRulesCore.backing(dx2[4]) .≈ (1.0, 0.5)) + + y4, bk4 = rrule(CFG, copy∘broadcasted, BT1, sin∘cos, x) # split 4 + dx4 = bk4(Tangent{Any}(1.0, 1.0f0)) + @test dx4[4] isa Tangent{typeof(x), typeof(x)} + @test all(ChainRulesCore.backing(dx4[4]) .≈ (-cos(cos(1.0)) * sin(1.0), -cos(cos(2.0f0)) * sin(2.0f0))) + end @testset "unbroadcast with NTuple" begin # https://github.com/JuliaDiff/ChainRules.jl/pull/661 @test ChainRules.unbroadcast((1, 2, [3]), [4, 5, [6]]) isa Tangent # earlier, NTuple demanded same type @test ChainRules.unbroadcast(broadcasted(-, (1, 2), 3), (4, 5)) == (4, 5) # earlier, called ndims(::Tuple) diff --git a/test/unzipped.jl b/test/unzipped.jl index 6bc508f3f..4bc8e4c77 100644 --- a/test/unzipped.jl +++ b/test/unzipped.jl @@ -27,6 +27,11 @@ using ChainRules: unzip_broadcast, unzip #, unzip_map @test fun(tuple, (1,2,3), [4,5,6]) == ([1, 2, 3], [4, 5, 6]) # mix tuple & vector end + @testset "unzip tuple of tuples is inferred" begin + @test @inferred(unzip(((1, 2.0), (3, 4.0)))) == ((1, 3), (2.0, 4.0)) + @test @inferred(unzip_broadcast(x -> (x, 2x), (1, 2.0))) == ((1, 2.0), (2, 4.0)) + end + @testset "rrules" begin # These exist to allow for second derivatives From e391d9d09ce763f393554c5db8772101d5c74b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 11 Nov 2026 15:07:54 +0100 Subject: [PATCH 2/5] Unwrap tuple cotangents in the fused broadcast rules The fused rules for `*`, `^2`, `/` and `imag` broadcast over the cotangent too, so they need `_bc_cotangent` as well. Bump the version to 1.74.14. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 +- src/rulesets/Base/broadcast.jl | 13 +++++++------ test/rulesets/Base/broadcast.jl | 29 +++++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Project.toml b/Project.toml index faf9a412c..1d8c5e7b6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ChainRules" uuid = "082447d4-558c-5d27-93f4-14fc19e9eca2" -version = "1.74.13" +version = "1.74.14" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" diff --git a/src/rulesets/Base/broadcast.jl b/src/rulesets/Base/broadcast.jl index 726c1e031..ac626e1a2 100644 --- a/src/rulesets/Base/broadcast.jl +++ b/src/rulesets/Base/broadcast.jl @@ -2,8 +2,9 @@ using Base.Broadcast: Broadcast, broadcasted, Broadcasted, BroadcastStyle const RCR = RuleConfig{>:HasReverseMode} const TRI_NO = (NoTangent(), NoTangent(), NoTangent()) -# The cotangent of a tuple-valued broadcast is a `Tangent` (`Tangent{Any}` from Zygote). Broadcasting -# over that would `collect` it into a `Vector`, losing the element types, so use its backing tuple. +# The cotangent of a tuple-valued broadcast can be a `Tangent` (`Tangent{Any}` from Zygote). +# Broadcast over its backing tuple, since broadcasting over a `Tangent` either loses the element +# types or throws. _bc_cotangent(dys::Tangent{<:Any, <:Tuple}) = ChainRulesCore.backing(dys) _bc_cotangent(dys) = dys @@ -191,7 +192,7 @@ end function rrule(::typeof(broadcasted), ::typeof(*), x::NumericOrBroadcast, y::NumericOrBroadcast) @debug("broadcasting: times") function bc_times_back(Δraw) - Δ = unthunk(Δraw) + Δ = _bc_cotangent(unthunk(Δraw)) return (NoTangent(), NoTangent(), _back_star(x, y, Δ), _back_star(y, x, Δ)) end return broadcasted(*, x, y), bc_times_back @@ -217,7 +218,7 @@ end function rrule(::typeof(broadcasted), ::typeof(Base.literal_pow), ::typeof(^), x::NumericOrBroadcast, ::Val{2}) @debug("broadcasting: square") function bc_square_back(dy_raw) - dx = @thunk ProjectTo(x)(2 .* unthunk(dy_raw) .* conj.(x)) + dx = @thunk ProjectTo(x)(2 .* _bc_cotangent(unthunk(dy_raw)) .* conj.(x)) return (NoTangent(), NoTangent(), NoTangent(), dx, NoTangent()) end return broadcasted(Base.literal_pow, ^, x, Val(2)), bc_square_back @@ -228,7 +229,7 @@ function rrule(::typeof(broadcasted), ::typeof(/), x::NumericOrBroadcast, y::Num # z = broadcast(/, x, y) z = broadcasted(/, x, y) function bc_divide_back(dz_raw) - dz = unthunk(dz_raw) + dz = _bc_cotangent(unthunk(dz_raw)) dx = @thunk unbroadcast(x, dz ./ conj.(y)) # dy = @thunk -LinearAlgebra.dot(z, dz) / conj(y) # the reason to be eager is to allow dot here dy = @thunk -sum(Broadcast.instantiate(broadcasted(*, broadcasted(conj, z), dz))) / conj(y) # complete sum is fast @@ -299,7 +300,7 @@ rrule(::typeof(broadcasted), ::typeof(real), x::AbstractArray{<:Real}) = rrule(i function rrule(::typeof(broadcasted), ::typeof(imag), x::NumericOrBroadcast) @debug("broadcasting: imag") - bc_imag_back(dz) = (NoTangent(), NoTangent(), @thunk(im .* real.(unthunk(dz)))) + bc_imag_back(dz) = (NoTangent(), NoTangent(), @thunk(im .* real.(_bc_cotangent(unthunk(dz))))) return broadcasted(imag, x), bc_imag_back end rrule(::typeof(broadcasted), ::typeof(imag), x::Number) = rrule(imag, x) |> _prepend_zero diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index ad37cd5c5..7825cd507 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -95,6 +95,9 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @gpu test_rrule(copy∘broadcasted, *, rand(3), rand(3)) @gpu test_rrule(copy∘broadcasted, *, rand(3), rand()) @gpu test_rrule(copy∘broadcasted, *, rand(), rand(3)) + test_rrule(copy∘broadcasted, *, Tuple(rand(3)), rand()) + test_rrule(copy∘broadcasted, *, rand(), Tuple(rand(3))) + test_rrule(copy∘broadcasted, *, Tuple(rand(3)), Tuple(rand(3))) test_rrule(copy∘broadcasted, *, rand(3) .+ im, rand(3) .+ 2im) test_rrule(copy∘broadcasted, *, rand(3) .+ im, rand() + 3im) @@ -121,9 +124,11 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @gpu test_rrule(copy∘broadcasted, Base.literal_pow, ^, rand(3), Val(2)) @gpu test_rrule(copy∘broadcasted, Base.literal_pow, ^, rand(3) .+ im, Val(2)) + test_rrule(copy∘broadcasted, Base.literal_pow, ^, Tuple(rand(3)), Val(2)) @gpu test_rrule(copy∘broadcasted, /, rand(3), rand()) @gpu test_rrule(copy∘broadcasted, /, rand(3) .+ im, rand() + 3im) + test_rrule(copy∘broadcasted, /, Tuple(rand(3)), rand()) end @testset "identity etc" begin test_rrule(copy∘broadcasted, identity, rand(3)) @@ -144,6 +149,8 @@ BT1 = Broadcast.BroadcastStyle(Tuple) test_rrule(copy∘broadcasted, imag, rand(3)) test_rrule(copy∘broadcasted, imag, rand(3) .+ im .* rand.()) + test_rrule(copy∘broadcasted, imag, Tuple(rand(3))) + test_rrule(copy∘broadcasted, imag, Tuple(rand(3) .+ im .* rand.())) test_rrule(copy∘broadcasted, complex, rand(3)) end @@ -198,8 +205,6 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @test bk3(Tangent{Tuple{}}()) == (NoTangent(), NoTangent(), NoTangent(), ZeroTangent(), ZeroTangent()) end @testset "tuple broadcast with Tangent{Any} cotangent" begin - # Zygote passes the cotangent of a tuple as `Tangent{Any}`, which broadcasting used to - # `collect` into a `Vector`, losing the element types. x = (1.0, 2.0f0) y2, bk2 = rrule(CFG, copy∘broadcasted, BT1, log, x) # split 2 dx2 = @inferred bk2(Tangent{Any}(1.0, 1.0f0)) @@ -210,6 +215,26 @@ BT1 = Broadcast.BroadcastStyle(Tuple) dx4 = bk4(Tangent{Any}(1.0, 1.0f0)) @test dx4[4] isa Tangent{typeof(x), typeof(x)} @test all(ChainRulesCore.backing(dx4[4]) .≈ (-cos(cos(1.0)) * sin(1.0), -cos(cos(2.0f0)) * sin(2.0f0))) + + @test bk2(@thunk(Tangent{Any}(1.0, 1.0f0))) == dx2 + @test bk4(@thunk(Tangent{Any}(1.0, 1.0f0))) == dx4 + + _, bkm = rrule(CFG, copy∘broadcasted, BT1, atan, x, x) # split 2, several arguments + dxm = @inferred bkm(Tangent{Any}(1.0, 1.0f0)) + @test dxm[4] isa Tangent{typeof(x), typeof(x)} + @test dxm[5] isa Tangent{typeof(x), typeof(x)} + + x3 = (1.0, 2.0) # split 3 needs a concrete eltype + _, bk3 = rrule(CFG, copy∘broadcasted, BT1, flog, x3) + dx3 = bk3(Tangent{Any}(1.0, 1.0)) + @test dx3[4] isa Tangent{typeof(x3), typeof(x3)} + @test all(ChainRulesCore.backing(dx3[4]) .≈ (1.0, 0.5)) + + dy = Tangent{Any}(1.0, 1.0f0) # fused rules + @test unthunk(rrule(CFG, copy∘broadcasted, *, x, 2.0)[2](dy)[3]) isa Tangent{typeof(x)} + @test unthunk(rrule(CFG, copy∘broadcasted, Base.literal_pow, ^, x, Val(2))[2](dy)[4]) isa Tangent{typeof(x)} + @test unthunk(rrule(CFG, copy∘broadcasted, /, x, 2.0)[2](dy)[3]) isa Tangent{typeof(x)} + @test unthunk(rrule(CFG, copy∘broadcasted, imag, x)[2](dy)[3]) isa Tangent{typeof(x)} end @testset "unbroadcast with NTuple" begin # https://github.com/JuliaDiff/ChainRules.jl/pull/661 @test ChainRules.unbroadcast((1, 2, [3]), [4, 5, [6]]) isa Tangent # earlier, NTuple demanded same type From 3408a2b3984c87bfd9865f535de8bfd21ed92442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 11 Nov 2026 15:12:26 +0100 Subject: [PATCH 3/5] Fix tuple cotangents in the remaining fused broadcast rules - `+`, binary `-`, `T.`, `float.` and `complex.` pass the cotangent to `unbroadcast`, which fails for a `Tangent` cotangent when an argument is a lazy `Broadcasted` (e.g. `float.(real.(x))`), so unwrap it first. - Unary `-`, `conj`, `real` and `imag` applied their function to the whole cotangent (`real(::Tangent)`, `conj(::Tuple)` etc. have no method) and `imag` returned an unprojected `Tuple`. Broadcast over the unwrapped cotangent and project with `unbroadcast`. - Drop the `@thunk` from the single-input fused rules (unary `-`, `^2`, `T.`, `float.`, `conj`, `real`, `imag`, `complex`): their only cotangent is always needed, so the thunk is pure overhead. Co-Authored-By: Claude Opus 5.5 --- src/rulesets/Base/broadcast.jl | 20 +++++++++---------- test/rulesets/Base/broadcast.jl | 35 ++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/rulesets/Base/broadcast.jl b/src/rulesets/Base/broadcast.jl index ac626e1a2..99e590560 100644 --- a/src/rulesets/Base/broadcast.jl +++ b/src/rulesets/Base/broadcast.jl @@ -168,7 +168,7 @@ const NumericOrBroadcast = Union{Number, AbstractArray{<:Number}, NTuple{<:Any,N function rrule(::typeof(broadcasted), ::typeof(+), xs::NumericOrBroadcast...) @debug("broadcasting: plus", length(xs)) function bc_plus_back(dy_raw) - dy = unthunk(dy_raw) + dy = _bc_cotangent(unthunk(dy_raw)) return (NoTangent(), NoTangent(), map(x -> unbroadcast(x, dy), xs)...) # no copies, this may return dx2 === dx3 end return broadcasted(+, xs...), bc_plus_back @@ -177,7 +177,7 @@ end function rrule(::typeof(broadcasted), ::typeof(-), x::NumericOrBroadcast, y::NumericOrBroadcast) @debug("broadcasting: minus 2") function bc_minus_back(dz_raw) - dz = unthunk(dz_raw) + dz = _bc_cotangent(unthunk(dz_raw)) return (NoTangent(), NoTangent(), @thunk(unbroadcast(x, dz)), @thunk(-unbroadcast(y, dz))) end return broadcasted(-, x, y), bc_minus_back @@ -185,7 +185,7 @@ end function rrule(::typeof(broadcasted), ::typeof(-), x::NumericOrBroadcast) @debug("broadcasting: minus 1") - bc_minus_back(dy) = (NoTangent(), NoTangent(), @thunk -unthunk(dy)) + bc_minus_back(dy) = (NoTangent(), NoTangent(), unbroadcast(x, .-_bc_cotangent(unthunk(dy)))) return broadcasted(-, x), bc_minus_back end @@ -218,7 +218,7 @@ end function rrule(::typeof(broadcasted), ::typeof(Base.literal_pow), ::typeof(^), x::NumericOrBroadcast, ::Val{2}) @debug("broadcasting: square") function bc_square_back(dy_raw) - dx = @thunk ProjectTo(x)(2 .* _bc_cotangent(unthunk(dy_raw)) .* conj.(x)) + dx = ProjectTo(x)(2 .* _bc_cotangent(unthunk(dy_raw)) .* conj.(x)) return (NoTangent(), NoTangent(), NoTangent(), dx, NoTangent()) end return broadcasted(Base.literal_pow, ^, x, Val(2)), bc_square_back @@ -263,14 +263,14 @@ rrule(::typeof(broadcasted), ::typeof(identity), x::Number) = rrule(identity, x) function rrule(::typeof(broadcasted), ::Type{T}, x::NumericOrBroadcast) where {T<:Number} @debug("broadcasting: type", T) - bc_type_back(dz) = (NoTangent(), NoTangent(), @thunk(unbroadcast(x, unthunk(dz)))) + bc_type_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, _bc_cotangent(unthunk(dz)))) return broadcasted(T, x), bc_type_back end rrule(::typeof(broadcasted), ::Type{T}, x::Number) where {T<:Number} = rrule(T, x) |> _prepend_zero function rrule(::typeof(broadcasted), ::typeof(float), x::NumericOrBroadcast) @debug("broadcasting: float") - bc_float_back(dz) = (NoTangent(), NoTangent(), @thunk(unbroadcast(x, unthunk(dz)))) + bc_float_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, _bc_cotangent(unthunk(dz)))) return broadcasted(float, x), bc_float_back end rrule(::typeof(broadcasted), ::typeof(float), x::Number) = rrule(float, x) |> _prepend_zero @@ -280,7 +280,7 @@ rrule(::typeof(broadcasted), ::typeof(float), x::Number) = rrule(float, x) |> _p for conj in [:conj, :adjoint] # identical as we know eltype <: Number @eval begin function rrule(::typeof(broadcasted), ::typeof($conj), x::NumericOrBroadcast) - bc_conj_back(dx) = (NoTangent(), NoTangent(), conj(unthunk(dx))) + bc_conj_back(dx) = (NoTangent(), NoTangent(), unbroadcast(x, conj.(_bc_cotangent(unthunk(dx))))) return broadcasted($conj, x), bc_conj_back end rrule(::typeof(broadcasted), ::typeof($conj), x::Number) = rrule($conj, x) |> _prepend_zero @@ -292,7 +292,7 @@ end function rrule(::typeof(broadcasted), ::typeof(real), x::NumericOrBroadcast) @debug("broadcasting: real") - bc_real_back(dz) = (NoTangent(), NoTangent(), @thunk(real(unthunk(dz)))) + bc_real_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, real.(_bc_cotangent(unthunk(dz))))) return broadcasted(real, x), bc_real_back end rrule(::typeof(broadcasted), ::typeof(real), x::Number) = rrule(real, x) |> _prepend_zero @@ -300,7 +300,7 @@ rrule(::typeof(broadcasted), ::typeof(real), x::AbstractArray{<:Real}) = rrule(i function rrule(::typeof(broadcasted), ::typeof(imag), x::NumericOrBroadcast) @debug("broadcasting: imag") - bc_imag_back(dz) = (NoTangent(), NoTangent(), @thunk(im .* real.(_bc_cotangent(unthunk(dz))))) + bc_imag_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, im .* real.(_bc_cotangent(unthunk(dz))))) return broadcasted(imag, x), bc_imag_back end rrule(::typeof(broadcasted), ::typeof(imag), x::Number) = rrule(imag, x) |> _prepend_zero @@ -312,7 +312,7 @@ end function rrule(::typeof(broadcasted), ::typeof(complex), x::NumericOrBroadcast) @debug("broadcasting: complex") - bc_complex_back(dz) = (NoTangent(), NoTangent(), @thunk(unbroadcast(x, unthunk(dz)))) + bc_complex_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, _bc_cotangent(unthunk(dz)))) return broadcasted(complex, x), bc_complex_back end rrule(::typeof(broadcasted), ::typeof(complex), x::Number) = rrule(complex, x) |> _prepend_zero diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index 7825cd507..43c922fa1 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -141,11 +141,15 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @testset "complex" begin test_rrule(copy∘broadcasted, conj, rand(3)) test_rrule(copy∘broadcasted, conj, rand(3) .+ im) + test_rrule(copy∘broadcasted, conj, Tuple(rand(3))) + test_rrule(copy∘broadcasted, conj, Tuple(rand(3) .+ im .* rand.())) test_rrule(copy∘broadcasted, adjoint, rand(3)) test_rrule(copy∘broadcasted, adjoint, rand(3) .+ im) test_rrule(copy∘broadcasted, real, rand(3)) test_rrule(copy∘broadcasted, real, rand(3) .+ im) + test_rrule(copy∘broadcasted, real, Tuple(rand(3))) + test_rrule(copy∘broadcasted, real, Tuple(rand(3) .+ im .* rand.())) test_rrule(copy∘broadcasted, imag, rand(3)) test_rrule(copy∘broadcasted, imag, rand(3) .+ im .* rand.()) @@ -230,11 +234,32 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @test dx3[4] isa Tangent{typeof(x3), typeof(x3)} @test all(ChainRulesCore.backing(dx3[4]) .≈ (1.0, 0.5)) - dy = Tangent{Any}(1.0, 1.0f0) # fused rules - @test unthunk(rrule(CFG, copy∘broadcasted, *, x, 2.0)[2](dy)[3]) isa Tangent{typeof(x)} - @test unthunk(rrule(CFG, copy∘broadcasted, Base.literal_pow, ^, x, Val(2))[2](dy)[4]) isa Tangent{typeof(x)} - @test unthunk(rrule(CFG, copy∘broadcasted, /, x, 2.0)[2](dy)[3]) isa Tangent{typeof(x)} - @test unthunk(rrule(CFG, copy∘broadcasted, imag, x)[2](dy)[3]) isa Tangent{typeof(x)} + # Fused rules + dy = Tangent{Any}(1.0, 1.0f0) + _, _, dxt, dyt = map(unthunk, rrule(CFG, copy∘broadcasted, *, x, 2.0)[2](dy)) + @test dxt == Tangent{typeof(x)}(2.0, 2.0f0) + @test dyt ≈ 3.0 + _, _, _, dxs, _ = map(unthunk, rrule(CFG, copy∘broadcasted, Base.literal_pow, ^, x, Val(2))[2](dy)) + @test dxs == Tangent{typeof(x)}(2.0, 4.0f0) + _, _, dxd, dyd = map(unthunk, rrule(CFG, copy∘broadcasted, /, x, 2.0)[2](dy)) + @test dxd == Tangent{typeof(x)}(0.5, 0.5f0) + @test dyd ≈ -0.75 + xc = (1.0 + 2im, 3.0 - 1im) + @test unthunk(rrule(CFG, copy∘broadcasted, real, xc)[2](dy)[3]) == Tangent{typeof(xc)}(1.0 + 0im, 1.0 + 0im) + @test unthunk(rrule(CFG, copy∘broadcasted, imag, xc)[2](dy)[3]) == Tangent{typeof(xc)}(1.0im, 1.0im) + dyc = Tangent{Any}(1.0 + 1im, 2.0 - 1im) + @test unthunk(rrule(CFG, copy∘broadcasted, conj, xc)[2](dyc)[3]) == Tangent{typeof(xc)}(1.0 - 1im, 2.0 + 1im) + # e.g. from `real.(conj.(x))`, where the outer rule returns a `Tuple` cotangent + @test unthunk(rrule(CFG, copy∘broadcasted, conj, xc)[2]((1.0, 1.0))[3]) == Tangent{typeof(xc)}(1.0 + 0im, 1.0 + 0im) + @test rrule(CFG, copy∘broadcasted, -, x)[2]((1.0, 1.0f0))[3] == Tangent{typeof(x)}(-1.0, -1.0f0) + + # Lazy arguments, e.g. from `float.(real.(x))` + xl = broadcasted(real, x) + @test unthunk(rrule(broadcasted, +, xl, 1.0)[2](dy)[3]) == (1.0, 1.0f0) + @test unthunk(rrule(broadcasted, -, xl, 1.0)[2](dy)[3]) == (1.0, 1.0f0) + @test rrule(broadcasted, float, xl)[2](dy)[3] == (1.0, 1.0f0) + @test rrule(broadcasted, Float32, xl)[2](dy)[3] == (1.0, 1.0f0) + @test rrule(broadcasted, complex, xl)[2](dy)[3] == (1.0, 1.0f0) end @testset "unbroadcast with NTuple" begin # https://github.com/JuliaDiff/ChainRules.jl/pull/661 @test ChainRules.unbroadcast((1, 2, [3]), [4, 5, [6]]) isa Tangent # earlier, NTuple demanded same type From 1613b10f3ec8caebc5aa5bccbae088582bf86376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 11 Nov 2026 22:07:34 +0100 Subject: [PATCH 4/5] Apply JuliaFormatter suggestions Co-Authored-By: Claude Opus 5.5 --- src/rulesets/Base/broadcast.jl | 26 +++++--- test/rulesets/Base/broadcast.jl | 103 ++++++++++++++++++-------------- 2 files changed, 75 insertions(+), 54 deletions(-) diff --git a/src/rulesets/Base/broadcast.jl b/src/rulesets/Base/broadcast.jl index 99e590560..a4185ddf0 100644 --- a/src/rulesets/Base/broadcast.jl +++ b/src/rulesets/Base/broadcast.jl @@ -5,7 +5,7 @@ const TRI_NO = (NoTangent(), NoTangent(), NoTangent()) # The cotangent of a tuple-valued broadcast can be a `Tangent` (`Tangent{Any}` from Zygote). # Broadcast over its backing tuple, since broadcasting over a `Tangent` either loses the element # types or throws. -_bc_cotangent(dys::Tangent{<:Any, <:Tuple}) = ChainRulesCore.backing(dys) +_bc_cotangent(dys::Tangent{<:Any,<:Tuple}) = ChainRulesCore.backing(dys) _bc_cotangent(dys) = dys function rrule(::typeof(copy), bc::Broadcasted) @@ -185,7 +185,8 @@ end function rrule(::typeof(broadcasted), ::typeof(-), x::NumericOrBroadcast) @debug("broadcasting: minus 1") - bc_minus_back(dy) = (NoTangent(), NoTangent(), unbroadcast(x, .-_bc_cotangent(unthunk(dy)))) + bc_minus_back(dy) = + (NoTangent(), NoTangent(), unbroadcast(x, .-_bc_cotangent(unthunk(dy)))) return broadcasted(-, x), bc_minus_back end @@ -263,14 +264,16 @@ rrule(::typeof(broadcasted), ::typeof(identity), x::Number) = rrule(identity, x) function rrule(::typeof(broadcasted), ::Type{T}, x::NumericOrBroadcast) where {T<:Number} @debug("broadcasting: type", T) - bc_type_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, _bc_cotangent(unthunk(dz)))) + bc_type_back(dz) = + (NoTangent(), NoTangent(), unbroadcast(x, _bc_cotangent(unthunk(dz)))) return broadcasted(T, x), bc_type_back end rrule(::typeof(broadcasted), ::Type{T}, x::Number) where {T<:Number} = rrule(T, x) |> _prepend_zero function rrule(::typeof(broadcasted), ::typeof(float), x::NumericOrBroadcast) @debug("broadcasting: float") - bc_float_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, _bc_cotangent(unthunk(dz)))) + bc_float_back(dz) = + (NoTangent(), NoTangent(), unbroadcast(x, _bc_cotangent(unthunk(dz)))) return broadcasted(float, x), bc_float_back end rrule(::typeof(broadcasted), ::typeof(float), x::Number) = rrule(float, x) |> _prepend_zero @@ -280,7 +283,11 @@ rrule(::typeof(broadcasted), ::typeof(float), x::Number) = rrule(float, x) |> _p for conj in [:conj, :adjoint] # identical as we know eltype <: Number @eval begin function rrule(::typeof(broadcasted), ::typeof($conj), x::NumericOrBroadcast) - bc_conj_back(dx) = (NoTangent(), NoTangent(), unbroadcast(x, conj.(_bc_cotangent(unthunk(dx))))) + bc_conj_back(dx) = ( + NoTangent(), + NoTangent(), + unbroadcast(x, conj.(_bc_cotangent(unthunk(dx)))), + ) return broadcasted($conj, x), bc_conj_back end rrule(::typeof(broadcasted), ::typeof($conj), x::Number) = rrule($conj, x) |> _prepend_zero @@ -292,7 +299,8 @@ end function rrule(::typeof(broadcasted), ::typeof(real), x::NumericOrBroadcast) @debug("broadcasting: real") - bc_real_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, real.(_bc_cotangent(unthunk(dz))))) + bc_real_back(dz) = + (NoTangent(), NoTangent(), unbroadcast(x, real.(_bc_cotangent(unthunk(dz))))) return broadcasted(real, x), bc_real_back end rrule(::typeof(broadcasted), ::typeof(real), x::Number) = rrule(real, x) |> _prepend_zero @@ -300,7 +308,8 @@ rrule(::typeof(broadcasted), ::typeof(real), x::AbstractArray{<:Real}) = rrule(i function rrule(::typeof(broadcasted), ::typeof(imag), x::NumericOrBroadcast) @debug("broadcasting: imag") - bc_imag_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, im .* real.(_bc_cotangent(unthunk(dz))))) + bc_imag_back(dz) = + (NoTangent(), NoTangent(), unbroadcast(x, im .* real.(_bc_cotangent(unthunk(dz))))) return broadcasted(imag, x), bc_imag_back end rrule(::typeof(broadcasted), ::typeof(imag), x::Number) = rrule(imag, x) |> _prepend_zero @@ -312,7 +321,8 @@ end function rrule(::typeof(broadcasted), ::typeof(complex), x::NumericOrBroadcast) @debug("broadcasting: complex") - bc_complex_back(dz) = (NoTangent(), NoTangent(), unbroadcast(x, _bc_cotangent(unthunk(dz)))) + bc_complex_back(dz) = + (NoTangent(), NoTangent(), unbroadcast(x, _bc_cotangent(unthunk(dz)))) return broadcasted(complex, x), bc_complex_back end rrule(::typeof(broadcasted), ::typeof(complex), x::Number) = rrule(complex, x) |> _prepend_zero diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index 43c922fa1..01d897555 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -92,12 +92,12 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @gpu test_rrule(copy∘broadcasted, -, rand(3)) test_rrule(copy∘broadcasted, -, Tuple(rand(3))) - @gpu test_rrule(copy∘broadcasted, *, rand(3), rand(3)) - @gpu test_rrule(copy∘broadcasted, *, rand(3), rand()) - @gpu test_rrule(copy∘broadcasted, *, rand(), rand(3)) - test_rrule(copy∘broadcasted, *, Tuple(rand(3)), rand()) - test_rrule(copy∘broadcasted, *, rand(), Tuple(rand(3))) - test_rrule(copy∘broadcasted, *, Tuple(rand(3)), Tuple(rand(3))) + @gpu test_rrule(copy ∘ broadcasted, *, rand(3), rand(3)) + @gpu test_rrule(copy ∘ broadcasted, *, rand(3), rand()) + @gpu test_rrule(copy ∘ broadcasted, *, rand(), rand(3)) + test_rrule(copy ∘ broadcasted, *, Tuple(rand(3)), rand()) + test_rrule(copy ∘ broadcasted, *, rand(), Tuple(rand(3))) + test_rrule(copy ∘ broadcasted, *, Tuple(rand(3)), Tuple(rand(3))) test_rrule(copy∘broadcasted, *, rand(3) .+ im, rand(3) .+ 2im) test_rrule(copy∘broadcasted, *, rand(3) .+ im, rand() + 3im) @@ -122,13 +122,13 @@ BT1 = Broadcast.BroadcastStyle(Tuple) # GPU error from dot(x::JLArray{Float32, 1}, y::JLArray{ComplexF32, 2}) end - @gpu test_rrule(copy∘broadcasted, Base.literal_pow, ^, rand(3), Val(2)) - @gpu test_rrule(copy∘broadcasted, Base.literal_pow, ^, rand(3) .+ im, Val(2)) - test_rrule(copy∘broadcasted, Base.literal_pow, ^, Tuple(rand(3)), Val(2)) + @gpu test_rrule(copy ∘ broadcasted, Base.literal_pow, ^, rand(3), Val(2)) + @gpu test_rrule(copy ∘ broadcasted, Base.literal_pow, ^, rand(3) .+ im, Val(2)) + test_rrule(copy ∘ broadcasted, Base.literal_pow, ^, Tuple(rand(3)), Val(2)) - @gpu test_rrule(copy∘broadcasted, /, rand(3), rand()) - @gpu test_rrule(copy∘broadcasted, /, rand(3) .+ im, rand() + 3im) - test_rrule(copy∘broadcasted, /, Tuple(rand(3)), rand()) + @gpu test_rrule(copy ∘ broadcasted, /, rand(3), rand()) + @gpu test_rrule(copy ∘ broadcasted, /, rand(3) .+ im, rand() + 3im) + test_rrule(copy ∘ broadcasted, /, Tuple(rand(3)), rand()) end @testset "identity etc" begin test_rrule(copy∘broadcasted, identity, rand(3)) @@ -139,22 +139,22 @@ BT1 = Broadcast.BroadcastStyle(Tuple) test_rrule(copy∘broadcasted, float, rand(3)) end @testset "complex" begin - test_rrule(copy∘broadcasted, conj, rand(3)) - test_rrule(copy∘broadcasted, conj, rand(3) .+ im) - test_rrule(copy∘broadcasted, conj, Tuple(rand(3))) - test_rrule(copy∘broadcasted, conj, Tuple(rand(3) .+ im .* rand.())) - test_rrule(copy∘broadcasted, adjoint, rand(3)) - test_rrule(copy∘broadcasted, adjoint, rand(3) .+ im) - - test_rrule(copy∘broadcasted, real, rand(3)) - test_rrule(copy∘broadcasted, real, rand(3) .+ im) - test_rrule(copy∘broadcasted, real, Tuple(rand(3))) - test_rrule(copy∘broadcasted, real, Tuple(rand(3) .+ im .* rand.())) - - test_rrule(copy∘broadcasted, imag, rand(3)) - test_rrule(copy∘broadcasted, imag, rand(3) .+ im .* rand.()) - test_rrule(copy∘broadcasted, imag, Tuple(rand(3))) - test_rrule(copy∘broadcasted, imag, Tuple(rand(3) .+ im .* rand.())) + test_rrule(copy ∘ broadcasted, conj, rand(3)) + test_rrule(copy ∘ broadcasted, conj, rand(3) .+ im) + test_rrule(copy ∘ broadcasted, conj, Tuple(rand(3))) + test_rrule(copy ∘ broadcasted, conj, Tuple(rand(3) .+ im .* rand.())) + test_rrule(copy ∘ broadcasted, adjoint, rand(3)) + test_rrule(copy ∘ broadcasted, adjoint, rand(3) .+ im) + + test_rrule(copy ∘ broadcasted, real, rand(3)) + test_rrule(copy ∘ broadcasted, real, rand(3) .+ im) + test_rrule(copy ∘ broadcasted, real, Tuple(rand(3))) + test_rrule(copy ∘ broadcasted, real, Tuple(rand(3) .+ im .* rand.())) + + test_rrule(copy ∘ broadcasted, imag, rand(3)) + test_rrule(copy ∘ broadcasted, imag, rand(3) .+ im .* rand.()) + test_rrule(copy ∘ broadcasted, imag, Tuple(rand(3))) + test_rrule(copy ∘ broadcasted, imag, Tuple(rand(3) .+ im .* rand.())) test_rrule(copy∘broadcasted, complex, rand(3)) end @@ -210,48 +210,59 @@ BT1 = Broadcast.BroadcastStyle(Tuple) end @testset "tuple broadcast with Tangent{Any} cotangent" begin x = (1.0, 2.0f0) - y2, bk2 = rrule(CFG, copy∘broadcasted, BT1, log, x) # split 2 + y2, bk2 = rrule(CFG, copy ∘ broadcasted, BT1, log, x) # split 2 dx2 = @inferred bk2(Tangent{Any}(1.0, 1.0f0)) - @test dx2[4] isa Tangent{typeof(x), typeof(x)} + @test dx2[4] isa Tangent{typeof(x),typeof(x)} @test all(ChainRulesCore.backing(dx2[4]) .≈ (1.0, 0.5)) - y4, bk4 = rrule(CFG, copy∘broadcasted, BT1, sin∘cos, x) # split 4 + y4, bk4 = rrule(CFG, copy ∘ broadcasted, BT1, sin ∘ cos, x) # split 4 dx4 = bk4(Tangent{Any}(1.0, 1.0f0)) - @test dx4[4] isa Tangent{typeof(x), typeof(x)} - @test all(ChainRulesCore.backing(dx4[4]) .≈ (-cos(cos(1.0)) * sin(1.0), -cos(cos(2.0f0)) * sin(2.0f0))) + @test dx4[4] isa Tangent{typeof(x),typeof(x)} + @test all( + ChainRulesCore.backing(dx4[4]) .≈ + (-cos(cos(1.0)) * sin(1.0), -cos(cos(2.0f0)) * sin(2.0f0)), + ) @test bk2(@thunk(Tangent{Any}(1.0, 1.0f0))) == dx2 @test bk4(@thunk(Tangent{Any}(1.0, 1.0f0))) == dx4 - _, bkm = rrule(CFG, copy∘broadcasted, BT1, atan, x, x) # split 2, several arguments + _, bkm = rrule(CFG, copy ∘ broadcasted, BT1, atan, x, x) # split 2, several arguments dxm = @inferred bkm(Tangent{Any}(1.0, 1.0f0)) - @test dxm[4] isa Tangent{typeof(x), typeof(x)} - @test dxm[5] isa Tangent{typeof(x), typeof(x)} + @test dxm[4] isa Tangent{typeof(x),typeof(x)} + @test dxm[5] isa Tangent{typeof(x),typeof(x)} x3 = (1.0, 2.0) # split 3 needs a concrete eltype - _, bk3 = rrule(CFG, copy∘broadcasted, BT1, flog, x3) + _, bk3 = rrule(CFG, copy ∘ broadcasted, BT1, flog, x3) dx3 = bk3(Tangent{Any}(1.0, 1.0)) - @test dx3[4] isa Tangent{typeof(x3), typeof(x3)} + @test dx3[4] isa Tangent{typeof(x3),typeof(x3)} @test all(ChainRulesCore.backing(dx3[4]) .≈ (1.0, 0.5)) # Fused rules dy = Tangent{Any}(1.0, 1.0f0) - _, _, dxt, dyt = map(unthunk, rrule(CFG, copy∘broadcasted, *, x, 2.0)[2](dy)) + _, _, dxt, dyt = map(unthunk, rrule(CFG, copy ∘ broadcasted, *, x, 2.0)[2](dy)) @test dxt == Tangent{typeof(x)}(2.0, 2.0f0) @test dyt ≈ 3.0 - _, _, _, dxs, _ = map(unthunk, rrule(CFG, copy∘broadcasted, Base.literal_pow, ^, x, Val(2))[2](dy)) + _, _, _, dxs, _ = map( + unthunk, + rrule(CFG, copy ∘ broadcasted, Base.literal_pow, ^, x, Val(2))[2](dy), + ) @test dxs == Tangent{typeof(x)}(2.0, 4.0f0) - _, _, dxd, dyd = map(unthunk, rrule(CFG, copy∘broadcasted, /, x, 2.0)[2](dy)) + _, _, dxd, dyd = map(unthunk, rrule(CFG, copy ∘ broadcasted, /, x, 2.0)[2](dy)) @test dxd == Tangent{typeof(x)}(0.5, 0.5f0) @test dyd ≈ -0.75 xc = (1.0 + 2im, 3.0 - 1im) - @test unthunk(rrule(CFG, copy∘broadcasted, real, xc)[2](dy)[3]) == Tangent{typeof(xc)}(1.0 + 0im, 1.0 + 0im) - @test unthunk(rrule(CFG, copy∘broadcasted, imag, xc)[2](dy)[3]) == Tangent{typeof(xc)}(1.0im, 1.0im) + @test unthunk(rrule(CFG, copy ∘ broadcasted, real, xc)[2](dy)[3]) == + Tangent{typeof(xc)}(1.0 + 0im, 1.0 + 0im) + @test unthunk(rrule(CFG, copy ∘ broadcasted, imag, xc)[2](dy)[3]) == + Tangent{typeof(xc)}(1.0im, 1.0im) dyc = Tangent{Any}(1.0 + 1im, 2.0 - 1im) - @test unthunk(rrule(CFG, copy∘broadcasted, conj, xc)[2](dyc)[3]) == Tangent{typeof(xc)}(1.0 - 1im, 2.0 + 1im) + @test unthunk(rrule(CFG, copy ∘ broadcasted, conj, xc)[2](dyc)[3]) == + Tangent{typeof(xc)}(1.0 - 1im, 2.0 + 1im) # e.g. from `real.(conj.(x))`, where the outer rule returns a `Tuple` cotangent - @test unthunk(rrule(CFG, copy∘broadcasted, conj, xc)[2]((1.0, 1.0))[3]) == Tangent{typeof(xc)}(1.0 + 0im, 1.0 + 0im) - @test rrule(CFG, copy∘broadcasted, -, x)[2]((1.0, 1.0f0))[3] == Tangent{typeof(x)}(-1.0, -1.0f0) + @test unthunk(rrule(CFG, copy ∘ broadcasted, conj, xc)[2]((1.0, 1.0))[3]) == + Tangent{typeof(xc)}(1.0 + 0im, 1.0 + 0im) + @test rrule(CFG, copy ∘ broadcasted, -, x)[2]((1.0, 1.0f0))[3] == + Tangent{typeof(x)}(-1.0, -1.0f0) # Lazy arguments, e.g. from `float.(real.(x))` xl = broadcasted(real, x) From f1f3b782787534c7caabf386e32677791afc2928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Thu, 12 Nov 2026 00:05:15 +0100 Subject: [PATCH 5/5] Project all cotangents in the fused broadcast rules with `unbroadcast` - `*` with a scalar argument and `/` (cotangent of the divisor) returned a complex cotangent for a real scalar; they now go through `unbroadcast`, which projects it. - `^2` uses `unbroadcast` like the other rules (no change in behaviour). - `real` dispatches on the cotangent: `real(dz)` for arrays doesn't copy a real cotangent, `real.(dz)` for tuples. Co-Authored-By: Claude Opus 5.5 --- src/rulesets/Base/broadcast.jl | 14 ++++++++++---- test/rulesets/Base/broadcast.jl | 12 +++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/rulesets/Base/broadcast.jl b/src/rulesets/Base/broadcast.jl index a4185ddf0..16ba6466e 100644 --- a/src/rulesets/Base/broadcast.jl +++ b/src/rulesets/Base/broadcast.jl @@ -199,7 +199,7 @@ function rrule(::typeof(broadcasted), ::typeof(*), x::NumericOrBroadcast, y::Num return broadcasted(*, x, y), bc_times_back end _back_star(x, y, Δ) = @thunk unbroadcast(x, Δ .* conj.(y)) # this case probably isn't better than generic -_back_star(x::Number, y, Δ) = @thunk LinearAlgebra.dot(y, Δ) # ... but this is why the rule exists +_back_star(x::Number, y, Δ) = @thunk unbroadcast(x, LinearAlgebra.dot(y, Δ)) # ... but this is why the rule exists _back_star(x::Bool, y, Δ) = NoTangent() _back_star(x::Complex{Bool}, y, Δ) = NoTangent() # e.g. for fun.(im.*x) @@ -219,7 +219,7 @@ end function rrule(::typeof(broadcasted), ::typeof(Base.literal_pow), ::typeof(^), x::NumericOrBroadcast, ::Val{2}) @debug("broadcasting: square") function bc_square_back(dy_raw) - dx = ProjectTo(x)(2 .* _bc_cotangent(unthunk(dy_raw)) .* conj.(x)) + dx = unbroadcast(x, 2 .* _bc_cotangent(unthunk(dy_raw)) .* conj.(x)) return (NoTangent(), NoTangent(), NoTangent(), dx, NoTangent()) end return broadcasted(Base.literal_pow, ^, x, Val(2)), bc_square_back @@ -233,7 +233,11 @@ function rrule(::typeof(broadcasted), ::typeof(/), x::NumericOrBroadcast, y::Num dz = _bc_cotangent(unthunk(dz_raw)) dx = @thunk unbroadcast(x, dz ./ conj.(y)) # dy = @thunk -LinearAlgebra.dot(z, dz) / conj(y) # the reason to be eager is to allow dot here - dy = @thunk -sum(Broadcast.instantiate(broadcasted(*, broadcasted(conj, z), dz))) / conj(y) # complete sum is fast + # complete sum is fast + dy = @thunk unbroadcast( + y, + -sum(Broadcast.instantiate(broadcasted(*, broadcasted(conj, z), dz))) / conj(y), + ) return (NoTangent(), NoTangent(), dx, dy) end return z, bc_divide_back @@ -300,9 +304,11 @@ end function rrule(::typeof(broadcasted), ::typeof(real), x::NumericOrBroadcast) @debug("broadcasting: real") bc_real_back(dz) = - (NoTangent(), NoTangent(), unbroadcast(x, real.(_bc_cotangent(unthunk(dz))))) + (NoTangent(), NoTangent(), unbroadcast(x, _real(_bc_cotangent(unthunk(dz))))) return broadcasted(real, x), bc_real_back end +_real(dz) = real(dz) # doesn't copy a real array +_real(dz::Tuple) = real.(dz) rrule(::typeof(broadcasted), ::typeof(real), x::Number) = rrule(real, x) |> _prepend_zero rrule(::typeof(broadcasted), ::typeof(real), x::AbstractArray{<:Real}) = rrule(identity, x) |> _prepend_zero diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index 01d897555..07bd1710f 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -102,6 +102,7 @@ BT1 = Broadcast.BroadcastStyle(Tuple) test_rrule(copy∘broadcasted, *, rand(3) .+ im, rand(3) .+ 2im) test_rrule(copy∘broadcasted, *, rand(3) .+ im, rand() + 3im) test_rrule(copy∘broadcasted, *, rand() + im, rand(3) .+ 4im) + test_rrule(copy ∘ broadcasted, *, rand(), rand(3) .+ im) # projects the cotangent of the real scalar @test_skip test_rrule(copy∘broadcasted, *, im, rand(3)) # MethodError: no method matching randn(::Random._GLOBAL_RNG, ::Type{Complex{Bool}}) @test_skip test_rrule(copy∘broadcasted, *, rand(3), im) # MethodError: no method matching randn(::Random._GLOBAL_RNG, ::Type{Complex{Bool}}) @@ -129,6 +130,7 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @gpu test_rrule(copy ∘ broadcasted, /, rand(3), rand()) @gpu test_rrule(copy ∘ broadcasted, /, rand(3) .+ im, rand() + 3im) test_rrule(copy ∘ broadcasted, /, Tuple(rand(3)), rand()) + test_rrule(copy ∘ broadcasted, /, rand(3) .+ im, rand()) # projects the cotangent of the real scalar end @testset "identity etc" begin test_rrule(copy∘broadcasted, identity, rand(3)) @@ -251,15 +253,15 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @test dxd == Tangent{typeof(x)}(0.5, 0.5f0) @test dyd ≈ -0.75 xc = (1.0 + 2im, 3.0 - 1im) - @test unthunk(rrule(CFG, copy ∘ broadcasted, real, xc)[2](dy)[3]) == + @test rrule(CFG, copy ∘ broadcasted, real, xc)[2](dy)[3] == Tangent{typeof(xc)}(1.0 + 0im, 1.0 + 0im) - @test unthunk(rrule(CFG, copy ∘ broadcasted, imag, xc)[2](dy)[3]) == + @test rrule(CFG, copy ∘ broadcasted, imag, xc)[2](dy)[3] == Tangent{typeof(xc)}(1.0im, 1.0im) dyc = Tangent{Any}(1.0 + 1im, 2.0 - 1im) - @test unthunk(rrule(CFG, copy ∘ broadcasted, conj, xc)[2](dyc)[3]) == + @test rrule(CFG, copy ∘ broadcasted, conj, xc)[2](dyc)[3] == Tangent{typeof(xc)}(1.0 - 1im, 2.0 + 1im) - # e.g. from `real.(conj.(x))`, where the outer rule returns a `Tuple` cotangent - @test unthunk(rrule(CFG, copy ∘ broadcasted, conj, xc)[2]((1.0, 1.0))[3]) == + # Zygote passes the cotangent of the outer rule in e.g. `real.(conj.(x))` on as a `Tuple` + @test rrule(CFG, copy ∘ broadcasted, conj, xc)[2]((1.0, 1.0))[3] == Tangent{typeof(xc)}(1.0 + 0im, 1.0 + 0im) @test rrule(CFG, copy ∘ broadcasted, -, x)[2]((1.0, 1.0f0))[3] == Tangent{typeof(x)}(-1.0, -1.0f0)