Skip to content

Conversation

@timholy
Copy link
Contributor

@timholy timholy commented Jul 12, 2025

This package makes heavy use of polyalgorithms. This provides a lot of flexibility and performance, but one downside is that it hinders inferrability. The lack of inferrability does not seem to cause serious performance issues, but it does stand in the way of being able to precompile the package via direct-chain-of-inference. (See #643 for issues that prevent usage of PrecompileTools.jl, which otherwise would be an easy solution because it circumvents the need for high-quality inference.)

The goal of this PR is to make at least a subset of the solve pipeline (specifically, the part I'm using!) inferrable. It targets the following usage:

    solver, starts = solver_startsolutions(S, sols; start_parameters, target_parameters, ishomogeneous=false)
    Ssolve = solve(solver, starts; show_progress=false)

where S is a CompiledSystem. The eventual goal is that if users write their code this way, the entire pipeline can inferred and thus precompiled.

To get closer to this goal, this PR adds:

  • the ishomogeneous keyword to bypass a non-inferrable branch in parameter_homotopy
  • several Base.@constprop :aggressive annotations to help the compiler optimize branches that depend on keyword arguments

This does not put it over the finish line , but it is enough to let me use PrecompileTools in my own package, and that's really good news. (UPDATE: I was a bit hasty, the compiled IDs would need to populate TSYSTEM_TABLE and currently I don't have that working.)

If you're curious about the current point of failure in inferrability, it's

if m > 25 && optimize_data_structure

in the construction of the MatrixWorkspace for the Jacobian. It would seem that the best option is to allow the user to supply their own MatrixWorkspace to Jacobian and TrackerState, but that's starting to look pretty intrusive. So I decided to stop here and gauge thoughts from the maintainers.

Note: there's no urgency in merging this, best to wait until it gets a little more complete.

This package makes heavy use of polyalgorithms. This provides a lot of
flexibility and performance, but one downside is that it hinders
inferrability. The lack of inferrability does not seem to cause serious
performance issues, but it does stand in the way of being able to
precompile the package via direct-chain-of-inference. (See JuliaHomotopyContinuation#643 for
issues that prevent usage of PrecompileTools.jl, which otherwise would
be an easy solution because it circumvents the need for high-quality
inference.)

The goal of this PR is to make at least a subset of the `solve` pipeline
(specifically, the part I'm using!) inferrable. It targets the following
usage:

```julia
    solver, starts = solver_startsolutions(S, sols; start_parameters, target_parameters, ishomogeneous=false)
    Ssolve = solve(solver, starts; show_progress=false)
```

where `S` is a `CompiledSystem`. The eventual goal is that if users
write their code this way, the entire pipeline can inferred and thus
precompiled.

To get closer to this goal, this PR adds:

- the `ishomogeneous` keyword to bypass a non-inferrable branch in
  `parameter_homotopy`
- several `Base.@constprop :aggressive` annotations to help the compiler
  optimize branches that depend on keyword arguments
@timholy timholy marked this pull request as draft July 13, 2025 08:19
@PBrdng
Copy link
Collaborator

PBrdng commented Jul 16, 2025

Interesting. Thanks for the initiative.

One question: According to the docs Base.@constprop :aggressive can yield improved inference results at the cost of additional compile time. Isn't this contradicting the goal of reducing compile time?

@oameye
Copy link
Contributor

oameye commented Jul 16, 2025

I think we want to improve time-to-first-execute (TTFX)? We also suffer a lot from this in HarmonicBalance.jl

@timholy
Copy link
Contributor Author

timholy commented Jul 16, 2025

Yes to both. Here's the issue: if you can rely on @compile_workload from PrecompileTools, you can perform successful precompilation of specific methods even with poor-quality inference. However, with this package, complexities surrounding the (re)initialization of libsymengine defeated my attempts at this strategy (#643).

So the alternative is to use precompile(f, types). This does not actually execute the code, it only compiles it, and thus libsymengine only gets initialized in the user's process and not in the precompilation process. It will also compile the inferrable callees of f for those types. However, it does not track runtime dispatch. You can't plausibly do that without actually running the code.

So this PR begins the effort to try to make solve inferrable "all the way down." That should allow consumer packages to use precompile(f, types) for their methods, although HomotopyContinuation could exploit it on a few "exemplar" problems itself to ensure that many of its internal methods are already precompiled. The consequence of Base.@constprop :aggressive is indeed longer precompilation times, but since you precompile a package "once" and run it many times, that can be a win for everyone except the developers themselves (who tend to recompile frequently). However, developers can locally disable precompilation in their development directory using a Preferences.jl-based trick.

Now, keep in mind that if we could solve the issues surrounding reinitialization of libsymengine, it would be considerably easier to go back to #643.

Does that help clarify the goals of this PR?

@timholy
Copy link
Contributor Author

timholy commented Jul 16, 2025

BTW @PBrdng, if you're not familiar with the consequences of precompilation, my advice is to try this:

tim@hypnotic:/tmp$ mkdir pctest
tim@hypnotic:/tmp$ cd pctest
tim@hypnotic:/tmp/pctest$ julia -q --startup-file=no
julia> using Pkg; Pkg.activate("."); Pkg.add("GLMakie")
 
 
julia> using GLMakie; @time @eval(display(lines(randn(10))))
  6.904183 seconds (273.82 k allocations: 24.814 MiB, 6.94% compilation time)
GLMakie.Screen(...)

and then see what happens when you disable usage of pkgimages:

tim@hypnotic:/tmp/pctest$ julia --project -q --startup-file=no --pkgimages=no
julia> using GLMakie; @time @eval(display(lines(randn(10))))
 38.212825 seconds (1.84 M allocations: 87.696 MiB, 0.82% gc time, 82.30% compilation time)
GLMakie.Screen(...)

It's a big reduction in TTFX, thanks to the fact that the Makie developers have put some effort into their precompilation pipeline (e.g., https://github.com/MakieOrg/Makie.jl/blob/master/Makie/src/precompiles.jl). They clearly had some similar issues to those encountered here, since they use a mix of @compile_workload and explicit precompile statements.

The goal here is to confer some of the same benefits to packages that use HomotopyContinuation.

@PBrdng
Copy link
Collaborator

PBrdng commented Jul 17, 2025

@timholy Indeed, I'm not familiar with precompilation and all that. But I'm interested to make it work. Would you have time for a meeting at some point this summer to explain it to me?

@timholy
Copy link
Contributor Author

timholy commented Jul 17, 2025

Yes, happy to do that. I'm at JuliaCon next week, any chance you'll be there?

If not, sometime in August we could get together.

Meanwhile, check the release announcement for Julia 1.9: https://julialang.org/blog/2023/04/julia-1.9-highlights/#caching_of_native_code

@PBrdng
Copy link
Collaborator

PBrdng commented Jul 19, 2025

I will not be at JuliaCon. Our community has almost no intersection to the JuliaCon community (unfortunately!). Let's talk in august.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants