How to use ForwardDiff.jl with inv and matrix exponential?

I have a question listed here: Why does ForwardDiff.jl give me all-zero Jacobian matrix?, and I found it might be due to part of the function which contains inv and exp (matrix exponential…) from ExponentialUtilities. I am not sure whether ForwardDiff.jl could fail with inv of matrix or exp?

one example of my function could see below:

# ------------------------------------------------------------------
# solve_odes_water: matrix-exponential solution of dW/dt = A × W + b.
#
# Analytical solution over one timestep ΔT:
#   W(t+ΔT) = e^(A·ΔT) · (W₀ + A⁻¹·b)  −  A⁻¹·b
#
# Derivation: substituting u = W + A⁻¹b → du/dt = A·u → u(t) = e^(At)·u₀
#   → W(t+ΔT) = e^(A·ΔT)·(W₀ + A⁻¹·b) − A⁻¹·b
#
# Falls back to W(t+ΔT) = W₀ when A is singular (isSingular_water = true),
# which avoids NaN propagation at very low carbon / dry spinup.
#
# Mathematically identical to solve_odes_3_water in original.
# ------------------------------------------------------------------
function solve_odes_water(new_vegW, deriv_mat, deriv_const, init_cond, deltaT, z_zero, helpers)
    if isSingular_water(deriv_mat, z_zero)
        for ix ∈ eachindex(new_vegW)
            @rep_elem init_cond[ix] ⇒ (new_vegW, ix, :vegW)
        end
    else
        inv_deriv = inv(deriv_mat)
        new_vegW  = real(-inv_deriv * deriv_const +
                         exp(deltaT * deriv_mat) * (init_cond + inv_deriv * deriv_const))
    end
    return new_vegW
end

Btw, in my code/models, I am using static arrays and static matrix to reduce allocations, and at some places I forced to use Float32. In the final cost function, I set it as dual type. I guess this is not the problem? because I run same codes with different parameter values and input time series data (parameter values coming from optimization over different sites using different input data) the model over 40 independent sites (40 independent runs), only 5-8 sites return zero-valued Jacobians…others are fine…

It’s easy to try — if you set A = rand(3,3), you can easily see that ForwardDiff.jacobian(inv, A) succeeds, and indeed matches the analytical result (-A^T \otimes A):

