From 7a9ab31e7a9b87bfeaa462d1aca63644596434ca Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Wed, 26 Nov 2025 16:39:47 +0100 Subject: [PATCH 01/12] Enable users to map from GV to Julia value --- src/driver.jl | 5 ++- src/irgen.jl | 9 +++- src/jlgen.jl | 88 +++++++++++++++++++++++++++++++++++++-- test/helpers/native.jl | 6 ++- test/native.jl | 19 +++++++-- test/native/precompile.jl | 31 +++++++++++++- 6 files changed, 144 insertions(+), 14 deletions(-) diff --git a/src/driver.jl b/src/driver.jl index 950ea272..827eafb7 100644 --- a/src/driver.jl +++ b/src/driver.jl @@ -197,7 +197,7 @@ const __llvm_initialized = Ref(false) end @tracepoint "IR generation" begin - ir, compiled = irgen(job) + ir, compiled, gv_to_value = irgen(job) if job.config.entry_abi === :specfunc entry_fn = compiled[job.source].specfunc else @@ -256,6 +256,7 @@ const __llvm_initialized = Ref(false) dyn_ir, dyn_meta = codegen(:llvm, CompilerJob(dyn_job; config)) dyn_entry_fn = LLVM.name(dyn_meta.entry) merge!(compiled, dyn_meta.compiled) + merge!(gv_to_value, dyn_meta.gv_to_value) @assert context(dyn_ir) == context(ir) link!(ir, dyn_ir) changed = true @@ -422,7 +423,7 @@ const __llvm_initialized = Ref(false) @tracepoint "verification" verify(ir) end - return ir, (; entry, compiled) + return ir, (; entry, compiled, gv_to_value) end @locked function emit_asm(@nospecialize(job::CompilerJob), ir::LLVM.Module, diff --git a/src/irgen.jl b/src/irgen.jl index a7c36a60..5149e9f0 100644 --- a/src/irgen.jl +++ b/src/irgen.jl @@ -1,7 +1,7 @@ # LLVM IR generation function irgen(@nospecialize(job::CompilerJob)) - mod, compiled = @tracepoint "emission" compile_method_instance(job) + mod, compiled, gv_to_value = @tracepoint "emission" compile_method_instance(job) if job.config.entry_abi === :specfunc entry_fn = compiled[job.source].specfunc else @@ -55,6 +55,11 @@ function irgen(@nospecialize(job::CompilerJob)) new_name = safe_name(old_name) if old_name != new_name LLVM.name!(val, new_name) + val = get(gv_to_value, old_name, nothing) + if val !== nothing + delete!(gv_to_value, old_name) + gv_to_value[new_name] = val + end end end @@ -120,7 +125,7 @@ function irgen(@nospecialize(job::CompilerJob)) can_throw(job) || lower_throw!(mod) end - return mod, compiled + return mod, compiled, gv_to_value end diff --git a/src/jlgen.jl b/src/jlgen.jl index 0d380cbf..97ebd26a 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -652,6 +652,31 @@ end CompilationPolicyExtern = 1 end +@static if VERSION < v"1.13.0-DEV.623" + const AL_N_INLINE = 29 + + # Mirrors arraylist_t + mutable struct ArrayList + len::Csize_t + max::Csize_t + items::Ptr{Ptr{Cvoid}} + _space::NTuple{AL_N_INLINE, Ptr{Cvoid}} + + function ArrayList() + list = new(0, AL_N_INLINE, Ptr{Ptr{Cvoid}}(C_NULL), ntuple(_ -> Ptr{Cvoid}(C_NULL), AL_N_INLINE)) + list.items = Base.pointer_from_objref(list) + fieldoffset(typeof(list), 4) + + finalizer(list) do list + if list.items != Base.pointer_from_objref(list) + fieldoffset(typeof(list), 4) + Libc.free(list.items) + end + end + return list + end + end + +end + """ precompile(job::CompilerJob) @@ -670,6 +695,10 @@ function Base.precompile(@nospecialize(job::CompilerJob)) return true end +function precompiling() + return (@ccall jl_generating_output()::Cint) == 1 +end + function compile_method_instance(@nospecialize(job::CompilerJob)) if job.source.def.primary_world > job.world error("Cannot compile $(job.source) for world $(job.world); method is only valid from world $(job.source.def.primary_world) onwards") @@ -766,24 +795,77 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) cache_gbl = nothing end + # Maintain a map from global variables to their initialized Julia values. + # The objects pointed to are perma-rooted, during codegen. + # It is legal to call `Base.unsafe_pointer_to_objref` on `values(gv_to_value)`, + # but x->pointer_from_objref(Base.unsafe_pointer_to_objref(x)) is not idempotent, + # thus we store raw pointers here. + # Currently GVs are privatized, so users may have to handle embedded pointers, + # but this dictionary provides a clear indication that the embedded pointer is + # valid Julia object. + gv_to_value = Dict{String, Ptr{Cvoid}}() + + # TODO: To enable relocation we should strip out the initializers here. if VERSION >= v"1.13.0-DEV.623" # Since Julia 1.13, the caller is responsible for initializing global variables that # point to global values or bindings with their address in memory. num_gvars = Ref{Csize_t}(0) @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, - C_NULL::Ptr{Cvoid})::Nothing + C_NULL::Ptr{Cvoid} + )::Nothing gvs = Vector{Ptr{LLVM.API.LLVMOpaqueValue}}(undef, num_gvars[]) @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, - gvs::Ptr{LLVM.API.LLVMOpaqueValue})::Nothing + gvs::Ptr{LLVM.API.LLVMOpaqueValue} + )::Nothing + inits = Vector{Ptr{Cvoid}}(undef, num_gvars[]) @ccall jl_get_llvm_gv_inits(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, inits::Ptr{Cvoid})::Nothing for (gv_ref, init) in zip(gvs, inits) gv = GlobalVariable(gv_ref) + gv_to_value[LLVM.name(gv)] = init + # set the initializer val = const_inttoptr(ConstantInt(Int64(init)), LLVM.PointerType()) initializer!(gv, val) end + else + # Prior to version v"1.13.0-DEV.623" we only had access to the values that the global variables + # were initialized with, so we have to match them up manually. + + # get the global values + if VERSION >= v"1.12.0-DEV.1703" + num_gvars = Ref{Csize_t}(0) + @ccall jl_get_llvm_gvs( + native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, + C_NULL::Ptr{Cvoid} + )::Nothing + gvalues = Vector{Ptr{Cvoid}}(undef, num_gvars[]) + @ccall jl_get_llvm_gvs( + native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, + gvalues::Ptr{Cvoid} + )::Nothing + else + # On older version of Julia we have to use `arraylist_t` which doesn't have a Julia API. + gvars = ArrayList() + GC.@preserve gvars begin + p_gvars = Base.pointer_from_objref(gvars) + @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, p_gvars::Ptr{Cvoid})::Nothing + gvalues = Vector{Ptr{Cvoid}}(undef, gvars.len) + for i in 1:gvars.len + gvalues[i] = unsafe_load(gvars.items, i) + end + end + end + # Currently we have no reliable way to match the `globals(llvm_mod)`` to their initializers `gvars`, + # so for now we only place a marker + for gv in globals(llvm_mod) + if !haskey(metadata(gv), "julia.constgv") + continue + end + gv_to_value[LLVM.name(gv)] = C_NULL + end + @assert length(gv_to_value) == length(gvalues) end if VERSION >= v"1.13.0-DEV.1120" @@ -854,7 +936,7 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) # ensure that the requested method instance was compiled @assert haskey(compiled, job.source) - return llvm_mod, compiled + return llvm_mod, compiled, gv_to_value end # partially revert JuliaLangjulia#49391 diff --git a/test/helpers/native.jl b/test/helpers/native.jl index d53ff172..656028f4 100644 --- a/test/helpers/native.jl +++ b/test/helpers/native.jl @@ -14,8 +14,10 @@ struct CompilerParams <: AbstractCompilerParams new(entry_safepoint, method_table) end +module Runtime end + NativeCompilerJob = CompilerJob{NativeCompilerTarget,CompilerParams} -GPUCompiler.runtime_module(::NativeCompilerJob) = TestRuntime +GPUCompiler.runtime_module(::NativeCompilerJob) = Runtime GPUCompiler.method_table(@nospecialize(job::NativeCompilerJob)) = job.config.params.method_table GPUCompiler.can_safepoint(@nospecialize(job::NativeCompilerJob)) = job.config.params.entry_safepoint @@ -24,7 +26,7 @@ function create_job(@nospecialize(func), @nospecialize(types); entry_safepoint::Bool=false, method_table=test_method_table, kwargs...) config_kwargs, kwargs = split_kwargs(kwargs, GPUCompiler.CONFIG_KWARGS) source = methodinstance(typeof(func), Base.to_tuple_type(types), Base.get_world_counter()) - target = NativeCompilerTarget() + target = NativeCompilerTarget(;jlruntime=true) params = CompilerParams(entry_safepoint, method_table) config = CompilerConfig(target, params; kernel=false, config_kwargs...) CompilerJob(source, config), kwargs diff --git a/test/native.jl b/test/native.jl index da08764f..f1db21c5 100644 --- a/test/native.jl +++ b/test/native.jl @@ -36,16 +36,19 @@ end @testset "compilation database" begin mod = @eval module $(gensym()) @noinline inner(x) = x+1 - function outer(x) + function outer(x, sym) + if sym == :a return inner(x) end + return x + end end - job, _ = Native.create_job(mod.outer, (Int,)) + job, _ = Native.create_job(mod.outer, (Int, Symbol)) JuliaContext() do ctx - ir, meta = GPUCompiler.compile(:llvm, job) + ir, meta = GPUCompiler.compile(:llvm, job; validate=false) - meth = only(methods(mod.outer, (Int,))) + meth = only(methods(mod.outer, (Int, Symbol))) mis = filter(mi->mi.def == meth, keys(meta.compiled)) @test length(mis) == 1 @@ -53,6 +56,14 @@ end other_mis = filter(mi->mi.def != meth, keys(meta.compiled)) @test length(other_mis) == 1 @test only(other_mis).def in methods(mod.inner) + + @test_broken length(meta.gv_to_value) >= 1 + # TODO: Global values get privatized, so we can't find them by name anymore. + # %.not = icmp eq ptr %"sym::Symbol", inttoptr (i64 140096668482288 to ptr), !dbg !38 + # for (name, v) in meta.gv_to_value + # gv = globals(ir)[name] + # @test LLVM.initializer(gv) === v + # end end end diff --git a/test/native/precompile.jl b/test/native/precompile.jl index 6fe981a5..fd80f5d7 100644 --- a/test/native/precompile.jl +++ b/test/native/precompile.jl @@ -13,12 +13,35 @@ precompile_test_harness("Inference caching") do load_path A[1] = x return end + + function kernel_w_global(A, x, sym) + if sym == :A + A[1] = x + end + return + end + + function func_with_return(box, x) + box[] = x + return box[]::Float64 + end let job, _ = NativeCompiler.Native.create_job(kernel, (Vector{Int}, Int)) precompile(job) end + let + job, _ = NativeCompiler.Native.create_job(kernel_w_global, (Vector{Int}, Int, Symbol)) + precompile(job) + end + + let + NativeCompiler.Native.code_llvm(stdout, func_with_return, (Base.RefValue{Any}, Float64,), entry_abi=:func, dump_module=true, optimize=false) + job, _ = NativeCompiler.Native.create_job(func_with_return, (Base.RefValue{Any}, Float64,)) + precompile(job) + end + # identity is foreign @setup_workload begin job, _ = NativeCompiler.Native.create_job(identity, (Int,)) @@ -28,7 +51,7 @@ precompile_test_harness("Inference caching") do load_path end end) |> string) - Base.compilecache(Base.PkgId("NativeBackend")) + Base.compilecache(Base.PkgId("NativeBackend"), stderr, stdout) @eval let import NativeCompiler @@ -47,6 +70,12 @@ precompile_test_harness("Inference caching") do load_path kernel_mi = GPUCompiler.methodinstance(typeof(NativeBackend.kernel), Tuple{Vector{Int}, Int}) @test check_presence(kernel_mi, token) + kernel_w_global_mi = GPUCompiler.methodinstance(typeof(NativeBackend.kernel_w_global), Tuple{Vector{Int}, Int, Symbol}) + @test check_presence(kernel_w_global_mi, token) + + func_with_return_mi = GPUCompiler.methodinstance(typeof(NativeBackend.func_with_return), Tuple{Base.RefValue{Any}, Float64}) + @test check_presence(func_with_return_mi, token) + # check that identity survived @test check_presence(identity_mi, token) broken=VERSION>=v"1.12.0-DEV.1268" From a2ba7b6b23fd10fce1ef623db06949bc949cb19d Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Thu, 15 Jan 2026 11:58:55 +0100 Subject: [PATCH 02/12] fixup! Enable users to map from GV to Julia value --- test/native.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/native.jl b/test/native.jl index f1db21c5..b4aebffc 100644 --- a/test/native.jl +++ b/test/native.jl @@ -36,12 +36,12 @@ end @testset "compilation database" begin mod = @eval module $(gensym()) @noinline inner(x) = x+1 - function outer(x, sym) - if sym == :a - return inner(x) + function outer(x, sym) + if sym == :a + return inner(x) + end + return x end - return x - end end job, _ = Native.create_job(mod.outer, (Int, Symbol)) From 0ead56f7aa337903e1150df8142dd0d42622c19f Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Thu, 15 Jan 2026 12:39:13 +0100 Subject: [PATCH 03/12] add reference to core issue --- src/jlgen.jl | 2 ++ test/native.jl | 4 +++- test/native/precompile.jl | 13 ++++++------- test/ptx/precompile.jl | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/jlgen.jl b/src/jlgen.jl index 97ebd26a..12078247 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -859,6 +859,8 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) end # Currently we have no reliable way to match the `globals(llvm_mod)`` to their initializers `gvars`, # so for now we only place a marker + # TODO: To fix https://github.com/JuliaGPU/GPUCompiler.jl/issues/753 we would need to initialize the + # global variables here properly. for gv in globals(llvm_mod) if !haskey(metadata(gv), "julia.constgv") continue diff --git a/test/native.jl b/test/native.jl index b4aebffc..83339ea9 100644 --- a/test/native.jl +++ b/test/native.jl @@ -57,7 +57,9 @@ end @test length(other_mis) == 1 @test only(other_mis).def in methods(mod.inner) - @test_broken length(meta.gv_to_value) >= 1 + if VERSION >= v"1.12" + @test length(meta.gv_to_value) == 1 + end # TODO: Global values get privatized, so we can't find them by name anymore. # %.not = icmp eq ptr %"sym::Symbol", inttoptr (i64 140096668482288 to ptr), !dbg !38 # for (name, v) in meta.gv_to_value diff --git a/test/native/precompile.jl b/test/native/precompile.jl index fd80f5d7..d4c0a7ac 100644 --- a/test/native/precompile.jl +++ b/test/native/precompile.jl @@ -21,9 +21,8 @@ precompile_test_harness("Inference caching") do load_path return end - function func_with_return(box, x) - box[] = x - return box[]::Float64 + function square(x) + return x*x end let @@ -37,8 +36,8 @@ precompile_test_harness("Inference caching") do load_path end let - NativeCompiler.Native.code_llvm(stdout, func_with_return, (Base.RefValue{Any}, Float64,), entry_abi=:func, dump_module=true, optimize=false) - job, _ = NativeCompiler.Native.create_job(func_with_return, (Base.RefValue{Any}, Float64,)) + # Emit the func abi to box the return + job, _ = NativeCompiler.Native.create_job(square, (Float64,), entry_abi=:func) precompile(job) end @@ -73,8 +72,8 @@ precompile_test_harness("Inference caching") do load_path kernel_w_global_mi = GPUCompiler.methodinstance(typeof(NativeBackend.kernel_w_global), Tuple{Vector{Int}, Int, Symbol}) @test check_presence(kernel_w_global_mi, token) - func_with_return_mi = GPUCompiler.methodinstance(typeof(NativeBackend.func_with_return), Tuple{Base.RefValue{Any}, Float64}) - @test check_presence(func_with_return_mi, token) + square_mi = GPUCompiler.methodinstance(typeof(NativeBackend.square), Tuple{Float64}) + @test check_presence(square_mi, token) # check that identity survived @test check_presence(identity_mi, token) broken=VERSION>=v"1.12.0-DEV.1268" diff --git a/test/ptx/precompile.jl b/test/ptx/precompile.jl index b5f980c9..e0739df0 100644 --- a/test/ptx/precompile.jl +++ b/test/ptx/precompile.jl @@ -25,7 +25,7 @@ precompile_test_harness("Inference caching") do load_path end end) |> string) - Base.compilecache(Base.PkgId("PTXBackend")) + Base.compilecache(Base.PkgId("PTXBackend"), stderr, stdout) @eval let import PTXCompiler From 907be97fb25d0d3ae0cd56cc254a97bbec628d2e Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Thu, 15 Jan 2026 13:04:16 +0100 Subject: [PATCH 04/12] cleanup implementation and prepare for backports PRs --- src/jlgen.jl | 75 ++++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/jlgen.jl b/src/jlgen.jl index 12078247..2148e8ff 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -795,17 +795,8 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) cache_gbl = nothing end - # Maintain a map from global variables to their initialized Julia values. - # The objects pointed to are perma-rooted, during codegen. - # It is legal to call `Base.unsafe_pointer_to_objref` on `values(gv_to_value)`, - # but x->pointer_from_objref(Base.unsafe_pointer_to_objref(x)) is not idempotent, - # thus we store raw pointers here. - # Currently GVs are privatized, so users may have to handle embedded pointers, - # but this dictionary provides a clear indication that the embedded pointer is - # valid Julia object. - gv_to_value = Dict{String, Ptr{Cvoid}}() - - # TODO: To enable relocation we should strip out the initializers here. + gvs = nothing + inits = nothing if VERSION >= v"1.13.0-DEV.623" # Since Julia 1.13, the caller is responsible for initializing global variables that # point to global values or bindings with their address in memory. @@ -821,45 +812,48 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) inits = Vector{Ptr{Cvoid}}(undef, num_gvars[]) @ccall jl_get_llvm_gv_inits(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, inits::Ptr{Cvoid})::Nothing - - for (gv_ref, init) in zip(gvs, inits) - gv = GlobalVariable(gv_ref) - gv_to_value[LLVM.name(gv)] = init - # set the initializer - val = const_inttoptr(ConstantInt(Int64(init)), LLVM.PointerType()) - initializer!(gv, val) - end else - # Prior to version v"1.13.0-DEV.623" we only had access to the values that the global variables - # were initialized with, so we have to match them up manually. + # TODO: https://github.com/JuliaGPU/GPUCompiler.jl/issues/753 + # Obtain the global variables `gvs` in the same order as `inits` - # get the global values + # get the global variable initializers if VERSION >= v"1.12.0-DEV.1703" num_gvars = Ref{Csize_t}(0) @ccall jl_get_llvm_gvs( native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, C_NULL::Ptr{Cvoid} )::Nothing - gvalues = Vector{Ptr{Cvoid}}(undef, num_gvars[]) + inits = Vector{Ptr{Cvoid}}(undef, num_gvars[]) @ccall jl_get_llvm_gvs( native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, - gvalues::Ptr{Cvoid} + inits::Ptr{Cvoid} )::Nothing else # On older version of Julia we have to use `arraylist_t` which doesn't have a Julia API. - gvars = ArrayList() - GC.@preserve gvars begin - p_gvars = Base.pointer_from_objref(gvars) - @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, p_gvars::Ptr{Cvoid})::Nothing - gvalues = Vector{Ptr{Cvoid}}(undef, gvars.len) - for i in 1:gvars.len - gvalues[i] = unsafe_load(gvars.items, i) + inits_list = ArrayList() + GC.@preserve inits_list begin + p_inits = Base.pointer_from_objref(inits_list) + @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, p_inits::Ptr{Cvoid})::Nothing + inits = Vector{Ptr{Cvoid}}(undef, inits_list.len) + for i in 1:inits_list.len + inits[i] = unsafe_load(inits_list.items, i) end end end - # Currently we have no reliable way to match the `globals(llvm_mod)`` to their initializers `gvars`, - # so for now we only place a marker - # TODO: To fix https://github.com/JuliaGPU/GPUCompiler.jl/issues/753 we would need to initialize the + end + + # Maintain a map from global variables to their initialized Julia values. + # The objects pointed to are perma-rooted, during codegen. + # It is legal to call `Base.unsafe_pointer_to_objref` on `values(gv_to_value)`, + # but x->pointer_from_objref(Base.unsafe_pointer_to_objref(x)) is not idempotent, + # thus we store raw pointers here. + # Currently GVs are privatized, so users may have to handle embedded pointers, + # but this dictionary provides a clear indication that the embedded pointer is + # valid Julia object. + gv_to_value = Dict{String, Ptr{Cvoid}}() + + # On certain version of Julia we have no reliable way to match the `gvs` to their initializers `inits`. + if gvs === nothing # global variables here properly. for gv in globals(llvm_mod) if !haskey(metadata(gv), "julia.constgv") @@ -867,7 +861,18 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) end gv_to_value[LLVM.name(gv)] = C_NULL end - @assert length(gv_to_value) == length(gvalues) + else + @assert init !== nothing + for (gv_ref, init) in zip(gvs, inits) + gv = GlobalVariable(gv_ref) + gv_to_value[LLVM.name(gv)] = init + # set the initializer + # TODO: To enable full relocation we should strip out the initializers here. + if initializer(gv) === nothing + val = const_inttoptr(ConstantInt(Int64(init)), LLVM.PointerType()) + initializer!(gv, val) + end + end end if VERSION >= v"1.13.0-DEV.1120" From 5ebe300848a93e139260a3ff27e78b9fc3e9e28a Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Thu, 15 Jan 2026 20:47:22 +0100 Subject: [PATCH 05/12] Use backported API --- src/jlgen.jl | 75 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/src/jlgen.jl b/src/jlgen.jl index 2148e8ff..ecf351e1 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -675,6 +675,38 @@ end end end + function get_llvm_global_vars(native_code::Ptr{Cvoid}) + gvs_list = ArrayList() + GC.@preserve gvs_list begin + p_gvs = Base.pointer_from_objref(gvs_list) + @ccall jl_get_llvm_gvs_globals(native_code::Ptr{Cvoid}, p_gvs::Ptr{Cvoid})::Nothing + gvs = Vector{Ptr{LLVM.API.LLVMOpaqueValue}}(undef, gvs_list.len) + items = Base.unsafe_convert(Ptr{Ptr{LLVM.API.LLVMOpaqueValue}}, gvs_list.items) + for i in 1:gvs_list.len + gvs[i] = unsafe_load(items, i) + end + end + return gvs + end + + function get_llvm_global_inits(native_code::Ptr{Cvoid}) + inits_list = ArrayList() + GC.@preserve inits_list begin + p_inits = Base.pointer_from_objref(inits_list) + @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, p_inits::Ptr{Cvoid})::Nothing + inits = Vector{Ptr{Cvoid}}(undef, inits_list.len) + for i in 1:inits_list.len + inits[i] = unsafe_load(inits_list.items, i) + end + end + return inits + end +end + +import Libdl + +if VERSION < v"1.13.0-DEV.623" + const HAS_LLVM_GVS_GLOBALS = Libdl.dlsym(Libdl.dlopen(""), :jl_get_llvm_gvs_globals, throw_error=false) !== nothing end """ @@ -695,10 +727,6 @@ function Base.precompile(@nospecialize(job::CompilerJob)) return true end -function precompiling() - return (@ccall jl_generating_output()::Cint) == 1 -end - function compile_method_instance(@nospecialize(job::CompilerJob)) if job.source.def.primary_world > job.world error("Cannot compile $(job.source) for world $(job.world); method is only valid from world $(job.source.def.primary_world) onwards") @@ -795,11 +823,14 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) cache_gbl = nothing end + # Since Julia 1.13, the caller is responsible for initializing global variables that + # point to global values or bindings with their address in memory. + # Similarly on previous versions when imaging=true, it is also the caller's responsibility + # (see https://github.com/JuliaGPU/GPUCompiler.jl/issues/753), but we can support this on versions + # that have HAS_LLVM_GVS_GLOBALS. gvs = nothing inits = nothing - if VERSION >= v"1.13.0-DEV.623" - # Since Julia 1.13, the caller is responsible for initializing global variables that - # point to global values or bindings with their address in memory. + @static if VERSION >= v"1.13.0-DEV.623" num_gvars = Ref{Csize_t}(0) @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, C_NULL::Ptr{Cvoid} @@ -812,33 +843,23 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) inits = Vector{Ptr{Cvoid}}(undef, num_gvars[]) @ccall jl_get_llvm_gv_inits(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, inits::Ptr{Cvoid})::Nothing - else - # TODO: https://github.com/JuliaGPU/GPUCompiler.jl/issues/753 - # Obtain the global variables `gvs` in the same order as `inits` - - # get the global variable initializers + elseif HAS_LLVM_GVS_GLOBALS if VERSION >= v"1.12.0-DEV.1703" num_gvars = Ref{Csize_t}(0) - @ccall jl_get_llvm_gvs( - native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, + @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, C_NULL::Ptr{Cvoid} )::Nothing + gvs = Vector{Ptr{LLVM.API.LLVMOpaqueValue}}(undef, num_gvars[]) + @ccall jl_get_llvm_gvs_globals(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, + gvs::Ptr{LLVM.API.LLVMOpaqueValue} + )::Nothing inits = Vector{Ptr{Cvoid}}(undef, num_gvars[]) - @ccall jl_get_llvm_gvs( - native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, + @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t}, inits::Ptr{Cvoid} )::Nothing else - # On older version of Julia we have to use `arraylist_t` which doesn't have a Julia API. - inits_list = ArrayList() - GC.@preserve inits_list begin - p_inits = Base.pointer_from_objref(inits_list) - @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, p_inits::Ptr{Cvoid})::Nothing - inits = Vector{Ptr{Cvoid}}(undef, inits_list.len) - for i in 1:inits_list.len - inits[i] = unsafe_load(inits_list.items, i) - end - end + gvs = get_llvm_global_vars(native_code) + inits = get_llvm_global_inits(native_code) end end @@ -849,7 +870,7 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) # thus we store raw pointers here. # Currently GVs are privatized, so users may have to handle embedded pointers, # but this dictionary provides a clear indication that the embedded pointer is - # valid Julia object. + # indeed avalid Julia object. gv_to_value = Dict{String, Ptr{Cvoid}}() # On certain version of Julia we have no reliable way to match the `gvs` to their initializers `inits`. From c5f2c4736be0953acd8b6e5bcf74db80fbbcce00 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Thu, 15 Jan 2026 20:53:13 +0100 Subject: [PATCH 06/12] fixup! Use backported API --- src/jlgen.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jlgen.jl b/src/jlgen.jl index ecf351e1..b278fa15 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -883,7 +883,7 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) gv_to_value[LLVM.name(gv)] = C_NULL end else - @assert init !== nothing + @assert inits !== nothing for (gv_ref, init) in zip(gvs, inits) gv = GlobalVariable(gv_ref) gv_to_value[LLVM.name(gv)] = init From 2a2f8ab9017b14d67a6278a00a7c4fcd37e44019 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Thu, 15 Jan 2026 21:01:14 +0100 Subject: [PATCH 07/12] fixup! fixup! Use backported API --- src/jlgen.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jlgen.jl b/src/jlgen.jl index b278fa15..b29e038d 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -888,8 +888,8 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) gv = GlobalVariable(gv_ref) gv_to_value[LLVM.name(gv)] = init # set the initializer - # TODO: To enable full relocation we should strip out the initializers here. - if initializer(gv) === nothing + # TODO: To enable full relocation we should actually strip out the initializers here. + if initializer(gv) isa LLVM.PointerNull val = const_inttoptr(ConstantInt(Int64(init)), LLVM.PointerType()) initializer!(gv, val) end From f0816c8d283c90334abe18c1ee26e6246167481f Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Thu, 15 Jan 2026 21:49:40 +0100 Subject: [PATCH 08/12] Make it work on MacOS and Windows --- src/jlgen.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/jlgen.jl b/src/jlgen.jl index b29e038d..09aa1ea4 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -706,7 +706,8 @@ end import Libdl if VERSION < v"1.13.0-DEV.623" - const HAS_LLVM_GVS_GLOBALS = Libdl.dlsym(Libdl.dlopen(""), :jl_get_llvm_gvs_globals, throw_error=false) !== nothing + const HAS_LLVM_GVS_GLOBALS = Libdl.dlsym( + unsafe_load(cglobal(:jl_libjulia_handle, Ptr{Cvoid})), :jl_get_llvm_gvs_globals, throw_error=false) !== nothing end """ From 5c6e87578d3fe18a644d4c4eec71b2aee4473787 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Fri, 16 Jan 2026 09:05:38 +0100 Subject: [PATCH 09/12] Apply suggestion --- src/jlgen.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jlgen.jl b/src/jlgen.jl index 09aa1ea4..8a454912 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -889,8 +889,8 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) gv = GlobalVariable(gv_ref) gv_to_value[LLVM.name(gv)] = init # set the initializer - # TODO: To enable full relocation we should actually strip out the initializers here. - if initializer(gv) isa LLVM.PointerNull + # TODO(vc): To enable full relocation we should actually strip out the initializers here. + if LLVM.isnull(initializer(gv)) val = const_inttoptr(ConstantInt(Int64(init)), LLVM.PointerType()) initializer!(gv, val) end From d5cc13c8866f60a272282492888b21a3b4a51e1c Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Fri, 16 Jan 2026 09:05:48 +0100 Subject: [PATCH 10/12] Apply suggestion from --- src/driver.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/driver.jl b/src/driver.jl index 827eafb7..b1d55d33 100644 --- a/src/driver.jl +++ b/src/driver.jl @@ -256,7 +256,9 @@ const __llvm_initialized = Ref(false) dyn_ir, dyn_meta = codegen(:llvm, CompilerJob(dyn_job; config)) dyn_entry_fn = LLVM.name(dyn_meta.entry) merge!(compiled, dyn_meta.compiled) - merge!(gv_to_value, dyn_meta.gv_to_value) + if haskey(dyn_meta, :gv_to_value) + merge!(gv_to_value, dyn_meta.gv_to_value) + end @assert context(dyn_ir) == context(ir) link!(ir, dyn_ir) changed = true From a807baff1aed440809f2b8572e8b727637d4de0a Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Fri, 16 Jan 2026 09:09:07 +0100 Subject: [PATCH 11/12] merge version blocks --- src/jlgen.jl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/jlgen.jl b/src/jlgen.jl index 8a454912..f5513ecb 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -653,6 +653,11 @@ end end @static if VERSION < v"1.13.0-DEV.623" + import Libdl + + const HAS_LLVM_GVS_GLOBALS = Libdl.dlsym( + unsafe_load(cglobal(:jl_libjulia_handle, Ptr{Cvoid})), :jl_get_llvm_gvs_globals, throw_error=false) !== nothing + const AL_N_INLINE = 29 # Mirrors arraylist_t @@ -703,13 +708,6 @@ end end end -import Libdl - -if VERSION < v"1.13.0-DEV.623" - const HAS_LLVM_GVS_GLOBALS = Libdl.dlsym( - unsafe_load(cglobal(:jl_libjulia_handle, Ptr{Cvoid})), :jl_get_llvm_gvs_globals, throw_error=false) !== nothing -end - """ precompile(job::CompilerJob) From 2b8ec3ddfb7d639f199c8f88da9600733278c069 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Fri, 16 Jan 2026 14:21:42 +0100 Subject: [PATCH 12/12] Move to utils --- src/jlgen.jl | 56 ---------------------------------------------------- src/utils.jl | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/jlgen.jl b/src/jlgen.jl index f5513ecb..0e2f6d3a 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -652,62 +652,6 @@ end CompilationPolicyExtern = 1 end -@static if VERSION < v"1.13.0-DEV.623" - import Libdl - - const HAS_LLVM_GVS_GLOBALS = Libdl.dlsym( - unsafe_load(cglobal(:jl_libjulia_handle, Ptr{Cvoid})), :jl_get_llvm_gvs_globals, throw_error=false) !== nothing - - const AL_N_INLINE = 29 - - # Mirrors arraylist_t - mutable struct ArrayList - len::Csize_t - max::Csize_t - items::Ptr{Ptr{Cvoid}} - _space::NTuple{AL_N_INLINE, Ptr{Cvoid}} - - function ArrayList() - list = new(0, AL_N_INLINE, Ptr{Ptr{Cvoid}}(C_NULL), ntuple(_ -> Ptr{Cvoid}(C_NULL), AL_N_INLINE)) - list.items = Base.pointer_from_objref(list) + fieldoffset(typeof(list), 4) - - finalizer(list) do list - if list.items != Base.pointer_from_objref(list) + fieldoffset(typeof(list), 4) - Libc.free(list.items) - end - end - return list - end - end - - function get_llvm_global_vars(native_code::Ptr{Cvoid}) - gvs_list = ArrayList() - GC.@preserve gvs_list begin - p_gvs = Base.pointer_from_objref(gvs_list) - @ccall jl_get_llvm_gvs_globals(native_code::Ptr{Cvoid}, p_gvs::Ptr{Cvoid})::Nothing - gvs = Vector{Ptr{LLVM.API.LLVMOpaqueValue}}(undef, gvs_list.len) - items = Base.unsafe_convert(Ptr{Ptr{LLVM.API.LLVMOpaqueValue}}, gvs_list.items) - for i in 1:gvs_list.len - gvs[i] = unsafe_load(items, i) - end - end - return gvs - end - - function get_llvm_global_inits(native_code::Ptr{Cvoid}) - inits_list = ArrayList() - GC.@preserve inits_list begin - p_inits = Base.pointer_from_objref(inits_list) - @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, p_inits::Ptr{Cvoid})::Nothing - inits = Vector{Ptr{Cvoid}}(undef, inits_list.len) - for i in 1:inits_list.len - inits[i] = unsafe_load(inits_list.items, i) - end - end - return inits - end -end - """ precompile(job::CompilerJob) diff --git a/src/utils.jl b/src/utils.jl index 095f22dc..674d8f9b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -182,3 +182,59 @@ function kernels(mod::LLVM.Module) end return vals end + +@static if VERSION < v"1.13.0-DEV.623" + import Libdl + + const HAS_LLVM_GVS_GLOBALS = Libdl.dlsym( + unsafe_load(cglobal(:jl_libjulia_handle, Ptr{Cvoid})), :jl_get_llvm_gvs_globals, throw_error=false) !== nothing + + const AL_N_INLINE = 29 + + # Mirrors arraylist_t + mutable struct ArrayList + len::Csize_t + max::Csize_t + items::Ptr{Ptr{Cvoid}} + _space::NTuple{AL_N_INLINE, Ptr{Cvoid}} + + function ArrayList() + list = new(0, AL_N_INLINE, Ptr{Ptr{Cvoid}}(C_NULL), ntuple(_ -> Ptr{Cvoid}(C_NULL), AL_N_INLINE)) + list.items = Base.pointer_from_objref(list) + fieldoffset(typeof(list), 4) + + finalizer(list) do list + if list.items != Base.pointer_from_objref(list) + fieldoffset(typeof(list), 4) + Libc.free(list.items) + end + end + return list + end + end + + function get_llvm_global_vars(native_code::Ptr{Cvoid}) + gvs_list = ArrayList() + GC.@preserve gvs_list begin + p_gvs = Base.pointer_from_objref(gvs_list) + @ccall jl_get_llvm_gvs_globals(native_code::Ptr{Cvoid}, p_gvs::Ptr{Cvoid})::Nothing + gvs = Vector{Ptr{LLVM.API.LLVMOpaqueValue}}(undef, gvs_list.len) + items = Base.unsafe_convert(Ptr{Ptr{LLVM.API.LLVMOpaqueValue}}, gvs_list.items) + for i in 1:gvs_list.len + gvs[i] = unsafe_load(items, i) + end + end + return gvs + end + + function get_llvm_global_inits(native_code::Ptr{Cvoid}) + inits_list = ArrayList() + GC.@preserve inits_list begin + p_inits = Base.pointer_from_objref(inits_list) + @ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, p_inits::Ptr{Cvoid})::Nothing + inits = Vector{Ptr{Cvoid}}(undef, inits_list.len) + for i in 1:inits_list.len + inits[i] = unsafe_load(inits_list.items, i) + end + end + return inits + end +end