Skip to content

Commit bab10f4

Browse files
add some docs
1 parent 39b1513 commit bab10f4

File tree

4 files changed

+123
-5
lines changed

4 files changed

+123
-5
lines changed

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pages = [
3131
"basics/MTKModel_Connector.md",
3232
"basics/Validation.md",
3333
"basics/DependencyGraphs.md",
34+
"basics/Precompilation.md",
3435
"basics/FAQ.md"],
3536
"System Types" => Any["systems/ODESystem.md",
3637
"systems/SDESystem.md",

docs/src/basics/Precompilation.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Working with Precompilation and Binary Building
2+
3+
## tl;dr, I just want precompilation to work
4+
5+
The tl;dr is, if you want to make precompilation work then instead of
6+
7+
```julia
8+
ODEProblem(sys, u0, tspan, p)
9+
```
10+
11+
use:
12+
13+
```julia
14+
ODEProblem(sys, u0, tspan, p, eval_module = @__MODULE__, eval_expression = true)
15+
```
16+
17+
As a full example, here's an example of a module that would precompile effectively:
18+
19+
```julia
20+
module PrecompilationMWE
21+
using ModelingToolkit
22+
23+
@variables x(ModelingToolkit.t_nounits)
24+
@named sys = ODESystem([ModelingToolkit.D_nounits(x) ~ -x + 1], ModelingToolkit.t_nounits)
25+
prob = ODEProblem(structural_simplify(sys), [x => 30.0], (0, 100), [],
26+
eval_expression = true, eval_module = @__MODULE__)
27+
28+
end
29+
```
30+
31+
If you use that in your package's code then 99% of the time that's the right answer to get
32+
precompilation working.
33+
34+
## I'm doing something fancier and need a bit more of an explanation
35+
36+
Oh you dapper soul, time for the bigger explanation. Julia's `eval` function evaluates a
37+
function into a module at a specified world-age. If you evaluate a function within a function
38+
and try to call it from within that same function, you will hit a world-age error. This looks like:
39+
40+
```julia
41+
function worldageerror()
42+
f = eval(:((x) -> 2x))
43+
f(2)
44+
end
45+
```
46+
47+
```
48+
julia> worldageerror()
49+
ERROR: MethodError: no method matching (::var"#5#6")(::Int64)
50+
51+
Closest candidates are:
52+
(::var"#5#6")(::Any) (method too new to be called from this world context.)
53+
@ Main REPL[12]:2
54+
```
55+
56+
This is done for many reasons, in particular if the code that is called within a function could change
57+
at any time, then Julia functions could not ever properly optimize because the meaning of any function
58+
or dispatch could always change and you would lose performance by guarding against that. For a full
59+
discussion of world-age, see [this paper](https://arxiv.org/abs/2010.07516).
60+
61+
However, this would be greatly inhibiting to standard ModelingToolkit usage because then something as
62+
simple as building an ODEProblem in a function and then using it would get a world age error:
63+
64+
```julia
65+
function wouldworldage()
66+
prob = ODEProblem(sys, [], (0.0, 1.0))
67+
sol = solve(prob)
68+
end
69+
```
70+
71+
The reason is because `prob.f` would be constructed via `eval`, and thus `prob.f` could not be called
72+
in the function, which means that no solve could ever work in the same function that generated the
73+
problem. That does mean that:
74+
75+
```julia
76+
function wouldworldage()
77+
prob = ODEProblem(sys, [], (0.0, 1.0))
78+
end
79+
sol = solve(prob)
80+
```
81+
82+
is fine, or putting
83+
84+
```julia
85+
prob = ODEProblem(sys, [], (0.0, 1.0))
86+
sol = solve(prob)
87+
```
88+
89+
at the top level of a module is perfectly fine too. They just cannot happen in the same function.
90+
91+
This would be a major limitation to ModelingToolkit, and thus we developed
92+
[RuntimeGeneratedFunctions](https://github.com/SciML/RuntimeGeneratedFunctions.jl) to get around
93+
this limitation. It will not be described beyond that, it is dark art and should not be investigated.
94+
But it does the job. But that does mean that it plays... oddly with Julia's compilation.
95+
96+
There are ways to force RuntimeGeneratedFunctions to perform their evaluation and caching within
97+
a given module, but that is not recommended because it does not play nicely with Julia v1.9's
98+
introduction of package images for binary caching.
99+
100+
Thus when trying to make things work with precompilation, we recommend using `eval`. This is
101+
done by simply adding `eval_expression=true` to the problem constructor. However, this is not
102+
a silver bullet because the moment you start using eval, all potential world-age restrictions
103+
apply, and thus it is recommended this is simply used for evaluating at the top level of modules
104+
for the purpose of precompilation and ensuring binaries of your MTK functions are built correctly.
105+
106+
However, there is one caveat that `eval` in Julia works depending on the module that it is given.
107+
If you have `MyPackage` that you are precompiling into, or say you are using `juliac` or PackageCompiler
108+
or some other static ahead-of-time (AOT) Julia compiler, then you don't want to accidentally `eval`
109+
that function to live in ModelingToolkit and instead want to make sure it is `eval`'d to live in `MyPackage`
110+
(since otherwise it will not cache into the binary). ModelingToolkit cannot know that in advance, and thus
111+
you have to pass in the module you wish for the functions to "live" in. This is done via the `eval_module`
112+
argument.
113+
114+
Hence `ODEProblem(sys, u0, tspan, p, eval_module=@__MODULE__, eval_expression=true)` will work if you
115+
are running this expression in the scope of the module you wish to be precompiling. However, if you are
116+
attempting to AOT compile a different module, this means that `eval_module` needs to be appropriately
117+
chosen. And, because `eval_expression=true`, all caveats of world-age apply.

src/systems/diffeqs/abstractodesystem.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem,
378378
checkbounds = checkbounds, kwargs...)
379379
jac_oop, jac_iip = eval_expression ? eval_module.eval.(jac_gen) :
380380
(drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in jac_gen)
381-
381+
382382
_jac(u, p, t) = jac_oop(u, p, t)
383383
_jac(J, u, p, t) = jac_iip(J, u, p, t)
384384
_jac(u, p::Tuple{Vararg{Number}}, t) = jac_oop(u, p, t)
@@ -502,7 +502,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys)
502502
checkbounds = checkbounds, kwargs...)
503503
jac_oop, jac_iip = eval_expression ? eval_module.eval.(jac_gen) :
504504
(drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in jac_gen)
505-
505+
506506
_jac(du, u, p, ˍ₋gamma, t) = jac_oop(du, u, p, ˍ₋gamma, t)
507507
_jac(du, u, p::MTKParameters, ˍ₋gamma, t) = jac_oop(du, u, p..., ˍ₋gamma, t)
508508

