Skip to content

Commit 7232d5b

Browse files
yebaiclaude
andcommitted
Add cache-reuse tests; validate NamedTuple shape on AD entry
- `:cache_reuse` conformance group in `AbstractPPLTestExt` drives `value_and_{gradient,jacobian}!!` three times per case against a single `prepared` evaluator to catch backend cache corruption between calls. - `_assert_namedtuple_shape` always validates regardless of `CheckInput`; the `{false}` callable opts out by not calling here. Fixes a silent wrong-shape gradient when `prepare(...; check_dims=false)` was used with a NamedTuple input whose nested array sizes differ from the prototype. Mooncake regression test added. - DI ext sub-environment now also loads `ReverseDiff` and exercises `AutoReverseDiff(compile=true)` against the conformance suite, covering the `_prepare_di(::AutoReverseDiff{true}, …)` compiled-tape path. - Trimmed verbose `check_dims` clarifications in docstrings and `docs/src/evaluators.md` to one sentence each. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0bd8515 commit 7232d5b

6 files changed

Lines changed: 83 additions & 8 deletions

File tree

docs/src/evaluators.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ library invokes the inner callable many times with same-length dual arrays
138138
derived from a single user-supplied `x`; re-validating on each invocation
139139
would be redundant work in the hot path.
140140

141+
`check_dims=false` only disables the inner-evaluator hot path. Public AD
142+
entry points still validate input shape against the prepared evaluator —
143+
backend caches are tied to the prepared shape, so mismatches would otherwise
144+
silently produce wrong-shape derivatives.
145+
141146
## Without an AD backend
142147

143148
The two-argument form `prepare(problem, x)` is available without any AD

