# New "easy rule" system for Enzyme

**URL:** <https://discourse.julialang.org/t/new-easy-rule-system-for-enzyme/133522>\
**Category:** Machine Learning\
**Tags:** autodiff, enzyme, chainrules\
**Created:** [October 29, 2025, 7:06pm UTC](https://discourse.julialang.org/t/new-easy-rule-system-for-enzyme/133522 "2025-10-29T19:06:54Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [October 29, 2025, 7:06pm UTC](https://discourse.julialang.org/t/new-easy-rule-system-for-enzyme/133522/1 "2025-10-29T19:06:54Z")

</div>

Hi there @wsmoses and friends,

Thanks for the new [easy rule system](https://enzymead.github.io/Enzyme.jl/dev/#man-easy-rule)! Making rules more accessible is a great idea to get more people into autodiff (that’s how I got hooked in the first place).

I’m curious to understand how it works for functions with vector inputs and outputs, especially when their size may be large:

- Does everything have to be stored in tuples of tuples? Will it strain the compiler if people (programmatically) define tuples with a million elements because their arrays have that size?
- If the user only specifies the equivalent of a Jacobian matrix, how does Enzyme internally generate efficient JVPs and VJPs that don’t require materializing the full matrix?

Thanks again!

---

<div class="post-metadata">

**Author:** ![vchuravy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/vchuravy/32/8_2.png) [@vchuravy](https://discourse.julialang.org/u/vchuravy)\
**Post date:** [October 29, 2025, 8:40pm UTC](https://discourse.julialang.org/t/new-easy-rule-system-for-enzyme/133522/2 "2025-10-29T20:40:40Z")

</div>

The docstring has some more [information](https://enzymead.github.io/Enzyme.jl/dev/api/#EnzymeCore.EnzymeRules.@easy_rule-Tuple%7BAny,%20Any,%20Vararg%7BAny%7D%7D)

> Does everything have to be stored in tuples of tuples? Will it strain the compiler if people (programmatically) define tuples with a million elements because their arrays have that size?

As far as I understand it no. The length of the tuple is bounded by the number of arguments.

As an example from the tests (cleaned up an actually using the correct jacobian)

```julia-auto
# 3 -> 2 vector valued function
function f(v)
    return [sin(v[1]), cos(v[2]) * v[3]]
end

# Return the jacobian of f 
function jacobian_f(v)
   [cos(v[1]), 0, 0
   0 , -v[3]*sin(v[2]), cos(v[2])]
end

EnzymeRules.@easy_rule(
    f(v),
    @setup(),
    (jacobian_f(v),)
)

```

---

<div class="post-metadata">

**Author:** ![wsmoses](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/wsmoses/32/26497_2.png) [@wsmoses](https://discourse.julialang.org/u/wsmoses)\
**Post date:** [October 29, 2025, 8:41pm UTC](https://discourse.julialang.org/t/new-easy-rule-system-for-enzyme/133522/3 "2025-10-29T20:41:51Z")

</div>

Rules support arrays or scalars as inputs/outputs. There’s one row in the easyrule per each input to the function (either a scalar input or an array input). The corresponding rule expression then should be an abstractarray of shape `(size(output[j])..., size(input[i])...)` (the docs should say this, if not clear help us make it more clear 🙂 ). It needs to be an abstractarray, it need not be a materialized dense array (which can therefore avoid materialization).

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [October 29, 2025, 8:51pm UTC](https://discourse.julialang.org/t/new-easy-rule-system-for-enzyme/133522/4 "2025-10-29T20:51:10Z")

</div>

> [@vchuravy](#):
>
> The docstring has some more [information](https://enzymead.github.io/Enzyme.jl/dev/api/#EnzymeCore.EnzymeRules.@easy_rule-Tuple%7BAny,%20Any,%20Vararg%7BAny%7D%7D)

My bad, I had read the docstring it but intuitively I understood the notation

```julia-auto
@easy_rule(f(x₁::T1, x₂::T2, ...),
             @setup(statement₁, statement₂, ...),
             (∂f₁_∂x₁, ∂f₁_∂x₂, ...),
             (∂f₂_∂x₁, ∂f₂_∂x₂, ...),
             ...)

```

as referring to a function

```julia-auto
f(x1, x2, ...) = [f1, f2, ...] # an array output

```

instead of what was probably meant, which is

```julia-auto
f(x1, x2, ...) = (f1, f2, ...) # a tuple output

```

Retrospectively, I think this confusion of mine stems from the idea that Julia functions don’t really have “several outputs”, they just output a tuple or an array which is in the end one single object. Not sure how it can be worded better though.

> [@wsmoses](#):
>
> It needs to be an abstractarray, it need not be a materialized dense array (which can therefore avoid materialization).

So if I want to implement an easy rule where the Jacobian is not materialized at each forward or reverse pass (which is n times slower than necessary), I have to define a new `AbstractArray` subtype which implements multiplication using a JVP and transpose-multiplication using a VJP? Something like this?

```julia-auto
struct MyLazyJacobian <: AbstractArray
    # stuff
end

function Base.:*(J::MyLazyJacobian, v)
    #stuff
end

function Base.:*(J::Transpose{MyLazyJacobian}, v)
    #stuff
end

```

---

<div class="post-metadata">

**Author:** ![wsmoses](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/wsmoses/32/26497_2.png) [@wsmoses](https://discourse.julialang.org/u/wsmoses)\
**Post date:** [October 29, 2025, 9:27pm UTC](https://discourse.julialang.org/t/new-easy-rule-system-for-enzyme/133522/5 "2025-10-29T21:27:57Z")

</div>

So if you wanted to have a custom materialization (rather than general array), I think currently you’d need:

```julia-auto
Jacobian * Number 
LinearAlgebra.dot(Jacobian, Abstract Array of same dim)
LinearAlgebra.dot(Abstract Array of same dim, Jacobian)
Base.reshape
Base.length
Base.similar
LinearAlgebra.mul!(AbstractArray, Jacobian, AbstractArray)

```

Alternatively (and not presently well trodden since we’ve currently assumed the jacobian is a standard number/abstracyarray), you can override `EnzymeCore.EnzymeRules.multiply_fwd_into(previous, jacobian, dx)`  
which should logically compute previous += jacobian \* dx, if previous is not nothing, and return anew if it is nothing (and this by default will call into the utilities above, if not specialized).
