Hey it’s me again,
I keep encountering memory-related issues with the first call to solve(prob)
, where prob
is an ODEProblem
(see details below). While this is probably expected, as it is most likely related to the “time to first plot”-problem, it becomes problematic when the first call actually crashes due to running out-of-memory (OOM), or when the first call takes an extraordinary amount of time.
In my specific case, this is sadly exactly what happens when trying to simulate (relatively) large systems of coupled ODEs. In some recent posts, I have already been investigating some issues related to saving, callbacks, EnsembleProblems
, and more, yet this underlying issue has not yet been resolved for me. Sadly, the systems I am interested in are of sizes for which a first call to solve(...)
does not resolve as I appear to be running OOM.
To illustrate my problem, as always, I consider the generalized disordered Lotka-Volterra model:
where entries of the interaction matrix A are random. Code is as follows, using ModelingToolkit.jl
and DifferentialEquations.jl
;
using DifferentialEquations
using ModelingToolkit
using ModelingToolkit: t_nounits as t, D_nounits as D
using Random
using SparseArrays
# ODESystem
function create_odesystem(S::Int)
@variables (x(t))[1:S]
@parameters A[1:S,1:S]
eqns = [D(x[i]) ~ x[i]*(1 - sum(A[i,:].*x)) for i in 1:S]
@named sys = ODESystem(eqns, t, [x...], [A])
return complete(sys)
end
# ODEProblem
function create_odeproblem(
sys::ODESystem;
c=0.5, tspan=(0.0,1e3), rng=Random.Xoshiro(1234)
)
S = length(sys.x)
#/ Create parameters
Av = sprandn(rng, S, S, c) ./ sqrt(S)
for i in 1:S
Av[i,i] = 1.0
end
xv = rand(rng, S)
pmap = [sys.A => Av]
xmap = [sys.x => xv]
#/ Create ODEProblem
prob = ODEProblem(sys, xmap, tspan, pmap)
return prob
end
# Solve ODEProblem
function solve_odeproblem(prob::ODEProblem; save=false)
return solve(
prob,
AutoTsit5(TRBDF2()),
callback=PositiveDomain(save=false),
save_everystep=save,
save_start=save
)
end
Then (note that include(...)
statements are excluded for brevity):
julia> S = 16;
julia> sys = create_odesystem(S);
julia> prob = create_odeproblem(sys);
julia> @time sol = solve_odeproblem(prob);
4.444015 seconds (20.86 M allocations: 1.066 GiB, 7.36% gc time)
julia> @time sol = solve_odeproblem(prob);
0.007990 seconds (168.19 k allocations: 9.213 MiB)
Note the large no. of allocations and memory usage on the first call. In this case, there’s not a big problem, of course; just do it once, and subsequent calls are very fast. This raises the question whether using PackageCompiler.jl
to make a sysimage of DifferentialEquations
(or perhaps OrdinaryDiffEq
) may be worth it — yet trying it out I did not see any improvements (so far).
The problem is bigger in larger systems
julia> S = 100;
julia> sys = create_odesystem(S);
julia> prob = create_odeproblem(sys);
julia> @time sol = solve_odeproblem(prob);
8.313252 seconds (42.05 M allocations: 2.202 GiB, 6.66% gc time, 96.34% compilation time)
For my current projects I intend to investigate systems like the one above, but with additional processes/interactions, yet these often need to be investigated relatively large systems, which with my current implementation I simply cannot do.
Note that I am also interested in running many distinct instances, so any potential solutions should also apply to EnsembleProblem
s.
Questions
- What is the main reason that the first call to
solve
takes so much memory? In addition, subsequent calls with a differentS
also appear to take up quite some time/memory, so I cannot simply call it for, say,S=1
, and afterwards investigate a larger system. - Is there a way to overcome the memory overhead of the first call somehow? Can I exploit things like
PackageCompiler.jl
, or other implementations of the code above to drastically reduce the overhead of the first call?
All advice, help and tips are greatly appreciated. Thanks a lot in advance!