Skip to content

Commit 451bfa6

Browse files
yebaiclaude
andauthored
Add Mooncake extension; extend AD conformance suite with NamedTuple and cache-reuse coverage (#160)
* Add Mooncake extension Mooncake AD-backend extension built on the evaluator interface, with the shared conformance suite extended to cover NamedTuple inputs and empty-input arity errors. Squashed from prior incremental commits: - AbstractPPLMooncakeExt: cache reuse, scalar/vector dispatch, NamedTuple inputs via VectorEvaluator/NamedTupleEvaluator wrappers; integration test in test/ext/mooncake. - Evaluators._ad_output_arity: lift the duplicated `Union{Number, AbstractVector}` output check from both extensions into one helper that returns `:scalar` / `:vector` for downstream dispatch. - Empty-input arity tagging (`Val(:scalar)` / `Val(:vector)`) so the empty-input fast path raises the same "requires a scalar/vector-valued function" error as the DI path instead of silently succeeding. - AbstractPPLTestExt: add `Val(:namedtuple)` group (one ValueCase + one ErrorCase); tighten regex assertions on the existing arity-mismatch cases. - check_dims threaded through the inner `prepare` call so AD hot paths can skip per-call shape checks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Factor AD-input validation; reorder NamedTuple test group `_check_ad_input(evaluator, x)` in `Evaluators` replaces the duplicated `T <: Integer` rejection plus length check that appeared at six AD entry points (two in the DI extension, four in Mooncake). Compile-time `T` elision is preserved. Move `generate_testcases(::Val{:namedtuple})` and `run_testcases(::Val{:namedtuple})` to sit alongside the `:vector` and `:edge` definitions so the file reads generate-then-run for all three groups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add cache-reuse tests; tidy AD arity errors - `: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. - 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. - Lift the duplicated `value_and_{gradient,jacobian}!!` arity-mismatch `ArgumentError` strings into shared `Evaluators._throw_*` helpers used by both the DI and Mooncake extensions. - `generate_testcases` docstring lists `:namedtuple` and `:cache_reuse` alongside `:vector` / `:edge` as reserved group keys. - Trim 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> * AGENTS.md: refresh env before tests / doc builds Stale manifests cause subtle resolution and loading issues; document the expected `Pkg.update()` step alongside the existing test commands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Cut AD hot-path overhead in VectorEvaluator/DICache Two regressions visible on tiny-model gradients went through the new AbstractPPL evaluator interface: - `_check_ad_input` always ran on `value_and_{gradient,jacobian}!!` entry, even when the evaluator was prepared with `check_dims=false`. Now dispatch-gated on `VectorEvaluator{CheckInput}`: the `{false}` overload is a no-op, so the `DimensionMismatch` and integer-rejection paths are elided from the LLVM IR of the AD hot path. - `DICache` stored `use_context::Bool` as a runtime field, leaving a branch in the compiled call selecting the context vs no-context DI form. `UseContext` is now a type parameter and the branch is resolved by dispatch via `_di_value_and_{gradient,jacobian}` helpers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Mooncake ext: skip re-zeroing the evaluator tangent each gradient call `Mooncake.value_and_gradient!!(cache, evaluator, x)` reset the evaluator's tangent buffer on every call, even though AbstractPPL discards `∂f` and only surfaces `∂x`. For evaluators that wrap a model with large fields (e.g. a 128-tuple of `Float64`), the zeroing was the dominant per-call overhead at tiny model sizes. Pass `args_to_zero=(false, true)` to the reverse-mode `Mooncake.Cache` path to skip the `∂f` reset while still zeroing the `∂x` buffer. The forward-mode `Mooncake.ForwardCache` doesn't accept the kwarg, so the branch is `isa`-dispatched on the concrete cache type and constant-folds at compile time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Comment perf-critical dispatch and tangent-zero sites Concise inline notes on: - `VectorEvaluator{true|false}` callable bodies (shared `T <: Integer` compile-time elision, and the `{false}` skip of `_check_vector_length`). - Mooncake ext empty-input and arity-mismatch methods (compile-time dispatch via `MooncakeCache{…,Nothing}` and `MooncakeCache{:scalar|:vector}` to avoid runtime branches). - `args_to_zero=(false, true)` at both Mooncake gradient call sites (skipping the evaluator's tangent re-zeroing per call — `∂f` is discarded). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Mooncake ext: hide evaluator from tangent derivation via tangent_type Mooncake was deriving a nested `Tangent{NamedTuple{f::Tangent{...}}}` for every `VectorEvaluator`/`NamedTupleEvaluator` it received, then walking that structure on every backward pass. The evaluators are AbstractPPL's own wrapper types and never appear as a downstream gradient target — the public API only returns `(value, ∂x)`. Register `Mooncake.tangent_type(::Type{<:VectorEvaluator}) = NoTangent` (and the same for `NamedTupleEvaluator`) so the cache carries no tangent for the user's problem fields. The `args_to_zero=(false, true)` mitigation and the `_ConstantEvaluator` wrapper from the prior pass are both no longer needed; the call sites pass `p.evaluator` directly. Verified on the MWE setup: `Mooncake.Tangent{` count in the prepared cache type is 0; value and gradient match a direct `logdensity_at(x, state, …)` call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Mooncake ext: add `raw_gradient_target` for lowered AD entry shape Callers who know an equivalent raw `f(x, contexts...) ≡ problem(x)` can pass it via `prepare(AutoMooncake(), problem, x; raw_gradient_target=(f, contexts))`. Mooncake then compiles the tape on the raw call shape with `args_to_zero= (false, true, false, …)` instead of the generic `evaluator(x)` wrapper — sidestepping the fixed-overhead seen on tiny scalar-vector problems. `prepared(x)` still calls `problem(x)`; only the AD entry uses the lowered cache (a new `MooncakeLoweredCache` carries `cache`, `f`, `contexts`, and `args_to_zero`). Scoped strictly to reverse-mode `AutoMooncake` and scalar arity with non-empty input — anything else errors at prepare time. Jacobian on a lowered cache surfaces the existing arity-mismatch error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Merge MooncakeLoweredCache into MooncakeCache; accept raw_gradient_target on all AD prepare methods - Collapsed `MooncakeLoweredCache` into `MooncakeCache{A,C,F,CT,AZ}`. The three new type params default to `Nothing` via the existing constructor; the lowered-path constructor populates them. Dispatch on `CT<:Tuple` (excluding the `Nothing` default) picks the lowered AD entry. No new type, no runtime branching. - DI extension's `prepare(::AbstractADType, ...)` now accepts `raw_gradient_target=nothing` and silently ignores it. Same for the Mooncake NamedTuple `prepare`. Generic user code that passes the kwarg to non-Mooncake backends (or to the Mooncake NamedTuple path) no longer hits a MethodError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop `args_to_zero` field from MooncakeCache; trim noise comments - `args_to_zero` was a derived value (`(false, true, false×length(contexts))`) stored as a struct field plus a 5th type parameter. Moved the construction to the AD entry; the tuple constant-folds for any concrete `contexts` arity. Saves one type parameter and one field. - Dropped two trailing comments on `raw_gradient_target=nothing` kwargs (the comment didn't explain WHY — the kwarg name and surrounding context already convey "this is a backend-specific optimization that defaults off"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Mooncake ext: reject non-dense vectors; document NamedTuple check delegation and raw_gradient_target as unsafe Addresses PR #160 review comments: - Throw a clear `ArgumentError` for non-`DenseVector` inputs instead of letting Mooncake return a shape-incorrect tangent (reverse) or crash inside Mooncake (forward/Jacobian). - Document that NamedTuple input-shape validation is intentionally delegated to Mooncake's `PreparedCacheSpec` to avoid duplicating checks on every AD call. - Add a docstring on the vector `prepare` method describing `raw_gradient_target` as an unsafe escape hatch that bypasses evaluator indirection and shape checks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Replace raw_gradient_target with context::Tuple=() on prepare Lift the lowered-AD escape hatch into a first-class API: every vector `prepare` now accepts `context::Tuple=()`, the prepared evaluator computes `problem(x, context...)`, and AD differentiates only `x`. `VectorEvaluator` carries the context as a third type parameter so callers can recover it from the evaluator without going through the kwarg again. Mooncake ext - Compile every scalar gradient cache on the raw `evaluator.f` / `evaluator.context` (not the raw `problem`/`context` kwargs), so a downstream override of structural `prepare` that returns a different `f`/`context` doesn't desync from the hot path. - Forward-mode `AutoMooncakeForward` now also accepts non-empty `context`. `_mooncake_value_and_gradient` dispatches reverse-mode to the `args_to_zero` kwarg form and forward-mode to the splat-only form (`ForwardCache` rejects `args_to_zero`). - Vector-jacobian path now also runs on `evaluator.f` for the same reason. - Empty input with non-empty `context` is supported (was rejected). The `MooncakeCache{arity,Nothing}` empty-input shortcut already evaluates `evaluator(x)` without invoking Mooncake. DI ext - `DICache{Mode}`: `Mode == :closure` for compiled-tape ReverseDiff, `Mode::Int == length(evaluator.context)` for the constants path. The `Int` doubles as documentation of how many `DI.Constant`s the AD call passes (`N + 1`, including `f`). - Single shared AD target `_call_evaluator(x, f, ctx::Vararg{Any,N}) where {N}`. `Vararg{Any,N}` forces specialization on the trailing arity. Docs/tests - New `Constant context arguments` section in `docs/src/evaluators.md`. - New regression tests covering: context threading via structural `prepare`, forward-mode Mooncake context, empty-input + non-empty context shortcut, and `DICache` mode-tag pinning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Mooncake ext: inline single-call helpers, trim stale design-history comments Three single-call helpers folded into their unique call sites: - 4-arg `_mooncake_gradient_cache(adtype, f, x, context::Tuple)` and the `_mooncake_jacobian_cache` pair → one `if adtype isa AutoMooncake ... end` branch in vector `prepare`. The `isa` is compile-folded since `adtype`'s concrete type lives in the method's specialization. - `_mooncake_value_and_gradient(::Auto*, ...)` → an inlined branch in the scalar-gradient hot path's `value_and_gradient!!` body, using the same compile-folded `isa`. Kept in a single `<:_MooncakeAD` method so the empty-input shortcut's cache specificity (`MooncakeCache{:scalar,Nothing}`) doesn't clash with a per-AD-type method. The 3-arg `_mooncake_gradient_cache(::Auto*, f, x)` pair (NamedTuple path) stays factored — it's reused by the NamedTuple `prepare` and has a distinct call shape (evaluator + values, not raw `f` + context splat). Comment cleanup: - `tangent_type` defensive guard now describes the current state rather than the refactor that produced it ("after the raw-target merge"). - Cache-prep block trimmed from two paragraphs to one — kept the evaluator-as-source-of-truth rationale and the forward-mode unification note, dropped the redundant splat-no-op elaboration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * HISTORY.md: add 0.15.0 entry for evaluator and AD interface Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 10a4182 commit 451bfa6

16 files changed

Lines changed: 658 additions & 86 deletions

File tree

.github/workflows/CI.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ jobs:
6161
matrix:
6262
label:
6363
- ext/differentiationinterface
64+
- ext/mooncake
6465
version:
6566
- '1'
6667
- 'min'

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ AbstractPPL.jl provides shared interfaces and utilities for probabilistic progra
3232
- Full package tests: `julia --project=. -e 'using Pkg; Pkg.test()'`
3333
- Docs: `julia --project=docs docs/make.jl`
3434

35+
Always refresh each environment (`Pkg.update()` / `up`) before tests or doc builds — a stale manifest can cause subtle resolution and loading issues.
36+
3537
Run the smallest relevant test first, then broaden when changing public interfaces, extensions, or downstream-facing behaviour. Do not weaken tests just to make CI pass.
3638

3739
## Documentation

HISTORY.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
## 0.15.0
2+
3+
New evaluator-preparation and AD interface: `prepare` binds a callable to a sample input (vector or `NamedTuple`); `value_and_gradient!!` / `value_and_jacobian!!` return value-and-derivative pairs from the resulting `Prepared` wrapper. The `!!` suffix signals the returned derivative may alias the cache — copy if you need to keep it.
4+
5+
```julia
6+
using ADTypes, Mooncake # or DifferentiationInterface + ForwardDiff
7+
using AbstractPPL: prepare, value_and_gradient!!
8+
prepared = prepare(AutoMooncake(), x -> -0.5 * sum(abs2, x), zeros(3))
9+
val, grad = value_and_gradient!!(prepared, [1.0, 2.0, 3.0])
10+
# val == -7.0; grad == [-1.0, -2.0, -3.0]
11+
```
12+
13+
Two new AD-backend extensions ship with it: `AbstractPPLDifferentiationInterfaceExt` (any DI backend) and `AbstractPPLMooncakeExt` (`AutoMooncake`, `AutoMooncakeForward`). `AbstractPPLTestExt` gains a conformance harness via `generate_testcases` / `run_testcases` (reserved groups: `:vector`, `:namedtuple`, `:edge`, `:cache_reuse`).
14+
15+
See [`docs/src/evaluators.md`](docs/src/evaluators.md) for the full interface, the `check_dims` and `context::Tuple` options, the `NamedTuple` input path, and extension-author guidance.
16+
117
## 0.14.2
218

319
Fix string serialisation of VarNames such that the order of keyword arguments is preserved (this was previously guaranteed, but JSON.jl v1.5.0 introduced a change that caused the keyword arguments to always be sorted.)

Project.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf"
33
keywords = ["probabilistic programming"]
44
license = "MIT"
55
desc = "Common interfaces for probabilistic programming"
6-
version = "0.14.3"
6+
version = "0.15"
77

88
[deps]
99
ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"
@@ -21,11 +21,13 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
2121
[weakdeps]
2222
DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63"
2323
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
24+
Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6"
2425
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2526

2627
[extensions]
2728
AbstractPPLDifferentiationInterfaceExt = ["DifferentiationInterface"]
2829
AbstractPPLDistributionsExt = ["Distributions", "LinearAlgebra"]
30+
AbstractPPLMooncakeExt = ["Mooncake"]
2931
AbstractPPLTestExt = ["Test"]
3032

3133
[compat]
@@ -39,6 +41,7 @@ Distributions = "0.25"
3941
JSON = "0.19 - 0.21, 1"
4042
LinearAlgebra = "<0.0.1, 1"
4143
MacroTools = "0.5"
44+
Mooncake = "0.5.27"
4245
OrderedCollections = "1.8.1"
4346
Random = "1.6"
4447
StatsBase = "0.32, 0.33, 0.34"

docs/src/evaluators.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,23 @@ 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+
## Constant context arguments
142+
143+
When the underlying callable naturally takes the form `f(x, context...)`
144+
where everything after `x` is constant state — pass `context` as a tuple to
145+
the vector form of `prepare`. AD differentiates only w.r.t. `x`; every
146+
value in `context` is treated as inactive:
147+
148+
```julia
149+
affine(x, scale, offset) = scale * sum(x) + offset
150+
prepared = prepare(adtype, affine, zeros(3); context=(2.0, 1.0))
151+
val, grad = value_and_gradient!!(prepared, [1.0, 2.0, 3.0])
152+
# val == 2.0 * 6.0 + 1.0; grad == [2.0, 2.0, 2.0]
153+
```
154+
155+
`prepared(x)` evaluates `f(x, context...)`, and `context=()` (the default)
156+
preserves the unary `f(x)` shape.
157+
141158
## Without an AD backend
142159

143160
The two-argument form `prepare(problem, x)` is available without any AD
Lines changed: 83 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,131 @@
11
module AbstractPPLDifferentiationInterfaceExt
22

33
using AbstractPPL: AbstractPPL
4-
using AbstractPPL.Evaluators: Evaluators, Prepared, VectorEvaluator
4+
using AbstractPPL.Evaluators: Evaluators, Prepared, VectorEvaluator, _ad_output_arity
55
using ADTypes: AbstractADType, AutoReverseDiff
66
using DifferentiationInterface: DifferentiationInterface as DI
77

8-
# Differentiate only `x`; the evaluator is passed as a `DI.Constant` context so
9-
# that in DynamicPPL the model and other evaluator state stay constant.
10-
@inline _call_evaluator(x, evaluator) = evaluator(x)
8+
# AD target used by both `DICache` modes. `Vararg{Any,N}` with a free `N`
9+
# forces specialization on the trailing arity (a bare `Vararg{Any}` would
10+
# skip it). DI invokes this as `_call_evaluator(x, f, c1, …, cN)` on the
11+
# constants path, and as `_call_evaluator(x, evaluator)` (via `Fix2`) on
12+
# the closure path — empty `ctx` then makes the splat a no-op.
13+
@inline _call_evaluator(x, f::F, ctx::Vararg{Any,N}) where {F,N} = f(x, ctx...)
1114

12-
struct DICache{F,GP,JP}
15+
# `Mode` tags the cache shape:
16+
# * `:closure` — compiled-tape ReverseDiff: target is a `Fix2` closure,
17+
# the AD call passes **0** `DI.Constant`s.
18+
# * `N::Int` — constants path: `N == length(evaluator.context)`, the
19+
# AD call passes **N + 1** `DI.Constant`s (`f` plus the
20+
# `N` context values).
21+
# Encoding `Mode` in the type resolves the dispatch in `_di_value_and_*`
22+
# at compile time without a runtime branch.
23+
struct DICache{Mode,F,GP,JP}
1324
target::F
1425
gradient_prep::GP
1526
jacobian_prep::JP
16-
use_context::Bool
27+
function DICache{Mode}(target::F, gp::GP, jp::JP) where {Mode,F,GP,JP}
28+
return new{Mode,F,GP,JP}(target, gp, jp)
29+
end
1730
end
1831

1932
# Compiled ReverseDiff only reuses a compiled tape on the one-argument path;
2033
# `DI.Constant` deactivates tape recording, so close the evaluator into the
21-
# target and call DI without contexts.
34+
# target and call DI without constants. Context (if any) is captured inside
35+
# the evaluator closure rather than lowered out — the lowered path would also
36+
# require a closure here, so the wrapper cost is unavoidable for compiled tapes.
2237
function _prepare_di(prep::F, adtype::AutoReverseDiff{true}, x, evaluator) where {F}
2338
target = Base.Fix2(_call_evaluator, evaluator)
24-
return target, prep(target, adtype, x), false
39+
return target, prep(target, adtype, x), Val(:closure)
2540
end
2641

2742
function _prepare_di(prep::F, adtype::AbstractADType, x, evaluator) where {F}
28-
return _call_evaluator, prep(_call_evaluator, adtype, x, DI.Constant(evaluator)), true
43+
constants = (DI.Constant(evaluator.f), map(DI.Constant, evaluator.context)...)
44+
return (
45+
_call_evaluator,
46+
prep(_call_evaluator, adtype, x, constants...),
47+
Val(length(evaluator.context)),
48+
)
2949
end
3050

51+
@inline _wrap_cache(target, gp, jp, ::Val{Mode}) where {Mode} =
52+
DICache{Mode}(target, gp, jp)
53+
3154
function AbstractPPL.prepare(
32-
adtype::AbstractADType, problem, x::AbstractVector{<:Real}; check_dims::Bool=true
55+
adtype::AbstractADType,
56+
problem,
57+
x::AbstractVector{<:Real};
58+
check_dims::Bool=true,
59+
context::Tuple=(),
3360
)
34-
evaluator = AbstractPPL.prepare(problem, x; check_dims)::VectorEvaluator
35-
y = evaluator(x)
36-
y isa Union{Number,AbstractVector} || throw(
37-
ArgumentError(
38-
"A prepared AD evaluator must return a scalar or AbstractVector; got $(typeof(y)).",
39-
),
40-
)
61+
evaluator = AbstractPPL.prepare(problem, x; check_dims, context)::VectorEvaluator
62+
arity = _ad_output_arity(evaluator(x))
4163
if length(x) == 0
42-
# DI prep crashes on length-0 input (e.g. ForwardDiff `BoundsError`); the
43-
# `Val(0)` sentinel keeps the `gradient_prep === nothing` arity check meaningful.
44-
gp, jp = y isa Number ? (Val(0), nothing) : (nothing, Val(0))
45-
return Prepared(adtype, evaluator, DICache(_call_evaluator, gp, jp, true))
64+
# DI prep crashes on length-0 input (e.g. ForwardDiff `BoundsError`).
65+
# `Val(0)` is an arity sentinel for the `gradient_prep === nothing`
66+
# check below; the AD entry short-circuits before any DI call.
67+
gp, jp = arity === :scalar ? (Val(0), nothing) : (nothing, Val(0))
68+
cache = _wrap_cache(_call_evaluator, gp, jp, Val(length(context)))
69+
return Prepared(adtype, evaluator, cache)
4670
end
47-
if y isa Number
48-
target, gradient_prep, use_context = _prepare_di(
49-
DI.prepare_gradient, adtype, x, evaluator
50-
)
71+
if arity === :scalar
72+
target, gradient_prep, mode = _prepare_di(DI.prepare_gradient, adtype, x, evaluator)
5173
return Prepared(
52-
adtype, evaluator, DICache(target, gradient_prep, nothing, use_context)
74+
adtype, evaluator, _wrap_cache(target, gradient_prep, nothing, mode)
5375
)
5476
end
55-
target, jacobian_prep, use_context = _prepare_di(
56-
DI.prepare_jacobian, adtype, x, evaluator
57-
)
58-
return Prepared(adtype, evaluator, DICache(target, nothing, jacobian_prep, use_context))
77+
target, jacobian_prep, mode = _prepare_di(DI.prepare_jacobian, adtype, x, evaluator)
78+
return Prepared(adtype, evaluator, _wrap_cache(target, nothing, jacobian_prep, mode))
5979
end
6080

81+
# Hot-path dispatch is by `Mode` (closure vs constants), resolved at compile
82+
# time. The unconstrained method matches every non-`:closure` `Mode` (i.e.
83+
# any `Int N`); `:closure` is strictly more specific and wins for compiled
84+
# tapes. On the constants path we always pass `DI.Constant(eval.f)` plus the
85+
# `N` context constants — `N == 0` collapses the `map` splat to nothing.
86+
@inline _di_value_and_gradient(c::DICache{:closure}, ad, x, _) =
87+
DI.value_and_gradient(c.target, c.gradient_prep, ad, x)
88+
@inline _di_value_and_gradient(c::DICache, ad, x, eval) = DI.value_and_gradient(
89+
c.target,
90+
c.gradient_prep,
91+
ad,
92+
x,
93+
DI.Constant(eval.f),
94+
map(DI.Constant, eval.context)...,
95+
)
96+
97+
@inline _di_value_and_jacobian(c::DICache{:closure}, ad, x, _) =
98+
DI.value_and_jacobian(c.target, c.jacobian_prep, ad, x)
99+
@inline _di_value_and_jacobian(c::DICache, ad, x, eval) = DI.value_and_jacobian(
100+
c.target,
101+
c.jacobian_prep,
102+
ad,
103+
x,
104+
DI.Constant(eval.f),
105+
map(DI.Constant, eval.context)...,
106+
)
107+
61108
@inline function AbstractPPL.value_and_gradient!!(
62109
p::Prepared{<:AbstractADType,<:VectorEvaluator,<:DICache}, x::AbstractVector{T}
63110
) where {T<:Real}
64-
p.cache.gradient_prep === nothing &&
65-
throw(ArgumentError("`value_and_gradient!!` requires a scalar-valued function."))
66-
T <: Integer && Evaluators._reject_integer_input(x)
67-
Evaluators._check_vector_length(p.evaluator.dim, x)
111+
p.cache.gradient_prep === nothing && Evaluators._throw_gradient_needs_scalar()
112+
Evaluators._check_ad_input(p.evaluator, x)
68113
# Bypass DI on length-0 input — DI prep paths fail (e.g. ForwardDiff
69114
# `BoundsError`); typed `T[]` matches the caller's element type.
70115
length(x) == 0 && return (p.evaluator(x), T[])
71-
return if p.cache.use_context
72-
DI.value_and_gradient(
73-
p.cache.target, p.cache.gradient_prep, p.adtype, x, DI.Constant(p.evaluator)
74-
)
75-
else
76-
DI.value_and_gradient(p.cache.target, p.cache.gradient_prep, p.adtype, x)
77-
end
116+
return _di_value_and_gradient(p.cache, p.adtype, x, p.evaluator)
78117
end
79118

80119
@inline function AbstractPPL.value_and_jacobian!!(
81120
p::Prepared{<:AbstractADType,<:VectorEvaluator,<:DICache}, x::AbstractVector{T}
82121
) where {T<:Real}
83-
p.cache.jacobian_prep === nothing &&
84-
throw(ArgumentError("`value_and_jacobian!!` requires a vector-valued function."))
85-
T <: Integer && Evaluators._reject_integer_input(x)
86-
Evaluators._check_vector_length(p.evaluator.dim, x)
122+
p.cache.jacobian_prep === nothing && Evaluators._throw_jacobian_needs_vector()
123+
Evaluators._check_ad_input(p.evaluator, x)
87124
if length(x) == 0
88125
val = p.evaluator(x)
89126
return (val, similar(x, length(val), 0))
90127
end
91-
return if p.cache.use_context
92-
DI.value_and_jacobian(
93-
p.cache.target, p.cache.jacobian_prep, p.adtype, x, DI.Constant(p.evaluator)
94-
)
95-
else
96-
DI.value_and_jacobian(p.cache.target, p.cache.jacobian_prep, p.adtype, x)
97-
end
128+
return _di_value_and_jacobian(p.cache, p.adtype, x, p.evaluator)
98129
end
99130

100131
end # module

0 commit comments

Comments
 (0)