julia> ForwardDiff.jacobian(inv, A) ≈ -kron(inv(A)', inv(A))
true

but with the matrix exponential it throws a MethodError:

julia> ForwardDiff.jacobian(exp, A)
ERROR: MethodError: no method matching exp!(::Matrix{ForwardDiff.Dual{ForwardDiff.Tag{typeof(exp), Float64}, Float64, 9}})

and indeed there is an open issue for this: support for `Base.expm` (need advice implementing) · Issue #174 · JuliaDiff/ForwardDiff.jl · GitHub

(ChainRules.jl has rules for the matrix exponential, however, so if you use something like Zygote.jl or Enzyme.jl that supports ChainRules it should be easier to get it working. Or use a different implementation of exp like expv in ExponentialUtilities.jl)

Yes I am using exp from ExponentialUtilities.jl…but seems to be broken too…

julia> ForwardDiff.jacobian(ExponentialUtilities.exp, Float32.(A))
ERROR: MethodError: no method matching exp!(::Matrix{ForwardDiff.Dual{ForwardDiff.Tag{typeof(exp), Float32}, Float32, 9}})
The function `exp!` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  exp!(::StridedMatrix{T}) where T<:Union{Float32, Float64, ComplexF64, ComplexF32}
   @ LinearAlgebra /viper/ptmp1/xshan/julia/depot/juliaup/julia-1.11.6+0.x64.linux.gnu/share/julia/stdlib/v1.11/LinearAlgebra/src/dense.jl:677

Stacktrace:
 [1] exp(A::Matrix{ForwardDiff.Dual{ForwardDiff.Tag{typeof(exp), Float32}, Float32, 9}})
   @ LinearAlgebra /viper/ptmp1/xshan/julia/depot/juliaup/julia-1.11.6+0.x64.linux.gnu/share/julia/stdlib/v1.11/LinearAlgebra/src/dense.jl:622
 [2] vector_mode_dual_eval!(f::typeof(exp), cfg::ForwardDiff.JacobianConfig{ForwardDiff.Tag{…}, Float32, 9, Matrix{…}}, x::Matrix{Float32})
   @ ForwardDiff ~/.julia/packages/ForwardDiff/z3jRk/src/apiutils.jl:24
 [3] vector_mode_jacobian(f::typeof(exp), x::Matrix{Float32}, cfg::ForwardDiff.JacobianConfig{ForwardDiff.Tag{…}, Float32, 9, Matrix{…}})
   @ ForwardDiff ~/.julia/packages/ForwardDiff/z3jRk/src/jacobian.jl:129
 [4] jacobian(f::typeof(exp), x::Matrix{Float32}, cfg::ForwardDiff.JacobianConfig{ForwardDiff.Tag{…}, Float32, 9, Matrix{…}}, ::Val{true})
   @ ForwardDiff ~/.julia/packages/ForwardDiff/z3jRk/src/jacobian.jl:22
 [5] jacobian(f::typeof(exp), x::Matrix{Float32}, cfg::ForwardDiff.JacobianConfig{ForwardDiff.Tag{typeof(exp), Float32}, Float32, 9, Matrix{ForwardDiff.Dual{…}}})
   @ ForwardDiff ~/.julia/packages/ForwardDiff/z3jRk/src/jacobian.jl:19
 [6] jacobian(f::typeof(exp), x::Matrix{Float32})
   @ ForwardDiff ~/.julia/packages/ForwardDiff/z3jRk/src/jacobian.jl:19
 [7] top-level scope
   @ REPL[13]:1
 [8] top-level scope
   @ none:1
Some type information was truncated. Use `show(err)` to see complete types.

For ExponentialUtilities you would use expv for the exponential–vector product. But that doesn’t seem to support ForwardDiff either.

Also open in issue #51008.

I’ve needed this before too so I slammed together my own implementation of \phi_1(x) = \frac{\exp(x)-1}{x}. It should work with StaticArrays and ForwardDiff. No warranty but here it is:

using LinearAlgebra

"""
    expm1o(A)

Compute `(exp(A) - I) / A` via a division-free calculation that is suitable for singular `A`.
"""
function expm1o(A)
    # note: this functionality is available as `last(ExponentialUtilities.phi(A, 1))`
    # see https://www.maths.uq.edu.au/expokit/paper.pdf or https://www.sciencedirect.com/science/article/pii/S0377042720300492 for allegedly better ways to compute it

    function maybematrixpoly(x::Union{AbstractMatrix,UniformScaling},p)
        if eltype(p) <: Number
            return evalpoly(x,UniformScaling.(p))
        else
            return evalpoly(x,p)
        end
    end
    maybematrixpoly(x,p) = evalpoly(x,p)

	nA = norm(A,2) # frobenius norm
    T = typeof(nA)
	nA = nA > floatmin(T) ? nA : floatmin(T) # avoid issue with degenerate nA
	ex = ceil(Int, log2(nA))
	logscale = max(4 + ex, 0)
    p = inv.(cumprod(ntuple(T, Val(9)))) # a Pade approximation would probably be faster/more accurate
	scale = exp2(T(-logscale))
	sA = A * scale
	expmsA = maybematrixpoly(sA, p)
	expmA = expmsA
	for _ in 1:logscale
		expmA = (expmA * expmA) * A * (scale / 2) + expmA
		scale *= 2
	end
	return expmA
end

You should be able to write your implementation in terms of this (and probably don’t need to branch on singular matrices any more).

Use exp_generic. That is tested to work for this.

Not working…I am using StaticMatrix and StaticArray…this is required by the model to create zero allocations…

julia> B = dt * A
4×4 SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16} with indices SOneTo(4)×SOneTo(4):
 Dual{Nothing}(-55.1042,2.67628e20)    Dual{Nothing}(28.9682,4.77492e22)   …    Dual{Nothing}(0.0,0.0)
  Dual{Nothing}(55.1042,-2.67628e20)  Dual{Nothing}(-57.9383,-9.55121e22)       Dual{Nothing}(0.0,0.0)
   Dual{Nothing}(0.0,0.0)              Dual{Nothing}(28.9701,4.77628e22)       Dual{Nothing}(33.8189,-6.4245e19)
   Dual{Nothing}(0.0,0.0)               Dual{Nothing}(0.0,0.0)                Dual{Nothing}(-67.6425,5.67798e19)

