Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support PrecompileTools on nightly #47

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Support PrecompileTools on nightly #47

wants to merge 10 commits into from

Conversation

timholy
Copy link
Member

@timholy timholy commented Mar 18, 2025

xref JuliaLang/julia#57074

The old Compiler.Timings infrastructure is disabled, so here we
leverage the same newly_compiled infrastructure used during
precompilation.

CC @vtjnash. There's one failing test that needs some investigation.

timholy added 2 commits March 18, 2025 06:17
xref JuliaLang/julia#57074

The old `Compiler.Timings` infrastructure is disabled, so here we
leverage the same `newly_compiled` infrastructure used during
precompilation.
@timholy
Copy link
Member Author

timholy commented Mar 18, 2025

The failing test is a failure to precompile Tuple{typeof(findfirst), Base.Fix2{typeof(==), T}, Vector{T}} where T for PC_A.MyType. Not sure why the compiler doesn't specialize this?

@timholy
Copy link
Member Author

timholy commented Mar 18, 2025

Curiouser and curiouser. This diff:

diff --git a/src/PrecompileTools.jl b/src/PrecompileTools.jl
index c76a98b..9515aef 100644
--- a/src/PrecompileTools.jl
+++ b/src/PrecompileTools.jl
@@ -7,11 +7,17 @@ export @setup_workload, @compile_workload, @recompile_invalidations
 const verbose = Ref(false)    # if true, prints all the precompiles

 function precompile_mi(mi::Core.MethodInstance)
-    precompile(mi.specTypes) # TODO: Julia should allow one to pass `mi` directly (would handle `invoke` properly)
+    precompile(mi)
     verbose[] && println(mi)
     return
 end

+function precompile_ci(ci::Core.CodeInstance)
+    precompile(ci.def)
+    verbose[] && (println(ci.def); @show ci.precompile)
+    return
+end
+
 include("workloads.jl")
 include("invalidations.jl")

diff --git a/src/workloads.jl b/src/workloads.jl
index 0480b1b..b40b067 100644
--- a/src/workloads.jl
+++ b/src/workloads.jl
@@ -14,8 +14,7 @@ end

 function precompile_newly_inferred(cis)
     while !isempty(cis)
-        ci = pop!(cis)
-        precompile_mi(ci.def)
+        precompile_ci(pop!(cis))
     end
     return cis
 end
diff --git a/test/PC_A/src/PC_A.jl b/test/PC_A/src/PC_A.jl
index e524ab6..dde4396 100644
--- a/test/PC_A/src/PC_A.jl
+++ b/test/PC_A/src/PC_A.jl
@@ -1,6 +1,8 @@
 module PC_A

-using PrecompileTools: @setup_workload, @compile_workload
+using PrecompileTools: PrecompileTools, @setup_workload, @compile_workload
+
+PrecompileTools.verbose[] = true

 struct MyType
     x::Int

yields the following output:

[ Info: Precompiling PC_A [13c4d82c-fea6-47db-9cd5-35bd2686c3b0] (cache misses: wrong dep version loaded (20))
MethodInstance for Base.unsafe_convert(::Type{Any}, ::Nothing)
ci.precompile = false
MethodInstance for Base.throw_boundserror(::Vector{PC_A.MyType}, ::Tuple{Int64})
ci.precompile = true
MethodInstance for findnext(::Base.Fix2{typeof(==), PC_A.MyType}, ::Vector{PC_A.MyType}, ::Int64)
ci.precompile = true
MethodInstance for (::Base.Fix2{typeof(==), PC_A.MyType})(::PC_A.MyType)
ci.precompile = true
MethodInstance for Base.var"#_#57"(::Base.Pairs{Symbol, Union{}, Tuple{}, @NamedTuple{}}, ::Base.Fix2{typeof(==), PC_A.MyType}, ::PC_A.MyType)
ci.precompile = true
MethodInstance for ==(::PC_A.MyType, ::PC_A.MyType)
ci.precompile = true
MethodInstance for getproperty(::Base.Fix2{typeof(==), PC_A.MyType}, ::Symbol)
ci.precompile = true
MethodInstance for findfirst(::Function, ::Vector{PC_A.MyType})               <-------------- MI we want
ci.precompile = true
MethodInstance for findnext(::Function, ::Vector{PC_A.MyType}, ::Int64)
ci.precompile = true
MethodInstance for nextind(::Vector{PC_A.MyType}, ::Int64)
ci.precompile = true
MethodInstance for getindex(::Vector{PC_A.MyType}, ::Int64)
ci.precompile = true
MethodInstance for Base.throw_boundserror(::Vector{PC_A.MyType}, ::Tuple{Int64})
ci.precompile = false
MethodInstance for length(::Vector{PC_A.MyType})
ci.precompile = true
MethodInstance for keys(::Vector{PC_A.MyType})
ci.precompile = true
MethodInstance for LinearIndices(::Vector{PC_A.MyType})
ci.precompile = true
MethodInstance for axes(::Vector{PC_A.MyType})
ci.precompile = true
MethodInstance for size(::Vector{PC_A.MyType})
ci.precompile = true
MethodInstance for ==(::PC_A.MyType)
ci.precompile = true
MethodInstance for (Base.Fix2)(::typeof(==), ::PC_A.MyType)
ci.precompile = true
MethodInstance for Base._stable_typeof(::PC_A.MyType)
ci.precompile = false
MethodInstance for PC_A.call_findfirst(::PC_A.MyType, ::Vector{PC_A.MyType})
ci.precompile = true
MethodInstance for PC_A.inferencebarrier(::Vector{PC_A.MyType})
ci.precompile = true
MethodInstance for Base.inferencebarrier(::Vector{PC_A.MyType})
ci.precompile = true
MethodInstance for ==(::Any)
ci.precompile = true
MethodInstance for (Base.Fix2)(::typeof(==), ::Any)
ci.precompile = true
MethodInstance for PC_A.inferencebarrier(::PC_A.MyType)
ci.precompile = true
MethodInstance for Base.inferencebarrier(::PC_A.MyType)
ci.precompile = true
┌ Warning: Code in setup_workload block was precompiled
└ @ Main ~/.julia/dev/PrecompileTools/test/runtests.jl:18
PrecompileTools.jl: Test Failed at /home/tim/.julia/dev/PrecompileTools/test/runtests.jl:40
  Expression: count == 1
   Evaluated: 0 == 1

Stacktrace:
 [1] top-level scope
   @ ~/.julia/dev/PrecompileTools/test/runtests.jl:8
 [2] macro expansion
   @ ~/src/juliaw/usr/share/julia/stdlib/v1.13/Test/src/Test.jl:1771 [inlined]
 [3] macro expansion
   @ ~/.julia/dev/PrecompileTools/test/runtests.jl:40 [inlined]
 [4] macro expansion
   @ ~/src/juliaw/usr/share/julia/stdlib/v1.13/Test/src/Test.jl:679 [inlined]

So it doesn't seem to be cached despite having been tagged precompile=true.

@timholy
Copy link
Member Author

timholy commented Mar 18, 2025

timholy/PkgCacheInspector.jl#33 shows that the failure is in the caching step (that MethodInstance is not part of the cache).

Copy link

codecov bot commented Mar 18, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 80.55%. Comparing base (e11b408) to head (039cd9d).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #47      +/-   ##
==========================================
- Coverage   82.97%   80.55%   -2.43%     
==========================================
  Files           3        3              
  Lines          94       72      -22     
==========================================
- Hits           78       58      -20     
+ Misses         16       14       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timholy
Copy link
Member Author

timholy commented Mar 18, 2025

Am I right in thinking we need jl_get_newly_inferred? There can be multiple @compile_workload blocks in a package and they don't have to be last. So I think we need the logic

oldcache = steal_newly_inferred()
try
    <workload>
finally
    restore_newly_inferred(oldcache)
    precompile_newly_inferred()
end

where steal_newly_inferred() = ccall(jl_get_newly_inferred, Vector{CodeInstance}, ()).

@timholy
Copy link
Member Author

timholy commented Mar 18, 2025

Note that this is passing, however, so I think your fix worked. Thanks!

@vtjnash
Copy link
Member

vtjnash commented Mar 18, 2025

Am I right in thinking we need jl_get_newly_inferred? There can be multiple @compile_workload blocks in a package and they don't have to be last. So I think we need the logic

oldcache = steal_newly_inferred()
try
    <workload>
finally
    restore_newly_inferred(oldcache)
    precompile_newly_inferred()
end

where steal_newly_inferred() = ccall(jl_get_newly_inferred, Vector{CodeInstance}, ()).

There are some thread-safety problems with that, so I opted not to suggest that. Each block sets up its own newly_inferred state, so that is not particularly an issue, and we likely don't want to include the code in between compile_workload blocks either. It however does mean they wouldn't nest successfully (the first nested block will terminate tracking for all of the parents also), but I don't know if there's really anything to be done about that, unless you wanted to add a ScopedValue to neutralize the entire compile_workload block if it is already inside a compile_workload block?

@timholy
Copy link
Member Author

timholy commented Mar 18, 2025

I'm less worried about nesting than

@compile_workload begin
    workload1
end

const default = compute_defaults()

With this current design, we may lose external CodeInstances that get compiled during the call compute_defaults() that ordinarily would be cached (those that have edges to the package being precompiled).

@timholy
Copy link
Member Author

timholy commented Mar 18, 2025

Here's a thought: what if we define (in Base) a pair of "indicator" CodeInstances

function tag_all_inc()
    Base.Experimental.@force_compile
    return nothing
end
tag_all_inc()
const TAG_ALL_INC = which(tag_all_inc, ()).specializations.cache

and similarly for TAG_ALL_DEC. Then have PrecompileTools just push!(Base.newly_inferred, TAG_ALL_INC) to turn on exhaustive precompilation, and push!(Base.newly_inferred, TAG_ALL_DEC) to turn it off. staticdata.c/queue_external_cis could then keep a count (+1 for INC, -1 for DEC). When the count is positive, keep everything regardless of whether it has edges back to the worklist; when the count is zero, keep only those things with such edges.

@vtjnash
Copy link
Member

vtjnash commented Mar 18, 2025

If we're changing base, it might be easier to piggy back on the jl_force_trace_dispatch_enabled flag instead then, and use that to atomically set the precompile flag directly during dispatch, or during jl_push_newly_inferred to set the precompile flag then if the TAG_ALL_INC flag is non-zero (where this macro just manages the incrementing and decrementing of that ScopedValue)

timholy added a commit to JuliaLang/julia that referenced this pull request Mar 19, 2025
With the "classic" inference timing disabled, PrecompileTools and
SnoopCompile are non-functional on 1.12 & master. #57074 added timing
data to the CodeInstances themselves, which should help restore
SnoopCompile. However, without the tree structure provided by the old
inference timing system, we still need a way to tag MethodInstances that
get inferred inside `PrecompileTools.@compile_workload` blocks.

This adds a simple mechanism modeled after `@trace_compile` that
switching to tagging MethodInstances in `jl_push_newly_inferred`.
JuliaLang/PrecompileTools.jl#47 contains the
corresponding changes to PrecompileTools.jl.
@timholy
Copy link
Member Author

timholy commented Mar 19, 2025

Convenient link: JuliaLang/julia#57828

JeffBezanson pushed a commit to JuliaLang/julia that referenced this pull request Mar 20, 2025
With the "classic" inference timing disabled, PrecompileTools and
SnoopCompile are non-functional on 1.12 & master. #57074 added timing
data to the CodeInstances themselves, which should help restore
SnoopCompile. However, without the tree structure provided by the old
inference timing system, we still need a way to tag MethodInstances that
get inferred inside `PrecompileTools.@compile_workload` blocks.

This adds a simple mechanism modeled after `@trace_compile` that
switches to tagging MethodInstances in `jl_push_newly_inferred`.
JuliaLang/PrecompileTools.jl#47 contains (or
will contain) the corresponding changes to PrecompileTools.jl.
KristofferC pushed a commit to JuliaLang/julia that referenced this pull request Mar 20, 2025
With the "classic" inference timing disabled, PrecompileTools and
SnoopCompile are non-functional on 1.12 & master. #57074 added timing
data to the CodeInstances themselves, which should help restore
SnoopCompile. However, without the tree structure provided by the old
inference timing system, we still need a way to tag MethodInstances that
get inferred inside `PrecompileTools.@compile_workload` blocks.

This adds a simple mechanism modeled after `@trace_compile` that
switches to tagging MethodInstances in `jl_push_newly_inferred`.
JuliaLang/PrecompileTools.jl#47 contains (or
will contain) the corresponding changes to PrecompileTools.jl.

(cherry picked from commit c89b1ff)
@timholy timholy closed this Mar 21, 2025
@timholy timholy reopened this Mar 21, 2025
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.

2 participants