Defining Rules
Most of the time, Mooncake.jl can just differentiate your code, but you will need to intervene if you make use of a language feature which is unsupported. However, this does not always necessitate writing your own rrule!!
from scratch. In this section, we detail some useful strategies which can help you avoid having to write rrule!!
s in many situations, which we discuss before discussing the more involved process of actually writing rules.
Simplfiying Code via Overlays
Mooncake.@mooncake_overlay
— Macro@mooncake_overlay method_expr
Define a method of a function which only Mooncake can see. This can be used to write versions of methods which can be successfully differentiated by Mooncake if the original cannot be.
For example, suppose that you have a function
julia> foo(x::Float64) = bar(x)
foo (generic function with 1 method)
where Mooncake.jl fails to differentiate bar
for some reason. If you have access to another function baz
, which does the same thing as bar
, but does so in a way which Mooncake.jl can differentiate, you can simply write:
julia> Mooncake.@mooncake_overlay foo(x::Float64) = baz(x)
When looking up the code for foo(::Float64)
, Mooncake.jl will see this method, rather than the original, and differentiate it instead.
A Worked Example
To demonstrate how to use @mooncake_overlay
s in practice, we here demonstrate how the answer that Mooncake.jl gives changes if you change the definition of a function using a @mooncake_overlay
. Do not do this in practice – this is just a simple way to demonostrate how to use overlays!
First, consider a simple example:
julia> scale(x) = 2x
scale (generic function with 1 method)
julia> rule = Mooncake.build_rrule(Tuple{typeof(scale), Float64});
julia> Mooncake.value_and_gradient!!(rule, scale, 5.0)
(10.0, (NoTangent(), 2.0))
We can use @mooncake_overlay
to change the definition which Mooncake.jl sees:
julia> Mooncake.@mooncake_overlay scale(x) = 3x
julia> rule = Mooncake.build_rrule(Tuple{typeof(scale), Float64});
julia> Mooncake.value_and_gradient!!(rule, scale, 5.0)
(15.0, (NoTangent(), 3.0))
As can be seen from the output, the result of differentiating using Mooncake.jl has changed to reflect the overlay-ed definition of the method.
Additionally, it is possible to use the usual multi-line syntax to declare an overlay:
julia> Mooncake.@mooncake_overlay function scale(x)
return 4x
end
julia> rule = Mooncake.build_rrule(Tuple{typeof(scale), Float64});
julia> Mooncake.value_and_gradient!!(rule, scale, 5.0)
(20.0, (NoTangent(), 4.0))
Functions with Zero Adjoint
If the above strategy does not work, but you find yourself in the surprisingly common situation that the adjoint of the derivative of your function is always zero, you can very straightforwardly write a rule by making use of the following:
Mooncake.@zero_adjoint
— Macro@zero_adjoint ctx sig
Equivalent to @zero_derivative ctx sig ReverseMode
. Consult the docstring for @zero_derivative
for more information.
Mooncake.zero_adjoint
— Functionzero_adjoint(f::CoDual, x::Vararg{CoDual, N}) where {N}
Utility functionality for constructing rrule!!
s for functions whose adjoints always return zero.
NOTE: you should only make use of this function if you cannot make use of the @zero_adjoint
macro.
You make use of this functionality by writing a method of Mooncake.rrule!!
, and passing all of its arguments (including the function itself) to this function. For example:
julia> import Mooncake: zero_adjoint, DefaultCtx, zero_fcodual, rrule!!, is_primitive, CoDual
julia> foo(x::Vararg{Int}) = 5
foo (generic function with 1 method)
julia> is_primitive(::Type{DefaultCtx}, ::Type{<:Tuple{typeof(foo), Vararg{Int}}}) = true;
julia> rrule!!(f::CoDual{typeof(foo)}, x::Vararg{CoDual{Int}}) = zero_adjoint(f, x...);
julia> rrule!!(zero_fcodual(foo), zero_fcodual(3), zero_fcodual(2))[2](NoRData())
(NoRData(), NoRData(), NoRData())
WARNING: this is only correct if the output of primal(f)(map(primal, x)...)
does not alias anything in f
or x
. This is always the case if the result is a bits type, but more care may be required if it is not. ```
Using ChainRules.jl
ChainRules.jl provides a large number of rules for differentiating functions in reverse-mode. These rules are methods of the ChainRulesCore.rrule
function. There are some instances where it is most convenient to implement a Mooncake.rrule!!
by wrapping an existing ChainRulesCore.rrule
.
There is enough similarity between these two systems that most of the boilerplate code can be avoided.
Mooncake.@from_rrule
— Macro@from_rrule ctx sig [has_kwargs=false]
Equivalent to @from_chainrules ctx sig has_kwargs ReverseMode
. See @from_chainrules
for more information.
Adding Methods To rrule!!
And build_primitive_rrule
If the above strategies do not work for you, you should first implement a method of Mooncake.is_primitive
for the signature of interest:
Mooncake.is_primitive
— Functionis_primitive(::Type{Ctx}, ::Type{M}, sig) where {Ctx,M}
Returns a Bool
specifying whether the methods specified by sig
are considered primitives in the context of contexts of type Ctx
in mode M
.
is_primitive(DefaultCtx, ReverseMode, Tuple{typeof(sin), Float64})
will return if calling sin(5.0)
should be treated as primitive when the context is a DefaultCtx
.
Observe that this information means that whether or not something is a primitive in a particular context depends only on static information, not any run-time information that might live in a particular instance of Ctx
.
Then implement a method of one of the following:
Mooncake.rrule!!
— Functionrrule!!(f::CoDual, x::CoDual...)
Performs the forwards-pass of AD. The tangent
field of f
and each x
should contain the forwards tangent data (fdata) associated to each corresponding primal
field.
Returns a 2-tuple. The first element, y
, is a CoDual
whose primal
field is the value associated to running f.primal(map(x -> x.primal, x)...)
, and whose tangent
field is its associated fdata
. The second element contains the pullback, which runs the reverse-pass. It maps from the rdata associated to y
to the rdata associated to f
and each x
.
using Mooncake: zero_fcodual, CoDual, NoFData, rrule!!
y, pb!! = rrule!!(zero_fcodual(sin), CoDual(5.0, NoFData()))
pb!!(1.0)
# output
(NoRData(), 0.28366218546322625)
Mooncake.build_primitive_rrule
— Functionbuild_primitive_rrule(sig::Type{<:Tuple})
Construct an rrule for signature sig
. For this function to be called in build_rrule
, you must also ensure that is_primitive(context_type, ReverseMode, sig)
is true
. The callable returned by this must obey the rrule interface, but there are no restrictions on the type of callable itself. For example, you might return a callable struct
. By default, this function returns rrule!!
so, most of the time, you should just implement a method of rrule!!
.
Extended Help
The purpose of this function is to permit computation at rule construction time, which can be re-used at runtime. For example, you might wish to derive some information from sig
which you use at runtime (e.g. the fdata type of one of the arguments). While constant propagation will often optimise this kind of computation away, it will sometimes fail to do so in hard-to-predict circumstances. Consequently, if you need certain computations not to happen at runtime in order to guarantee good performance, you might wish to e.g. emit a callable struct
with type parameters which are the result of this computation. In this context, the motivation for using this function is the same as that of using staged programming (e.g. via @generated
functions) more generally.