Make GPUCompiler IR relocatable across Julia sessions#878
Conversation
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
|
Okay, this is starting to look good. IR should be fully relocatable after this, at least for back-ends supporting run-time relocations. |
| # 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)) |
There was a problem hiding this comment.
Should we also do this for the job capturing?
There was a problem hiding this comment.
You mean make them all by-name references?
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
vchuravy
left a comment
There was a problem hiding this comment.
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?
|
Another suggestion by @vchuravy: Try this out on AllocCheck.jl |
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>
`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>
|
AllocCheck.jl integration: JuliaLang/AllocCheck.jl#113 @vchuravy I think this is ready for another look. |
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