Skip to content

DiffResults objects are not re-aliased properly #251

Closed

Description

@gdalle

The DiffResults documentation clearly states that whenever a DiffResult object is used, it must be realiased at the end of the function call:

result = do_stuff!(result, args...)

See for instance the docstrings in https://juliadiff.org/DiffResults.jl/stable/#Mutating-a-DiffResult, or the issue DiffResults#17

This is not done by ReverseDiff, for instance here:

function gradient(f, input, cfg::GradientConfig = GradientConfig(input))
tape = GradientTape(f, input, cfg)
result = construct_result(input_hook(tape))
seeded_reverse_pass!(result, tape)
empty!(cfg.tape)
return result
end

Am I right in deducing that it can lead to incorrectness?

Activity

  1. gdalle commented on Mar 15, 2024

    @gdalle
    MemberAuthor
  2. gdalle commented on Mar 15, 2024

    @gdalle
    MemberAuthor

    Here's an MWE demonstrating the bug:

    julia> using StaticArrays, ReverseDiff, DiffResults
    
    julia> x = MVector{2}(3.0, 4.0);
    
    julia> result_before = DiffResults.GradientResult(x)
    ImmutableDiffResult(3.0, ([3.0, 4.0],))
    
    julia> result_after = ReverseDiff.gradient!(result_before, sum, x)
    ImmutableDiffResult(3.0, ([1.0, 1.0],))  # the first value should be 7 = sum(x)
    
    julia> x = [3.0, 4.0];
    
    julia> result_before = DiffResults.GradientResult(x)
    MutableDiffResult(3.0, ([6.365987374e-314, 1.0e-323],))
    
    julia> result_after = ReverseDiff.gradient!(result_before, sum, x)
    MutableDiffResult(7.0, ([1.0, 1.0],))  # the first value is 7 = sum(x)
  3. added a commit that references this issue on Sep 16, 2026
    ac2f9c6
  4. devmotion commented on Sep 16, 2026

    @devmotion
    Member

    Fixed by #295, which realiases the whole chain — gradients.jl, jacobians.jl, hessians.jl, tape.jl and utils.jl.

    The rebind has to be unbroken. The leaf extract_result!/extract_result_value! methods in utils.jl already realiased correctly, but every caller discarded the result, so patching any single layer leaves the bug in place.

    Tuple results needed a slightly different fix: those methods looped over result[i], which is not assignable, so the returned struct had nowhere to go. They now map over the tuple instead, which also removes the pattern that allowed the bug — map has no place to drop a return value. The result and input tuples are constrained to a common NTuple{N,Any} so a mismatched arity is a dispatch error rather than a silent truncation. Allocations and inference are unchanged.

    f(x, y) = sum(abs2, x) + sum(abs2, y)
    x, y = MVector{2}(3.0, 5.0), MVector{2}(2.0, 4.0)
    res = (DiffResults.GradientResult(MVector(x)), DiffResults.GradientResult(MVector(y)))
    res = ReverseDiff.gradient!(res, f, (x, y))
    
    map(DiffResults.value, res)     # was (3.0, 2.0), now (54.0, 54.0)
    map(DiffResults.gradient, res)  # ([6.0, 10.0], [4.0, 8.0])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions