Skip to content

Precompilation in the SnoopPrecompile/Julia 1.9+ world #2226

Closed
@timholy

Description

@timholy

Because of its popularity, I'd like to use JuMP as one of several "showcases" for the impact of pkgimages in Julia 1.9 (CC @vchuravy, @vtjnash, @KristofferC). It turns out that to really showcase this work, JuMP might need a few tweaks. Normally I just submit PRs, but due to the "orthogonality" of solvers, JuMP presents some interesting challenges, and so I decided to instead open this issue.

JuMP and its ecosystem have had a lot of nice work done on invalidations and precompilation already, and these lay the foundation and make everything I'm about to show much easier. (Thanks!) The main remaining gap is due, I think, to the fact that the precompilation work occurred before the arrival of SnoopPrecompile and pkgimages in Julia 1.9.

Let me begin by showing that there's opportunity for substantial further improvement. All tests were conducted on a reasonably up-to-date Julia master:

julia> @time using JuMP, GLPK
  7.286941 seconds (9.35 M allocations: 604.153 MiB, 4.49% gc time, 0.79% compilation time)

julia> @time @eval begin
               let
                   model = Model(GLPK.Optimizer)
                   @variable(model, x >= 0)
                   @variable(model, 0 <= y <= 3)
                   @objective(model, Min, 12x + 20y)
                   @constraint(model, c1, 6x + 8y >= 100)
                   @constraint(model, c2, 7x + 12y >= 120)
                   optimize!(model)
               end
           end;
  8.465601 seconds (10.10 M allocations: 1.385 GiB, 4.05% gc time, 99.78% compilation time)

Now, let me create a new package, StartupJuMP, purely for the purpose of extra precompilation. (That's a viable strategy on Julia 1.8 and higher, with the big impact arriving in Julia 1.9.) The source code is a single file, src/StartupJuMP.jl, with contents:

module StartupJuMP

using GLPK
using JuMP
using SnoopPrecompile

@precompile_all_calls begin
    # Because lots of the work is done by macros, and macros are expanded
    # at lowering time, not much of this would get precompiled without `@eval`
    @eval begin
        let
            model = Model(GLPK.Optimizer)
            @variable(model, x >= 0)
            @variable(model, 0 <= y <= 3)
            @objective(model, Min, 12x + 20y)
            @constraint(model, c1, 6x + 8y >= 100)
            @constraint(model, c2, 7x + 12y >= 120)
            optimize!(model)
        end
    end
end

end # module StartupJuMP

Now:

julia> @time using JuMP, GLPK, StartupJuMP
  6.297161 seconds (9.80 M allocations: 630.934 MiB, 4.43% gc time, 0.35% compilation time)

julia> @time @eval begin
               let
                   model = Model(GLPK.Optimizer)
                   @variable(model, x >= 0)
                   @variable(model, 0 <= y <= 3)
                   @objective(model, Min, 12x + 20y)
                   @constraint(model, c1, 6x + 8y >= 100)
                   @constraint(model, c2, 7x + 12y >= 120)
                   optimize!(model)
               end
           end;
  0.331432 seconds (154.94 k allocations: 10.237 MiB, 97.93% compilation time)

You can see a decrease in load time, which TBH I find puzzling (I expected a modest increase). But more importantly, you see a massive decrease in time-to-first-execution (TTFX), to the point where TTFX just doesn't feel like a problem anymore. And keep in mind that this is on top of all the nice work the JuMP ecosystem has already done to minimize TTFX: this small SnoopPrecompile workload improves the quality of precompilation substantially.

Now, ordinarily I'd just suggest putting that @precompile_all_calls block in JuMP. However, a big issue is that this precompilation workload relies on GLPK, and I'm guessing you don't want to precompile GLPK "into" JuMP. I'm not sufficiently familiar with JuMP to pick a good alternative, but in very rough terms I imagine there are at least three potential paths, which might be used alone or in combination:

  • run this precompile workload with some kind of default/dummy solver, which doesn't actually perform optimization but at least allows completion (optimize! might return nothing, for example). This might (?) precompile a lot of the machinery.
  • identify the missing precompiles, and add them to the current precompile code. One issue, though, is that this seems a bit more fragile to internal Julia changes than using SnoopPrecompile. For instance, this precompile directive will cause precompilation failure on Julia 1.9, because Julia 1.9 will eliminate kwfuncs. The advantage of SnoopPrecompile is that you write everything in terms of the public interface and dispatch will generate the right precompiles on each different version of Julia.
  • use the upcoming weakdeps infrastructure to create the analog of my StartupJuMP for each solver. The advantage of weakdeps compared to similar solutions like Requires.jl is that the "extension packages" get precompiled and cached.

I'm happy to help, but given the issues I think it would be ideal if a JuMP developer helped choose the approach and shepherd the changes through.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions