Skip to content

Add evaluator interface with AD backend extensions#155

Open
yebai wants to merge 52 commits intomainfrom
evaluator-interface
Open

Add evaluator interface with AD backend extensions#155
yebai wants to merge 52 commits intomainfrom
evaluator-interface

Conversation

@yebai
Copy link
Copy Markdown
Member

@yebai yebai commented Apr 14, 2026

Summary

Adds a prepared evaluator API to AbstractPPL that decouples log-density
evaluation from automatic differentiation, following the interface sketched
in docs/src/interface.md.

Autograd backends are optional. For example, removing the Enzyme package extension would simply fall back to DifferentiationInterface for AutoEnzyme. The broader aim is to rely only on public APIs of autodiff backends, while keeping gradient definitions independent of any particular backend implementation.

TODOs

  • make sure the evaluator interface is sufficient for DynamicPPL
  • make sure the evaluator interface is sufficient for AdvancedVI
  • make sure the evaluator interface is sufficient for Bijectors
 using AbstractPPL, ADTypes

 # 1. Define a problem type and its structural evaluator
 struct MyProblem end
 struct MyEvaluator end

 AbstractPPL.prepare(::MyProblem, ::NamedTuple) = MyEvaluator()
 (::MyEvaluator)(nt::NamedTuple) = nt.x^2 + sum(nt.y .^ 2)

 # 2. Prepare an AD-aware evaluator (works with any supported backend)
 using ForwardDiff
 prepared = prepare(AutoForwardDiff(), MyProblem(), (x=0.0, y=[0.0, 0.0]))

 # 3. Evaluate and differentiate — gradients keep the named structure
 val, grad = value_and_gradient(prepared, (x=3.0, y=[1.0, 2.0]))
 # val    = 14.0
 # grad.x = 6.0
 # grad.y = [2.0, 4.0]

 # 4. Verify gradients against finite differences (in tests)
using FiniteDifferences
 test_autograd(prepared, (x=3.0, y=[1.0, 2.0]))

yebai and others added 2 commits April 14, 2026 23:34
Implement the named evaluator interface with DerivativeOrder, capabilities,
prepare, value_and_gradient, and dimension. Add NamedTuple <-> flat vector
utilities shared by all AD extensions.

Package extensions for four AD backends:
- AbstractPPLForwardDiffExt: ForwardDiff native API (GradientConfig)
- AbstractPPLMooncakeExt: Mooncake native API (prepare_gradient_cache)
- AbstractPPLEnzymeExt: Enzyme native API (autodiff ReverseWithPrimal)
- AbstractPPLDifferentiationInterfaceExt: DI generic fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Prune evaluator interface to minimum load-bearing changes
- Fix silent vector truncation, Float64 coercion, and DerivativeOrder ordering
- Add isolated Julia environments per AD backend under test/ext/
- Add AbstractPPLFiniteDifferencesExt and test_autograd utility
- Run ext tests in separate parallel CI jobs; allow Enzyme to fail
- Apply JuliaFormatter pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

JuliaFormatter v1.0.62

[JuliaFormatter v1.0.62] reported by reviewdog 🐶

@test !is_dynamic(@varname(x[k = i]))


[JuliaFormatter v1.0.62] reported by reviewdog 🐶

@test @varname(x[k = idx]) == @varname(x[k = 3])
@test @varname(x[k = 2 * idx]) == @varname(x[k = 6])

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 55.62500% with 71 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.08%. Comparing base (76dc6df) to head (30bd5dc).

Files with missing lines Patch % Lines
ext/AbstractPPLDifferentiationInterfaceExt.jl 0.00% 36 Missing ⚠️
ext/AbstractPPLLogDensityProblemsExt.jl 0.00% 15 Missing ⚠️
src/ADProblems.jl 65.78% 13 Missing ⚠️
src/utils.jl 89.85% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #155      +/-   ##
==========================================
- Coverage   84.51%   78.08%   -6.44%     
==========================================
  Files          10       14       +4     
  Lines         562      721     +159     
==========================================
+ Hits          475      563      +88     
- Misses         87      158      +71     

☔ View full report in Codecov by Sentry.
📢 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
Copy link
Copy Markdown
Contributor

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yebai yebai force-pushed the evaluator-interface branch from d3c32d3 to 011c123 Compare April 14, 2026 23:18
…ecks

Replace Any[]-based _unflatten with recursive peel approach that avoids
Union types Enzyme cannot differentiate through. Add @inline to all
value_and_gradient methods to prevent boxing the (value, grad) sret tuple.
Add DimensionMismatch length checks to all vector adapters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gdalle
Copy link
Copy Markdown

gdalle commented Apr 15, 2026

@wsmoses you may want to take a look at this Enzyme ext, especially since the test doesn't fully cover it yet?

Comment thread ext/AbstractPPLEnzymeExt.jl Outdated
evaluator = AbstractPPL.prepare(problem, prototype)
x0 = AbstractPPL.flatten_to_vec(prototype)
f_vec = let evaluator = evaluator, prototype = prototype
x -> evaluator(AbstractPPL.unflatten_from_vec(prototype, x))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

don't make a closure/capture the evaluator here, just call evaluator directly in autodiff with all of the arguments. Also don't add this extra shim to flatten/unflatten

Enforce prepare-time runtime compatibility for structured evaluator inputs and add direct floating-point vector dispatch across AD backends, including Mooncake.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@gdalle gdalle left a comment

Choose a reason for hiding this comment

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

This 1000-LOC PR introduces a lot of duplication across backends, for reasons that I still don't fully understand. Again, if DI is insufficient in any way for Turing's purposes, feel free to point it out here and I'll work on it: JuliaDiff/DifferentiationInterface.jl#992

Comment on lines +40 to +42
x = AbstractPPL.flatten_to_vec(values)
val, dx = DI.value_and_gradient(p.f_vec, p.prep, p.backend, x)
grad_nt = AbstractPPL.unflatten_from_vec(p.prototype, dx)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why flatten and unflatten when you could keep the structured representation?

Comment thread ext/AbstractPPLEnzymeExt.jl Outdated
struct EnzymePrepared{E,F,T<:Real,P}
evaluator::E
f_vec::F
gradient_buffer::Vector{T}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why a Vector instead of whatever array type is appropriate (e.g. on GPU)?

dim::Int
end

AbstractPPL.capabilities(::Type{<:DIPrepared}) = DerivativeOrder{1}()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Some DI backends can go up to order 2

Comment thread ext/AbstractPPLEnzymeExt.jl Outdated
f_vec = let evaluator = evaluator, prototype = prototype
x -> evaluator(AbstractPPL.unflatten_from_vec(prototype, x))
end
grad_buf = zeros(eltype(x0), length(x0))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
grad_buf = zeros(eltype(x0), length(x0))
grad_buf = zero(x0)

Comment thread ext/AbstractPPLEnzymeExt.jl Outdated

@inline function AbstractPPL.value_and_gradient(p::EnzymePrepared, values::NamedTuple)
x = AbstractPPL.flatten_to_vec(values)
fill!(p.gradient_buffer, 0.0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
fill!(p.gradient_buffer, 0.0)
fill!(p.gradient_buffer, zero(eltype(x)))

Comment thread src/evaluator.jl Outdated
return offset
end

function flatten_to_vec(nt::NamedTuple)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There are probably packages for flattening stuff more cleanly

Comment thread src/evaluator.jl Outdated

function flatten_to_vec(nt::NamedTuple)
n = _scalar_count(nt)
vec = Vector{_vec_eltype(nt)}(undef, n)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why use a Vector to flatten e.g. a named tuple of GPU arrays?

Comment thread src/evaluator.jl Outdated
end
function _unflatten(proto::AbstractArray{<:Real}, vec::AbstractVector, offset::Int)
n = length(proto)
result = reshape(@view(vec[offset:(offset + n - 1)]), size(proto))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The unflattening doesn't have the same type as the initial input

Comment on lines +25 to +26
# FiniteDifferences has no native AbstractPPL extension, so AbstractPPLDifferentiationInterfaceExt
# is the only applicable dispatch path for this backend.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

That is obviously not true, you (or Claude?) literally added that extension above

const fdm = FiniteDifferences.central_fdm(5, 1)
const adtype = ADTypes.AutoFiniteDifferences(; fdm)

@testset "AbstractPPLDifferentiationInterfaceExt" begin
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The test code is duplicated across all backends, which is a bit of a shame

Copy link
Copy Markdown

@gdalle gdalle left a comment

Choose a reason for hiding this comment

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

Forgot to pick the correct review option

yebai and others added 4 commits April 16, 2026 16:45
Tighten the evaluator interface and backend implementations for structured and vector inputs, expand targeted backend coverage, and restore CI matrix job names so required checks report again.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… core interface.

This moves the evaluator surface into a self-contained ADProblems module and replaces the old finite-difference test helper with test_autograd backed by AutoFiniteDifferences.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yebai yebai force-pushed the evaluator-interface branch from 80fff03 to 3bbe3aa Compare April 17, 2026 17:17
… core interface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yebai yebai force-pushed the evaluator-interface branch from 3bbe3aa to 4932058 Compare April 17, 2026 17:45
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yebai yebai force-pushed the evaluator-interface branch from e58de0a to e1bccf7 Compare April 20, 2026 15:12
yebai and others added 2 commits April 21, 2026 16:36
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yebai yebai force-pushed the evaluator-interface branch from 39f5b9c to dc795a4 Compare April 26, 2026 20:54
Copy link
Copy Markdown
Member

@penelopeysm penelopeysm left a comment

Choose a reason for hiding this comment

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

As requested, I've not looked at the AD extensions. The biggest comments I have are

  • Consider moving to another package please
  • Docs would be really useful.

function AbstractPPL.prepare(
adtype::ADTypes.AbstractADType,
problem,
x::AbstractVector{<:AbstractFloat};
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.

Is there a reason why AbstractFloat rather than Real? A looser type bound of Real would allow for e.g. higher-order or sparse AD without affecting the common use case of Float64

problem,
x::AbstractVector{<:AbstractFloat};
check_dims::Bool=true,
mode::Symbol=:gradient,
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.

I think this argument shouldn't be a Symbol (notice that if you pass mode=:NotARealMode, it will do the Jacobian which is probably not what was intended). Either a custom type would be better

abstract type Mode end
struct Gradient <: Mode end
struct Jacobian <: Mode end

struct Foo{F<:Mode}
   ...
end

Comment thread ext/AbstractPPLDifferentiationInterfaceExt.jl
Comment thread src/ADProblems.jl
Comment thread src/AbstractPPL.jl Outdated
Comment on lines +14 to +15
using .ADProblems: prepare, value_and_gradient, value_and_jacobian, test_autograd
export prepare, value_and_gradient, value_and_jacobian, test_autograd
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.

Is it mandatory to re-export at the top level? Would it be sufficient to export from the submodule and call it a day?

Comment thread .github/workflows/CI.yml Outdated
- logdensityproblems
version:
- '1'
- 'lts'
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.

is there a reason to use lts instead of min which the original CI job uses?

Comment thread .github/workflows/CI.yml Outdated
Comment thread src/ADProblems.jl Outdated
@@ -0,0 +1,295 @@
module ADProblems

using ADTypes: ADTypes
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.

Is this dep actually used in src/? I couldn't see any usage. In which case, this doesn't seem like it's needed here or in Project.toml.

Comment thread src/ADProblems.jl Outdated
# follows the most specific positional method, so this fallback cannot forward to
# a backend method that doesn't itself accept these kwargs.
function prepare(
adtype,
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.

Like if this was adtype::AbstractADType then I could see us needing the ADTypes dep. But right now that's not the case

Comment thread src/ADProblems.jl Outdated
Comment on lines +76 to +80
throw(
ArgumentError(
"`prepare($(nameof(typeof(adtype)))(), ...)` requires loading the corresponding AD backend.",
),
)
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.

@yebai
Copy link
Copy Markdown
Member Author

yebai commented Apr 27, 2026

Thanks for the review. Changes made:

AbstractFloatReal — applied across src/ADProblems.jl and all AD extension files.

Removed explicit derivative modeprepare no longer takes mode=:gradient/:jacobian. The derivative kind is
inferred from the prepared function’s prototype output:

  • scalar output → use value_and_gradient
  • vector output → use value_and_jacobian
  • calling the wrong derivative API gives a direct ArgumentError.

Dropped AbstractPPL.ADProblems.dimension — the LDP-facing case is handled by LogDensityProblems.dimension.

Removed AbstractPrepared{:gradient/:jacobian} — prepared evaluators now subtype plain AbstractPrepared. The one remaining capability query uses an internal _supports_gradient trait instead.

LDP capabilities — avoided evaluating user code inside LogDensityProblems.capabilities(::AbstractPrepared).
Type-level capabilities still default conservatively to LogDensityOrder{0}() per the LDP convention; value-level methods expose gradient capability where the prepared object can actually support it.

Error handling — removed throwing fallback methods for prepare(adtype, ...), value_and_gradient, and
value_and_jacobian where they could cause method-ambiguity issues; missing backend guidance is handled with
register_error_hint in __init__. test_autograd’s missing-backend throw changed from ArgumentError to error().
The integer-vector throw(MethodError(...)) overloads on VectorEvaluator are intentionally kept because otherwise
AbstractVector{Int} silently matches the AbstractVector call method.

CheckedValidate — renamed throughout both evaluator types and their docstrings, including a note on the Trivial type-stability implication.

NT inputs in LDP ext — removed NamedTupleEvaluator-specific logdensity, dimension, and capabilities overloads; NT-backed evaluators should be used via value_and_gradient(prepared, values::NamedTuple) directly.

ADTypes dep — removed using ADTypes: ADTypes from src/; kept in [deps] since the extension files import from it.
Aqua’s stale-deps check is configured to ignore it.

Docs — added a dedicated evaluator/AD docs page covering structural vs AD preparation, vector inputs, NamedTuple
inputs, Jacobians, non-AD structural preparation, test_autograd, and supported backends.

Docstring typo (line 28), CI (ltsmin, cleaner test command) — fixed.


Not changed:

  • LDP as weak dep — kept as an extension because it is only needed for interop and does not need to be loaded for the core API.
  • test_autograd generalisation — deferred; FiniteDifferences-only for now.
  • CI continue-on-error — not redundant with fail-fast: false: the former prevents Enzyme failures from failing the overall workflow check; the latter prevents other matrix jobs from being cancelled. Both are needed.

yebai and others added 3 commits April 27, 2026 19:35
- Broaden AbstractVector{<:AbstractFloat} to {<:Real} across all AD ext
  files and ADProblems.jl
- Rename Checked→Validate on VectorEvaluator/NamedTupleEvaluator; add
  Trivial type-stability note to docstring
- Replace throwing prepare/value_and_gradient/value_and_jacobian fallback
  methods with register_error_hint in __init__; removes method-ambiguity
  hazard while keeping informative messages
- Restructure _assert_gradient_capability as two methods; change
  test_autograd missing-backend throw from ArgumentError to error()
- Remove unused `using ADTypes: ADTypes` from src/ADProblems.jl; tell
  Aqua to ignore the stale-dep finding (ADTypes is used by ext files)
- Add type-level capabilities(::Type{<:AbstractPrepared{...}}) to LDP
  ext per LDP convention; keep value-level for NT-backed gradient case
- Remove NamedTupleEvaluator-specific logdensity/dimension/capabilities
  from LDP ext (LDP expects flat-vector interface)
- CI: lts→min in ext job; cleaner julia --project=. test/run_ext_tests.jl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@penelopeysm
Copy link
Copy Markdown
Member

Could you (or Claude) explain why you are rejecting the parts of the review that you rejected?

@penelopeysm
Copy link
Copy Markdown
Member

(I think it's important for future readers to understand why the decisions were made that way)

yebai and others added 4 commits April 27, 2026 19:45
- Change `AbstractVector{<:AbstractFloat}` to `<:Real` in all test-local
  prepare/callable methods to match the branch-wide convention change
- Fix @test_throws ArgumentError -> MethodError for value_and_gradient
  on jacobian-mode prepared objects; the throwing fallback was replaced
  by an error hint, so the exception is now MethodError
- Fix minor docstring line break in prepare docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New docs/src/adproblems.md: explains the prepare/value_and_gradient API
  with runnable @example blocks covering vector inputs, NamedTuple inputs,
  Jacobians, the no-AD structural path, and a supported-backends table
- Move the API @docs block from pplapi.md to adproblems.md; replace with
  a cross-reference
- Add ForwardDiff to docs/Project.toml and trigger its extension in make.jl
  so the @example blocks execute correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use eval(Meta.parse("public ...")) inside @static if VERSION >= v"1.11.0"
so the declaration is invisible to Julia 1.10's parser. On 1.10 the symbols
are still accessible as AbstractPPL.prepare etc. but not injected into the
caller's namespace via `using AbstractPPL`.

Update all callers that relied on the unqualified export:
- test/ADProblems.jl: add explicit `using AbstractPPL: prepare, ...`
- test/ext/ad_tests.jl: qualify test_autograd as AbstractPPL.test_autograd
- docs/src/adproblems.md: add explicit import in setup block; use
  fully-qualified names in @docs blocks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the explicit gradient/jacobian mode knob so prepared evaluators choose the derivative API from the prototype output, while keeping misuse errors and LDP capabilities explicit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yebai
Copy link
Copy Markdown
Member Author

yebai commented Apr 27, 2026

Thanks, Penny. Docs have been added. All other raised issues should be addressed or explained in the comments above.

As this will likely only be implemented within TuringLang, I don't think registering a new package for it would be worth
it. I created AbstractPPL specifically for functionalities like those in this PR.

EDIT: fixed a few edge cases for AbstractPPLDIExt (1) AutoReverseDiff needs to respect the provided compiled tag (2) AutoEnzyme need DI.Const for evaluator objects.

yebai and others added 9 commits April 27, 2026 21:09
- Add type-level capabilities(::Type{<:VectorEvaluator}) to LDP ext,
  satisfying the LDP convention that capabilities must be defined at both
  type and value level (AGENTS.md)
- Use p.f in _test_autograd_ref(NamedTuple) instead of recreating an
  equivalent closure
- Inline unflatten into unflatten_to!!, removing the redundant wrapper
- Revise docs/src/adproblems.md for clarity, consistency, and precision:
  rewrite intro, rename section, tighten prose, add Jacobian shape note,
  fix DifferentiationInterface table row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_test_autograd_ref is called on any AbstractPrepared, not only FDPrepared.
MooncakePrepared (and others) have no f field, so p.f FieldErrors at
runtime. Reconstruct the flat closure from p.evaluator instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use the generic DifferentiationInterface extension for Enzyme so backend-specific Enzyme support can live in the integration test path instead of package extensions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Route DifferentiationInterface fallback calls through an explicit constant context so Enzyme does not differentiate evaluator/model state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nsistency

- Remove unreachable `_value_and_jacobian(DIPrepared{false})` (jacobian path
  always creates DIPrepared{true})
- Remove unused `UnknownPrepared`, `run_shared_namedtuple_tests`, and
  `QuadraticNTPrepared` test fixtures
- Add `capabilities(::Type{T}) where {T<:VectorEvaluator{<:Any,true}}` so
  type-level and value-level dispatch agree (AGENTS.md requirement)
- Avoid redundant `flat_length` recomputation in `flatten_to!!(::Nothing, x)`
- Flatten single-element for-loop in missing-extension test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread .github/workflows/CI.yml Outdated
Comment thread Project.toml
version = "0.14.2"

[deps]
ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"
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.

Claude is also wrong about keeping this as a dep. If you need something that is only loaded in the extensions, it should go under weakdeps.

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.

ADTypes is actually needed as a hard dependency for robust error handling -- it is on the fence, but I think it is useful as a hard dependency as it simplifies weak deps triggering, eg, ForwardDiff extension only needs using AbstractPPL, ForwardDiff.

Comment thread Project.toml
Comment thread test/Aqua.jl Outdated
Comment thread src/utils.jl
Comment on lines +130 to +137
function unflatten_to!!(x, buf::AbstractVector)
n = flat_length(x)
length(buf) == n || throw(
DimensionMismatch("Expected a vector of length $n, but got length $(length(buf))."),
)
value, _ = _unflatten(x, buf, 1)
return value
end
Copy link
Copy Markdown
Member

@penelopeysm penelopeysm Apr 28, 2026

Choose a reason for hiding this comment

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

This function is not type-stable for NamedTuples which will probably translate into perf issues:

julia> using AbstractPPL

julia> @inferred AbstractPPL.Utils.unflatten_to!!((a=1, b=2, c=3), zeros(3))
ERROR: return type @NamedTuple{a::Float64, b::Float64, c::Float64} does not match inferred return type NamedTuple

julia> @inferred AbstractPPL.Utils.unflatten_to!!((a=1, b=[2, 3], c=3), zeros(4))
ERROR: return type @NamedTuple{a::Float64, b::Vector{Float64}, c::Float64} does not match inferred return type NamedTuple

It's very likely that you need a generated function to ensure type stability for NamedTuples.

Also tests to check type stability for the various inputs would probably be a good thing. There are loads of examples in DynamicPPL's VNT tests which might be helpful inspiration

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.

Good catch, here, too.

yebai and others added 5 commits April 29, 2026 20:09
- src/utils.jl: rewrite `_unflatten(::NamedTuple)` as `@generated` to
  unroll over `Names`. The previous recursive `merge`/`Base.tail(Names)`
  pattern lost the `Names` parameter under inference, breaking
  `@inferred` callers on NamedTuple inputs.
- test/utils.jl: add `@inferred` cases for unflatten_to!! across scalar,
  mixed, nested, NT-with-tuple, empty-NT, and tuple-with-array inputs.
- src/ADProblems.jl: use `ADTypes.AbstractADType` to narrow the
  prepare/MethodError hint so it only fires for AD backends.
- test/Aqua.jl: drop the stale-deps ignore for ADTypes; the dep is now
  used in src/.
- .github/workflows/CI.yml: remove `continue-on-error` from the Ext job.
  Each matrix entry is its own PR check, so the flag was masking only
  the workflow-run summary in the Actions tab — Enzyme failures should
  surface as a red check rather than be silently green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/ADProblems.jl, ext/*: reintroduce `_supports_gradient` trait so
  `LogDensityProblems.capabilities` advertises `LogDensityOrder{1}` only
  for prepared shapes that actually implement gradients. Previous
  evaluator-type-based dispatch advertised gradient capability for any
  `VectorEvaluator`-backed `AbstractPrepared`, even ones without a
  gradient implementation. The DI extension overrides the trait to track
  whether `gradient_prep` is non-`nothing`.
- src/ADProblems.jl: replace integer-input `MethodError` with a clear
  `ArgumentError` explaining how to fix the call.
- src/ADProblems.jl: drop redundant `public` declaration; the same
  declaration in `src/AbstractPPL.jl` already covers the user-facing
  surface.
- src/ADProblems.jl, ext/AbstractPPLDifferentiationInterfaceExt.jl: WHY
  comments on the integer-vector rejection and on the missing
  `_value_and_jacobian{false}` overload (unreachable by construction).
- test/ext/logdensityproblems/: split the capability test into the
  default (no-gradient) case and an override case; the prior test
  asserted the over-eager behaviour.
- test/utils.jl, test/ADProblems.jl: move the flatten/unflatten edge
  cases next to the rest of the utility tests.
- test/ADProblems.jl, test/ext/ad_tests.jl: assert against the new
  ArgumentError message instead of `MethodError`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`AbstractPrepared` is the AD-aware shape by design; subtyping it asserts
that `value_and_gradient` is implemented. The trait was redundant
machinery for a contract the type hierarchy already encodes.

- src/ADProblems.jl: remove `_supports_gradient` and document the
  contract on the `AbstractPrepared` docstring.
- ext/AbstractPPLLogDensityProblemsExt.jl: simplify capability dispatch
  to always return `LogDensityOrder{1}` for `AbstractPrepared`. Bare
  `VectorEvaluator` keeps its trivial-dim distinction.
- ext/AbstractPPLDifferentiationInterfaceExt.jl: drop the
  `DIPrepared`-specific override.
- test/ext/logdensityproblems/: replace the override-based test with a
  single assertion that any `AbstractPrepared` subtype reports order 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Type-parameterised dispatch on UseContext was a runtime-determined Bool
encoded into the type signature; a runtime field with a single-method
branch is equivalent in performance (one well-predicted compare) and
removes one parameter from the struct's type signature. Hot-path type
stability still passes `@inferred` for both gradient and jacobian
specialisations.

Also tighten the integer-rejection comment in src/ADProblems.jl from
four lines to three.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the principle that VectorEvaluator/NamedTupleEvaluator are not
themselves differentiable — gradient capability is the contract of the
wrapping `AbstractPrepared`.

- src/ADProblems.jl: drop the `Trivial` type parameter from
  `VectorEvaluator` and remove the trivial-dim
  `value_and_gradient`/`value_and_jacobian` methods.
- ext/AbstractPPLLogDensityProblemsExt.jl: bare `VectorEvaluator`
  unconditionally reports `LogDensityOrder{0}`; remove the
  `{V,true}` capability override and the matching
  `logdensity_and_gradient` overload.
- ext/AbstractPPLDifferentiationInterfaceExt.jl: empty inputs now
  return a `DIPrepared` with `nothing` preps; `value_and_gradient`
  and `value_and_jacobian` short-circuit when `length(x) == 0`,
  bypassing DI (many backends fail on length-zero arrays).
- test/ADProblems.jl: drop the bare-VectorEvaluator zero-dim test.
- test/ext/differentiation_interface/: replace it with an end-to-end
  empty-input test that goes through `prepare(adtype, ...)`.
- test/ext/logdensityproblems/: replace the trivial-dim VectorEvaluator
  gradient test with the corrected order-0 assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

rename LogDensityFunction to NamedLogDensity

4 participants