Skip to content

Keep tracked values on the tape in deepcopy - #329

Merged
devmotion merged 4 commits into
masterfrom
dmw/deepcopy
Nov 6, 2026
Merged

devmotion merged 4 commits into
masterfrom
dmw/deepcopy

Conversation

@devmotion

Copy link
Copy Markdown
Member

Fixes #327.

deepcopy calls deepcopy_internal when it recurses, so the existing Base.deepcopy(::TrackedReal) method only applied at the top level. Tracked numbers inside a Vector, Tuple or NamedTuple, and every TrackedArray, were copied together with their InstructionTape. Later operations recorded onto that orphan tape, and the gradients silently came back as zero.

Following the deepcopy docstring, this PR specialises deepcopy_internal instead of deepcopy:

  • While its tape is recording, a tracked value is returned unchanged. It acts as a number on the tape, matching copy, ForwardDiff (whose Duals are isbits) and Zygote (where the derivative of deepcopy is the identity).
  • Once the tape has finished, it is copied together with its tape, so deepcopy(tape) stays independent. Returning the value unchanged in all cases would break this, as noted in the issue.
  • Copies keep NULL_TAPE itself, since ReverseDiff recognises it by identity.
  • Copies of a CompiledTape are recompiled. Copied FunctionWrappers still point to the original tape's instructions, so before this PR deepcopy(compile(tape)) returned zero gradients as well. This affects e.g. AbstractMCMC's MCMCThreads, which deepcopies models, together with LogDensityProblemsAD's compiled ReverseDiff tapes.

deepcopy_internal has no docstring and is not public, but the deepcopy docstring names it as the method to specialise, so it is added to the ExplicitImports ignore list.

The new tests cover the cases from the issue with gradient and with replays of plain and compiled tapes, plus independence of copied tapes. Without the fix, 17 of the 24 new tests fail.

🤖 Generated with Claude Code

`deepcopy` recurses with `deepcopy_internal`, so the `Base.deepcopy` method
for `TrackedReal` was skipped inside containers and for `TrackedArray`s, whose
copies then recorded onto an orphan tape and got zero gradients.

Specialise `deepcopy_internal` instead: while its tape is recording, a tracked
value is returned as is; afterwards it is copied together with its tape, so
copies of tapes stay independent. Copies keep `NULL_TAPE`, and copies of a
`CompiledTape` are recompiled, since copied `FunctionWrapper`s still point to
the original instructions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.32%. Comparing base (f6c9ddd) to head (93c9995).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #329      +/-   ##
==========================================
+ Coverage   94.29%   94.32%   +0.03%     
==========================================
  Files          19       19              
  Lines        1699     1710      +11     
==========================================
+ Hits         1602     1613      +11     
  Misses         97       97              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/api/tape.jl Resolved
Comment thread src/api/tape.jl Resolved
@devmotion
devmotion merged commit 608569b into master Nov 6, 2026
9 checks passed
@devmotion
devmotion deleted the dmw/deepcopy branch November 6, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant