I am not happy with the TTFX of our MTK models (see: RamAirKites.jl). We already cache our simplified model after building it and use a custom system image. But nevertheless, the time to load a model from our cache is still about 35s, even though a second run in the same REPL takes only 5.5s.
This is what AI explains:
ModelingToolkit.build_functiondoesn’t produce an ordinary Julia method. It produces aRuntimeGeneratedFunction{argnames, cache_tag, context_tag, id, B}— a wrapper whose body (anExpr) is stashed in aDict(_cache_body, line ~226) that lives inside a dynamically-created module, and whose actual calls go through a@generated function generated_callfunc_body(...)indirection (line 194).- The first time that generated function is called with a given
id, Julia has to run type inference + LLVM codegen on it, same as any other freshly-seen method — that’s real JIT work, tied to this specific model’s RHS/Jacobian/getter expressions (129 pts, 108 segs, 6 twist groups, 4 winches).- Once compiled, the resulting native code sits in the process’s ordinary method-instance cache for the rest of that Julia session. That’s why re-running
init!(even on a freshsam, since the disk deserialize repopulates the sameids) in the same REPL only costs 5.5 s — you’re paying deserialize + object reconstruction, but thegenerated_callfunc_bodymethods for thoseids are already compiled in this process, so no JIT.- A sysimage is built by
PackageCompilerwalking the precompile caches of the packages passed tocreate_sysimage(:StableAWEModels,:VortexStepMethod, …) — i.e. methods that exist statically in those packages’ module method tables atusingtime. TheRuntimeGeneratedFunctionmethods created byevalwhiletest_for_precompile.jlran during the sysimage build live in a transient module/idcreated in that build process. They were real, compiled native code — but only inside that now-dead build process. PackageCompiler’s snapshot doesn’t walk “every method that got JIT-compiled while the precompile script executed,” only the target packages’ own precompiled method tables, so this per-model,eval-minted code never makes it into the.so.
So every fresh julia -J bin/kps-image-1.12.so process starts with an empty method-instance cache for this model’s RuntimeGeneratedFunction ids and has to redo that JIT work once — that’s your 38 s. Inside that same process, it’s free from then on — that’s your 5.5 s. This is a known SciML/ModelingToolkit + PackageCompiler limitation (the whole point of the RuntimeGeneratedFunction indirection is to avoid invalidating a huge sysimage’s method cache every time a user builds a new symbolic model — the tradeoff is that the model-specific code can never be pre-baked into that sysimage).
Is there no workaround to include this compiled code in the system image for production use?