Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b234e7d
Put broadcasting on Julia's documented custom-broadcast interface
devmotion Sep 22, 2026
290b9d6
Recurse into the element type when looking for hidden tracked values
devmotion Sep 22, 2026
d862c2b
Cover the `TrackedStyle{Any}`, wrapper, `Type` and `Union` paths
devmotion Sep 23, 2026
065b878
Leave `map` over a `TrackedReal` to `Base`
devmotion Sep 23, 2026
5d3365c
Trace a broadcast with non-`Real` results scalar-wise
devmotion Sep 23, 2026
1c336c9
Let `map` stop at its shortest argument again
devmotion Sep 23, 2026
46d2014
Claim only arrays whose element type fixes the derivative type
devmotion Sep 23, 2026
ce84f7b
Read a known partial's arguments once per reverse pass
devmotion Sep 23, 2026
256c22f
Drop a redundant `trackresults` method and two `_maxdim` methods
devmotion Sep 23, 2026
a0708a8
Name the closed-form reverse pass `contract_increment_deriv!`
devmotion Sep 23, 2026
b7fdb91
Link the perturbation-confusion TODO to #45
devmotion Sep 23, 2026
20dfa83
Merge remote-tracking branch 'origin/master' into dmw/broadcast-inter…
devmotion Sep 23, 2026
d37b386
Handle uninstantiated broadcasts in `copy` (fixes tests on Julia 1.13)
devmotion Sep 24, 2026
6b23a28
Strip `∇broadcast`'s `Dual`s in `value` (fixes tests on Julia 1.13)
devmotion Sep 24, 2026
f44a5a1
Merge remote-tracking branch 'origin/master' into dmw/broadcast-inter…
devmotion Sep 24, 2026
ae10d2a
Expect `@skip` broadcasts to be untracked, as on master
devmotion Sep 24, 2026
f370cb2
Read argument values in the reverse pass only for known partials
devmotion Sep 25, 2026
cb5be0f
Include test files by relative path
devmotion Sep 25, 2026
a9a2b37
Index broadcast partials by tracked argument only
devmotion Sep 25, 2026
9217f19
Keep tapeless tracked scalars constant and fuse constant tuples
devmotion Sep 25, 2026
d20947f
Unwrap structured matrices by dispatch instead of stripping `Dual`s i…
devmotion Sep 28, 2026
2146fbb
Differentiate `map` w.r.t. an array of `TrackedReal`s next to a `Trac…
devmotion Sep 28, 2026
6581b2a
Drop broadcast perturbations in `@skip` only, and unwrap only `Diagon…
devmotion Sep 28, 2026
13e2096
Record broadcasts by inferred type, keep static fallbacks static, and…
devmotion Sep 28, 2026
e4a7628
Capture the broadcast tag by instance so Julia 1.10 infers the dualiz…
devmotion Sep 29, 2026
a2642ae
Keep static broadcast results writable, simplify `_mayhidetracked`, a…
devmotion Sep 29, 2026
d30d654
Merge master before the Runic reformat into dmw/broadcast-interface
devmotion Oct 2, 2026
9ae87ca
Merge the Runic reformat into dmw/broadcast-interface
devmotion Oct 2, 2026
7404a1b
Merge master into dmw/broadcast-interface
devmotion Oct 2, 2026
171b7c2
Reject 0-dimensional `TrackedArray` destinations, test tags end to en…
devmotion Oct 2, 2026
9fda02a
Cover the remaining broadcast paths and tidy up the review leftovers
devmotion Oct 2, 2026
d27fbef
Catch `TrackedArray` broadcast destinations by type, and test the sta…
devmotion Oct 2, 2026
9862443
Keep widened broadcast values on replay, drop the dead `Union` branch…
devmotion Oct 3, 2026
adf6bb0
Bound a widened broadcast's replayed values by `Real`
devmotion Oct 4, 2026
6aca060
Promote a broadcast's inferred result union instead of widening it
devmotion Oct 5, 2026
0f32365
Say what handles mismatched shapes in the propagation header
devmotion Oct 5, 2026
66b3f1c
Sharpen the broadcast tests and say why a broadcast target may be unt…
devmotion Oct 5, 2026
341c921
Unwrap `NotTracked` before a scalar broadcast and tidy the broadcast …
devmotion Oct 5, 2026
f9ca5ec
Let `setindex!` reject `TrackedArray` broadcast destinations and fill…
devmotion Oct 5, 2026
dd47fc9
Take the scalar rules for perturbations the derivative cannot hold
devmotion Oct 5, 2026
dd16d65
Test the `besselj` `NaN` partial fused with a tracked factor
devmotion Oct 6, 2026
2e04217
Merge branch 'master' into dmw/broadcast-interface
devmotion Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "ReverseDiff"
uuid = "37e2e3b7-166d-5795-8a7a-e32c996b4267"
version = "1.18.5"
version = "1.18.6"

