Skip to content

Make GPUCompiler IR relocatable across Julia sessions#878

Open
maleadt wants to merge 44 commits into
mainfrom
tb/relocations
Open

Make GPUCompiler IR relocatable across Julia sessions#878
maleadt wants to merge 44 commits into
mainfrom
tb/relocations

Conversation

@maleadt

@maleadt maleadt commented Jul 14, 2026

Copy link
Copy Markdown
Member

Keep Julia values and libjulia runtime globals as symbolic relocations until final backend lowering, instead of persisting session-specific host addresses in IR or cached code. Relocations can target Julia object identities or named runtime globals, through either dedicated pointer-sized slots or words embedded in device-side object representations.

Relocation metadata remains associated with the IR through linking and optimization, with stable, collision-safe slot names. Backends may preserve relocations for their loader to resolve and patch at runtime, while the default lowering eagerly embeds addresses from the current Julia session.

CUDA.jl uses this in JuliaGPU/CUDA.jl#3200 to materialize relocations as CuGlobal slots populated at load time, allowing generated PTX to be cached across Julia sessions. Other backends retain the existing eager behavior.

Supersedes #125 and #348

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.93281% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.13%. Comparing base (4a4be52) to head (19091ac).

Files with missing lines Patch % Lines
src/jlgen.jl 84.34% 18 Missing ⚠️
src/validation.jl 81.57% 7 Missing ⚠️
src/interface.jl 85.71% 2 Missing ⚠️
src/mcgen.jl 97.14% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #878      +/-   ##
==========================================
+ Coverage   81.75%   82.13%   +0.37%     
==========================================
  Files          26       26              
  Lines        4879     5032     +153     
==========================================
+ Hits         3989     4133     +144     
- Misses        890      899       +9     

☔ 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 and others added 21 commits July 23, 2026 10:16
Add a `relocation_lowering(job)` trait choosing `:bake` (default), `:patch`,
or `:import`. `:bake` resolves host references into the IR eagerly during
`emit_llvm` (where `relocate_gvs!` used to run), so the `:llvm` result is
execution-ready and back-ends that drive `emit_asm(job, ir, format)` directly
(Metal.jl) keep working; a restored three-argument `emit_asm` forwards to the
metadata-carrying method. Non-baking strategies keep references symbolic for
their loader and lower them at object emission.

`lower_relocations!` becomes an internal dispatcher on the trait, subsuming the
per-back-end override; the previously test-local import lowering moves in as
`emit_imported_relocations!`. Also add `Base.copy(::Relocations)` for loaders
that bake from cached metadata.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `library::Union{Nothing,String}` field to `CGlobalRef`. The default
`nothing` keeps the current behavior (libjulia global via `jl_cglobal`'s
process-wide lookup); an explicit library is loaded and searched with Libdl.
Both fields are plain data, so serialized relocation metadata stays portable.

Drop the `String(symbol)` conversion in resolution: `jl_cglobal` accepts the
symbol directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only GPULinkRuntime is added by name (to reuse the instance holding the
Relocations owned by optimize!); the sibling passes are stateless and
constructed fresh. Spell that out at the call site.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Whether relocations are exposed symbolically is the runtime capability
`supports_relocatable_ir()` (jl_get_llvm_gvs_globals was backported to 1.11/1.12
point releases), not a fixed version. Replace the `VERSION >=` gates in the
relocation tests with the probe.

The tests that observe symbolic slots surviving to `:llvm` now compile with a
non-baking back-end (native `jit=true`, a new PTX `patch=true`), since a baking
job resolves and folds those slots away before the output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add coverage for the paths the strategy trait introduces:

- eager `:bake` default: `compile(:llvm)` leaves no relocation metadata;
- `:patch`: load an object, patch every site at `global + offset`, execute, and
  check the null-init / extinit / llvm.used shape (native, plus a PTX `.global`
  assertion);
- validation error paths in `foreach_relocation` and `link_relocatable!`, and a
  direct `prune_dead_relocations!` test;
- a deferred (Mock Enzyme) child whose relocations must merge into the parent.

The native test back-end gains a `:patch` mode alongside its `:import` JIT mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The relocation metadata now surfaced through `compile`/`emit_asm` and the
`relocation_lowering` trait is additive: back-ends that don't opt in keep the
prior baking behavior. Minor bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maleadt and others added 5 commits July 23, 2026 14:10
`jl_get_llvm_gvs_globals` was backported to 1.10, so `HAS_LLVM_GVS_GLOBALS`
alone reports 1.10 as relocation-capable. But 1.10's codegen still bakes host
references inline (as `inttoptr` constants) in the JIT (non-imaging) mode we
compile in, rather than emitting the relocatable global declarations the
relocation machinery collects, so no relocation sites are ever produced. Only
1.11+ emits those declarations, so gate the predicate on the version too.

Fixes the relocation testset failures in native.jl and ptx.jl on 1.10, which
all run under `if supports_relocatable_ir()`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An all-zero box -- a patchable header over an all-zero payload, as produced for
`SVector(0f0, 0f0)` and friends -- is folded by LLVM to a `zeroinitializer`.
That ConstantAggregateZero reports no operands, so `bake_relocations!` built an
empty field vector and threw `BoundsError` when writing the resolved header
word (JuliaGPU/oneAPI.jl "#55: invalid integers created by alloc_opt").

Rebuild the explicit per-field constants from the struct's element types when
the initializer is a ConstantAggregateZero, and add a regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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.

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