From 2708ed274e0a1714efbb889a65b3c82382597a3f Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Fri, 21 Aug 2026 11:55:52 +0200 Subject: [PATCH] Give patchable relocation globals protected visibility. Julia emits relocation globals `dso_local`, so backends address them PC-relatively (`@rel32` on AMDGPU). `:patch` lowering defined them as weak ODR with default visibility, which ELF treats as preemptible; lld then rejects the `R_AMDGPU_REL32_*` relocations in AMDGPU.jl's `-shared` link ("recompile with -fPIC"). Mark the record globals protected: non-preemptible, consistent with `dso_local`, yet still in the dynamic symbol table so loaders (CUDA, HIP, ORC) keep resolving them by name. NVPTX ignores visibility, and JITLink/RuntimeDyld export STV_PROTECTED symbols like default ones. Add a `patch` option to the GCN test helper and a regression test. Co-Authored-By: Claude Fable 5 --- src/interface.jl | 4 +++- src/relocation.jl | 11 +++++++++-- test/gcn.jl | 24 ++++++++++++++++++++++++ test/helpers/gcn.jl | 14 +++++++++++--- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index a44adeda..c91c2c10 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -335,7 +335,9 @@ A loader may hold several compiled functions in one symbol namespace without ren anything: record names are namespaced per compiler job, and where two objects do define one record — a relocation-carrying runtime-library function keeps its own namespace in every kernel it is linked into — `:patch` defines it weakly, so the definitions coalesce and -patching the survivor serves both. +patching the survivor serves both. Record globals have protected visibility: they stay +visible to the loader by name, yet are never preemptible, matching the `dso_local` +references Julia emits. The manifest is frozen once lowered: it then describes emitted code, so adding, dropping or reordering a record errors. Take a [`copy`](@ref) to work on one afterwards, as diff --git a/src/relocation.jl b/src/relocation.jl index 6436688d..c3e9b6f4 100644 --- a/src/relocation.jl +++ b/src/relocation.jl @@ -660,8 +660,9 @@ end emit_patchable_relocations!(mod, relocs) Emit slots as writable, null-initialized definitions, and leave interior records as the -`extinit` definitions they already are. The loader must patch every record by `(name, -offset)` after loading the object ([`resolved_relocations`](@ref)). +`extinit` definitions they already are. Every record global is a weak, protected-visibility +definition. The loader must patch every record by `(name, offset)` after loading the object +([`resolved_relocations`](@ref)). """ function emit_patchable_relocations!(mod::LLVM.Module, relocs::Relocations) used = GlobalVariable[] @@ -681,6 +682,12 @@ function emit_patchable_relocations!(mod::LLVM.Module, relocs::Relocations) # anchors them against DCE, and `externally_initialized` still stops the optimizer # from believing the null initializer. linkage!(gv, LLVM.API.LLVMWeakODRLinkage) + # Julia emits these globals `dso_local`, so backends address them PC-relatively + # (e.g. `@rel32` on AMDGPU). A weak definition with default visibility is however + # preemptible in an ELF shared link, which `ld.lld` rejects ("recompile with -fPIC"). + # Protected visibility keeps the symbol in the dynamic symbol table, so loaders can + # still find it by name, while honouring the non-preemptible promise. + visibility!(gv, LLVM.API.LLVMProtectedVisibility) push!(used, gv) end isempty(used) || set_used!(mod, used...) diff --git a/test/gcn.jl b/test/gcn.jl index 003d3ef2..9396ade5 100644 --- a/test/gcn.jl +++ b/test/gcn.jl @@ -203,6 +203,30 @@ end ############################################################################################ @testset "assembly" begin +@testset "patchable relocation visibility" begin + # AMDGPU.jl links the object into a shared library with ld.lld. Julia references the + # record globals `dso_local` (PC-relative `@rel32`), so a weak default-visibility + # definition would be rejected as preemptible ("recompile with -fPIC"); the symbol + # must be protected. + if GPUCompiler.supports_relocatable_ir() + mod = @eval module $(gensym()) + function kernel(out::Ptr{Bool}, s::Symbol) + unsafe_store!(out, s === :foo) + return + end + end + asm = sprint(io->GCN.code_native(io, mod.kernel, Tuple{Ptr{Bool},Symbol}; + kernel=true, patch=true)) + m = match(r"(?m)^\s*\.protected\s+(\S+jl_sym_foo\S*)", asm) + @test m !== nothing + if m !== nothing + name = m.captures[1] + @test occursin(r"(?m)^\s*\.weak\s+" * name, asm) + @test occursin("$(name)@rel32@lo", asm) + end + end +end + @testset "s_load for kernarg struct access" begin mod = @eval module $(gensym()) struct MyStruct diff --git a/test/helpers/gcn.jl b/test/helpers/gcn.jl index b5745b56..ef81ce0d 100644 --- a/test/helpers/gcn.jl +++ b/test/helpers/gcn.jl @@ -3,14 +3,22 @@ module GCN using ..GPUCompiler import ..TestRuntime -struct CompilerParams <: AbstractCompilerParams end +struct CompilerParams <: AbstractCompilerParams + patch::Bool + CompilerParams(patch::Bool=false) = new(patch) +end GPUCompiler.runtime_module(::CompilerJob{<:Any,CompilerParams}) = TestRuntime -function create_job(@nospecialize(func), @nospecialize(types); backend::Symbol=:external, kwargs...) +# `patch=true` keeps relocations symbolic (as AMDGPU.jl does); plain jobs resolve them in IR. +GPUCompiler.relocation_lowering(@nospecialize(job::CompilerJob{<:Any,CompilerParams})) = + job.config.params.patch ? :patch : :bake + +function create_job(@nospecialize(func), @nospecialize(types); backend::Symbol=:external, + patch::Bool=false, kwargs...) config_kwargs, kwargs = split_kwargs(kwargs, GPUCompiler.CONFIG_KWARGS) source = methodinstance(typeof(func), Base.to_tuple_type(types), Base.get_world_counter()) target = GCNCompilerTarget(dev_isa="gfx900"; backend) - params = CompilerParams() + params = CompilerParams(patch) config = CompilerConfig(target, params; kernel=false, config_kwargs...) CompilerJob(source, config), kwargs end