Skip to content

Call-time context overrides (#167); harden reused reverse-mode caches#177

Open
yebai wants to merge 3 commits into
mainfrom
fix/mooncake-order1-notangent-leak
Open

Call-time context overrides (#167); harden reused reverse-mode caches#177
yebai wants to merge 3 commits into
mainfrom
fix/mooncake-order1-notangent-leak

Conversation

@yebai

@yebai yebai commented Jul 7, 2026

Copy link
Copy Markdown
Member

In reverse mode Mooncake's order=1 path passed the problem function f and any context to Mooncake as ordinary arguments, without zeroing their cotangents. When context carries differentiable data reached through an in-place linear solve — whose reverse rule reads its argument's cotangent back — a reused prepared cache let that cotangent accumulate across calls and returned an incorrect x-gradient after the first one (Mooncake #1238). The fix closes f and context into a NoTangent-typed _ADTarget so they carry no cotangent and x is the only active argument, which also unifies the AutoMooncake/AutoMooncakeForward paths; a reused-prep regression test (a differentiable context routed through a triangular solve, checked against a fresh prep and finite differences) guards it.

It also adds a context= keyword to value_and_gradient!! / value_gradient_and_hessian!! (closes #167) that overrides the frozen context for a single call, without re-preparing, across all three AD extensions: the default nothing leaves the zero-allocation hot path unchanged, and a matching-shape Tuple reuses the type-keyed cache. Compiled-tape ReverseDiff and Mooncake's Hessian bake context into their prepared state and throw an ArgumentError on such an override (empty input always accepts one).

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.34211% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.13%. Comparing base (85a1837) to head (74aa795).

Files with missing lines Patch % Lines
src/evaluators/Evaluators.jl 95.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #177      +/-   ##
==========================================
+ Coverage   91.41%   92.13%   +0.72%     
==========================================
  Files          17       17              
  Lines        2003     2111     +108     
==========================================
+ Hits         1831     1945     +114     
+ Misses        172      166       -6     

☔ 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.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

AbstractPPL.jl documentation for PR #177 is available at:
https://TuringLang.github.io/AbstractPPL.jl/previews/PR177/

@yebai yebai force-pushed the fix/mooncake-order1-notangent-leak branch 10 times, most recently from b631929 to bce3fc2 Compare July 7, 2026 14:16
@yebai yebai marked this pull request as ready for review July 7, 2026 14:18
@yebai yebai requested a review from sunxd3 July 7, 2026 14:19
@yebai yebai force-pushed the fix/mooncake-order1-notangent-leak branch 8 times, most recently from 5662789 to 8308ab1 Compare July 7, 2026 17:25
yebai and others added 2 commits July 7, 2026 19:23
The order=1 scalar-gradient path passed the problem function `f` and any
`context` to Mooncake as ordinary arguments, without zeroing their cotangents.
When `context` carries *differentiable* data reached through an in-place linear
solve — whose reverse rule reads its argument's cotangent back — a reused
reverse-mode cache let that cotangent accumulate across calls and returned an
incorrect `x`-gradient after the first evaluation (Mooncake issue #1238); only
the first evaluation on a reused prep was correct.

Close `f` and `context` into a `NoTangent`-typed `_ADTarget` instead, so they
carry no cotangent by construction and `x` is the only active argument — nothing
else for Mooncake to zero, and no leftover cotangent to accumulate. This also
unifies the `AutoMooncake` / `AutoMooncakeForward` code paths on that target,
dropping the per-mode branching. The target is deliberately field-minimal
(`f` + `context`, no `dim`): an extra non-differentiable field defeats
Mooncake's forward-mode type inference and zero-allocation. A regression test
(a differentiable `context` routed through a triangular solve on a reused prep,
checked against a fresh prep and finite differences) guards it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`context` was frozen at `prepare`. Allow `value_and_gradient!!` and
`value_gradient_and_hessian!!` to take a `context=` keyword that overrides it
for a single call without re-preparing, across the Mooncake,
DifferentiationInterface, and ForwardDiff extensions.

`context=nothing` (the default) reuses the target frozen at `prepare` — the
zero-allocation, type-stable hot path is unchanged. A `Tuple` of matching
element types and shapes rebuilds the target (or swaps the prepared constants'
values) and reuses the type-keyed cache. The override is per-call and does not
mutate the frozen context.

The `nothing`/`Tuple` resolution is unified into one
`Evaluators._resolve_context`; each backend builds its own target from the
result (constants, an `_ADTarget`, or a `Fix2` evaluator).

Two backends cannot reuse the prep for an override and throw a clear
`ArgumentError` (re-`prepare` instead): compiled-tape ReverseDiff, whose
context is baked into the tape, and Mooncake's Hessian, whose cache binds its
target by object identity. Empty input runs no derivative machinery, so an
override is always accepted there.

Consolidate the AD conformance tests: the per-backend override testsets become
a shared `:context_override` TestCase tag + runner (with
`gradient_override`/`hessian_override` flags), and the differentiable-context
reused-cache regression test becomes a shared `:cache_reuse_context` case run
across all backends. Tested across all three backends, including the rejection
and empty-input paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@yebai yebai force-pushed the fix/mooncake-order1-notangent-leak branch from 8308ab1 to 74b5488 Compare July 7, 2026 18:26
…ian!!`

The override contract stated in the docs — a call-time `context` must match
the frozen context's element types and shapes — was enforced by no one:
`_resolve_context` was a pass-through, and violations behaved differently on
every backend (silently honoured on ForwardDiff, an internals-leaking
`PreparedCacheSpecError` on Mooncake, a raw `MethodError` for non-`Tuple`
input). `_resolve_context` now validates every override in one place:
`typeof === ` catches arity and element types, top-level array elements are
compared by `size`, and a non-`Tuple` override gets a dedicated
`ArgumentError`. The empty-input path (`_evaluate_with_context`) routes
through the same validation and now applies the evaluator callables' static
integer-eltype rejection, which the raw `e.f` call used to bypass.

`value_and_jacobian!!` gains the `context=` keyword, completing #167 across
all three entry points: DI threads the resolved override as `Constant`s
(compiled-tape ReverseDiff rejects, as on the other entry points), ForwardDiff
rebuilds its target via the existing `_fd_target`, and Mooncake — whose
vector-arity preps are context-free by construction — accepts exactly the
empty override via the central check, with no special-casing.

Tests: an array-valued-context override fixture (previously untested), a
Jacobian override fixture with a `jacobian_override`
(:honor/:reject/:prepare_rejects) runner flag, contract-violation cases
(arity/eltype/shape/non-`Tuple`) asserted on every backend, and a regression
for the integer-guard bypass. HISTORY also drops the "zero-allocation hot
path" claim, which held only for scalar contexts.

@sunxd3 sunxd3 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes a lot of sense, thanks for the fix.

@sunxd3

sunxd3 commented Jul 10, 2026

Copy link
Copy Markdown
Member

@yebai I pushed a commit contains:

  • context argument for value_and_jacobian
  • more input validation for _evaluate_with_context
  • minor doc clarification

commit message (74aa795#diff-f34a3deb90d5817c924be448c257eeb9ab80df79744c1720173ccb1c0164d146) gives a bit more details. again, feel free to drop any of the things I added, the PR contains no major issue pre-my-commit

@yebai yebai left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @sunxd3 -- all good catches. Some comments below. Can you take this forward?

Comment on lines +293 to +294
@inline _evaluate_with_context(e::VectorEvaluator, x, context) =
_resolve_context(e, context)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is confusing as _resolve_context is used in a weird way to recycle its error message. Better to explicitly error here.

@inline function _resolve_context(e::VectorEvaluator, context::Tuple)
typeof(context) === typeof(e.context) ||
_throw_context_type_mismatch(e.context, context)
_check_context_sizes(e.context, context)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context type and size checks were intentionally omitted because they introduce constant overhead for every gradient evaluation. The design assumes that users are responsible for providing the correct context.

Looking at the implementation, the type checks appear to incur only compile-time overhead. If the benchmarks confirm this, I’m happy to keep them.

I do think the size checks should be removed. In principle, users can pass arbitrary data as context, so enforcing size constraints is difficult to do in a general and meaningful way.

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.

Allow value_and_gradient/jacobian/hessian!! accept contexts

2 participants