|
| 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. |
0 commit comments