Models precompilation and general modelling platform architecture

Hi, could you please help me with the following workflow question.
Let’s say I have a simulation platform as julia package X and a number of models as julia .jl files. I would like say using X then precompile and upload a model and run the simulations without first-call compilation delay (then perhaps change smth in the model file and re-upload it). I thought it could be done by wrapping each model as julia Module but to precompile a separate Module I have to do something tricky like . Another idea is to wrap all model files into a separate package… Perhaps there is a better and simpler way to do it?

Maybe you could use PackageCompiler to build a system image for the platform, possibly using some (or all) existing models to build a precompile_execution_file that is as complete as possible.

You would then run the platform with this system image, and load your models in it hopefully avoiding all compilation time. You’d have to recreate the system image each time the platform (package X) is modified, but if your models a cleanly separated from the platform, you should be able to modify them and reuse the same system image.

Would that make sense?

1 Like

Thanks ! It is a possible solution. However, if understand correctly, in case I need to update the model code in .jl file or to add a new model, I will need to recreate the sysimage and restart Julia. The “ideal” workflow would be load_model() which means include and precompile, run simulations, then change the model, load it again, etc. The need to store models as separate files is dictated by the fact that they are generated by external (non Julia) software

If models are only plain Julia “scripts” that call functions defined in your X package (the simulation platform), then you shouldn’t need to re-create the system image as models change; only when the platform itself changes.

When you create your system image for the simulation platform, it embeds all function definitions loaded with the

using X

statement. Meaning that if you change X.jl, Julia will not see the changes, hence the need to re-build the system image to avoid having results that are inconsistent with the latest version of your code.

The system image also embeds compiled versions of all methods called in the precompile_execution_file (which would include a few of you models). But this is a rather weak link: if the system image has been built with the exact models that you want to use, then this should mostly eliminate the need to compile anything when running your models. But if your models evolve and call your platform in ways that haven’t been previously seen, Julia will compile the required methods as needed. So the worst that could happen is if your models are completely out-of-sync with the precompile_execution_file, in which case you’d have to pay some latency to compile methods when you run your models, but the results would still be correct.

Maybe you can think of it this way: any Julia program makes use of functions in Base. Which is baked into the default system image that Julia ships with, in order to dramatically reduce latency. Although your program most likely uses function from Base in ways that were not foreseen and integrated into the system image, it still works: Julia will compile everything just ahead of time as needed. But you’d normally have to update your system image in order to see any change you’d make to the source files of the Base module.

In the same way, each one of your models is a Julia program that makes use of your platform. You can reduce latency by baking the simulation platform into the system image, but you don’t have to update it as your models evolve. You only really need to update the system image when the platform itself changes.

1 Like

@ffevotte Thanks a lot for clarification! Yes, making sysimage with some typical models sounds a good idea. Still the thing I miss is that such sysimage will reduce load time in case my model consists only of methods defined in X platform, but my model consists of user-defined functions. For example, if this X platform is DifferentialEquations.jl pkg and my model consists of ode-related functions, like:

using DiffEqBase, OrdinaryDiffEq, DiffEqCallbacks

### ODE
function ode(du, u, p, t)
    (C,S2,k1,k2) = p
    (S1,S3) = u 

    reaction1 = C * k1 * S1 * S2
    reaction2 = C * k2 * S3

    du[1] = -reaction1+reaction2  # dS1/dt
    du[2] =  reaction1-reaction2  # dS3/dt

### events
function condition(out,u,t,integrator)
    out[1] = 0.75 - u[1]
    out[2] = 1.4 - u[2]

function affect!(integrator, idx)
    if idx == 1
        integrator.p[2] = 1.0
    elseif idx ==2
        integrator.u[1] = 1.0
vccb = VectorContinuousCallback(condition, affect!, 2, save_positions=(true,true))

# initial values and parameters
u0 = [1.0, 1.0] # S1, S3
p0 = [1.0, 2.0, 0.75, 0.25] # C, S2, k1, k2

prob = ODEProblem(ode, u0, (0.,3.), p0, callback=vccb)

We know that a model consists of functions defining ode condition, affect! but their concrete form is each time user-defined.

It looks like the functions defined in your models will take almost no time to compile, but there will be some compilation involved for functions from DiffEq. So you’re right, it’s true that in this case there will be compilation latency remaining.

That being said, I haven’t anything else to suggest. I might be wrong, but such problems (involving compilation of methods defined in one package, with argument of a type defined in another package) are – in my experience – not so well handled by precompilation alone (by which I mean .ji files). So even if you made each model into its separate package in order to automatically benefit from precompilation, I’m not sure it would be so useful in this case.

Again, I might be missing something and hopefully someone else will chime in with a better idea. But in the meantime, I suggest you still try building a system image and see where this gets you. (And you can also try making one of your models into a package, and benchmark where precompilation gets you in order to make an informed decision about how best to proceed)

(I don’t really recommend using this. It only performs “precompilation” and not the full compilation that happens when you first call a method. In practice I found it made only a minimal to negative difference for small scripts, and if you make anything very complex you should put it into a package anyway which julia will automatically precompile for you on first using.)

@ffevotte @non-Jedi Thanks! I will try sysimages. The example I have posted of course is simple and doesn’t require pre-comilation. However big ODE models can load for several minutes during the first function call while a user is more inclined to wait during the “load/precomilation/warmup” step, than during the first simulation run. Perhaps sysimage together with some warm-up trick can help here

Note that Julia v1.6 has some pretty massive compilation fixes for DiffEq, so one thing you might want to ask is whether it’s worth the effort.

But in terms of how to fully get around it, there’s a few things you can do. First, yes build a system image with the ODE definition in it is always a route you can take. Another thing you can do is kick out C code and build binaries from ModelingToolkit.

@ChrisRackauckas could you, please, provide more details about how to “kick out C code and build binaries from ModelingToolkit”?