Skip to content

Fix batch-forward typeassert: Enzyme returns AnonymousStruct, not NTuple#50

Merged
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:fix-batch-forward-typeassert
May 21, 2026
Merged

Fix batch-forward typeassert: Enzyme returns AnonymousStruct, not NTuple#50
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:fix-batch-forward-typeassert

Conversation

@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor

⚠️ Please ignore this PR until reviewed by @ChrisRackauckas.

Summary

Fixes the long-failing Enzyme batch forward mode (width > 1) testset on main. The failure existed before issue #48 and is unrelated to it — surfaced while running the test suite during #48 investigation.

The forward rules for batch width > 1 asserted:

shadow_result = Enzyme.autodiff(mode, Const(f_orig), BatchDuplicated{T, W}, args...)
return shadow_result[1]::NTuple{W, T}

But Enzyme's batch shadow comes back wrapped in Enzyme.Compiler.AnonymousStruct, which is defined (Enzyme/src/compiler/utils.jl:480) as:

@inline AnonymousStruct(::Type{U}) where {U<:Tuple} =
    NamedTuple{ntuple(Symbol, Val(length(U.parameters))), U}

So the runtime type is @NamedTuple{1::Float64, 2::Float64}, not Tuple{Float64, Float64} — the typeassert fires with:

TypeError: in typeassert, expected Tuple{Float64, Float64},
got a value of type @NamedTuple{1::Float64, 2::Float64}

Fix

Wrap with Tuple(...) before the typeassert in both batch-mode rule branches:

return Tuple(shadow_result[1])::NTuple{W, T}      # shadow-only
shadows = Tuple(shadow_result[1])::NTuple{W, T}   # ForwardWithPrimal

Tuple(::NamedTuple) strips the field-name wrapper and yields a plain NTuple{W, T} (the underlying storage), which is what the BatchDuplicated shadow contract expects.

Bisect

Not an Enzyme regression. Tested Enzyme 0.13.100 → 0.13.148 — every version returns the AnonymousStruct/NamedTuple shape. The FWW rule's assertion was wrong from the moment it was written (introduced in PR #45 / v1.8.0).

Smallest reproducer

using FunctionWrappersWrappers, Enzyme, EnzymeCore
f(x) = x^2
fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,))
Enzyme.autodiff(Forward, Const(fww), BatchDuplicated, BatchDuplicated(3.0, (1.0, 2.0)))
# Before: TypeError in typeassert
# After:  ((var"1" = 6.0, var"2" = 12.0),)

Tests

The existing Enzyme batch forward mode (width > 1) testset (test/enzyme_tests.jl:55) — which was the failing one — now passes. Full local suite on Julia 1.11 + Enzyme v0.13.147:

Testset Pass Total
FunctionWrappersWrappers.jl 48 48
BigFloat support 5 5
UnionAll return types 2 2
Enzyme extension 44 44
Mooncake extension 13 13

Enzyme extension was previously 39 pass / 1 error.

Relationship to #49

Independent of PR #49 (the issue #48 fix). This PR branches off main; #49 should rebase cleanly on top of (or under) this one. Could be merged in either order.

Test plan

  • Failing Enzyme batch forward mode (width > 1) testset passes locally
  • No other testsets regressed (44 / 44 in Enzyme extension)
  • Bisected against Enzyme 0.13.100 → 0.13.148 — same NamedTuple return shape, so fix is forward-compatible
  • CI green on all jobs

🤖 Generated with Claude Code

ChrisRackauckas and others added 2 commits May 21, 2026 05:17
The forward rules for batch width > 1 asserted
`shadow_result[1]::NTuple{W, T}` but `Enzyme.autodiff(Forward, …,
BatchDuplicated{T,W}, …)` returns the batch shadow wrapped in
`Enzyme.Compiler.AnonymousStruct` — a
`NamedTuple{(:1, :2, …), NTuple{W, T}}` (see
`Enzyme/src/compiler/utils.jl:480`).

The mismatch tripped the existing `Enzyme batch forward mode (width > 1)`
testset on `main` with:

```
TypeError: in typeassert, expected Tuple{Float64, Float64},
got a value of type @NamedTuple{1::Float64, 2::Float64}
```

Wrap the shadow in `Tuple(...)` before the type-assert so the rule's
return matches the `BatchDuplicated` shadow contract that Enzyme expects
from a forward rule.  Applies to both `{false, true, W>1, …}`
(shadow-only) and `{true, true, W>1, …}` (ForwardWithPrimal) rules.

The existing testset (which was failing on `main`) now passes.  Full
local test summary on Julia 1.11 + Enzyme v0.13.147:

```
FunctionWrappersWrappers.jl |   48     48
BigFloat support            |    5      5
UnionAll return types       |    2      2
Enzyme extension            |   44     44
Mooncake extension          |   13     13
```

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Calls `EnzymeRules.forward` directly so the rule's actual return value
is observable.  The pre-existing `Enzyme.autodiff(Forward, …)`-driven
testset doesn't catch a regression on its own because the outer
`Enzyme.autodiff` ALSO wraps in `AnonymousStruct`, and `shadow[1]`
indexing works on both `NamedTuple` and `Tuple`.

The new testset asserts:
- the shadow-only rule (`{false, true, W=2, …}`) returns `NTuple{2, Float64}`,
  not `NamedTuple`;
- the ForwardWithPrimal rule (`{true, true, W=2, …}`) puts an
  `NTuple{2, Float64}` (not a `NamedTuple`) into `result.dval`;
- the conversion generalises to W = 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review May 21, 2026 09:25
@ChrisRackauckas ChrisRackauckas merged commit 01bcfe7 into SciML:main May 21, 2026
6 of 7 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.

2 participants