julia> E_base = exp(B)
4×4 SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16} with indices SOneTo(4)×SOneTo(4):
 Dual{Nothing}(0.00445987,NaN)  Dual{Nothing}(0.00414118,NaN)  Dual{Nothing}(0.00325967,NaN)  Dual{Nothing}(0.00173045,NaN)
 Dual{Nothing}(0.00787748,NaN)  Dual{Nothing}(0.00731459,NaN)  Dual{Nothing}(0.00575757,NaN)  Dual{Nothing}(0.0030565,NaN)
 Dual{Nothing}(0.00906899,NaN)  Dual{Nothing}(0.00842096,NaN)  Dual{Nothing}(0.00662844,NaN)  Dual{Nothing}(0.00351882,NaN)
 Dual{Nothing}(0.00282028,NaN)  Dual{Nothing}(0.00261875,NaN)  Dual{Nothing}(0.00206131,NaN)  Dual{Nothing}(0.00109428,NaN)

julia> E_generic = exp_generic(B)
ERROR: setindex!(::SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16}, value, ::Int) is not defined.
 Hint: Use `MArray` or `SizedArray` to create a mutable static array
Stacktrace:
  [1] error(s::String)
    @ Base ./error.jl:35
  [2] setindex!(a::SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16}, value::ForwardDiff.Dual{Nothing, Float64, 1}, i::Int64)
    @ StaticArrays ~/.julia/packages/StaticArrays/0cEwi/src/indexing.jl:3
  [3] macro expansion
    @ ~/.julia/packages/StaticArrays/0cEwi/src/broadcast.jl:159 [inlined]
  [4] _broadcast!
    @ ~/.julia/packages/StaticArrays/0cEwi/src/broadcast.jl:143 [inlined]
  [5] _copyto!
    @ ~/.julia/packages/StaticArrays/0cEwi/src/broadcast.jl:70 [inlined]
  [6] copyto!
    @ ~/.julia/packages/StaticArrays/0cEwi/src/broadcast.jl:63 [inlined]
  [7] materialize!
    @ ./broadcast.jl:883 [inlined]
  [8] materialize!
    @ ./broadcast.jl:880 [inlined]
  [9] exp_gen!(cache::Vector{MMatrix{4, 4, ForwardDiff.Dual{…}, 16}}, A::SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16}, ::Val{10})
    @ ExponentialUtilities ~/.julia/packages/ExponentialUtilities/mb4U7/src/exp_generated/exp_10.jl:22
 [10] exponential!(A::SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16}, method::ExpMethodHigham2005, _cache::T)
    @ ExponentialUtilities ~/.julia/packages/ExponentialUtilities/mb4U7/src/exp_noalloc.jl:106
 [11] exponential!(A::SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16}, method::ExpMethodHigham2005)
    @ ExponentialUtilities ~/.julia/packages/ExponentialUtilities/mb4U7/src/exp_noalloc.jl:88
 [12] exponential!(A::SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16})
    @ ExponentialUtilities ~/.julia/packages/ExponentialUtilities/mb4U7/src/exp.jl:15
 [13] exp_generic(args::SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16}; kwargs::@Kwargs{})
    @ ExponentialUtilities ./deprecated.jl:116
 [14] exp_generic(args::SMatrix{4, 4, ForwardDiff.Dual{Nothing, Float32, 1}, 16})
    @ ExponentialUtilities ./deprecated.jl:113
 [15] top-level scope
    @ REPL[343]:1
 [16] top-level scope
    @ none:1
Some type information was truncated. Use `show(err)` to see complete types.

does exp_generic work on static array/matrix? @ChrisRackauckas

Thank you for your suggestions and link to another issues…but how to solve it for cos? our model also have cos and sin in other places…

Okay, so a few things here, @Xu_Shan you say

but please note that ExponentialUtilities.exp is just Base.exp:

julia> ExponentialUtilities.exp === Base.exp
true

Next, you mention that you are using StaticArrays, but on my machine at least, ForwardDiff.exp works totally fine on SMatrix:

julia> using ForwardDiff, StaticArrays

julia> let A = rand(SMatrix{3, 3})
           ForwardDiff.jacobian(exp, A)
       end
