Skip to content

Conversation

@maleadt
Copy link
Member

@maleadt maleadt commented Feb 4, 2026

Non-native compilers that use the AbstractInterpreter framework create CodeInstances with a non-nothing owner field to partition their compilation results from the native code cache. Since #54894, queue_external_cis filters CIs with ci->owner == jl_nothing, which sometimes means non-native CIs for external methods are silently dropped during serialization. @vchuravy This may explain the breakage seen in GPUCompiler.jl/CompilerCaching.jl for the test you added? I don't fully understand the whole mechanism for discovering/caching CIs though.

This PR adds an explicit Base.precompile(ci::CodeInstance) API to registers CIs into a separate newly_inferred_external array. During jl_create_system_image, these CIs are merged directly into new_ext_cis after the filtering step, bypassing the owner check entirely.

IIUC This would also be useful for compiling non-native workloads with JuliaC.jl, as a simpler alternative to invoke_in_cache or invoke_with_absint.

Corresponding CompilerCaching.jl PR: maleadt/CompilerCaching.jl#3

cc @gbaraldi This resembles what we discussed yesterday.
cc @vchuravy You've worked on this before
cc @vtjnash Having authored #54894

…iler caching

Non-native compilers (e.g. GPU compilers) that use the AbstractInterpreter
framework create CodeInstances with a non-nothing `owner` field to partition
their compilation results from the native code cache. Since v1.12.0-DEV.1268,
`queue_external_cis` filters CIs with `ci->owner == jl_nothing`, which means
non-native CIs for external methods (methods not defined in the package being
precompiled, e.g. `Base.identity`) are silently dropped during serialization.

CIs for internal methods (defined within the package) survive regardless because
the MethodInstance cache is serialized with the module. However, external method
CIs must pass through the `new_ext_cis` pathway, where the owner filter rejects
them. Simply pushing CIs into `newly_inferred` via `jl_push_newly_inferred` does
not help, since that array feeds into `queue_external_cis` which applies the same
owner filter.

This adds a `Base.precompile(ci::CodeInstance)` method that registers CIs into a
separate `newly_inferred_external` array. During `jl_create_system_image`, these
CIs are merged directly into `new_ext_cis` after the filtering step, bypassing
the owner check entirely. This provides an explicit opt-in API for non-native
compilers to persist their external-method CIs into the package image.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@maleadt maleadt marked this pull request as draft February 4, 2026 15:40
@gbaraldi gbaraldi requested a review from topolarity February 5, 2026 12:19
@vtjnash
Copy link
Member

vtjnash commented Feb 5, 2026

Seems okay, though I don't see why you needed a separate array for this

@maleadt
Copy link
Member Author

maleadt commented Feb 5, 2026

Valentin pointed me to #60747, which I hadn't seen and I presume would fix this without any intervention. However, for JuliaC we would probably still need something like this, for now.

@vchuravy
Copy link
Member

vchuravy commented Feb 9, 2026

Terminology:

  • Foreign CodeInstance: Owned by not the native interpreter ci->native !== nothing
  • External CodeInstance: Inference result that is newly inferred during an incremental precompilation, and belongs to an already existing MethodInstance.

It used to be the case that pushing foreign & external CIs into newly_inferred was enough to serialize those cache entries into a package image.

Now, we do a dance of going from newly_inferred CIs to MIs in

# Compute new_ext_cis using queue_external_cis with global newly_inferred
new_ext_cis = ccall(:jl_compute_new_ext_cis, Any, ())
if new_ext_cis !== nothing
for i in 1:length(new_ext_cis::Vector{Any})
ci = new_ext_cis[i]::CodeInstance
enqueue_specialization!(all, specialization_worklist, get_ci_mi(ci))
end
end
end
enqueue_specializations!(all, newmethods, specialization_worklist)
# Process the specialization worklist and prepare final tocompile worklist
tocompile = []
for item in specialization_worklist
if isa(item, Core.MethodInstance)
processed_mi = process_method_instance_for_compilation(item, latestworld)
if processed_mi !== nothing
push!(tocompile, processed_mi)
end
else
# Handle SimpleVector (ccallable entries)
push!(tocompile, item::Core.SimpleVector)
end
end
and then later we go back from MIs to CIs

julia/src/staticdata.c

Lines 3400 to 3421 in 4fe5312

// Save the inferred code from newly inferred, external methods
if (native_functions) {
arraylist_t CIs;
arraylist_new(&CIs, 0);
size_t num_cis;
jl_get_llvm_cis(native_functions, &num_cis, NULL);
arraylist_grow(&CIs, num_cis);
jl_get_llvm_cis(native_functions, &num_cis, (jl_code_instance_t**)CIs.items);
// Create a filtered list of the compiled code instances that are
// possibly not referenced via any other way but valid for the
// Method cache field of an external method
new_ext_cis = jl_alloc_vec_any(0);
for (size_t i = 0; i < num_cis; i++) {
jl_code_instance_t *ci = (jl_code_instance_t*)CIs.items[i];
if (ci_not_internal_cache(ci))
jl_array_ptr_1d_push(new_ext_cis, (jl_value_t*)ci);
}
arraylist_free(&CIs);
}
else {
new_ext_cis = jl_compute_new_ext_cis();
}

Now the crux is that when we go from CI to MI we naturally lose the owner, and then we are asking the question, "is this CI the newest one for the native interpreter", later the only way we can recreate CI is by using the native owner and thus we drop foreign & external CIs.

Copy link
Member

@vchuravy vchuravy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking we can avoid a precompile(ci) and a new global array.

In

for i in 1:length(new_ext_cis::Vector{Any})
ci = new_ext_cis[i]::CodeInstance
enqueue_specialization!(all, specialization_worklist, get_ci_mi(ci))
end
we can filter the external & foreign CIs and then pass them separately to jl_create_system_image

@vchuravy
Copy link
Member

#60747 now takes part of this approach, but removes the requirement of having a new API

@vchuravy vchuravy closed this Feb 10, 2026
@DilumAluthge DilumAluthge deleted the tb/precompile_ci branch February 10, 2026 22:41
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