src/systems/diffeqs/sdesystem.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ function DiffEqBase.SDEFunction{iip}(sys::SDESystem, dvs = unknowns(sys),
437437
kwargs...)
438438
tgrad_oop, tgrad_iip = eval_expression ? eval_module.eval.(tgrad_gen) :
439439
(drop_expr(@RuntimeGeneratedFunction(ex)) for ex in tgrad_gen)
440-
440+
441441
_tgrad(u, p, t) = tgrad_oop(u, p, t)
442442
_tgrad(u, p::MTKParameters, t) = tgrad_oop(u, p..., t)
443443
_tgrad(J, u, p, t) = tgrad_iip(J, u, p, t)
@@ -449,9 +449,9 @@ function DiffEqBase.SDEFunction{iip}(sys::SDESystem, dvs = unknowns(sys),
449449
if jac
450450
jac_gen = generate_jacobian(sys, dvs, ps; expression = Val{!eval_expression},
451451
sparse = sparse, kwargs...)
452-
jac_oop, jac_iip = eval_expression ? eval_module.eval.(jac_gen) :
452+
jac_oop, jac_iip = eval_expression ? eval_module.eval.(jac_gen) :
453453
(drop_expr(@RuntimeGeneratedFunction(ex)) for ex in jac_gen)
454-
454+
455455
_jac(u, p, t) = jac_oop(u, p, t)
456456
_jac(u, p::MTKParameters, t) = jac_oop(u, p..., t)
457457
_jac(J, u, p, t) = jac_iip(J, u, p, t)

0 commit comments

Comments
 (0)