-
Notifications
You must be signed in to change notification settings - Fork 44
Use native gradient API for ForwardDiff, Enzyme, Mooncake #458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
44d57ab
1688840
0b6b6d3
9774644
901c787
5e3d21e
9ccf137
669d4a8
a468093
81bbaa4
e4d8585
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| module BijectorsEnzymeExt | ||
|
|
||
| import Bijectors: _value_and_gradient, _value_and_jacobian | ||
| import ADTypes: AutoEnzyme | ||
| using Enzyme: Enzyme | ||
| using EnzymeCore: EnzymeCore | ||
|
|
||
| const DuplicatedFunctionAnnotations = Union{ | ||
| EnzymeCore.Duplicated,EnzymeCore.DuplicatedNoNeed,EnzymeCore.MixedDuplicated | ||
| } | ||
|
|
||
| function _annotate_function(f, backend::AutoEnzyme, mode) | ||
| annotation = typeof(backend).parameters[2] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accessing type parameters this way is not recommended since the field is internal (AFAICT) |
||
| if annotation === Nothing | ||
| return f | ||
| elseif annotation <: EnzymeCore.Const | ||
| return Enzyme.Const(f) | ||
| elseif annotation <: DuplicatedFunctionAnnotations | ||
| if Enzyme.guess_activity(typeof(f), mode) <: EnzymeCore.Const | ||
| return Enzyme.Const(f) | ||
| else | ||
| # Enzyme's sugar APIs only preserve function shadows for `Duplicated`, | ||
| # so normalize the duplicated-like annotations here. | ||
| return Enzyme.Duplicated(f, Enzyme.make_zero(f)) | ||
| end | ||
| else | ||
| throw(ArgumentError("unsupported Enzyme function annotation $annotation")) | ||
| end | ||
| end | ||
|
|
||
| function _value_and_gradient( | ||
| f, | ||
| backend::Union{AutoEnzyme{Nothing},AutoEnzyme{<:EnzymeCore.ReverseMode}}, | ||
| x::AbstractVector, | ||
| ) | ||
| mode = if backend isa AutoEnzyme{Nothing} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Enzyme.ReverseWithPrimal | ||
| else | ||
| Enzyme.WithPrimal(backend.mode) | ||
| end | ||
| annotated_f = _annotate_function(f, backend, mode) | ||
| dx = zero(x) | ||
| _, val = Enzyme.autodiff(mode, annotated_f, Enzyme.Active, Enzyme.Duplicated(x, dx)) | ||
| return val, dx | ||
| end | ||
|
|
||
| function _value_and_gradient( | ||
| f, backend::AutoEnzyme{<:EnzymeCore.ForwardMode}, x::AbstractVector | ||
| ) | ||
| mode = Enzyme.WithPrimal(backend.mode) | ||
| annotated_f = _annotate_function(f, backend, mode) | ||
| grad = zero(x) | ||
| value = f(x) | ||
| for i in eachindex(x) | ||
| dx = zero(x) | ||
| dx[i] = one(eltype(x)) | ||
| directional, primal = Enzyme.autodiff(mode, annotated_f, Enzyme.Duplicated(x, dx)) | ||
| grad[i] = directional | ||
| if i == firstindex(x) | ||
| value = primal | ||
| end | ||
| end | ||
|
Comment on lines
+54
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enzyme has a built-in forward-mode gradient function, which DI already uses in such cases. Any reason not to use it here too? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ++ |
||
| return value, grad | ||
| end | ||
|
|
||
| function _value_and_jacobian( | ||
| f, backend::AutoEnzyme{<:EnzymeCore.ReverseMode}, x::AbstractVector | ||
| ) | ||
| value = f(x) | ||
| if isempty(x) || isempty(value) | ||
| return value, Matrix{eltype(value)}(undef, length(value), length(x)) | ||
| end | ||
| annotated_f = _annotate_function(f, backend, backend.mode) | ||
| jacobian = only(Enzyme.jacobian(backend.mode, annotated_f, x)) | ||
| return value, reshape(jacobian, length(value), length(x)) | ||
| end | ||
|
|
||
| function _value_and_jacobian(f, ::AutoEnzyme{Nothing}, x::AbstractVector) | ||
| return _value_and_jacobian(f, AutoEnzyme(; mode=Enzyme.Forward), x) | ||
| end | ||
|
|
||
| function _value_and_jacobian( | ||
| f, backend::AutoEnzyme{<:EnzymeCore.ForwardMode}, x::AbstractVector | ||
| ) | ||
| mode = Enzyme.WithPrimal(backend.mode) | ||
| annotated_f = _annotate_function(f, backend, mode) | ||
| value = f(x) | ||
| J = nothing | ||
| for i in eachindex(x) | ||
| dx = zero(x) | ||
| dx[i] = one(eltype(x)) | ||
| directional, primal = Enzyme.autodiff(mode, annotated_f, Enzyme.Duplicated(x, dx)) | ||
| if i == firstindex(x) | ||
| value = primal isa AbstractArray ? copy(primal) : primal | ||
| J = Matrix{eltype(directional)}(undef, length(directional), length(x)) | ||
| end | ||
| J[:, i] .= directional | ||
| end | ||
|
Comment on lines
+89
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enzyme has a built-in forward Jacobian function, which DI already uses in such cases. Any reason not to use it here too? |
||
| if isnothing(J) | ||
| J = Matrix{eltype(value)}(undef, length(value), 0) | ||
| end | ||
| return value, J | ||
| end | ||
|
|
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,44 @@ | ||
| module BijectorsForwardDiffExt | ||
|
|
||
| using Bijectors: Bijectors, find_alpha | ||
| import Bijectors: Bijectors, find_alpha, _value_and_gradient, _value_and_jacobian | ||
| import ADTypes: AutoForwardDiff | ||
| using ForwardDiff: ForwardDiff | ||
|
|
||
| function _value_and_gradient( | ||
| f, backend::AutoForwardDiff{chunksize,T}, x::AbstractVector | ||
| ) where {chunksize,T} | ||
| if isempty(x) | ||
| return f(x), similar(x, 0) | ||
| end | ||
| result = ForwardDiff.DiffResults.GradientResult(x) | ||
| chunk = isnothing(chunksize) ? ForwardDiff.Chunk(x) : ForwardDiff.Chunk{chunksize}() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is type-unstable |
||
| tag = T === Nothing ? ForwardDiff.Tag(f, eltype(x)) : backend.tag | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| config = ForwardDiff.GradientConfig(nothing, x, chunk, tag) | ||
| if T === Nothing | ||
| ForwardDiff.checktag(config, f, x) | ||
| end | ||
| ForwardDiff.gradient!(result, f, x, config, Val(false)) | ||
|
Comment on lines
+17
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| return ForwardDiff.DiffResults.value(result), ForwardDiff.DiffResults.gradient(result) | ||
| end | ||
|
|
||
| function _value_and_jacobian( | ||
| f, backend::AutoForwardDiff{chunksize,T}, x::AbstractVector | ||
| ) where {chunksize,T} | ||
| y = f(x) | ||
| if isempty(x) | ||
| return y, Matrix{eltype(y)}(undef, length(y), 0) | ||
| end | ||
| result = ForwardDiff.DiffResults.JacobianResult(y, x) | ||
| chunk = isnothing(chunksize) ? ForwardDiff.Chunk(x) : ForwardDiff.Chunk{chunksize}() | ||
| tag = T === Nothing ? ForwardDiff.Tag(f, eltype(x)) : backend.tag | ||
| config = ForwardDiff.JacobianConfig(nothing, x, chunk, tag) | ||
| if T === Nothing | ||
| ForwardDiff.checktag(config, f, x) | ||
| end | ||
| ForwardDiff.jacobian!(result, f, x, config, Val(false)) | ||
| return ForwardDiff.DiffResults.value(result), ForwardDiff.DiffResults.jacobian(result) | ||
| end | ||
|
|
||
| Bijectors._eps(::Type{<:ForwardDiff.Dual{<:Any,Real}}) = Bijectors._eps(Real) | ||
| Bijectors._eps(::Type{<:ForwardDiff.Dual{<:Any,<:Integer}}) = Bijectors._eps(Real) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,101 @@ | ||
| module BijectorsMooncakeExt | ||
|
|
||
| using Mooncake: Mooncake | ||
| using Mooncake: | ||
| @is_primitive, MinimalCtx, Mooncake, CoDual, primal, tangent_type, @from_chainrules | ||
| @is_primitive, | ||
| MinimalCtx, | ||
| CoDual, | ||
| tangent_type, | ||
| @from_chainrules, | ||
| prepare_pullback_cache, | ||
| prepare_gradient_cache, | ||
| prepare_derivative_cache, | ||
| value_and_pullback!!, | ||
| value_and_gradient!!, | ||
| value_and_derivative!!, | ||
| zero_tangent, | ||
| Config, | ||
| _copy_output, | ||
| tangent_to_primal!! | ||
| using Bijectors: find_alpha, ChainRulesCore | ||
| import Bijectors: _value_and_gradient, _value_and_jacobian | ||
| import ADTypes: AutoMooncake, AutoMooncakeForward | ||
|
|
||
| _mooncake_config(::Union{AutoMooncake{Nothing},AutoMooncakeForward{Nothing}}) = Config() | ||
| _mooncake_config(backend::Union{AutoMooncake,AutoMooncakeForward}) = backend.config | ||
|
|
||
| function _mooncake_zero_tangent_or_primal( | ||
| x, backend::Union{AutoMooncake,AutoMooncakeForward} | ||
| ) | ||
| if _mooncake_config(backend).friendly_tangents | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is type-unstable |
||
| return tangent_to_primal!!(_copy_output(x), zero_tangent(x)) | ||
| else | ||
| return zero_tangent(x) | ||
| end | ||
| end | ||
|
|
||
| ## Reverse-mode implementations | ||
|
|
||
| function _value_and_gradient(f, backend::AutoMooncake, x::AbstractVector) | ||
| cache = prepare_gradient_cache(f, x; config=_mooncake_config(backend)) | ||
| val, (_, x_grad) = value_and_gradient!!(cache, f, x) | ||
| return val, x_grad | ||
| end | ||
|
|
||
| function _value_and_jacobian(f, backend::AutoMooncake, x::AbstractVector) | ||
| cache = prepare_pullback_cache(f, x; config=_mooncake_config(backend)) | ||
| n_out, n_in = length(cache.y_cache), length(x) | ||
| dy = zeros(eltype(cache.y_cache), n_out) | ||
| if n_out > 0 | ||
| dy[1] = one(eltype(cache.y_cache)) | ||
| end | ||
| val, (_, first_row) = value_and_pullback!!(cache, dy, f, x) | ||
| if n_out == 0 | ||
| return _copy_output(val), Matrix{eltype(x)}(undef, 0, n_in) | ||
| end | ||
| y = _copy_output(val) | ||
| J = Matrix{eltype(first_row)}(undef, n_out, n_in) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why always return a |
||
| J[1, :] .= first_row | ||
| for i in 2:n_out | ||
| fill!(dy, zero(eltype(cache.y_cache))) | ||
| dy[i] = one(eltype(cache.y_cache)) | ||
| _, (_, row) = value_and_pullback!!(cache, dy, f, x) | ||
| J[i, :] .= row | ||
| end | ||
| return y, J | ||
| end | ||
|
|
||
| ## Forward-mode implementations (column-by-column JVPs) | ||
|
|
||
| function _value_and_gradient(f, backend::AutoMooncakeForward, x::AbstractVector) | ||
| cache = prepare_gradient_cache(f, x; config=_mooncake_config(backend)) | ||
| val, (_, x_grad) = value_and_gradient!!(cache, f, x) | ||
| return val, x_grad | ||
| end | ||
|
|
||
| function _value_and_jacobian( | ||
| f, backend::AutoMooncakeForward, x::AbstractVector{T} | ||
| ) where {T} | ||
| y = f(x) | ||
| n_out, n_in = length(y), length(x) | ||
| cache = prepare_derivative_cache(f, x; config=_mooncake_config(backend)) | ||
| df = _mooncake_zero_tangent_or_primal(f, backend) | ||
| if n_in == 0 | ||
| return y, Matrix{eltype(y)}(undef, n_out, 0) | ||
| end | ||
| dx = zeros(T, n_in) | ||
| dx[1] = one(T) | ||
| _, first_jvp = value_and_derivative!!(cache, (f, df), (x, dx)) | ||
| J = Matrix{eltype(first_jvp)}(undef, n_out, n_in) | ||
| J[:, 1] .= first_jvp | ||
| for j in 2:n_in | ||
| fill!(dx, zero(T)) | ||
| dx[j] = one(T) | ||
| _, jvp = value_and_derivative!!(cache, (f, df), (x, dx)) | ||
| J[:, j] .= jvp | ||
| end | ||
| return y, J | ||
| end | ||
|
|
||
| @from_chainrules(MinimalCtx, Tuple{typeof(find_alpha),Float16,Float16,Float16}) | ||
| @from_chainrules(MinimalCtx, Tuple{typeof(find_alpha),Float32,Float32,Float32}) | ||
|
|
@@ -50,7 +143,9 @@ function Mooncake.rrule!!( | |
| msg = "Integer argument has tangent type $(tangent_type(I)), should be NoTangent." | ||
| throw(ArgumentError(msg)) | ||
| end | ||
| out, pb = ChainRulesCore.rrule(find_alpha, primal(x), primal(y), primal(z)) | ||
| out, pb = ChainRulesCore.rrule( | ||
| find_alpha, Mooncake.primal(x), Mooncake.primal(y), Mooncake.primal(z) | ||
| ) | ||
| function find_alpha_pb(dout::P) | ||
| _, dx, dy, _ = pb(dout) | ||
| return Mooncake.NoRData(), P(dx), P(dy), Mooncake.NoRData() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks a lot like https://github.com/JuliaDiff/DifferentiationInterface.jl/blob/a5ecbe0b2bc97eaaac53a1c5c0b13c17f22f1ae9/DifferentiationInterface/ext/DifferentiationInterfaceEnzymeExt/utils.jl#L42-L62, so I'm not sure where we save engineering effort