Skip to content

Derivatives using SVector{Dual} #28

Closed
@dbeach24

Description

@dbeach24

@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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions