Skip to content

ext/Mooncake: bridge copyto!(::ComponentArray, ::Mooncake.Tangent)#369

Merged
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:mooncake-copyto-componentarray
May 20, 2026
Merged

ext/Mooncake: bridge copyto!(::ComponentArray, ::Mooncake.Tangent)#369
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:mooncake-copyto-componentarray

Conversation

@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented May 18, 2026

Please ignore until reviewed by @ChrisRackauckas.

Summary

Bridge copyto!(::ComponentArray, ::Mooncake.Tangent) for both the flat-Array-backed and SubArray-backed ComponentVector tangent shapes so Mooncake gradients can be written back into a ComponentArray-shaped buffer.

The bug

using Optimization, OptimizationOptimJL, ComponentArrays
import Mooncake

f(x, _) = sum(abs2, x)
optf = OptimizationFunction(f, AutoMooncake())

# works
solve(OptimizationProblem(optf, randn(100)), LBFGS(); maxiters=10)

# fails:
#   MethodError: no method matching iterate(
#     ::Mooncake.Tangent{@NamedTuple{data::Vector{Float64}, axes::Mooncake.NoTangent}})
solve(OptimizationProblem(optf, ComponentArray(a=randn(100))), LBFGS(); maxiters=10)

Root cause

DifferentiationInterface.value_and_gradient!(::AutoMooncake, …) writes the gradient into the user-supplied grad buffer with an unconditional copyto!(grad, new_grad) (DI's DifferentiationInterfaceMooncakeExt/onearg.jl:153). For a plain Vector{Float64} primal this works because Mooncake.tangent_type(Vector{Float64}) === Vector{Float64}, so the source is an array. For a ComponentArray primal, however, Mooncake.tangent_type is a Mooncake.Tangent struct — not an AbstractArray — so the generic Base.copyto!(::AbstractArray, ::Any) fallback drops into the iterate-based path at abstractarray.jl:934 and throws MethodError for iterate.

DI's twoarg and forward call sites already handle the "tangent ≠ primal-shape" case via dy isa tangent_type(Y) ? dy : _copy_to_output!!(prep.dy_righttype, dy). The value_and_gradient! path is the one missed case; a parallel issue could be filed against DI to apply the same pattern there. Until that lands, defining copyto! for the (ComponentArray, Mooncake.Tangent) pairs fully unblocks the user-visible failure in Optimization.jl + AutoMooncake + ComponentArrays.

Coverage

I probed the obvious variations against this branch. Results:

# Case Status
1 Flat 1D ComponentVector ✅ original failing case — fixed by overload (a)
2 Nested CV (ComponentArray(u=…, p=ComponentArray(…))) ✅ still flat-backed underneath; same overload (a)
3 ComponentMatrix (2D backing) Matrix{P} <: Array{P} — overload (a) covers it; regression test added
4 Float32 CV _FloatLike covers it
5 SubArray-backed CV, full cover ✅ new overload (b); test added
6 SubArray-backed CV, partial cover ✅ clear ArgumentError (symmetric to existing _increment_subarray_fdata! guard); test added
7 Non-mutating DI.gradient(f, prep, ::AutoMooncake, ::CV) ⚠️ returns a Mooncake.Tangent, not a CV — outside CA's reach, needs DI/Mooncake-level fix
8 OptimizationProblem(optf, parent_cv.u) where u is a flat 1D component ⚠️ parent_cv.u returns a raw SubArray, not a CV — Mooncake-on-SubArrays issue, outside CA's scope

(7) is a latent DI contract issue: the non-mutating DI.gradient path runs _copy_output(new_grad) and returns the deep-copied Tangent, so the caller gets back a type that doesn't match the input's shape. The right fix is in DI (mirror the _copy_to_output!! pattern) or Mooncake's _copy_output dispatch. Mentioning here for visibility but deliberately not addressing in this PR.

(8) requires a Mooncake-side copyto!(::Vector, ::Mooncake.Tangent{…SubArray-tangent…}) since the destination isn't a ComponentArray at all once the wrapper is dropped.

What this PR does

  • ext/ComponentArraysMooncakeExt.jl: adds two Base.copyto! overloads —
    • (a) (ComponentArray{P,N,<:Array{P}}, Tangent{@NamedTuple{data::Array{P}, axes::NoTangent}}) → copies src.fields.data into getdata(dest).
    • (b) (ComponentArray{P,N,<:AbstractArray{P}}, Tangent{@NamedTuple{data::Tangent{@NamedTuple{parent::Vector{P}, indices::NoTangent, offset1::NoTangent, stride1::NoTangent}}, axes::NoTangent}}) → copies src.fields.data.fields.parent into getdata(dest), guarded by a length(parent) == length(getdata(dest)) full-cover check that throws a clear ArgumentError on partial-cover views (symmetric to the existing _increment_subarray_fdata! block).
  • test/autodiff/autodiff_tests.jl: adds four targeted tests in the Mooncake testset — flat CV, ComponentMatrix, full-cover SubArray-backed CV, and @test_throws ArgumentError for partial-cover SubArray-backed CV.
  • Project.toml: bumps version = "0.15.39".

Local verification

Autodiff test group passes with Mooncake testset at 15/15 (was 7/7 on main).

End-to-end with the dev'd ComponentArrays:

---- ComponentArray case ----
retcode = Success   objective = 0.0
typeof(sol.u) = ComponentVector{Float64, Vector{Float64}, …}
OK

Previous CI on the (a)-only commit was 18/18 green (all matrix entries for Julia 1, lts, pre × Core/Autodiff/Downstream/Reactant/GPU/nopre, plus Runic, build, doc-build).

Test plan

  • CI: Core group passes
  • CI: Autodiff group passes (new copyto!(::CV, ::Mooncake.Tangent) cases in test/autodiff/autodiff_tests.jl)
  • CI: Downstream group passes
  • CI: Reactant group passes
  • CI: nopre group passes

`Mooncake.tangent_type(ComponentVector{P,Vector{P},Axes}) ===
Mooncake.Tangent{@NamedTuple{data::Vector{P}, axes::NoTangent}}`.
That tangent is not an `AbstractArray`, so the generic
`Base.copyto!(::AbstractArray, ::Any)` fallback tries to iterate it and
throws `MethodError: no method matching iterate(::Mooncake.Tangent{…})`.

Optimization.jl + AutoMooncake + ComponentArrays hits this path because
`DifferentiationInterface.value_and_gradient!(::AutoMooncake, …)` writes
the gradient into a user-supplied `grad` buffer with an unconditional
`copyto!(grad, new_grad)`. The plain-`Vector` case works only because
Mooncake's tangent_type for `Vector{Float64}` is `Vector{Float64}` itself.

Define `Base.copyto!` for the flat-Array-backed `ComponentArray` case so
the tangent's `data` field flows directly into the ComponentArray's
underlying storage. SubArray-backed CVs are unaffected (the same kind of
overload could be added there once a concrete failure surfaces).

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@gdalle
Copy link
Copy Markdown
Contributor

gdalle commented May 19, 2026

I'm not sure that this is the correct fix. I think since Mooncake v0.5.25 (more specifically since chalk-lab/Mooncake.jl#1103), every package that defines types with "friendly tangents" needs to add some overloads. See e.g. the similar discussion around StaticArrays:

as well as more general background:

…atrix test

A `ComponentVector` obtained from `getproperty(::ComponentVector, ::Symbol)` on a
nested parent is `<:SubArray`-backed, so Mooncake's `tangent_type` for it nests
an inner `Tangent` that mirrors the SubArray's `(parent, indices, offset1,
stride1)` fields. The flat-Array `copyto!` overload doesn't dispatch on that
shape, so `DI.value_and_gradient!` would still throw a `MethodError: iterate`.

Add a parallel overload for the SubArray-backed CV tangent shape, symmetric to
the existing `_increment_subarray_fdata!` block. As with that path, the copy is
only well-defined when the view fully covers its parent (the SubArray indices
are not recoverable from Mooncake tangent shape alone); the partial-cover case
throws a clear `ArgumentError` rather than silently producing wrong gradients.

Also add a `ComponentMatrix` copyto! regression test — `Matrix{P} <: Array{P}`
already matches the flat-Array signature, this just locks in the behavior.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review May 20, 2026 09:15
@ChrisRackauckas
Copy link
Copy Markdown
Member

It probably could be handled more generically, but with the current development progression I have been suggested to go this direction, and this patch at least unblocks users for today. If things change in the core repo then we can remove the patch.

@ChrisRackauckas ChrisRackauckas merged commit 684f8fb into SciML:main May 20, 2026
18 checks passed
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.

3 participants