[deps]
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
Expand Down
1 change: 1 addition & 0 deletions src/api/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function seeded_reverse_pass!(result, output::TrackedReal, input, tape)
end

function seeded_reverse_pass!(result, output::Number, input, tape)
checkoutput(output)
result = extract_result!(result, output, input)
return result
end
Expand Down
479 changes: 272 additions & 207 deletions src/derivatives/broadcast.jl

Large diffs are not rendered by default.

676 changes: 97 additions & 579 deletions src/derivatives/elementwise.jl

Large diffs are not rendered by default.

12 changes: 4 additions & 8 deletions src/derivatives/linalg/arithmetic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,6 @@ end
# multiplication (*) #
######################

mulargvalue(x) = value(x)
mulargvalue(x::Adjoint) = adjoint(value(adjoint(x)))
mulargvalue(x::Transpose) = transpose(value(transpose(x)))

mulargpullvalue!(x) = pull_value!(x)
mulargpullvalue!(x::Adjoint) = pull_value!(adjoint(x))
mulargpullvalue!(x::Transpose) = pull_value!(transpose(x))
Expand Down Expand Up @@ -163,14 +159,14 @@ end

@inline function record_mul(x, y, ::Type{D}) where {D}
tp = tape(x, y)
out = track(*(mulargvalue(x), mulargvalue(y)), D, tp)
out = track(*(value(x), value(y)), D, tp)
cache = (similar(x, D), similar(y, D))
record!(tp, SpecialInstruction, *, (x, y), out, cache)
return out
end

@inline function record_mul!(out::TrackedArray{V, D}, x, y) where {V, D}
copyto!(mulargvalue(out), *(mulargvalue(x), mulargvalue(y)))
copyto!(value(out), *(value(x), value(y)))
cache = (similar(x, D), similar(y, D))
record!(tape(x, y), SpecialInstruction, *, (x, y), out, cache)
return out
Expand Down Expand Up @@ -256,9 +252,9 @@ LinearAlgebra.:*(x::StaticMatrix, y::TrackedVector{V, D}) where {V, D} = record_
mulargpullvalue!(a)
mulargpullvalue!(b)
if instruction.output isa Number
value!(instruction.output, mulargvalue(a) * mulargvalue(b))
value!(instruction.output, value(a) * value(b))
else
mul!(mulargvalue(instruction.output), mulargvalue(a), mulargvalue(b))
mul!(value(instruction.output), value(a), value(b))
end
return nothing
end
Expand Down
151 changes: 52 additions & 99 deletions src/derivatives/propagation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ amount scaled by the output's derivative(s). Sometimes, extra partial informatio
form of normal scalars/arrays, or `Dual` numbers) needs to accounted for as well.

Often, a function's input and output values are not similarly shaped. To account for these
cases, `broadcast` and `reduce` versions of the propagation functions have been implemented.
cases, the propagation functions take an index bound that clamps indices into the smaller array.
Accumulations into a scalar derivative go through `sum`, whose pairwise reduction is both more
accurate and faster than a sequential loop.

Expand Down Expand Up @@ -95,142 +95,95 @@ end
# diffresult_increment_deriv! #
###############################

@inline getpartial(r::DiffResults.ImmutableDiffResult{1, V, Tuple{D}}, p) where {V, D <: AbstractArray} = DiffResults.derivative(r)[p]
@inline getpartial(r::DiffResults.ImmutableDiffResult{1, V, Tuple{D}}, p) where {V, D <: Number} = DiffResults.derivative(r)
@inline getpartial(::Type, r::DiffResults.ImmutableDiffResult{1, V, Tuple{D}}, p) where {V, D <: AbstractArray} = DiffResults.derivative(r)[p]
@inline getpartial(::Type, r::DiffResults.ImmutableDiffResult{1, V, Tuple{D}}, p) where {V, D <: Number} = DiffResults.derivative(r)
@inline getpartial(::Type{T}, d::ForwardDiff.Dual, p) where {T} = ForwardDiff.partials(T, d, p)
@inline getpartial(::Type, x::Real, p) = zero(x)

