Redesign forward mode around Lifted{P,N,V} and chunked NDual duals - #1215
Redesign forward mode around Lifted{P,N,V} and chunked NDual duals#1215yebai wants to merge 744 commits into
Lifted{P,N,V} and chunked NDual duals#1215Conversation
…ast) Seventh increment of decoupling reverse mode from forward mode. Inline native `_pow_grad_x`/`_pow_grad_p` scalar helpers (pure math, with the x==0 removable-singularity limits) and use them for both binary `^` (via `_binary_deriv`, moved into the native binary loop) and the integer-exponent `pow_fast(x, n::Integer)` rrule — which previously called `Nfwd._nfwd_pow_grad_x` and so still depended on the forward module. `_rev_contract` keeps an inactive (zero-cotangent) lane exactly zero where the gradient is ±Inf. Remaining nfwd-backed binaries: mod (floor term), max/min (subgradient selection) — deferred to a careful fire. Verified: `test_rule` (both modes, widths 1-3) passes for ^ (incl. fractional exponent), pow_fast (incl. negative integer exponent), on 1.10 and 1.12; still-nfwd mod/max/min confirm the split left the rest intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…fully native Eighth increment of decoupling reverse mode from forward mode. Add native `_binary_deriv` methods for mod (d/dx=1, d/dy=-floor(x/y), both NaN at the integer-quotient discontinuities) and max/min (subgradient (1,0)/(0,1) selected by native `_pick_first_max`/`_pick_first_min`, matching Base's tie convention). This empties and removes the last nfwd-backed fixed-arity loop — the entire 2-arg scalar cluster (atan/atan_fast/log/^/mod/max/min) now has native reverse rrules. Verified: `test_rule` (both modes, widths 1-3) passes for mod (incl. negative dividend), max/min (both orderings), and regression on ^/atan, on 1.10 and 1.12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…seeder Ninth increment of decoupling reverse mode from forward mode. Give clamp(a,lo,hi) a native reverse rrule (subgradient: derivative 1 for the selected argument, 0 for the other two, matching Base's nested-ifelse selection) and hypot(x, xs...) a native vararg pullback (dhypot/dxᵢ = xᵢ/h, masked to 0 when xᵢ==0 which also collapses the all-zero 0/0=NaN case). Both drop their NDual-seeding reverse path. With every multi-argument reverse rule now native, `_nfwd_seed_inputs` (the width-M identity seeder) has no remaining callers, so remove it. Verified: `test_rule` (both modes, widths 1-3) passes for clamp (all three branches) and hypot (2-arg, 3-arg vararg, and a zero component), on 1.10 and 1.12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…FastMath.sincos Tenth increment — completes the reverse-mode decoupling. The last four nfwd-backed unary functions now have native reverse rrules: mod2pi/nextfloat/ prevfloat via `_unary_deriv` (local slope 1), and FastMath.sincos via a standalone tuple pullback (d(sin)/dx=cos, d(cos)/dx=-sin) mirroring sincosd. With no reverse rule seeding an NDual anymore, remove the now-dead reverse-only helpers `_nfwd_input_grads` and `_contract`, and rewrite the file header to describe the new split: forward frules run the `f(::NDual)` overloads (the only dependence on the Nfwd submodule); reverse rrules are direct native analytic pullbacks with no NDual/Nfwd/ChainRules dependency. `_nfwd_out_value`/`_typeof` remain as forward-mode output helpers. Verified: `test_rule` (both modes, widths 1-3) passes for all four on 1.10 and 1.12; the full rules/low_level_maths group passes on 1.10 (--check-bounds=yes, CI mode) as an integration gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…l_maths.jl Final step of task 159. The former `rules_via_nfwd.jl` no longer holds any reverse-mode-via-forward machinery (all its reverse rrules are now direct native analytic pullbacks), so its contents — the forward NDual `frule!!`s, the native reverse `rrule!!`s, the derivative-factor tables, and the `Val(:rules_via_nfwd)` test-case registry — move into `src/rules/low_level_maths.jl`, the sibling file that already registered the scalar-math test cases and ran this key. Drop the standalone file and its `include` from Mooncake.jl. Reference updates: AGENTS.md now points NDual-scalar-rules at low_level_maths.jl; the Nfwd.jl docstring, `scalar_rules_via_ndual.md`, and comments in foreigncall.jl/fastmath.jl are corrected to say reverse mode is native and no longer routes through Nfwd. The `Val(:rules_via_nfwd)` registry key is retained (referenced by the test driver). Verified: Mooncake loads without the file; the full rules/low_level_maths group (both `Val(:low_level_maths)` and `Val(:rules_via_nfwd)` cases) passes on 1.10 (--check-bounds=yes) and 1.12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
Port the deferred forward-mode GPU concat/permutedims rules to the Lifted/
NDualArray representation (they previously threw "not yet implemented"). Each is
a linear op, so the frule mirrors the existing reverse rrule: `arrayify` each
argument's primal and its per-lane partials through any wrapper, apply the op to
the primals for `y`, apply the same op per lane to build `y_partials`, and wrap
the dense result as `NDualArray{eltype(y),Nw,ndims(y),typeof(y)}(y, y_partials)`
— the same pattern as the reshape/view CUDA frules, extended to multiple args
(concat) and a permutation (permutedims). `arrayify` handles plain CuArrays
(NDualArray base case) and Adjoint/Transpose/SubArray wrappers identically to the
reverse path.
GPU-verified (CUDA_VISIBLE_DEVICES=7, Julia 1.10): `test_rule` in both forward and
reverse mode passes for `sum(vcat/hcat/cat/permutedims(...))` over CuArrays,
including an Adjoint-wrapped vcat, against finite differences and the reverse
`_cu_concat_pb!` oracle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
The two forward-mode "mixed GPU/CPU cat guards" assertions constructed argument slots with the removed two-field `Mooncake.Dual(...)` API, so the expressions errored (UndefVar) rather than reaching the guard `frule!!` — surfacing as two test failures. Switch them to `Mooncake.lift(primal, tangent)` (the width-1 Lifted slot builder already used by the direct-frule pointer tests in this file). The mixed-device guard `frule!!` methods themselves are already Lifted-based. GPU-verified (CUDA_VISIBLE_DEVICES=7): both the mixed vcat and the mixed cat-kwcall calls now dispatch to the guard and throw the "mix of GPU" error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…verload Review findings #164/#165. The forward frules stored the primal as the fused `fma_float`/`muladd_float(px,py,pz)` (single rounding) but built the inner NDual `.value` from `tangent(x)*tangent(y) + tangent(z)` — two separate roundings via the NDual `*` then `+`. Under cancellation these disagree (e.g. a=b=1+2^-27, z=-(a*b): the fused primal is 5.55e-17 but the non-fused inner value rounds to 0.0), violating the inner-value invariant. Route the inner value through the fused `fma`/`muladd` NDual overloads (Nfwd.jl) and read the primal back from the dual (`dy.value`) instead of recomputing it, so the inner value equals the primal exactly. Partials are unchanged. Regression: a direct assertion that the frule's inner `.value` equals the primal under the cancellation input — test_rule's approximate value check cannot express this ~1e-17 drift. Verified: builtins group passes on 1.10; the assertion and `test_rule` (both modes) pass on 1.10 and 1.12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
Review findings #167/#168. A `RefValue` has a single field, so `getfield(r, 1)` and `getfield(r, :x)` are equivalent, but the Ref-specific forward frules for `lgetfield` (2-arg and 3-arg-with-order) and `lsetfield!` matched only `Lifted{Val{:x}}`. A positional `Lifted{Val{1}}` fell through to the generic frule, which then hit `_get_lifted_field(::NDualRef, ::Int)` (lgetfield) or no matching method at all (lsetfield!) — a MethodError. Reverse mode already handles `Val{f}` generically, so this was a forward/reverse parity gap. Broaden the three Ref frule signatures to `Lifted{<:Union{Val{:x},Val{1}}}`; the bodies already use the `:x` literal, which is correct for field 1. The IR transform normalises positional Ref access, so the gap is only reachable via a direct `frule!!` call; the regression test therefore invokes the frule directly with `Val(1)` and `Val(:x)` (test_rule cannot express it). Verified: misc group passes on 1.10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
… arrays Review finding #166. The forward `jl_reshape_array` frule had exactly two element paths: numeric-leaf arrays (parallel-arrays `NDualArray` V) and a `Array{NoDual}` V for non-differentiable elements. An array of *differentiable non-numeric* elements — a struct (V = `Array{ImmutableDual{…}}`) or a tuple (V = `Array{Tuple{NDual,…}}`) — matched neither forward frule, so `reshape` errored in forward mode even though the reverse rrule is element-type-generic. Generalise the second frule from `AbstractArray{NoDual}` to any element-wise `Array{VE}` V (`VE` = the element dual: `NoDual`, `ImmutableDual`, tuple duals, …), reshaping the V generically as `Array{VE,M}`, mirroring the reverse. The numeric case still takes the `NDualArray` frule (`NDualArray` is not an `Array`, so there is no overlap). Regression: `test_rule` (forward mode) on reshaping an array of a two-field struct and an array of a 2-tuple. Verified: foreigncall group passes on 1.10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…is BlasFloat-only) Review finding #169. The `_kron!` `@is_primitive` covered all `IEEEFloat` matrices, but the wrapper-fallback frule (for Triangular/Symmetric/Adjoint/… inputs whose V is a struct lift) calls `arrayify`, which only supports `BlasFloat`. A Float16 wrapped input matched the primitive and the fallback but then hit a raw `MethodError` in `arrayify`, whereas the reverse rrule and derived forward mode both handle it. Split the `@is_primitive` into a dense case (`Array{T,2}³`, any `IEEEFloat` — served by the dense frule, which reads `NDualArray` partials directly and never touches `arrayify`, so Float16 dense stays primitive) and a general/wrapped case (`AbstractMatrix{T}³`, `BlasFloat` only). Float16 wrapped inputs are thus left non-primitive and handled by derived forward mode. Narrow the wrapper-fallback frule to `BlasFloat` to match. Regression: forward `test_rule` on a Float16 `UpperTriangular`-input `kron!` (loose tolerance — Float16 finite differences are imprecise; the point is it runs as derived rather than crashing). Verified: performance_patches group passes on 1.10, and Float32/Float64 wrapped `kron!` remain primitive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…Dual V Review findings #170/#171. `sum(predicate, ::CuArray)` (and the Adjoint/Transpose variant) crashed in forward mode when `f` maps to a non-differentiable type (e.g. a `Bool` predicate → `Int` reduction). Two problems, both now fixed: 1. `_gpu_decode_ndual_output(Val(:sum), …)` reduced with `init = zero(_nfwd_dual_primal_type(eltype(out)))`. For a `Bool` output that init is `false::Bool`, but summing `Bool`s promotes to `Int`, so the GPU reduction ran with a mismatched accumulator type and threw a `KernelException`. Branch on `is_diff`: differentiable output reduces the NDual `.value`s with the matching init; non-differentiable output uses plain `sum(out)` and lets the result type promote naturally (mirrors the `Val(:broadcast)` decode). 2. The `sum(f, x)` frules (dense and Adjoint/Transpose) then called `_wrap_scalar_v_lanes(primal_out, …)`, which only supports float scalars, so an `Int` `primal_out` would `MethodError` (the failure the review traced statically). Return a `NoDual` V in the `!is_diff` branch instead — the reduction is non-differentiable, matching the zero-derivative reverse rrule. GPU-verified (CUDA_VISIBLE_DEVICES=7): `test_rule` in both forward and reverse mode passes for `sum(x -> x > 0.5, x)` on a dense CuArray and an Adjoint; the differentiable `sum(abs2, x)` control still passes. Regression cases added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…not NaN) Review finding #172. The chunked `llvm.powi` forward frule scaled the partials with the unguarded `Nfwd._pt_scale`, unlike the `pow_fast` NDual overload it mirrors (which uses `_pt_guarded_scale`). At the `x == 0` negative-exponent singularity the local gradient is `±Inf`, so an inactive (zero-seed) lane became `0 * Inf = NaN` instead of staying `0.0`, polluting the other lanes' independence. Switch to `Nfwd._pt_guarded_scale`, which zeros an inactive lane before the multiply. Regression: a width-2 direct frule call at `x = 0.0`, exponent `-2`, with lane 1 seeded and lane 2 inactive, asserting lane 2's partial is exactly `0.0`. Verified: foreigncall group passes on 1.10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
….value Review finding #173. `_add_to_primal_internal(::MaybeCache, x::T, t::NDual{T,N}, ::Bool)` returned `x + t.value + sum(t.partials)`. But an inner `NDual`'s `.value` is the primal it shadows (inner-value invariant) — i.e. `x` itself — so adding it double-counted the primal: adding a zero-partials V returned `2x` instead of the identity `x` (reproduced: `_add_to_primal(3.0, NDual(3.0, (0,0)))` gave `6.0`). Add only the partials so the operation is `x + sum(t.partials)`; a zero-partials V is now the identity, and the struct/mutable overloads that recurse into this scalar case inherit the fix. Regression: direct assertions that `_add_to_primal(x, NDual(x, zeros)) == x` and `_add_to_primal(x, NDual(x, (1,2))) == x + 3`. Verified: basic group passes on 1.10; low_level_maths forward-FD group unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…re Float16 reverse) The #169 split declared both `_kron!` primitives with the two-argument `@is_primitive`, which registers a signature for BOTH forward and reverse mode. The wrapper-fallback declaration used `T<:BlasFloat` (which includes `Complex`) purely so the forward `arrayify`-based frule could cover complex wrapped inputs — but it also marked complex `_kron!` a *reverse* primitive, while the reverse rrule is real (`IEEEFloat`) only. Complex reverse-mode `_kron!`/`kron!` therefore hit a `MethodError` instead of routing through derived mode as it did on `main` (#177). The same narrowing to `BlasFloat` dropped wrapped-Float16's reverse-mode primitive status that `main` had (#180). Split by mode: forward stays dense-`IEEEFloat` (fast NDualArray path, incl. Float16) + wrapped-`BlasFloat` (arrayify fallback); reverse is a single `AbstractMatrix{T} where T<:IEEEFloat` declaration matching the reverse rrule's coverage exactly — no complex, with Float16, dense and wrapped — restoring `main`'s reverse behaviour. Regression test asserts the per-mode primitivity and runs complex reverse kron through derived mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…d main The native reverse rule added when `rules_via_nfwd.jl` was removed used `_unary_deriv(::typeof(mod2pi), x, y) = one(x)`, a constant local slope of 1. At a multiple of 2π (`isinteger(x/2π)`, e.g. the common `x == 0`), `mod2pi` is discontinuous: the forward `mod2pi(::NDual)` overload returns a NaN derivative coefficient there (`_nfwd_mod2pi_grad`), as did `main`'s reverse mode (which also seeded an NDual). The constant slope 1 therefore made reverse mode silently disagree with forward mode and with `main`, and was inconsistent with the branch's own `mod`/`^` discontinuity handling. Return NaN at the wrap. Adds a reverse-mode wrap regression test (pullback at x=0 and x=2π is NaN; away from the wrap, slope 1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
… base 0 `b^a` with a plain real base `b` and an NDual exponent scaled the partials with the unguarded `_pt_scale`. At the removable singularity `b == 0` (with a positive exponent) the primal is 0 and the derivative factor `b^a·log(b)` is `0·(-Inf) = NaN`, so an inactive (zero-seed) lane became NaN instead of staying exactly 0. Use `_pt_guarded_scale`, matching `log`/`sqrt`/`cbrt`/the `pow`/ `powi` rules — the active lane keeps the genuine singular NaN, inactive lanes stay 0. Regression test in the nfwd power testset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…ields
Writing a lane tangent through a `MutableDualTangentView` merged the new field
V into the parent `MutableDual`'s backing NamedTuple and wrote it back with a
bare `setfield!`. For a mutable struct with an abstract field type (e.g.
`x::Real`), the dual field NamedTuple is abstract (`@NamedTuple{x}`, x::Any),
and the `merge` narrows it to a concrete element type (`@NamedTuple{x::NDual}`)
which is not `isa` the stored abstract type — `setfield!` is strict, so it
threw a `TypeError`. Wrap the merge in `convert(typeof(nt), ...)`, mirroring the
`_setfield_tangent!(::MutableDual)` writeback that already guards this exact
case. Regression test lifts a struct with an abstract field and writes a lane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
The forward `unsafe_wrap` frules covered only scalar float/complex pointers
(`NDualEltype`, packed into an `NDualArray` V), pointer-to-scalar
(`Ptr{Ptr{R}}`), and non-differentiable pointers (`NoDual` V). A differentiable
pointer whose element `S` has `tangent_type(S) !== NoTangent` but is neither an
`NDualEltype` scalar nor a pointer-to-scalar (e.g. `Ptr{Tuple{Float64,Float64}}`
or `Ptr{Vector{Float64}}`) has V `NTuple{Nw,Ptr{tangent_type(S)}}`, matching no
method — a raw `MethodError`, even though the broad `@is_primitive` covers every
`Ptr` and the reverse rule handles all `T`. Add the same `NTuple{Nw,Ptr}`
catch-all the sibling pointerref/pointerset/atomic_pointerset rules use, failing
loudly with a clear `ArgumentError` instead. Regression test covers widths 1-2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
Both frules computed the primal `sqrt_llvm(primal(x))` separately from the dual
`sqrt(tangent(x))`, evaluating `sqrt` twice. The NDual `sqrt` overload already
computes the primal `sqrt` once, stores it as the result's `.value` (inner-value
invariant), and applies `_pt_guarded_scale`. Read the primal back from the dual
(`Lifted{T,Nw}(dy.value, dy)`), matching the diff's own fma_float/muladd_float
rules — behaviour-identical (negative input still throws at the dual `sqrt`, as
before), one `sqrt` instead of two. Covered by the existing `sqrt_llvm`/
`sqrt_llvm_fast` rule test cases (singular `T(0)` and stability at 5.0).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
…ndently
The forward `dotc`/`dotu` `@is_primitive` used a single type var `X` for both
array positions (`Tuple{...,X,Integer,X,Integer} where X<:...`), so a pair of
differently-typed arguments — e.g. a dense `Vector` dotted with a strided
`SubArray`/`Adjoint` — matched no single `X` and was not a primitive. Such
pairs then fell to the derived forward path, which (per this rule's own comment)
cannot land the complex per-lane partials the Ref roundtrip needs. The frule
method already binds the two arguments to independent `<:Union{Ptr,AbstractArray}`
bounds, so it was broader than its `@is_primitive`. Give `@is_primitive` two
independent type vars `X, Y` to match. Forward regression test dots a dense
`Vector` with a strided view for both dotc and dotu.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRMQzvS6gE7QGen3U7wURH
The `potrf!` frule computed the Cholesky pushforward one lane at a time — a `for lane in 1:Nw` loop of two `trsm!`s plus a `trmm!` per lane, ~3·Nw LAPACK calls. Every lane shares the same factor `A`, so the solves collapse into wide calls over the whole `(Nw, N, N)` partials block: the left- and right-oriented solves need opposite lane-stacking (a column-block and a row-block), bridged by one permute, cutting ~24 calls to 3 at Nw=8. The win is call-overhead-bound, so it fades with size: ~1.5x on the pushforward at N=11 (gp_pois-regr's kernel), ~1x by N>=25. End to end that takes gp_pois-regr forward from 3.0x to 2.5x ForwardDiff, at the cost of two temp buffers. Only the factor's own triangle is written back — `potrf!` leaves the other triangle of `A` untouched, so its partials stay equal to the input's (verified: rules/lapack passes unchanged, gradients match FD). Co-Authored-By: Claude Code <noreply@anthropic.com>
`cholesky(::AbstractMatrix{NDual})` ran a hand-written per-lane analytic
pushforward: factorise the primal once, then apply the L̇ = L·Φ(L⁻¹·Ȧ·L⁻ᵀ)
formula once per partial slot via triangular solves. Route it instead to
LinearAlgebra's generic (non-BLAS) Cholesky on the NDual elements.
The two produce the same derivative — both are the unique exact tangent of the
factorization, identical to machine precision, verified across condition numbers
1e2–1e12 — so there is no consistency or robustness loss versus the analytic
formula. But letting the generic algorithm carry the dual arithmetic through its
own factor entries is several times faster than an explicit
primal-factorise-plus-per-partial-solve at the small matrix sizes these models
use; the per-partial-solve approach only wins for large matrices. Drops the
custom body and the `_cholesky_ndual_fwd` helper.
gp_pois forward drops from 35.0µs to 7.1µs (0.40x ForwardDiff), a 4.9x speedup.
Co-Authored-By: Claude Code <noreply@anthropic.com>
There was a problem hiding this comment.
This is obviously great work. Here are some issues worked out between Claude, GPT, and me. Very happy to discuss the points deeper.
The main text is written by Claude, sorry about the readability!
Issues
traced objref round-trips segfault (src/rules/foreigncall.jl:125-175)
using Mooncake
f5(x) = (r = Ref(x); p = pointer_from_objref(r);
r2 = unsafe_pointer_to_objref(p)::Base.RefValue{Float64}; r2[])
cache = Mooncake.prepare_derivative_cache(f5, 1.0)
Mooncake.value_and_derivative!!(cache, (f5, Mooncake.NoTangent()), (1.0, 1.0))
# signal 11, deterministic — also mutation-free, with GC disabled, and in debug modeRoot cause: the pointer_from_objref(::Ref) frule returns Lifted{Ptr{Nothing},1,Tuple{Ptr{Float64}}} (lanes retyped to point into the partials buffer), while the transform annotates the slot with the canonical lifted_type(Val(1), Ptr{Nothing}) == Lifted{Ptr{Nothing},1,Tuple{Ptr{Nothing}}}. Both are concrete and Lifted is invariant, so the runtime value violates its own slot annotation — undefined behaviour in the compiled OpaqueClosure. The eltype mismatch is unique to the objref family (a pointer(::Array) chain has matching eltypes), which is why CI never trips it, and debug mode is blind to it too: verify_lifted_type has a blanket Ptr exemption.
Two adjacent problems in the same code path:
- the direct (non-traced)
unsafe_pointer_to_objreffrule rebuilds theNDualReffrom a fresh snapshot buffer, severing aliasing with the originalRef's tangent storage — writes through the recovered alias are lost. This is a regression vs main (the width-1Dualera recovered the aliased tangent). - once the lanes are made canonical (fully isbits), nothing roots the primal or the tangent object across the round-trip window, so a fix needs a rooting story as well.
nested mutable structs cannot be lane-extracted or seeded (src/tangents/lifted.jl:287-316)
mutable struct Inner; a::Float64 end
struct Outer; i::Inner; x::Float64 end
Mooncake.tangent(Mooncake.zero_lifted(Val(2), Outer(Inner(1.0), 2.0)), 1)
# MethodError: Cannot `convert` … MutableDualTangentView … to … MutableTangent
Mooncake.tangent(Mooncake.zero_lifted(Val(2), [Inner(1.0)]), 1) # same
# and the FunctionWrappers ext hits the same root cause at zero_lifted time:
# zero_lifted(Val(1), FunctionWrapper(MutableScale(2))) → MethodErrortangent(slot, lane) returns the MutableDualTangentView write proxy for any nested MutableDual, and there is no convert to the declared reverse MutableTangent backing. These accessors feed the chunked gradient/Jacobian extraction paths in interface.jl, so this is probably the highest user-impact item: structs with mutable struct fields (typical model objects) are exactly the inputs people will try first. Tuple-nested mutables work via _unlift_seed's MutableDual path — which also points at the fix: lane-projected materialisation for the struct-field/array-element cases, keeping the view proxy for direct rule-body access.
repeated mutable arguments: silent wrong JVPs, and forward ≠ reverse (src/interface.jl:712-716, :1642-1643)
mutable struct MSA; x::Float64 end
fs2(a, b) = a.x + 2b.x
a = MSA(1.0)
fcache = Mooncake.prepare_derivative_cache(fs2, a, a)
Mooncake.value_and_gradient!!(fcache, fs2, a, a) # gradients: (x = 3.0), (x = 3.0)
rcache = Mooncake.prepare_gradient_cache(fs2, a, a)
Mooncake.value_and_gradient!!(rcache, fs2, a, a) # gradients: (x = 1.0), (x = 2.0)Two related causes: the shared lift cache is first-lift-wins, so the second slot reuses the first slot's Lifted and its seed is discarded; and the chunked-gradient sweep's whole-tuple zero_lifted dedups repeated mutable primals while total_dof/the scatter count each argument independently. Array inputs are not affected (I verified 1.0/2.0 correct) — this is the struct-lift cache path only.
Whatever the intended aliasing contract is, the two modes have to agree on it, and the current forward answer matches neither reverse nor the documented forward contract (slot-local independent JVP directions). One constraint for the fix: the shared cache is deliberate for the rule-capture case (a reverse rule captured in grad_f shares fwds_oc/pb_oc state whose forward tangents must be shared — the comment at interface.jl:712 says why), so the fix is per-input-slot caches that keep within-slot sharing, not removing the cache.
silently wrong numbers in three rule families
CUDA mul! drops active alpha/beta tangents (ext/MooncakeCUDAExt/MooncakeCUDAExt.jl:2226-2263, :2343-2379). The frule signatures accept alpha::Lifted{<:Number}/beta::Lifted{<:Number} but the bodies read only primal(alpha)/primal(beta) — the coefficient tangents never enter the JVP. GPU MWEs: zero JVPs where the true values are 44/34 (GEMM dα/dβ) and 14/15 (GEMV). This is a regression from main, which required NoTangent here and failed loudly. Fix is either the missing dα·op(A)op(B) + dβ·C_old terms (hoisted once, per-lane) or restricting the coefficient slots to NoDual V so active coefficients keep failing loudly.
trsm! loses BLAS's α = 0 strong-zero contract (src/rules/blas.jl:1587-1606).
using Mooncake, LinearAlgebra.BLAS
const A = [1.0 1.0; 0.0 0.0] # singular triangular — fine at α = 0: BLAS never references A
f(B) = (BLAS.trsm!('L', 'U', 'N', 'N', 0.0, A, B); sum(B))
f([1.0 2.0; 3.0 4.0]) # 0.0 (exact zeros)
cache = Mooncake.prepare_derivative_cache(f, [1.0 2.0; 3.0 4.0])
Mooncake.value_and_derivative!!(cache, (f, Mooncake.NoTangent()), ([1.0 2.0; 3.0 4.0], ones(2, 2)))
# (NaN, NaN) — the returned *value* is wrong, not just the derivativeThe (otherwise correct) hoist-unscaled-solve optimisation performs the solve and then scales by α, so α = 0 with a singular/garbage A NaN-poisons both the primal result and the partials. Needs an explicit iszero(α) branch; trmm! has a related hazard under BLAS's "B need not be set at α = 0" contract (0·NaN in the unit-diagonal correction).
clamp diverges from Base, silently (src/nfwd/Nfwd.jl:1045-1054). Three ways, all verified:
- mixed precision narrows the bounds to
Tbefore comparing:clamp(x, 0.50000001, 1.0)on aFloat32dual at0.5f0returns0.5f0(derivative 0) where Base returns0.50000001::Float64— wrong value and type, an inner-value invariant violation; <=/>=means every exact boundary hit returns the bound (derivative 0) where Base'sx > hi ? hi : x < lo ? lo : xreturnsx(derivative 1);- signed zero:
clamp(-0.0, 0.0, 1.0)returns+0.0(derivative 0); Base returns-0.0(derivative 1). Crossed bounds (lo > hi) also resolve differently from Base, so forward and reverse disagree on the primal itself.
When a whole non-primitive function is nfwd-safe, `build_frule` runs the primal directly on the inner `NDual`/`NDualArray` forward-dual values (a stateless `NfwdFRule`) instead of deriving the per-op frule OpaqueClosure transform. This is the default; `nfwd=false` or `debug_mode` keeps the fully-checked transform, which is also the automatic fallback for anything not nfwd-safe. Native firing is a top-level whole-function decision only — there is no sub-method layer. nfwd-safety is a whitelist-posture classifier (`_nfwd_safe` → `_nfwd_body_safe`) that only vouches for code it can see through — statically-resolved `:invoke`s (recursed into) and structural `Core.Builtin`s in `_NFWD_SAFE_BUILTINS`. Every other op — `:foreigncall`, any intrinsic, an unresolved dynamic `:call`, a non-whitelisted builtin — is opaque: if it receives a dual-typed argument the derivative could be laundered, so the function falls back to the transform (never a silent wrong gradient, never an error). Unknown ⇒ unsafe by construction; a completeness test asserts every `Core.Builtin` is classified. It also requires the dual return type to equal `dual_type(Val(N), primal_return)` so functions that behave differently on duals than on floats (e.g. `TwicePrecision` collapsing an `NDual` to a scalar) fall back rather than yield a wrong-shaped result. The verdict is memoised per signature (world-keyed). Correctness matches ForwardDiff throughout; across the posteriordb log-densities the native path is at parity with ForwardDiff and well ahead of the transform. Co-Authored-By: Claude Code <noreply@anthropic.com>
Brings main's v0.5.x line (through cd15108) into the Lifted/nfwd redesign. basic passes (48397, 0 errors). Notable conflict resolutions: - NNlib σ/sigmoid_fast/tanh_fast and gather forward rules ported Dual->Lifted: σ/sigmoid_fast delegate to their existing NDual overloads; tanh_fast builds the NDual inline (it has no NDual overload); gather keeps its throw. copysign forward and reverse take main's flipsign(sign(x), y), which is also correct at y=0. - forward LazyFRule/DynamicFRule adopt main's #1218 world-pinning (predict Trule at the transform's world) while keeping the redesign's chunk-width field; the sub-rule builds still pass nfwd=false so NfwdFRule stays a top-level-only decision. - _assert_matching_tangent_shape takes main's #1259 applicable(size) guard. - Kept the redesign's single-argument HVP and Lifted-terminology docs; dropped main's multi-argument HVP tests as unsupported here. Co-Authored-By: Claude Code <noreply@anthropic.com>
`Nfwd.NDualMemoryRef` wraps `MemoryRef` and is defined only under `@static if VERSION >= v"1.11-rc4"`, but forward_mode.jl referenced it unconditionally (`_nfwd_primal(::NDualMemoryRef)` and `_TN_NDMEMREF`), so Mooncake failed to precompile on 1.10 with `UndefVarError: NDualMemoryRef not defined` — failing every LTS CI job. Guard both refs for 1.11+; on 1.10 alias the sentinel `_TN_NDMEMREF` to the array TypeName (already tested alongside it) so the memref branches stay inert. Verified: `using Mooncake` now precompiles on 1.10. Co-Authored-By: Claude Code <noreply@anthropic.com>
nfwd wrongly fired on `append!`: the classifier accepted it (top-level return
coherent, ops whitelisted), then ran it natively on the `NDualArray`, hitting
`resize!(::NDualArray)` — the fixed-shape parallel `NDualArray` has no grow/shrink
— a `MethodError` at runtime instead of falling back to the transform (which
handles growth correctly). Dynamic `:call`s to a mutator on a dual were already
rejected as opaque; the gap was a resolved `:invoke` (`append!` lowers to
`invoke push!(::NDualArray, …)`), recursed into rather than checked. Reject any
`:invoke` of a length-changing array op (`_NFWD_ARRAY_MUTATORS`) on a dual array,
so nfwd fires only where every op has proven NDual coverage. A name-based check,
not an inference-bottom heuristic — inference on the exotic dual types is
imprecise and returns `Union{}` for working reductions, which would false-reject
`sum(sin.(x))`. append!/push!/pushfirst! now differentiate via the transform.
Co-Authored-By: Claude Code <noreply@anthropic.com>
`copyto!(::Vector{ComplexF64}, ::Vector{Float64})` under forward mode stores real
duals (`NDual{Float64}`) into a complex `NDualArray`, but only same-eltype and
`Complex{NDual}` setindex! methods existed, so it hit Base's
`error_if_canonical_setindex` (`CanonicalIndexError`). Add the promoting method
(both block and parallel layouts): a real dual writes its value and partials into
the complex primal/block with zero imaginary part. Fixes 2 forward errors in the
array integration group.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Storing a `Vector{Float32}` into a `Dict{_,Vector{Float64}}` under forward mode
converts the value's element type, needing `convert(NDualArray{…F64…},
::NDualArray{…F32…})` — which had no method, crashing the perf benchmark with a
`convert` MethodError. Add it (both block and parallel layouts): convert the
primal and partials to the target element type, mirroring the scalar
`convert(::Type{NDual{T,N}}, ::NDual{S,N})`.
Co-Authored-By: Claude Code <noreply@anthropic.com>
…l->int
nfwd-native special functions (logerf, complex loggamma) call SpecialFunctions
internals with no NDual coverage: add differentiable overloads for _erfcx(::NDual)
(d/dx erfcx = 2x·erfcx − 2/√π) and _loggamma(::Complex{NDual}) (complex digamma
propagation). Separately, converting a forward dual to an integer discards the
tangent (dual-laundering); make Int64(::NDual)/convert(::Integer,::NDual) throw a
clear error rather than silently return a wrong (zero) gradient. Greens the
special_functions ext group (was 18 forward errors).
Co-Authored-By: Claude Code <noreply@anthropic.com>
…2 point BFloat16 is not an IEEEFloat, so it has no NDual forward-dual representation — the general nfwd/intrinsic forward path is unsupported (only specific hand-written rules like `^` work). Run the BFloat16 primitive `test_rule` cases in reverse mode only; the neg_float MethodError is gone. Separately, exp2's point P(1.12) is outside the fine-spacing range the BF16 FD oracle needs: the reverse rule is correct there (grad == exp2(x)·log2 == analytic) but the coarse spacing can't reconstruct the finite difference, so move it to P(0.15). Co-Authored-By: Claude Code <noreply@anthropic.com>
nfwd fires only when every differentiable leaf is
NDualEltype = Union{IEEEFloat, Complex{<:IEEEFloat}} (NDual scalar, NDualArray,
NDualMemoryRef); anything else (e.g. BFloat16 → generic NTuple dual) is not
projectable and routes to the transform. Note it at the `_nfwd_safe` arg gate.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Forward gradients can't be zero-alloc on 1.10: the zero-alloc NDualArray block layout needs an in-place-resizable (N, dims) buffer, but 1.10's reshape marks the underlying buffer shared, so shaped views over it allocate. 1.10 falls back to the parallel-arrays layout (src/nfwd/Nfwd.jl), whose forward gradient is still correct and type-stable, just not zero-alloc. Gate the 10 FCache count_allocs==0 assertions to 1.11+ via `_SKIP_FWD_ALLOC`; correctness checks still run on 1.10. Co-Authored-By: Claude Code <noreply@anthropic.com>
The loud dual->int guard `(::Type{I})(::NDual) where I<:Integer` is more specific than
`Bool(x::Real)` in its type argument but less specific in its value argument, so `Bool(::NDual)`
was ambiguous — failing Aqua's ambiguity test in the basic group on every version, and throwing an
ambiguity MethodError instead of the intended clear ArgumentError. Add an explicit
`Base.Bool(::NDual)` that routes to the same guard.
Co-Authored-By: Claude Code <noreply@anthropic.com>
An IdDict's canonical forward dual is IdDict{K,dual_type(V)}, a dedicated container rather than a
struct-lift, so the generic @generated frule!! hit its "not a struct-lift" guard and errored
(rules/new forward, all versions). Mirror the existing IdDict rrule!!: a _new_ed IdDict is built
from non-differentiable fields, so its dual is the empty dual dict. Verified the failing
_new_(IdDict{Int,Float64}, Memory{Any}, Int, Int) forward case now passes.
Co-Authored-By: Claude Code <noreply@anthropic.com>
`sum(sin.(x))` fires nfwd-native only on 1.11+. On 1.10 the broadcast materialises an
out-of-protocol Array{NDual} intermediate whose reduction takes a jl_array_ptr foreigncall (not
whitelisted), so the classifier correctly routes it to the transform — verified to produce the
right forward derivative. Assert the per-version behaviour instead of an unconditional true; this
was the last basic-1.10 failure. Whitelisting jl_array_ptr would be unsafe (trusts raw-pointer
reads of duals everywhere).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Brings in the MooncakeCUDA broadcast SubArray-leaf gradient fix (#1270) and the README hand-written-rules note (#1269). Only conflict was Project.toml version: kept our 0.6.0 over main's 0.5.44 bump. No core/forward-mode changes; the CUDA fix uses arrayify (present here), no removed-Dual API reintroduced. Co-Authored-By: Claude Code <noreply@anthropic.com>
…block
On 1.11+ the NDualArray partials are one element-major (N, size...) block, so the old
per-lane `sum(tangent_view(nda,k))` walked stride-N memory and never vectorised. Reinterpret the
block as NTuple{N,P} columns and tuple-fold across lanes: packed <N x double> adds, 0-alloc,
~5-6x faster (sum 2583->516 ns, sum(abs2) 3240->541 ns at N=8, verified @code_llvm). Guarded to
1.11+; 1.10 keeps the parallel-arrays per-lane path (already contiguous). rules/performance_patches
green on 1.12.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The dense _kron! frule looped per lane over stride-N tangent_view slices of the 1.11+ element-major partials block. Add _kron!_jvp_block! that walks the contiguous blocks once and writes all N lanes of each output element as one NTuple, vectorising the lane write (packed <N x double> fma), in-place 0-alloc, ~6x faster (8x8⊗8x8: 27.8->4.1us at N=8, verified @code_llvm). Guarded to 1.11+; 1.10 keeps the per-lane loop. Correctness: kron/_kron! forward test_rule 90/90 widths 1-3. Co-Authored-By: Claude Code <noreply@anthropic.com>
Both looped per lane over stride-Nw tangent_view slices of the 1.11+ element-major partials block. - dot: replace the Nw strided dots with two BLAS.gemv! over the (Nw,K) block (out = Xblock·y + Yblock·x), ~3.5×. Length-Nw output alloc is fine (no :allocs guard). - nrm2: for a contiguous array slot (incx==1), accumulate all Nw lanes in one pass over the reinterpreted NTuple columns (handles real+complex, 0-alloc, preserves the zero-vector guard), ~4×. Ptr slots / strided inputs keep the per-lane path (factored to _nrm2_lanes_perlane). Guarded to 1.11+; 1.10 keeps per-lane. Correctness: dot/nrm2 forward test_rule 250/250 (real, complex, strided, zero-vector). Co-Authored-By: Claude Code <noreply@anthropic.com>
Selection reductions over an NDualArray folded max/min across A[i], building one NDual per element. Instead scan the real primal for the arg-extreme and take a single getindex (layout-agnostic — works for both the 1.11+ block and 1.10 parallel-arrays forms), ~10x (maximum len=5000: 26->2.7us). Real elements only. Correctness: maximum/minimum forward test_rule 66/66. Co-Authored-By: Claude Code <noreply@anthropic.com>
sinpi(::NDual) computed sinpi(v) for the value and cospi(v) for the derivative factor as two separate libm calls; cospi mirrored it. One sincospi(v) call yields both, halving the transcendental cost (~1.5-2x). sind/cosd fusion was measured out (sincosd shares nothing on 1.12). Correctness: sinpi/cospi forward test_rule 168/168. Co-Authored-By: Claude Code <noreply@anthropic.com>
sum(sin.(x)) fires nfwd-native only on 1.12 (verified: 1.10 false, 1.11 false, 1.12 true), not on
all 1.11+ as the previous gate assumed — on 1.10/1.11 the broadcast materialises an out-of-protocol
Array{NDual} whose reduction takes jl_array_ptr, rejected; 1.12's lowering keeps it in-protocol.
Fixes the basic-1.11 failure introduced by the earlier 1.11-rc4 boundary.
Co-Authored-By: Claude Code <noreply@anthropic.com>
_tadd/_tscale used `where {L,P}` over `NTuple{L,P}`, which degenerates to `Tuple{}` at L=0 and
leaves P unbound — Aqua's unbound-args check (in the basic group, all versions) flagged it. Bind
only L; the element type is inferred. Introduced by the sum/sum(abs2) SIMD rewrite.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The block-path nrm2 rewrite was type-unstable two ways JET's report_opt (blas group stability check)
flagged: (1) getfield(tangent(X_dX), :partials_block) on the Union{Ptr,AbstractArray} slot is a
dynamic access — fixed by dispatching the lane JVP on slot kind (_nrm2_lanes) so the array path
reaches the type-stable _partials_block accessor; (2) the inline acc = ntuple(k -> acc[k] + ...)
captured the reassigned acc, boxing it to Any — fixed by _nrm2_accum/_nrm2_scale helpers taking acc
by value (the sum/_tadd pattern), bound on Nw only to avoid the NTuple{Nw,R}-at-0 Aqua trap.
rules/blas_Float64 green on 1.12.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Three forward-mode bugs in the CUDA extension (pre-existing, from the Lifted/NDual redesign; the Buildkite GPU pipeline was red). cuda.jl: 5913 pass/130 fail/38 error -> 6532 pass/0 fail/0 error. 1. `_lane_views(::CuArray NDualArray)` was never overridden: the CuArray partials block is lane-major (dims..., N), but the host generic slices the leading axis (element-major (N, dims...)), giving wrong-shaped lane views. This broke every forward array frule that calls `_lane_views` (reshape -> DimensionMismatch, sum/transpose/mean/varm/norm/dot/gemm/gemv/...). Override it to build from the lane-major `tangent_view`. 2. `fill!`'s @is_primitive covered only dense CuMaybeComplexArray, so wrapped (Adjoint/Transpose/SubArray) inputs built a derived rule that traced into an internal ccall and errored. Broaden to CuMaybeWrappedArray and route the frule through forward `arrayify`, mirroring the reverse rrule. 3. Broadcast over a non-contiguous SubArray leaf hit the loud "no per-lane extraction" guard; add the missing `_bc_tangent(::ImmutableDual, ::SubArray)` handler. Verified each with focused GPU test_rule MWEs, then the full green cuda.jl. No core src/ changes. Co-Authored-By: Claude Code <noreply@anthropic.com>
The forward-mode redesign's compile cost (forward frules × chunk widths × complex codegen) roughly doubled the reverse-only compile, so the combined rules/blas_ComplexF32/F64-1.12 jobs ran ~70 min and got reclaimed by the runner mid-run (compile-bound: measured 79% compile, fully amortized — not a bug). Split each rules/* group into forward and reverse jobs driven by one test script and a TEST_MODE env var: test_rule/run_rule_test_cases run only the enabled mode; unset ⇒ both, so local runs and downstream users are unchanged. basic/Nfwd/array_legacy stay single both-mode jobs (they do non-rule work that must not run twice). Verified on blas_ComplexF32: forward 1374s + reverse 398s = both 1770s (clean split, no coverage lost), each green, neither near the time budget. Co-Authored-By: Claude Code <noreply@anthropic.com>
The TEST_MODE split inserted _test_mode_filter between test_rule's docstring and its definition, so the docstring attached to the helper instead — Documenter failed to resolve [`test_rule`](@ref) in six pages. Move _test_mode_filter above the docstring. Co-Authored-By: Claude Code <noreply@anthropic.com>
On Julia 1.10 the nfwd classifier admitted composed CuArray reductions (sum(f,x), mapreduce, map, reduce, and norm/prod/mean wrappers over adjoints) to the nfwd-native path, which runs the primal element-wise over the dual array and so scalar-indexes the device array: ext/cuda was 6042 pass / 41 errored, all "Scalar indexing is disallowed". The hazard is version-independent — the invariant is that a GPU-backed NDualArray must never enter nfwd-native. Gate the NDualArray branch of `_nfwd_projectable` through a new `_nfwd_backing_projectable(A)` trait on the backing array type (default `true`, so host behaviour is bit-identical), and have the CUDA extension return `false` for `CuArray`, routing those ops to the transform's device frules. The NDualArray check must precede the `_TN_NDMEMREF` one: on 1.10 that sentinel aliases the NDualArray TypeName, so an earlier unguarded return would re-admit GPU arrays. Verified on GPU: cuda.jl 1.10 6498 pass / 0 fail / 0 error (from 41 errors), 1.12 6532 pass / 0 fail / 0 error (unchanged). Host classifier unaffected (`_nfwd_projectable` of a host NDualArray still true; sum(sin.(x)) still nfwd-native on 1.12). Co-Authored-By: Claude Code <noreply@anthropic.com>
Nothing in the nfwd-native path is specific to forward mode — it decides whether a whole function's primal can run directly on inner dual values — so move it out of forward_mode.jl (1148 -> 717 lines) into its own file. Two reasons: the soundness-critical classifier is easier to review and maintain in isolation (its builtin/foreigncall whitelists must track new Core additions), and reverse mode can share it as-is. Pure code move, no behaviour change. What remains of nfwd in forward_mode.jl is build_frule's `nfwd` kwarg, its three-line dispatch to NfwdFRule, and the two `nfwd=false` sub-rule builds. Included before forward_mode.jl since the _TN_* TypeName sentinels evaluate Nfwd types at load time. Verified on 1.10 and 1.12: loads clean, _nfwd_safe resolves from the new file, host projectability and the version-specific verdicts unchanged (sum(sin.(x)) nfwd-native on 1.12, transform on 1.10), append! still rejected; basic (incl. `nfwd primitive coverage`) and Nfwd groups green. Co-Authored-By: Claude Code <noreply@anthropic.com>
CI Summary — GitHub Actions
Documentation Preview
Mooncake.jl documentation for PR #1215 is available at:
https://chalk-lab.github.io/Mooncake.jl/previews/PR1215/
Performance
Performance Ratio:
Ratio of time to compute gradient and time to compute function.
Warning: results are very approximate! See here for more context.
Related: TuringLang/Bijectors.jl#480