From facd8efbff156e8722545cc8837114f4580715b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 11 Nov 2026 00:04:07 +0100 Subject: [PATCH] Use `NoTangent` instead of `nothing` in `@grad` `nothing` returned by a `@grad` pullback is converted to `NoTangent()`, so only ChainRulesCore tangent types reach `_add_to_deriv!`. The `fill` rule returns `NoTangent()` directly. Co-Authored-By: Claude Opus 5.5 --- src/ReverseDiff.jl | 2 +- src/derivatives/arrays.jl | 2 +- src/macros.jl | 14 +++++++++++--- test/ChainRulesTests.jl | 15 +++++++++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/ReverseDiff.jl b/src/ReverseDiff.jl index 3f1ed799..be999233 100644 --- a/src/ReverseDiff.jl +++ b/src/ReverseDiff.jl @@ -1,7 +1,7 @@ module ReverseDiff using Base: RefValue -using ChainRulesCore: ChainRulesCore, AbstractThunk, AbstractZero, unthunk +using ChainRulesCore: ChainRulesCore, AbstractThunk, AbstractZero, NoTangent, unthunk using DiffResults: DiffResults, DiffResult using DiffRules: DiffRules using ForwardDiff: ForwardDiff, Dual diff --git a/src/derivatives/arrays.jl b/src/derivatives/arrays.jl index b717ff85..a542004f 100644 --- a/src/derivatives/arrays.jl +++ b/src/derivatives/arrays.jl @@ -11,7 +11,7 @@ end @grad function fill(v::Real, dims...) function back(Δ) size(Δ) ≢ dims && error("Dimension mismatch") - return (sum(Δ), map(_ -> nothing, dims)...) + return (sum(Δ), map(_ -> NoTangent(), dims)...) end return fill(value(v), dims...), back end diff --git a/src/macros.jl b/src/macros.jl index 64c65372..a6d1425b 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -176,6 +176,9 @@ skipvalue(x::AbstractArray{<:TrackedReal}) = skipvalue(value(x)) return dot(xv, xv), Δ -> (Δ * 2 * xv,) end The `@grad` macro provides a way for the users to define custom adjoints for single-output functions wrt to their input numbers or arrays. + +The pullback returns a tuple with one tangent per positional argument: a number or an array, a +thunk, `ChainRulesCore.NoTangent()` or `ChainRulesCore.ZeroTangent()`. """ macro grad(expr) d = MacroTools.isdef(expr) ? MacroTools.splitdef(expr) : nothing @@ -231,8 +234,9 @@ macro grad(expr) output = instruction.output input = instruction.input back = instruction.cache[1] - input_derivs = back($ReverseDiff.deriv(output)) - $ReverseDiff._check_tangents($f, input_derivs, length(input)) + back_output = back($ReverseDiff.deriv(output)) + $ReverseDiff._check_tangents($f, back_output, length(input)) + input_derivs = map($ReverseDiff._totangent, back_output) $ReverseDiff._add_to_deriv!.(input, input_derivs) $ReverseDiff.unseed!(output) return nothing @@ -426,8 +430,12 @@ function _check_tangents(f, tangents, n) end end +# `@grad` pullbacks may use `nothing`, like in Tracker +_totangent(x) = x +_totangent(::Nothing) = NoTangent() + _add_to_deriv!(d1, d2) = nothing -_add_to_deriv!(d1::Union{TrackedReal, AbstractArray}, ::Union{AbstractZero, Nothing}) = nothing +_add_to_deriv!(d1::Union{TrackedReal, AbstractArray}, ::AbstractZero) = nothing function _add_to_deriv!(d1::Union{TrackedReal, AbstractArray}, d2::AbstractThunk) istracked(d1) && increment_deriv!(d1, unthunk(d2)) return nothing diff --git a/test/ChainRulesTests.jl b/test/ChainRulesTests.jl index 633a3218..088cccc1 100644 --- a/test/ChainRulesTests.jl +++ b/test/ChainRulesTests.jl @@ -372,6 +372,16 @@ ReverseDiff.@grad function k_zero(x, y) return sum(4xv) + sum(4yv), pb_k_zero end +l_zero(x, y) = sum(4x) + sum(4y) +function l_zero(x::ReverseDiff.TrackedArray, y::ReverseDiff.TrackedArray) + return ReverseDiff.track(l_zero, x, y) +end +ReverseDiff.@grad function l_zero(x, y) + xv, yv = ReverseDiff.value(x), ReverseDiff.value(y) + pb_l_zero(Δ) = (fill(3 * Δ, size(xv)), NoTangent()) + return sum(4xv) + sum(4yv), pb_l_zero +end + h_zero_vec(z) = h_zero(z[1], z[2]) @testset "zero tangents for tracked arguments" begin @@ -388,6 +398,11 @@ h_zero_vec(z) = h_zero(z[1], z[2]) results = ReverseDiff.gradient(k_zero, (x, y)) @test results[1] == fill(3, size(x)) @test results[2] == zeros(size(y)) + + # `NoTangent()` from a `@grad` pullback + results = ReverseDiff.gradient(l_zero, (x, y)) + @test results[1] == fill(3, size(x)) + @test results[2] == zeros(size(y)) end ### `where` clauses in the imported signature