# a partial known in closed form: the argument collects `op(seed, args...)` per element, with
# `args` naming broadcast arguments. `op` meets the seed, so `/` forms no reciprocal.
struct Contract{Op, A <: Tuple}
op::Op
args::A
end

_elem(v::Real, i) = v
_elem(v::AbstractArray, i) = v[i]

# `vals` holds the values of the arguments `e` names
_contract(e::Contract, seed, i, vals) = e.op(seed, map(v -> _elem(v, i), vals)...)

function diffresult_increment_deriv!(
input::AbstractArray, x::AbstractArray,
results, p::Int
)
::Type{T}, input::AbstractArray, x::AbstractArray,
results::AbstractArray, p::Int
) where {T}
for i in eachindex(x, results)
increment_deriv!(input, x[i] * getpartial(results[i], p), i)
increment_deriv!(input, x[i] * getpartial(T, results[i], p), i)
end
return nothing
end

function diffresult_increment_deriv!(
input::AbstractArray, x::AbstractArray,
results, p::Int, bound::CartesianIndex
)
::Type{T}, input::AbstractArray, x::AbstractArray,
results::AbstractArray, p::Int,
bound::CartesianIndex
) where {T}
axes(x) == axes(results) ||
throw(DimensionMismatch("`x` and `results` must have the same indices"))
for (xi, r, i) in zip(x, results, CartesianIndices(size(x)))
increment_deriv!(input, xi * getpartial(r, p), min(bound, i))
increment_deriv!(input, xi * getpartial(T, r, p), min(bound, i))
end
return nothing
end

function diffresult_increment_deriv!(
input::TrackedReal, x::AbstractArray,
results, p::Int, ::Nothing
)
::Type{T}, input::TrackedReal, x::AbstractArray,
results::AbstractArray, p::Int, ::Nothing
) where {T}
inds = eachindex(x, results)
isempty(inds) && return nothing
pull_deriv!(input)
input.deriv += sum(i -> x[i] * getpartial(results[i], p), inds)
input.deriv += sum(i -> x[i] * getpartial(T, results[i], p), inds)
push_deriv!(input)
return nothing
end

##############################
# broadcast_increment_deriv! #
##############################
#############################
# contract_increment_deriv! #
#############################

# without partials #
#------------------#

