Skip to content

fix(zygote ext): handle NamedTuple cotangents in vofa_u_adjoint (EnsembleSolution end-time grads)#623

Closed
ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-namedtuple-cotangent-ensemblesolution
Closed

fix(zygote ext): handle NamedTuple cotangents in vofa_u_adjoint (EnsembleSolution end-time grads)#623
ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-namedtuple-cotangent-ensemblesolution

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Please ignore until reviewed by @ChrisRackauckas.

What this PR is (and is not)

This is a local robustness fix in the Zygote extension: vofa_u_adjoint must not force structural (NamedTuple) cotangents into a VectorOfArray/DiffEqArray — that reconstruction calls ndims(::NamedTuple), which has no method. Structural per-element cotangents now pass through as a plain vector.

This does NOT fix SciML/DifferentialEquations.jl#1149. End-to-end verification (below) shows the failure merely moves downstream, and the underlying capability (reverse-mode gradients w.r.t. sol.t, e.g. terminate! halting times) does not exist in the adjoint path at all.

Original symptom

Zygote through an EnsembleSolution when the loss reads only a scalar field per trajectory:

T_stars = [sol.t[end] for sol in ensemble_sol.u]   # halting times via terminate! callback
ERROR: MethodError: no method matching ndims(::@NamedTuple{…})
  [1] RecursiveArrayTools.VectorOfArray(vec::Vector{@NamedTuple{…}})
  [2] vofa_u_adjoint(...)

Each per-trajectory cotangent is a structural NamedTuple ((u = nothing, t = OneElement(...), …)), not an array, because only sol.t[end] is differentiated. This is the structural-tangent sibling of the ZeroTangent case fixed in #606 (same function).

End-to-end verification of the full issue MRE (Julia 1.11.9, OrdinaryDiffEq v7.1.1, SciMLBase v3.31.0, SciMLSensitivity v7.113.0, Zygote v0.7.11)

With this patch, the ndims crash is gone and the NamedTuple cotangents (carrying the real t::ChainRules.OneElement seed) flow through. The failure then relocates to SciMLBase:

ERROR: BoundsError: attempt to access Tuple{Int64} at index [0]
  [5] EnsembleSolution_adjoint(p̄::Vector{@NamedTuple{u::Nothing, …, t::OneElement{…}, …}})
    @ SciMLBaseZygoteExt ~/.julia/packages/SciMLBase/V1DOE/ext/SciMLBaseZygoteExt.jl:83

(the generic p̄::AbstractArray{T,N} method assumes N ≥ 2; a Vector{NamedTuple} matches it with N = 1 since NamedTuple is not <:AbstractArray).

More importantly, fixing that plumbing would produce silently wrong gradients. Δ.t is consumed nowhere in SciMLSensitivity — the backpass only reads Δ.u. Verified on a minimal single-trajectory case (no ensemble, no RAT in the loop), loss(θ) = sol.t[end] with GaussAdjoint(ReverseDiffVJP()) + terminate!:

ForwardDiff: ‖∇‖ = 0.536
Zygote:      ‖∇‖ = 0.0     (adjoint runs, t-cotangent silently discarded, no error)

So the pre-existing state for the non-ensemble version of this loss is a silent zero gradient, which is arguably a worse bug than the crash reported in the issue.

Consequences

Test

Added to test/AD/adjoints.jl, mirroring the #606 test: NamedTuple cotangents through vofa_u_adjoint for both VectorOfArray and DiffEqArray, plus a mixed NamedTuple/nothing/ZeroTangent case. Verified locally: the new test reproduces the exact ndims(::NamedTuple) error on unmodified source and passes with the fix; full AD/adjoints.jl suite passes.

🤖 Generated with Claude Code

@ChrisRackauckas-Claude ChrisRackauckas-Claude force-pushed the fix-namedtuple-cotangent-ensemblesolution branch from 04cca47 to d5029ea Compare July 5, 2026 16:17
`vofa_u_adjoint` rewrapped the per-element cotangents of an
`AbstractVectorOfArray`/`AbstractDiffEqArray` in a `VectorOfArray`/`DiffEqArray`.
For an `EnsembleSolution` whose elements are solution objects, differentiating
only a scalar field (e.g. `sol.t[end]`) makes Zygote produce structural
`NamedTuple` cotangents per trajectory. These have no `ndims`, so the
reconstruction threw `MethodError: no method matching ndims(::NamedTuple)`.

Detect non-array cotangents and pass them through as a plain vector instead.
This extends the earlier `ZeroTangent`/`AbstractZero` handling (SciML#606) to the
structural-tangent case.

Note: this is a local robustness fix only; it does NOT fix
SciML/DifferentialEquations.jl#1149. The cotangent then fails downstream in
SciMLBase's `EnsembleSolution_adjoint`, and SciMLSensitivity never consumes a
`t` cotangent (verified: single-solve `sol.t[end]` loss silently returns zero
gradient under GaussAdjoint while ForwardDiff gives the true nonzero one).
See the PR description for full verification details.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude ChrisRackauckas-Claude force-pushed the fix-namedtuple-cotangent-ensemblesolution branch from d5029ea to c9fd4a7 Compare July 5, 2026 19:07
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.

Zygote Cant differentiate through EnsembleSolution end times when using terminate! callbacks

2 participants