ext/AbstractPPLTestExt.jl

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,37 @@ function AbstractPPL.run_testcases(
232232
return nothing
233233
end
234234

235+
# Drive `value_and_{gradient,jacobian}!!` twice with different inputs against
236+
# the same `prepared` evaluator to exercise cache reuse — catches backends
237+
# whose cache state is corrupted by a prior call.
238+
function AbstractPPL.run_testcases(
239+
::Val{:cache_reuse}, prepare_fn=AbstractPPL.prepare; adtype, atol=0, rtol=1e-10
240+
)
241+
@testset "scalar output, repeated calls" begin
242+
prepared = prepare_fn(adtype, QuadraticProblem(), zeros(3))
243+
for (x, value, gradient) in (
244+
([1.0, 2.0, 3.0], 14.0, [2.0, 4.0, 6.0]),
245+
([4.0, 5.0, 6.0], 77.0, [8.0, 10.0, 12.0]),
246+
([0.5, -1.0, 2.0], 5.25, [1.0, -2.0, 4.0]),
247+
)
248+
val, grad = AbstractPPL.value_and_gradient!!(prepared, x)
249+
@test val value atol = atol rtol = rtol
250+
@test grad gradient atol = atol rtol = rtol
251+
end
252+
end
253+
@testset "vector output, repeated calls" begin
254+
prepared = prepare_fn(adtype, VectorValuedProblem(), zeros(3))
255+
for (x, value, jacobian) in (
256+
([2.0, 3.0, 4.0], [6.0, 7.0], [3.0 2.0 0.0; 0.0 1.0 1.0]),
257+
([5.0, 1.0, 7.0], [5.0, 8.0], [1.0 5.0 0.0; 0.0 1.0 1.0]),
258+
([0.0, 4.0, -2.0], [0.0, 2.0], [4.0 0.0 0.0; 0.0 1.0 1.0]),
259+
)
260+
val, jac = AbstractPPL.value_and_jacobian!!(prepared, x)
261+
@test val value atol = atol rtol = rtol
262+
@test jac jacobian atol = atol rtol = rtol
263+
end
264+
end
265+
return nothing
266+
end
267+
235268
end # module

src/evaluators/Evaluators.jl

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ prepares gradient or jacobian machinery for vector inputs.
5454
`check_dims` (default `true`) controls whether the returned evaluator validates
5555
the input shape on each call. Pass `check_dims=false` to skip the per-call
5656
check, e.g. inside an AD backend's hot path where the input shape is already
57-
guaranteed.
57+
guaranteed. AD entry points (`value_and_gradient!!` / `value_and_jacobian!!`)
58+
still validate input shape against the prepared evaluator regardless — backend
59+
caches are tied to the prepared shape.
5860
5961
The three-argument AD-aware form may invoke `problem` once during preparation
6062
to detect output arity (scalar vs vector) and select gradient or jacobian
@@ -145,6 +147,10 @@ prototype's `typeof` is captured at preparation time using the original element
145147
types, so a `CheckInput=true` evaluator will reject inputs whose leaves are
146148
dual/shadow numbers (or any other widened element type) even when the structure
147149
is otherwise correct.
150+
151+
`CheckInput=false` only disables the inner-evaluator check; public AD entry
152+
points (`value_and_gradient!!`) always validate via `_assert_namedtuple_shape`
153+
to keep cache reuse safe.
148154
"""
149155
struct NamedTupleEvaluator{CheckInput,F,P<:NamedTuple}
150156
f::F
@@ -208,10 +214,12 @@ end
208214
Throw `ArgumentError` unless `values` has the same type as the prototype captured
209215
during preparation, including matching `size` for any nested `AbstractArray`
210216
leaves. Also throws if the prototype contains a leaf type outside the supported
211-
set (`Real`, `Complex`, `AbstractArray`, `Tuple`, `NamedTuple`). No-op when `e`
212-
was constructed with `CheckInput=false`.
217+
set (`Real`, `Complex`, `AbstractArray`, `Tuple`, `NamedTuple`).
218+
219+
Always validates regardless of the evaluator's `CheckInput` parameter; the
220+
`{false}` callable opts out by not calling here.
213221
"""
214-
function _assert_namedtuple_shape(e::NamedTupleEvaluator{true}, values)
222+
function _assert_namedtuple_shape(e::NamedTupleEvaluator, values)
215223
typeof(values) === typeof(e.inputspec) || throw(
216224
ArgumentError(
217225
"Expected the same NamedTuple structure that was used to prepare this evaluator.",
@@ -224,7 +232,6 @@ function _assert_namedtuple_shape(e::NamedTupleEvaluator{true}, values)
224232
)
225233
return nothing
226234
end
227-
_assert_namedtuple_shape(::NamedTupleEvaluator{false}, _) = nothing
228235

229236
# Classify the output of a probe `evaluator(x)` call into the two arities the
230237
# AD interface supports — `:scalar` routes to gradient prep, `:vector` to

test/ext/differentiationinterface/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"
44
DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63"
55
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
66
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
7+
ReverseDiff = "37e2e3b7-166d-5795-8a7a-e32c996b4267"
78
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
89

910
[compat]
1011
ADTypes = "1"
1112
DifferentiationInterface = "0.6, 0.7"
1213
ForwardDiff = "1"
14+
ReverseDiff = "1"
1315
julia = "1.10"

test/ext/differentiationinterface/main.jl

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,26 @@ Pkg.develop(; path=joinpath(@__DIR__, "..", "..", ".."))
44
Pkg.instantiate()
55

66
using AbstractPPL: run_testcases
7-
using ADTypes: AutoForwardDiff
7+
using ADTypes: AutoForwardDiff, AutoReverseDiff
88
using DifferentiationInterface: DifferentiationInterface as DI
99
using ForwardDiff
10+
using ReverseDiff
1011
using Test
1112

1213
@testset "AbstractPPLDifferentiationInterfaceExt" begin
13-
run_testcases(Val(:vector); adtype=AutoForwardDiff(), atol=1e-6, rtol=1e-6)
14-
run_testcases(Val(:edge); adtype=AutoForwardDiff())
14+
@testset "ForwardDiff" begin
15+
run_testcases(Val(:vector); adtype=AutoForwardDiff(), atol=1e-6, rtol=1e-6)
16+
run_testcases(Val(:cache_reuse); adtype=AutoForwardDiff(), atol=1e-6, rtol=1e-6)
17+
run_testcases(Val(:edge); adtype=AutoForwardDiff())
18+
end
19+
20+
# Compiled-tape ReverseDiff goes through the `_prepare_di(::AutoReverseDiff{true}, …)`
21+
# specialisation that closes the evaluator into a `Base.Fix2` target — the
22+
# `:cache_reuse` group exercises that path across multiple inputs.
23+
@testset "ReverseDiff (compiled tape)" begin
24+
adtype = AutoReverseDiff(; compile=true)
25+
run_testcases(Val(:vector); adtype=adtype, atol=1e-6, rtol=1e-6)
26+
run_testcases(Val(:cache_reuse); adtype=adtype, atol=1e-6, rtol=1e-6)
27+
run_testcases(Val(:edge); adtype=adtype)
28+
end
1529
end

test/ext/mooncake/main.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,21 @@ using Test
1616
@testset "$label" begin
1717
run_testcases(Val(:vector); adtype=adtype, atol=1e-6, rtol=1e-6)
1818
run_testcases(Val(:namedtuple); adtype=adtype, atol=1e-6, rtol=1e-6)
19+
run_testcases(Val(:cache_reuse); adtype=adtype, atol=1e-6, rtol=1e-6)
1920
run_testcases(Val(:edge); adtype=adtype)
2021
end
2122
end
23+
24+
# Regression: `check_dims=false` skips the per-call shape check on the AD
25+
# hot path, but the public `value_and_gradient!!` must still validate cache
26+
# compatibility — Mooncake caches are tied to the prototype's nested array
27+
# sizes, so a mismatched leaf would silently return a wrong-shape gradient.
28+
@testset "NamedTuple cache validates regardless of check_dims" begin
29+
f = vs -> vs.x^2 + sum(abs2, vs.y)
30+
proto = (x=0.0, y=zeros(2))
31+
prepared = prepare(AutoMooncake(), f, proto; check_dims=false)
32+
@test_throws r"Nested array shape" AbstractPPL.value_and_gradient!!(
33+
prepared, (x=3.0, y=[1.0])
34+
)
35+
end
2236
end

0 commit comments

Comments
 (0)