Skip to content

Make generated code relocatable across Julia sessions - #878

Draft
maleadt wants to merge 1 commit into
mainfrom
tb/relocations
Draft

Make generated code relocatable across Julia sessions#878
maleadt wants to merge 1 commit into
mainfrom
tb/relocations

Conversation

@maleadt

@maleadt maleadt commented Jul 14, 2026

Copy link
Copy Markdown
Member

Keep Julia values and libjulia globals symbolic until final backend lowering, instead of embedding session-specific host addresses in cached IR.

Relocation records are deterministic and remain attached while modules are linked and optimized. A backend selects one of three delivery strategies:

  • :bake resolves addresses during final emission. This preserves compatibility but keeps results session-local.
  • :patch emits named writable globals for a loader such as CUDA or ORC to patch after loading.
  • :table rewrites references through a runtime table for loaders such as Metal that cannot look up data symbols.

Cached backend results persist only when Julia exposes relocatable global metadata and the backend selects :patch or :table. Resolved Julia values are permanently rooted, runtime bitcode remains symbolic until it reaches the final module, and manifests are frozen after lowering so metadata cannot diverge from emitted code.

The implementation is exercised by CUDA.jl#3200, Metal.jl#916, and JuliaLang/AllocCheck.jl#113. Backends that have not opted in retain the eager :bake behavior; this was validated with OpenCL.jl on both its SPIR-V and OpenCL C paths.

Supersedes #125 and #348.

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.29101% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.95%. Comparing base (3ee1499) to head (abc93a8).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/relocation.jl 95.70% 18 Missing ⚠️
src/validation.jl 86.36% 3 Missing ⚠️
src/metal.jl 98.42% 2 Missing ⚠️
src/driver.jl 96.15% 1 Missing ⚠️
src/interface.jl 96.00% 1 Missing ⚠️
src/utils.jl 94.44% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #878      +/-   ##
==========================================
+ Coverage   81.79%   83.95%   +2.16%     
==========================================
  Files          26       27       +1     
  Lines        4893     5365     +472     
==========================================
+ Hits         4002     4504     +502     
+ Misses        891      861      -30     

☔ View full report in Codecov by Harness.
📢 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.

Comment thread src/interface.jl Outdated
@maleadt
maleadt marked this pull request as ready for review July 15, 2026 20:34
@maleadt

maleadt commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Okay, this is starting to look good. IR should be fully relocatable after this, at least for back-ends supporting run-time relocations.

Comment thread src/optim.jl Outdated
Comment on lines 329 to 333
# The pass builder resolves this name to the registered instance, which
# captures the Relocations object owned by optimize!.
add!(mpm, "GPULinkRuntime")
add!(mpm, GPULinkLibrariesPass(job))
add!(mpm, GPUFinishRuntimeIntrinsicsPass(job))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we also do this for the job capturing?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You mean make them all by-name references?

Comment thread src/relocation.jl
Comment thread src/relocation.jl
Comment on lines +75 to +80
function resolve_relocation_target(target::JuliaValueRef)
box = Any[target.value]
GC.@preserve box begin
return unsafe_load(Base.unsafe_convert(Ptr{UInt}, pointer(box)))
end
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the previous version of this uses jl_value_ptr directly?

Why the 1-element array instead of a Ref?

We had something similar in Enzyme, but we restricted it to only types. https://github.com/EnzymeAD/Enzyme.jl/blob/4c8b1c0a04689cce931bd4ad8aa02f6a0919cecc/src/utils.jl#L1-L52

I think for all other cases one can use pointer_from_objref.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why the 1-element array instead of a Ref?

Fable is convinced that jl_value_ptr on 1.14 can allocate temporary stack boxes and return its address, while Any[] forces the contents to be heap-allocated too.

@noinline jvp(x)  = UInt(ccall(:jl_value_ptr, Ptr{Cvoid}, (Any,), x))
@noinline slot(x) = (s = Any[x]; GC.@preserve s unsafe_load(Base.unsafe_convert(Ptr{UInt}, pointer(s))))

tag(a) = unsafe_load(Ptr{UInt}(a - 8)) & ~UInt(15)
const F64_TAG = jvp(Float64)                  # heap-interned DataType: fine either way
@noinline clobber(n) = n == 0 ? UInt(0) : clobber(n - 1) + hash(n)

function demo()
    for (name, a) in ("jl_value_ptr" => jvp(1.25), "Any[] slot" => slot(1.25))
        clobber(200)  # reuse the stack region the callee frame occupied
        println(rpad(name, 13), ": contents = ", rpad(unsafe_load(Ptr{Float64}(a)), 23),
                " valid type tag = ", tag(a) == F64_TAG)
    end
end
demo()
❯ jl +nightly mwe.jl
jl_value_ptr : contents = 6.92391611475455e-310   valid type tag = false
Any[] slot   : contents = 1.25                    valid type tag = true

Comment thread src/relocation.jl Outdated

@vchuravy vchuravy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thought is around GC safety. Julia currently throws everything that is reachable from a native compilation into a global roots list (this has changed over time, I think 1.10 still had per CodeInstance rooting) so the thing I am wondering about is: Could we lose a root by going through this?

@maleadt

maleadt commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

Another suggestion by @vchuravy: Try this out on AllocCheck.jl

@maleadt

maleadt commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

AllocCheck.jl integration: JuliaLang/AllocCheck.jl#113
Uses a new relocation strategy, :defer, since AllocCheck.jl really does its own linking. This will be similar to what Enzyme.jl will have to do if it wants to benefit from this. I left the :import strategy in place, with an example user in the tests.

@vchuravy I think this is ready for another look.

@maleadt
maleadt force-pushed the tb/relocations branch 2 times, most recently from 16fed03 to 44c1e41 Compare July 23, 2026 17:30
@maleadt

maleadt commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

One thought is around GC safety. Julia currently throws everything that is reachable from a native compilation into a global roots list (this has changed over time, I think 1.10 still had per CodeInstance rooting) so the thing I am wondering about is: Could we lose a root by going through this?

I don't think so. compile_method_instance reads the GV table after jl_create_native returns, i.e. after root promotion. So every object wrapped in a JuliaValueRef is already permanently rooted, no?

EDIT: although I guess we could just make sure to perma-root, and then get rid of the roots mechanism in here? 86d4dac

@maleadt

maleadt commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Turns out Metal.jl doesn't support patching relocations like we do in CUDA (no support for something CuGlobal like), so I'm investigating a new strategy where we have a GOT-like table that's passed as part of the KernelState.

@maleadt
maleadt marked this pull request as draft July 27, 2026 08:17
@maleadt
maleadt force-pushed the tb/relocations branch 2 times, most recently from 29e310a to 8b4a7dd Compare August 15, 2026 18:18
@maleadt maleadt changed the title Make GPUCompiler IR relocatable across Julia sessions Make generated code relocatable across Julia sessions Aug 15, 2026
Represent Julia values and runtime globals as a deterministic relocation manifest instead of embedding host addresses in cached IR. Back-ends can bake the manifest for compatibility, expose writable globals for loader patching, or read a table supplied at execution time.

Only persist compilation results when both Julia and the selected lowering make them session-independent. Keep runtime-library linking symbolic until final emission, permanently root resolved Julia values, and treat zero-sized singleton values as addressable relocation targets.

Exercise the three lowering strategies, linking and validation failures, cache persistence, PTX, SPIR-V, Metal-style table delivery, boxed constants, empty type objects, and interior relocation sites.
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