function broadcast_increment_deriv!(
input::AbstractArray, x::AbstractArray,
bound::CartesianIndex
function contract_increment_deriv!(
input::AbstractArray, x::AbstractArray, e::Contract,
args::Tuple
)
for (xi, i) in zip(x, CartesianIndices(size(x)))
increment_deriv!(input, xi, min(bound, i))
end
return nothing
end

function broadcast_increment_deriv!(input::TrackedReal, x::AbstractArray, ::Nothing)
isempty(x) && return nothing
pull_deriv!(input)
input.deriv += sum(x)
push_deriv!(input)
return nothing
end

# with partials #
#---------------#

@inline broadcast_increment_deriv!(input, x, partials, input_bound, partials_bound) =
_broadcast_increment_deriv!(*, input, x, partials, input_bound, partials_bound)

@inline broadcast_increment_div_deriv!(input, x, partials, input_bound, partials_bound) =
_broadcast_increment_deriv!(/, input, x, partials, input_bound, partials_bound)

# with partial array #
#--------------------#

function _broadcast_increment_deriv!(
op::F, input::AbstractArray, x::AbstractArray,
partials::AbstractArray,
input_bound::CartesianIndex,
partials_bound::CartesianIndex
) where {F}
for (xi, i) in zip(x, CartesianIndices(size(x)))
current_deriv = op(xi, partials[min(partials_bound, i)])
increment_deriv!(input, current_deriv, min(input_bound, i))
for i in eachindex(input, x)
increment_deriv!(input, _contract(e, x[i], i, args), i)
end
return nothing
end

function _broadcast_increment_deriv!(
op::F, input::TrackedReal, x::AbstractArray,
partials::AbstractArray, ::Nothing,
::CartesianIndex
) where {F}
inds = eachindex(x, partials)
isempty(inds) && return nothing
pull_deriv!(input)
input.deriv += sum(i -> op(x[i], partials[i]), inds)
push_deriv!(input)
return nothing
end

# with partial scalar #
#---------------------#

function _broadcast_increment_deriv!(
op::F, input::AbstractArray, x::AbstractArray,
partial::Real, input_bound::CartesianIndex,
::Nothing
) where {F}
for (xi, i) in zip(x, CartesianIndices(size(x)))
increment_deriv!(input, op(xi, partial), min(input_bound, i))
function contract_increment_deriv!(
input::AbstractArray, x::AbstractArray, e::Contract,
args::Tuple, bound::CartesianIndex
)
for i in CartesianIndices(size(x))
increment_deriv!(input, _contract(e, x[i], i, args), min(bound, i))
end
return nothing
end

##############################
# broadcast_decrement_deriv! #
##############################

function broadcast_decrement_deriv!(
input::AbstractArray, x::AbstractArray,
bound::CartesianIndex
function contract_increment_deriv!(
input::TrackedReal, x::AbstractArray, e::Contract,
args::Tuple, ::Nothing
)
for (xi, i) in zip(x, CartesianIndices(size(x)))
decrement_deriv!(input, xi, min(bound, i))
if !isempty(x)
pull_deriv!(input)
input.deriv += sum(i -> _contract(e, x[i], i, args), eachindex(x))
push_deriv!(input)
end
return nothing
end

function broadcast_decrement_deriv!(input::TrackedReal, x::AbstractArray, ::Nothing)
isempty(x) && return nothing
pull_deriv!(input)
input.deriv -= sum(x)
push_deriv!(input)
return nothing
end

##############################
# reduction_increment_deriv! #
##############################
Expand Down
9 changes: 5 additions & 4 deletions src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,11 @@ macro skip(ex)
return esc(annotate_func_expr(:SkipOptimize, __module__, ex))
end

@inline (self::SkipOptimize{F})(args...) where {F} = self.f(map(value, args)...)
@inline (self::SkipOptimize{F})(a) where {F} = self.f(value(a))
@inline (self::SkipOptimize{F})(a, b) where {F} = self.f(value(a), value(b))
@inline (self::SkipOptimize{F})(a, b, c) where {F} = self.f(value(a), value(b), value(c))
skipvalue(x) = value(x)
skipvalue(x::TrackedReal) = skipvalue(value(x))
skipvalue(x::AbstractArray{<:TrackedReal}) = skipvalue(value(x))

@inline (self::SkipOptimize{F})(args...) where {F} = self.f(map(skipvalue, args)...)

"""
f(x) = dot(x, x)
Expand Down
46 changes: 40 additions & 6 deletions src/tracked.jl
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ end
@inline value(x::AbstractArray) = istracked(x) ? map(value, x) : x
@inline value(t::TrackedReal) = t.value
@inline value(t::TrackedArray) = t.value
value(A::Adjoint) = adjoint(value(parent(A)))
value(A::Transpose) = transpose(value(parent(A)))
value(D::Diagonal) = Diagonal(value(D.diag))

@inline deriv(t::TrackedArray) = t.deriv
@inline deriv(t::TrackedReal) = t.deriv
Expand Down Expand Up @@ -206,7 +209,17 @@ push_deriv!(x::AbstractArray) = (istracked(x) && foreach(push_deriv!, x); nothin
# seed/unseed #
#-------------#

seed!(x) = nothing
# a `Dual` of an enclosing differentiation buries a tracked value, whose derivative would
# silently be zero
checkoutput(x) = nothing
function checkoutput(::Dual{<:Any, <:TrackedReal})
throw(ArgumentError("ForwardDiff cannot differentiate through ReverseDiff (see https://github.com/JuliaDiff/ReverseDiff.jl/issues/45)"))
end

function seed!(x)
throw(ArgumentError(LazyString("an output must hold real numbers, but holds an element of type ", typeof(x))))
end
seed!(x::Real) = checkoutput(x)
seed!(t::TrackedReal) = (t.deriv = one(derivtype(t)); push_deriv!(t); nothing)
seed!(t::TrackedArray, i) = (t.deriv[i] = one(derivtype(t)); nothing)
seed!(x::AbstractArray, i) = seed!(x[i])
Expand Down Expand Up @@ -273,6 +286,11 @@ function Base.convert(::Type{T}, x::R) where {T <: TrackedReal, R <: Real}
)
end

# wrapping cuts the tracked value off its tape
function Base.convert(::Type{<:TrackedReal}, ::Dual{<:Any, <:TrackedReal})
throw(ArgumentError("this nesting of ForwardDiff and ReverseDiff is not supported: a `Dual` of tracked numbers cannot be converted to a tracked number (see https://github.com/JuliaDiff/ReverseDiff.jl/issues/45)"))
end

Base.convert(::Type{T}, t::T) where {T <: TrackedReal} = t
Base.convert(::Type{T}, t::T) where {T <: TrackedArray} = t

Expand All @@ -292,6 +310,9 @@ end
# Avoid method ambiguities for ForwardDiff.Dual
Base.promote_rule(::Type{TrackedReal{V1, D, O}}, ::Type{Dual{T, V2, N}}) where {V1, D, O, T, V2, N} = TrackedReal{promote_type(V1, Dual{T, V2, N}), D, O}
Base.promote_rule(::Type{Dual{T, V1, N}}, ::Type{TrackedReal{V2, D, O}}) where {T, V1, N, V2, D, O} = TrackedReal{promote_type(Dual{T, V1, N}, V2), D, O}
# a `Dual` around a tracked value would be cut off its tape, so there is no common type
Base.promote_rule(::Type{TrackedReal{V1, D, O}}, ::Type{Dual{T, V2, N}}) where {V1, D, O, T, V2 <: TrackedReal, N} = Union{}
Base.promote_rule(::Type{Dual{T, V1, N}}, ::Type{TrackedReal{V2, D, O}}) where {T, V1 <: TrackedReal, N, V2, D, O} = Union{}

Base.promote_rule(::Type{TrackedReal{V1, D1, O1}}, ::Type{TrackedReal{V2, D2, O2}}) where {V1, V2, D1, D2, O1, O2} = TrackedReal{promote_type(V1, V2), promote_type(D1, D2), Nothing}

Expand Down Expand Up @@ -406,7 +427,7 @@ Base.@propagate_inbounds function Base.view(t::TrackedArray, inds::ViewIndex...)
return TrackedArray(v, view(deriv(t), inds...), tape(t))
end

Base.setindex!(t::TrackedArray, args...) = error("TrackedArrays do not support setindex!")
Base.setindex!(t::TrackedArray, args...) = throw(ArgumentError("`TrackedArray`s do not support `setindex!`, so they cannot be assigned to or used as a broadcast destination. Use `y = f.(x)` instead."))

Base.IndexStyle(::TrackedArray) = IndexLinear()

Expand Down Expand Up @@ -533,6 +554,10 @@ writable(x::SArray) = copyto!(similar(x), x)
writable(x::Adjoint) = adjoint(writable(parent(x)))
writable(x::Transpose) = transpose(writable(parent(x)))

# every forward pass writes into the value buffer, which a `TrackedArray` rejects
# TODO: self-nesting, and the `value`/`track` unwrapping elsewhere, risk perturbation confusion (#45)
track(x::TrackedArray, ::Type{D}, tp::InstructionTape = InstructionTape()) where {D} = track(collect(x), D, tp)

track!(t::TrackedArray, x::AbstractArray) = (value!(t, x); unseed!(t); t)

track!(t::TrackedReal, x::Real) = (value!(t, x); unseed!(t); t)
Expand All @@ -551,9 +576,18 @@ end
idstr(x) = string(objectid(x), base = 62)[1:3]

function Base.show(io::IO, t::TrackedReal)
tape_id = hastape(t) ? idstr(t.tape) : "---"
origin_id = hasorigin(t) ? "$(t.index), $(idstr(t.origin))" : "---"
id = idstr(t)
print(io, "TrackedReal<$(id)>($(value(t)), $(deriv(t)), $(tape_id), $(origin_id))")
print(io, "TrackedReal<", idstr(t), ">(", value(t), ", ", deriv(t), ", ")
if hastape(t)
print(io, idstr(t.tape))
else
print(io, "---")
end
print(io, ", ")
if hasorigin(t)
print(io, t.index, ", ", idstr(t.origin))
else
print(io, "---")
end
print(io, ")")
return nothing
end
3 changes: 1 addition & 2 deletions test/PrecisionTests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ setprecision(BigFloat, 256) do
@test ReverseDiff.gradient(f, x) ≈ ∇f(x) rtol = rtol
end

# fused broadcasts lose precision in `ForwardDiff._div_partials` (upstream)
let f = y -> sum(y .^ 2 ./ 7)
test_println("BigFloat gradients", f)
@test_broken ReverseDiff.gradient(f, x) ≈ 2 .* x ./ 7 rtol = rtol
@test ReverseDiff.gradient(f, x) ≈ 2 .* x ./ 7 rtol = rtol
end
end

Expand Down
Loading
Loading