9×9 SMatrix{9, 9, Float64, 81} with indices SOneTo(9)×SOneTo(9):
 1.76523     0.0467088   0.327616    0.806547   0.013858   0.100769  0.310702   0.00533908  0.0388228
 0.806547    1.59729     0.682086    0.245686   0.755658   0.207098  0.0946549  0.291097    0.0797884
 0.310702    0.0723638   1.67116     0.0946549  0.0224092  0.776938  0.0364675  0.0086335   0.299295
 0.0467088   0.0007806   0.00568669  1.59729    0.0438392  0.306742  0.0723638  0.00126505  0.00918839
 0.013858    0.0438392   0.0116795   0.755658   1.43989    0.639192  0.0224092  0.0677215   0.0188915
 0.00533908  0.00126505  0.0450359   0.291097   0.0677215  1.50936   0.0086335  0.00204291  0.0696659
 0.327616    0.00568669  0.0413231   0.682086   0.0116795  0.084947  1.67116    0.0450359   0.315479
 0.100769    0.306742    0.084947    0.207098   0.639192   0.174568  0.776938   1.50936     0.657123
 0.0388228   0.00918839  0.315479    0.0797884  0.0188915  0.657123  0.299295   0.0696659   1.58065

You also mention that you’re “forced to use Float32” at various points, and I suppose this is the actual problem. It seems the jacobian of exp(::SMatrix{N, N, Float32}) isn’t very well behaved and can sometimes be NaN:

julia> let A = rand(SMatrix{3, 3, Float32})
           ForwardDiff.jacobian(exp, A)
       end
9×9 SMatrix{9, 9, Float32, 81} with indices SOneTo(9)×SOneTo(9):
  0.610376   0.656253   0.547119  0.377831   -0.708216   0.067911  0.293016   0.0872581  -0.724079
 -0.097091   1.93593    0.744129  0.0534829  -0.0192576  0.107209  0.0449941  0.375067   -0.380432
 -0.0226828  0.280314   1.40075   0.0476514  -0.289407   0.378518  0.0397389  0.035158   -0.0227381
 -0.114842   0.178618   0.147041  1.93864     0.0291169  0.672426  0.277805   0.0731433  -0.725562
 -1.30146    0.801856   0.1931    0.458767    1.17613    0.894499  0.0250352  0.341649   -1.3225
 -0.274758   0.0704489  0.668607  0.374683   -0.0232118  1.93445   0.0328772  0.0287411  -0.0800439
 -0.11172    0.14564    0.119864  0.745587   -0.468221   0.161845  1.39804    0.670251   -0.111745
 -0.800691   0.671748   0.162845  0.109852    0.0235237  0.219857  0.375415   1.93717    -0.135987
 -0.709717   0.0463567  0.547772  0.0949469  -0.714231   0.73353   0.293344   0.282309    0.616263

julia> let A = rand(SMatrix{3, 3, Float32})
           ForwardDiff.jacobian(exp, A)
       end
9×9 SMatrix{9, 9, Float32, 81} with indices SOneTo(9)×SOneTo(9):
 NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
 NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
 NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
 NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
 NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
 NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
 NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
 NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
 NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN

In this case, it seems that the easiest path forward would be to simply not use Float32, and eat the modest performance loss, but not suffer a massive loss of accuracy and reliability.

Yes…we also found it…we run the model optimization and ForwardDiff.jl (on optimized parameter) by 40 independent runs, and 8 or 5 of them broke and in each time it broke at different sites (also different numbers, i.e. “8 or 5” varies..)…

For the choice of Float32, if I remember it correctly, it is required by ForwardDiff.jl, right? for StaticArrays or static matrix, they are forced to use in the model to reduce the allocations and improve the model running time…otherwise each optimization would run at least 2 times slower…we did extensive efforts to make operations on static arrays to avoid excessive allocations…

No? I literally show an example of it working on Float64. I suspect it was only tested at 64 bit precision, and that Float32 was an afterthought if it was even thought about at all (hence the NaN issues).

Again, I literally show an example in my comment of it working fine on StaticArrays of Float64 numbers. It’s regular arrays that don’t have a ForwardDiff exp method.

If you can get a working exp then one option is to use the Euler identity exp(x*im) == cos(x) + sin(x)*im (i.e., cos and sin are the real and imag parts of a complex exponential). If you don’t have a working exp:

  • you can modify expm1o to compute exp directly by changing the lines p = inv.(cumprod(ntuple(T, Val(9)))) to p = inv.( (one(T), cumprod(ntuple(T, Val(9)))... ) ) and expmA = (expmA * expmA) * A * (scale / 2) + expmA to expmA = expmA * expmA (test this as I haven’t verified it).
  • use exp(x) == expm1o(x) * x + I

If your cos and sin terms can actually be combined with adjacent terms into (1 - cos(x)) / x and sin(x) / x then you should use expm1o(x*im) to compute these expressions directly to avoid issues with singular x.


The frequent overflows in exp(::SMatrix) (especially the 3x3 case) are tracked in issue #785 which links to some other issues that are relevant as well.

The reason exp(::SMatrix{3,3,Float32}) overflows so frequently is because the Pade approximant coefficients used are large (like 1f16, if I recall correctly) and the 3x3 solve/inverse uses the det of the matrix which leads to overflow. If one downscaled the coefficients uniformly AND/OR one used lu to compute the Pade approximant the issue would largely disappear. I swear I had a comment on there talking about this but I can’t find it. The issue is present for 2x2 also but less of an issue because the det has fewer products. For 4x4+ it’s mostly fine because lu is used, so the large terms aren’t multiplied to overflow.

A few updates after digging into this:

  1. exp_generic no longer exists as a name — the current API in ExponentialUtilities.jl is exponential!(A, method). The setindex!(::SMatrix, …) error comes from the default method selection: exponential!(A) with no method argument picks the in-place ExpMethodHigham2005, which mutates and therefore can’t work on an SMatrix.

  2. The good news: exponential!(A, ExpMethodGeneric()) already works on static arrays today, including through ForwardDiff. ExpMethodGeneric has a non-mutating code path for immutable array types (Padé + scaling-and-squaring built only out of , /, and + cI), so:

using ExponentialUtilities, StaticArrays, ForwardDiff
A = @SMatrix rand(3, 3)
exponential!(A, ExpMethodGeneric()) 
ForwardDiff.jacobian(x -> exponential!(x, ExpMethodGeneric()), A) 

Works

  1. On the Float32 NaN issue @Mason found and @mikmoore diagnosed: confirmed, and ExpMethodGeneric avoids it. StaticArrays’ own exp (which is what exp(::SMatrix) calls, and which does work with dual numbers since its generic path is non-mutating) uses the Base-style Padé formulation with large integer coefficients (up to ~3.2e16), and those overflow Float32 on the way through the 3×3 cofactor-based solve. ExpMethodGeneric fixes both the mutation error and the Float32 instability at once.

  2. I’ve opened a PR to ExponentialUtilities.jl making the default exponential!(A) automatically fall back to ExpMethodGeneric for immutable matrices, so plain exponential!(A) on an SMatrix will just work: Fall back to ExpMethodGeneric for immutable matrices in exponential! - Pull Request #237 - SciML/ExponentialUtilities.jl - GitHub

So with that, “just” using ExponentialUtilities.jl will do what you expected now.

Thanks…but it seems that this one only sticks to Float64, right? it might use polynomial of Float64 internal?

julia> A = SMatrix{3, 3, Float32}(rand(Float32, 3, 3))
3×3 SMatrix{3, 3, Float32, 9} with indices SOneTo(3)×SOneTo(3):
 0.276386  0.954482  0.714192
 0.574739  0.3517    0.418869
 0.555151  0.3695    0.359785

julia> exponential!(A, ExpMethodGeneric()) 
3×3 SMatrix{3, 3, Float64, 9} with indices SOneTo(3)×SOneTo(3):
 2.11407  1.78511  1.49504
 1.13127  2.05394  1.03571
 1.08678  1.04209  1.95143

julia> ForwardDiff.jacobian(x -> exponential!(x, ExpMethodGeneric()), A) 
9×9 SMatrix{9, 9, Float64, 81} with indices SOneTo(9)×SOneTo(9):
 1.8236    0.838051  0.680035  0.523368   …  0.503557   0.158246   0.126138
 0.523368  1.82291   0.451652  0.101791      0.0980227  0.506292   0.0816883
 0.503557  0.441118  1.79091   0.0980227     0.0943939  0.0782926  0.501925
 0.838051  0.265264  0.211498  1.82291       0.441118   0.131362   0.104497
 0.164332  0.842565  0.137019  0.526213      0.0812944  0.443656   0.0674744
 0.158246  0.131362  0.835204  0.506292   …  0.0782926  0.0645242  0.440187
 0.680035  0.211498  0.16852   0.451652      1.79091    0.835204   0.677983
 0.130985  0.683771  0.109075  0.0848232     0.521681   1.7899     0.450526
 0.126138  0.104497  0.677983  0.0816883     0.501925   0.440187   1.75782

Handled.