Description
@andyferris Thanks for the offer to help. To avoid sabotaging #25, I'm putting my "Jacobian" code in this ticket.
Let's begin with a small, differentiable vector-valued function:
function cart_to_sphere(cart)
x, y, z = cart
r = sqrt(x^2 + y^2 + z^2)
θ = acos(z / r)
ϕ = atan2(y, x)
SVector(r, θ, ϕ)
end
Suppose I want to evaluate this function and its derivative simultaneously, using AD forward mode with dual numbers. Because this is a low-dimensional space, I'd like to use SVector
/ SMatrix
wherever possible.
@inbounds function eval_jacobian{N,T}(f::Function, x::SVector{N,T})
dualtype = ForwardDiff.Dual{N,T}
partials = make_partials(GetPartials{N,T}())
xp = SVector{N,dualtype}(dualtype(x[i], partials[i]) for i=1:N)
yp = f(xp)
y = map(ForwardDiff.value, yp)
P = length(yp)
J = SMatrix{P,N}((ForwardDiff.partials(yp[i])[j] for i=1:P, j=1:N) ...) # slow line?
(y, J)
end
Because the "diagonal" partials can be precomputed and re-used for any low-dimension space, I have moved these to a generated function. The function returns an MVector
to avoid copying the result on each invocation.
# just a type to invoke make_partials with
immutable GetPartials{N,T} end
@generated function make_partials{N,T}(::GetPartials{N,T})
partialtype = ForwardDiff.Partials{N,T}
partials = MVector{N,partialtype}(
partialtype(ntuple(N) do x x == i ? one(T) : zero(T) end)
for i=1:N
)
return :($partials)
end
Evaluating make_partials is essentially free:
julia> @code_native make_partials(GetPartials{9,Float64}())
.section __TEXT,__text,regular,pure_instructions
Filename: REPL[28]
pushq %rbp
movq %rsp, %rbp
Source line: 2
movabsq $4569288656, %rax ## imm = 0x11059CFD0
popq %rbp
retq
Based on timing tests, calling eval_jacobian(cart_to_sphere, SVector(1.,2.,3.))
is far more expensive than it could be:
julia> @time for i=1:10^5 eval_jacobian(cart_to_sphere, SVector(1.,2.,3.)) end
0.684567 seconds (4.21 M allocations: 227.805 MB, 5.21% gc time)
If I change eval_jacobian
to return yp
(skipping the formation of y
and J
), then the timing is substantially better:
julia> @time for i=1:10^5 eval_jacobian(cart_to_sphere, SVector(1.,2.,3.)) end
0.074104 seconds (1.40 M allocations: 57.983 MB, 9.93% gc time)
I think the cost of constructing the Jacobian matrix is the single biggest problem with my code right now.
Ideally, I'd like to see a version with no dynamic allocations (once compiled). Perhaps this will require crafting my own dual-number implementation using SVector
to hold the partials.
Thoughts?