Function compiles every time

I re-wrote my astrodynamics functions with ModelingToolkit. Now, when I call DifferentialEquations.solve(...) in my wrapper function, it compiles every time. Is there any way to check what is compiling?

using StaticArrays
using AstrodynamicalModels
using DifferentialEquations

function propagate(r, v, T; kwargs...)

  defaults = (; reltol=1e-14, abstol=1e-14, save_everystep=false)
  options  = merge(defaults, kwargs)

  problem   = ODEProblem(R2BP, MVector{6}(vcat(r,v)), (0.0, T), MVector{1}(398600.435))
  solutions = solve(problem; options...)

end

r = randn(3) * 1e4
v = randn(3) * 1e3
T = 10_000.0

@time propagate(r,v,T) #  28.067676 seconds (93.98 M allocations: 5.404 GiB, 3.61% gc time)
@time propagate(r,v,T) #   7.187065 seconds (13.05 M allocations: 704.718 MiB, 1.34% gc time, 99.70% compilation time)
@time propagate(r,v,T) #   7.154281 seconds (13.06 M allocations: 705.808 MiB, 1.79% gc time, 99.43% compilation time)

If I don’t put the solver in a function, the behavior is fine.

using StaticArrays
using AstrodynamicalModels
using DifferentialEquations

defaults = (; reltol=1e-14, abstol=1e-14, save_everystep=false)
kwargs   = (;)
options  = merge(defaults, kwargs)

r = randn(3) * 1e4
v = randn(3) * 1e3
T = 10_000.0

problem   = ODEProblem(R2BP, MVector{6}(vcat(r,v)), (0.0, T), MVector{1}(398600.435))

@time solutions = solve(problem; options...) #  15.728376 seconds (50.96 M allocations: 2.763 GiB, 4.17% gc time, 2.34% compilation time)
@time solutions = solve(problem; options...) #   0.012221 seconds (540.66 k allocations: 9.171 MiB)
@time solutions = solve(problem; options...) #   0.012453 seconds (540.66 k allocations: 9.171 MiB)

What am I doing wrong?

1 Like

Is R2BP an ODESystem? ODESystems are symbolic objects, so every time you go symbolic → code it has to compile. I would recommend compiling it once and then using tools like remake to change u0, tspan, and p.

4 Likes

Ah I see. @ChrisRackauckas what constitutes “compiling” in this case? If I’m exporting an ODESystem that I don’t expect users to really change, should my package also export ODEFunction objects? Are ODEFunctions compiled?

I tried checking Catalyst.jl source code but couldn’t answer my question.

Yes they are.

It builds a new source for a function f, so it’ll need to compile again when it’s rebuilt. There may be a way to fix that though… @c42f is there a way to make runtimegeneratedfunctions cache this better? They should have the same hash.

1 Like

If the expressions have exactly the same hash this should already work.

Demo:

julia> using RuntimeGeneratedFunctions

julia> ex = :(x -> x^2 - 1)
:(x->begin
          #= REPL[2]:1 =#
          x ^ 2 - 1
      end)

julia> RuntimeGeneratedFunctions.init(Main)

julia> f = @RuntimeGeneratedFunction(ex)
RuntimeGeneratedFunction(#=in Main=#, #=using Main=#, :((x,)->begin
          #= REPL[2]:1 =#
          x ^ 2 - 1
      end))

julia> @time f(1.0)
  0.051278 seconds (64.49 k allocations: 4.051 MiB, 99.96% compilation time)
0.0

julia> @time f(1.0)
  0.000004 seconds (1 allocation: 16 bytes)
0.0

julia> g = @RuntimeGeneratedFunction(ex)
RuntimeGeneratedFunction(#=in Main=#, #=using Main=#, :((x,)->begin
          #= REPL[2]:1 =#
          x ^ 2 - 1
      end))

julia> @time g(1.0)
  0.000008 seconds (1 allocation: 16 bytes)
0.0

julia> typeof(f) === typeof(g)
true

Maybe the hashes aren’t exactly equal here for some reason?

1 Like
:(function (var"##out#1185", var"##arg#1183", var"##arg#1184", t)
      #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:282 =#
      #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:283 =#
      let var"x(t)" = #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:169 =# @inbounds(var"##arg#1183"[1]), Ď„ = #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:169 =# @inbounds(var"##arg#1184"[1])
          #= C:\Users\accou\.julia\packages\Symbolics\sITWZ\src\build_function.jl:331 =#
          #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:329 =# @inbounds begin
                  #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:325 =#
                  var"##out#1185"[1] = (*)((inv)(Ď„), (+)(1, (*)(-1, var"x(t)")))
                  #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:327 =#
                  nothing
              end
      end
  end)
(generate_function(sys))[2] = :(function (var"##out#1188", var"##arg#1186", var"##arg#1187", t)
      #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:282 =#
      #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:283 =#
      let var"x(t)" = #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:169 =# @inbounds(var"##arg#1186"[1]), Ď„ = #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:169 =# @inbounds(var"##arg#1187"[1])
          #= C:\Users\accou\.julia\packages\Symbolics\sITWZ\src\build_function.jl:331 =#
          #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:329 =# @inbounds begin
                  #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:325 =#
                  var"##out#1188"[1] = (*)((inv)(Ď„), (+)(1, (*)(-1, var"x(t)")))
                  #= C:\Users\accou\.julia\packages\SymbolicUtils\kZD5O\src\code.jl:327 =#
                  nothing
              end
      end
  end)

Every time it generates the function it increments the gensyms. There’s probably a way to fix that? It’s all fine locally since these functions can’t hit globals.

@cadojo the better fix is Unique function argument names for no recompilation by ChrisRackauckas · Pull Request #272 · JuliaSymbolics/Symbolics.jl · GitHub

3 Likes

Nice! Yes I wondered if it was upstream a little from RuntimeGeneratedFunctions.