Skip to content

deepcopy of a container of tracked numbers gives zero gradients, with no error #327

Closed

Description

@PTWaade

Thank you for your incredible work. Below is a Claude-generated reduction of an error I ran into when using deepcopy - I hope its useful. Let me know if I can do anything, and my apologies if this is a known error.

-- claude --

deepcopy of anything that holds tracked numbers, such as a Vector, Tuple, NamedTuple or TrackedArray, cuts them off from the reverse pass. The gradient comes back as zeros, with no error or warning. A bare TrackedReal is fine.

using ReverseDiff

x = [3.0, 4.0]

println("No deepcopy:       ", ReverseDiff.gradient(x -> sum([x[1], x[2]]), x))
println("Bare numbers:      ", ReverseDiff.gradient(x -> deepcopy(x[1]) + deepcopy(x[2]), x))
println("Vector:            ", ReverseDiff.gradient(x -> sum(deepcopy([x[1], x[2]])), x))
println("Tuple:             ", ReverseDiff.gradient(x -> sum(deepcopy((x[1], x[2]))), x))
println("NamedTuple:        ", ReverseDiff.gradient(x -> sum(deepcopy((; a = x[1], b = x[2]))), x))
println("TrackedArray:      ", ReverseDiff.gradient(x -> sum(deepcopy(x)), x))
println("Next to original:  ", ReverseDiff.gradient(x -> x[1] + deepcopy((x[1],))[1], x))

Output:

No deepcopy:       [1.0, 1.0]
Bare numbers:      [1.0, 1.0]
Vector:            [0.0, 0.0]
Tuple:             [0.0, 0.0]
NamedTuple:        [0.0, 0.0]
TrackedArray:      [0.0, 0.0]
Next to original:  [1.0, 0.0]

The right answer is [1.0, 1.0] for all but the last, where it is [2.0, 0.0]. A compiled tape gives the same zeros for the Vector.

Where it seems to come from

src/tracked.jl has Base.deepcopy(t::T) where {T <: TrackedReal} = t, so a bare number stays on the tape. Inside a container, Julia calls Base.deepcopy_internal instead of deepcopy, and ReverseDiff has no method for it. The numbers then seem to get their own copy of the tape, which the reverse pass never reaches.

A possible fix, and its catch

With these two methods, all of the cases above are right. With only the first one, all but TrackedArray are right.

Base.deepcopy_internal(t::ReverseDiff.TrackedReal, ::IdDict) = t
Base.deepcopy_internal(t::ReverseDiff.TrackedArray, ::IdDict) = t

But with both of them, deepcopy of a tape no longer gives an independent tape. Running the copy changes the original:

tape = ReverseDiff.GradientTape(x -> sum(abs2, x), [3.0, 4.0])
copied = deepcopy(tape)
ReverseDiff.gradient!(zeros(2), copied, [1.0, 1.0])
ReverseDiff.value(tape.output)  # 25.0 without the methods, 2.0 with them

So the fix probably needs more care than this.

Versions

  • ReverseDiff v1.18.7, the newest version in the General registry on 2026-11-04
  • Julia 1.13.3
  • macOS, arm64 (Apple M5)

The example ran in a clean environment with only ReverseDiff installed. It hasn't been tried on other platforms.

Activity

  1. devmotion commented on Nov 4, 2026

    @devmotion
    Member

    Thanks, I'll have a look.

    Before checking the ReverseDiff side in more detail, maybe a general caveat: one should be very, very careful with using deepcopy at all JuliaLang/julia#42899 (comment)

  2. PTWaade commented on Nov 4, 2026

    @PTWaade
    Author

    Ah - that's good to know! Thank you for the heads-up and for being responsive :)

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions