Skip to content

Commit 959b6ef

Browse files
serenity4KristofferC
authored andcommitted
Support adding CodeInstances to JIT for interpreters defining a codegen cache (#57272)
Implements a way to add `CodeInstance`s compiled by external interpreters to JIT, such that they become legal targets for `invoke` calls. Based on a design proposed by @Keno, the `AbstractInterpreter` interface is extended to support providing a codegen cache that is filled during inference for future use with `add_codeinsts_to_jit!`. This allows `invoke(f, ::CodeInstance, args...)` to work on external interpreters, which is currently failing on `master` (see #57193). --------- Co-authored-by: Cédric Belmant <cedric.belmant@juliahub.com> (cherry picked from commit 9d2e9ed)
1 parent 0e4310c commit 959b6ef

File tree

4 files changed

+86
-37
lines changed

4 files changed

+86
-37
lines changed

Compiler/extras/CompilerDevTools/src/CompilerDevTools.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ struct SplitCacheInterp <: Compiler.AbstractInterpreter
99
inf_params::Compiler.InferenceParams
1010
opt_params::Compiler.OptimizationParams
1111
inf_cache::Vector{Compiler.InferenceResult}
12+
codegen_cache::IdDict{CodeInstance,CodeInfo}
1213
function SplitCacheInterp(;
1314
world::UInt = Base.get_world_counter(),
1415
inf_params::Compiler.InferenceParams = Compiler.InferenceParams(),
1516
opt_params::Compiler.OptimizationParams = Compiler.OptimizationParams(),
1617
inf_cache::Vector{Compiler.InferenceResult} = Compiler.InferenceResult[])
17-
new(world, inf_params, opt_params, inf_cache)
18+
new(world, inf_params, opt_params, inf_cache, IdDict{CodeInstance,CodeInfo}())
1819
end
1920
end
2021

@@ -23,10 +24,11 @@ Compiler.OptimizationParams(interp::SplitCacheInterp) = interp.opt_params
2324
Compiler.get_inference_world(interp::SplitCacheInterp) = interp.world
2425
Compiler.get_inference_cache(interp::SplitCacheInterp) = interp.inf_cache
2526
Compiler.cache_owner(::SplitCacheInterp) = SplitCacheOwner()
27+
Compiler.codegen_cache(interp::SplitCacheInterp) = interp.codegen_cache
2628

2729
import Core.OptimizedGenerics.CompilerPlugins: typeinf, typeinf_edge
2830
@eval @noinline typeinf(::SplitCacheOwner, mi::MethodInstance, source_mode::UInt8) =
29-
Base.invoke_in_world(which(typeinf, Tuple{SplitCacheOwner, MethodInstance, UInt8}).primary_world, Compiler.typeinf_ext, SplitCacheInterp(; world=Base.tls_world_age()), mi, source_mode)
31+
Base.invoke_in_world(which(typeinf, Tuple{SplitCacheOwner, MethodInstance, UInt8}).primary_world, Compiler.typeinf_ext_toplevel, SplitCacheInterp(; world=Base.tls_world_age()), mi, source_mode)
3032

3133
@eval @noinline function typeinf_edge(::SplitCacheOwner, mi::MethodInstance, parent_frame::Compiler.InferenceState, world::UInt, source_mode::UInt8)
3234
# TODO: This isn't quite right, we're just sketching things for now

Compiler/src/typeinfer.jl

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,10 @@ function finish!(interp::AbstractInterpreter, caller::InferenceState, validation
144144
ci, inferred_result, const_flag, first(result.valid_worlds), last(result.valid_worlds), encode_effects(result.ipo_effects),
145145
result.analysis_results, time_total, caller.time_caches, time_self_ns * 1e-9, di, edges)
146146
engine_reject(interp, ci)
147-
if !discard_src && isdefined(interp, :codegen) && uncompressed isa CodeInfo
147+
codegen = codegen_cache(interp)
148+
if !discard_src && codegen !== nothing && uncompressed isa CodeInfo
148149
# record that the caller could use this result to generate code when required, if desired, to avoid repeating n^2 work
149-
interp.codegen[ci] = uncompressed
150+
codegen[ci] = uncompressed
150151
if bootstrapping_compiler && inferred_result == nothing
151152
# This is necessary to get decent bootstrapping performance
152153
# when compiling the compiler to inject everything eagerly
@@ -186,8 +187,9 @@ function finish!(interp::AbstractInterpreter, mi::MethodInstance, ci::CodeInstan
186187
ccall(:jl_update_codeinst, Cvoid, (Any, Any, Int32, UInt, UInt, UInt32, Any, Float64, Float64, Float64, Any, Any),
187188
ci, nothing, const_flag, min_world, max_world, ipo_effects, nothing, 0.0, 0.0, 0.0, di, edges)
188189
code_cache(interp)[mi] = ci
189-
if isdefined(interp, :codegen)
190-
interp.codegen[ci] = src
190+
codegen = codegen_cache(interp)
191+
if codegen !== nothing
192+
codegen[ci] = src
191193
end
192194
engine_reject(interp, ci)
193195
return nothing
@@ -1195,7 +1197,10 @@ function typeinf_ext(interp::AbstractInterpreter, mi::MethodInstance, source_mod
11951197

11961198
ci = result.ci # reload from result in case it changed
11971199
@assert frame.cache_mode != CACHE_MODE_NULL
1198-
@assert is_result_constabi_eligible(result) || (!isdefined(interp, :codegen) || haskey(interp.codegen, ci))
1200+
@assert is_result_constabi_eligible(result) || begin
1201+
codegen = codegen_cache(interp)
1202+
codegen === nothing || haskey(codegen, ci)
1203+
end
11991204
@assert is_result_constabi_eligible(result) == use_const_api(ci)
12001205
@assert isdefined(ci, :inferred) "interpreter did not fulfill our expectations"
12011206
if !is_cached(frame) && source_mode == SOURCE_MODE_ABI
@@ -1261,44 +1266,55 @@ function collectinvokes!(wq::Vector{CodeInstance}, ci::CodeInfo)
12611266
end
12621267
end
12631268

1264-
# This is a bridge for the C code calling `jl_typeinf_func()` on a single Method match
1265-
function typeinf_ext_toplevel(mi::MethodInstance, world::UInt, source_mode::UInt8)
1266-
interp = NativeInterpreter(world)
1267-
ci = typeinf_ext(interp, mi, source_mode)
1268-
if source_mode == SOURCE_MODE_ABI && ci isa CodeInstance && !ci_has_invoke(ci)
1269-
inspected = IdSet{CodeInstance}()
1270-
tocompile = Vector{CodeInstance}()
1271-
push!(tocompile, ci)
1272-
while !isempty(tocompile)
1273-
# ci_has_real_invoke(ci) && return ci # optimization: cease looping if ci happens to get compiled (not just jl_fptr_wait_for_compiled, but fully jl_is_compiled_codeinst)
1274-
callee = pop!(tocompile)
1275-
ci_has_invoke(callee) && continue
1276-
callee in inspected && continue
1277-
src = get(interp.codegen, callee, nothing)
1269+
function add_codeinsts_to_jit!(interp::AbstractInterpreter, ci, source_mode::UInt8)
1270+
source_mode == SOURCE_MODE_ABI || return ci
1271+
ci isa CodeInstance && !ci_has_invoke(ci) || return ci
1272+
codegen = codegen_cache(interp)
1273+
codegen === nothing && return ci
1274+
inspected = IdSet{CodeInstance}()
1275+
tocompile = Vector{CodeInstance}()
1276+
push!(tocompile, ci)
1277+
while !isempty(tocompile)
1278+
# ci_has_real_invoke(ci) && return ci # optimization: cease looping if ci happens to get compiled (not just jl_fptr_wait_for_compiled, but fully jl_is_compiled_codeinst)
1279+
callee = pop!(tocompile)
1280+
ci_has_invoke(callee) && continue
1281+
callee in inspected && continue
1282+
src = get(codegen, callee, nothing)
1283+
if !isa(src, CodeInfo)
1284+
src = @atomic :monotonic callee.inferred
1285+
if isa(src, String)
1286+
src = _uncompressed_ir(callee, src)
1287+
end
12781288
if !isa(src, CodeInfo)
1279-
src = @atomic :monotonic callee.inferred
1280-
if isa(src, String)
1281-
src = _uncompressed_ir(callee, src)
1282-
end
1283-
if !isa(src, CodeInfo)
1284-
newcallee = typeinf_ext(interp, callee.def, source_mode)
1285-
if newcallee isa CodeInstance
1286-
callee === ci && (ci = newcallee) # ci stopped meeting the requirements after typeinf_ext last checked, try again with newcallee
1287-
push!(tocompile, newcallee)
1288-
#else
1289-
# println("warning: could not get source code for ", callee.def)
1290-
end
1291-
continue
1289+
newcallee = typeinf_ext(interp, callee.def, source_mode)
1290+
if newcallee isa CodeInstance
1291+
callee === ci && (ci = newcallee) # ci stopped meeting the requirements after typeinf_ext last checked, try again with newcallee
1292+
push!(tocompile, newcallee)
1293+
#else
1294+
# println("warning: could not get source code for ", callee.def)
12921295
end
1296+
continue
12931297
end
1294-
push!(inspected, callee)
1295-
collectinvokes!(tocompile, src)
1296-
ccall(:jl_add_codeinst_to_jit, Cvoid, (Any, Any), callee, src)
12971298
end
1299+
push!(inspected, callee)
1300+
collectinvokes!(tocompile, src)
1301+
ccall(:jl_add_codeinst_to_jit, Cvoid, (Any, Any), callee, src)
12981302
end
12991303
return ci
13001304
end
13011305

1306+
function typeinf_ext_toplevel(interp::AbstractInterpreter, mi::MethodInstance, source_mode::UInt8)
1307+
ci = typeinf_ext(interp, mi, source_mode)
1308+
ci = add_codeinsts_to_jit!(interp, ci, source_mode)
1309+
return ci
1310+
end
1311+
1312+
# This is a bridge for the C code calling `jl_typeinf_func()` on a single Method match
1313+
function typeinf_ext_toplevel(mi::MethodInstance, world::UInt, source_mode::UInt8)
1314+
interp = NativeInterpreter(world)
1315+
return typeinf_ext_toplevel(interp, mi, source_mode)
1316+
end
1317+
13021318
# This is a bridge for the C code calling `jl_typeinf_func()` on set of Method matches
13031319
# The trim_mode can be any of:
13041320
const TRIM_NO = 0

Compiler/src/types.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ the following methods to satisfy the `AbstractInterpreter` API requirement:
2323
- `get_inference_world(interp::NewInterpreter)` - return the world age for this interpreter
2424
- `get_inference_cache(interp::NewInterpreter)` - return the local inference cache
2525
- `cache_owner(interp::NewInterpreter)` - return the owner of any new cache entries
26+
27+
If `CodeInstance`s compiled using `interp::NewInterpreter` are meant to be executed with `invoke`,
28+
a method `codegen_cache(interp::NewInterpreter) -> IdDict{CodeInstance, CodeInfo}` must be defined,
29+
and inference must be triggered via `typeinf_ext_toplevel` with source mode `SOURCE_MODE_ABI`.
2630
"""
2731
abstract type AbstractInterpreter end
2832

@@ -446,6 +450,19 @@ to incorporate customized dispatches for the overridden methods.
446450
method_table(interp::AbstractInterpreter) = InternalMethodTable(get_inference_world(interp))
447451
method_table(interp::NativeInterpreter) = interp.method_table
448452

453+
"""
454+
codegen_cache(interp::AbstractInterpreter) -> Union{Nothing, IdDict{CodeInstance, CodeInfo}}
455+
456+
Optionally return a cache associating a `CodeInfo` to a `CodeInstance` that should be added to the JIT
457+
for future execution via `invoke(f, ::CodeInstance, args...)`. This cache is used during `typeinf_ext_toplevel`,
458+
and may be safely discarded between calls to this function.
459+
460+
By default, a value of `nothing` is returned indicating that `CodeInstance`s should not be added to the JIT.
461+
Attempting to execute them via `invoke` will result in an error.
462+
"""
463+
codegen_cache(interp::AbstractInterpreter) = nothing
464+
codegen_cache(interp::NativeInterpreter) = interp.codegen
465+
449466
"""
450467
By default `AbstractInterpreter` implements the following inference bail out logic:
451468
- `bail_out_toplevel_call(::AbstractInterpreter, sig, ::InferenceState)`: bail out from

Compiler/test/AbstractInterpreter.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,3 +534,17 @@ let interp = DebugInterp()
534534
end
535535
@test found
536536
end
537+
538+
@newinterp InvokeInterp
539+
struct InvokeOwner end
540+
codegen = IdDict{CodeInstance, CodeInfo}()
541+
Compiler.cache_owner(::InvokeInterp) = InvokeOwner()
542+
Compiler.codegen_cache(::InvokeInterp) = codegen
543+
let interp = InvokeInterp()
544+
source_mode = Compiler.SOURCE_MODE_ABI
545+
f = (+)
546+
args = (1, 1)
547+
mi = @ccall jl_method_lookup(Any[f, args...]::Ptr{Any}, (1+length(args))::Csize_t, Base.tls_world_age()::Csize_t)::Ref{Core.MethodInstance}
548+
ci = Compiler.typeinf_ext_toplevel(interp, mi, source_mode)
549+
@test invoke(f, ci, args...) == 2
550+
end

0 commit comments

Comments
 (0)