Skip to content

Commit 3ed3d8e

Browse files
wsmosesvchuravy
andauthored
Enable users to map from GV to Julia value and initialize them whenever possible (#752)
* Enable users to map from GV to Julia value * fixup! Enable users to map from GV to Julia value * add reference to core issue * cleanup implementation and prepare for backports PRs * Use backported API * fixup! Use backported API * fixup! fixup! Use backported API * Make it work on MacOS and Windows * Apply suggestion * Apply suggestion from * merge version blocks * Move to utils --------- Co-authored-by: Valentin Churavy <v.churavy@gmail.com>
1 parent bf25123 commit 3ed3d8e

8 files changed

Lines changed: 181 additions & 21 deletions

File tree

src/driver.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ const __llvm_initialized = Ref(false)
197197
end
198198

199199
@tracepoint "IR generation" begin
200-
ir, compiled = irgen(job)
200+
ir, compiled, gv_to_value = irgen(job)
201201
if job.config.entry_abi === :specfunc
202202
entry_fn = compiled[job.source].specfunc
203203
else
@@ -256,6 +256,9 @@ const __llvm_initialized = Ref(false)
256256
dyn_ir, dyn_meta = codegen(:llvm, CompilerJob(dyn_job; config))
257257
dyn_entry_fn = LLVM.name(dyn_meta.entry)
258258
merge!(compiled, dyn_meta.compiled)
259+
if haskey(dyn_meta, :gv_to_value)
260+
merge!(gv_to_value, dyn_meta.gv_to_value)
261+
end
259262
@assert context(dyn_ir) == context(ir)
260263
link!(ir, dyn_ir)
261264
changed = true
@@ -422,7 +425,7 @@ const __llvm_initialized = Ref(false)
422425
@tracepoint "verification" verify(ir)
423426
end
424427

425-
return ir, (; entry, compiled)
428+
return ir, (; entry, compiled, gv_to_value)
426429
end
427430

428431
@locked function emit_asm(@nospecialize(job::CompilerJob), ir::LLVM.Module,

src/irgen.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# LLVM IR generation
22

33
function irgen(@nospecialize(job::CompilerJob))
4-
mod, compiled = @tracepoint "emission" compile_method_instance(job)
4+
mod, compiled, gv_to_value = @tracepoint "emission" compile_method_instance(job)
55
if job.config.entry_abi === :specfunc
66
entry_fn = compiled[job.source].specfunc
77
else
@@ -55,6 +55,11 @@ function irgen(@nospecialize(job::CompilerJob))
5555
new_name = safe_name(old_name)
5656
if old_name != new_name
5757
LLVM.name!(val, new_name)
58+
val = get(gv_to_value, old_name, nothing)
59+
if val !== nothing
60+
delete!(gv_to_value, old_name)
61+
gv_to_value[new_name] = val
62+
end
5863
end
5964
end
6065

@@ -120,7 +125,7 @@ function irgen(@nospecialize(job::CompilerJob))
120125
can_throw(job) || lower_throw!(mod)
121126
end
122127

123-
return mod, compiled
128+
return mod, compiled, gv_to_value
124129
end
125130

126131

src/jlgen.jl

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -788,23 +788,76 @@ function compile_method_instance(@nospecialize(job::CompilerJob))
788788
cache_gbl = nothing
789789
end
790790

791-
if VERSION >= v"1.13.0-DEV.623"
792-
# Since Julia 1.13, the caller is responsible for initializing global variables that
793-
# point to global values or bindings with their address in memory.
791+
# Since Julia 1.13, the caller is responsible for initializing global variables that
792+
# point to global values or bindings with their address in memory.
793+
# Similarly on previous versions when imaging=true, it is also the caller's responsibility
794+
# (see https://github.com/JuliaGPU/GPUCompiler.jl/issues/753), but we can support this on versions
795+
# that have HAS_LLVM_GVS_GLOBALS.
796+
gvs = nothing
797+
inits = nothing
798+
@static if VERSION >= v"1.13.0-DEV.623"
794799
num_gvars = Ref{Csize_t}(0)
795800
@ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t},
796-
C_NULL::Ptr{Cvoid})::Nothing
801+
C_NULL::Ptr{Cvoid}
802+
)::Nothing
797803
gvs = Vector{Ptr{LLVM.API.LLVMOpaqueValue}}(undef, num_gvars[])
798804
@ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t},
799-
gvs::Ptr{LLVM.API.LLVMOpaqueValue})::Nothing
805+
gvs::Ptr{LLVM.API.LLVMOpaqueValue}
806+
)::Nothing
807+
800808
inits = Vector{Ptr{Cvoid}}(undef, num_gvars[])
801809
@ccall jl_get_llvm_gv_inits(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t},
802810
inits::Ptr{Cvoid})::Nothing
811+
elseif HAS_LLVM_GVS_GLOBALS
812+
if VERSION >= v"1.12.0-DEV.1703"
813+
num_gvars = Ref{Csize_t}(0)
814+
@ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t},
815+
C_NULL::Ptr{Cvoid}
816+
)::Nothing
817+
gvs = Vector{Ptr{LLVM.API.LLVMOpaqueValue}}(undef, num_gvars[])
818+
@ccall jl_get_llvm_gvs_globals(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t},
819+
gvs::Ptr{LLVM.API.LLVMOpaqueValue}
820+
)::Nothing
821+
inits = Vector{Ptr{Cvoid}}(undef, num_gvars[])
822+
@ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, num_gvars::Ptr{Csize_t},
823+
inits::Ptr{Cvoid}
824+
)::Nothing
825+
else
826+
gvs = get_llvm_global_vars(native_code)
827+
inits = get_llvm_global_inits(native_code)
828+
end
829+
end
803830

831+
# Maintain a map from global variables to their initialized Julia values.
832+
# The objects pointed to are perma-rooted, during codegen.
833+
# It is legal to call `Base.unsafe_pointer_to_objref` on `values(gv_to_value)`,
834+
# but x->pointer_from_objref(Base.unsafe_pointer_to_objref(x)) is not idempotent,
835+
# thus we store raw pointers here.
836+
# Currently GVs are privatized, so users may have to handle embedded pointers,
837+
# but this dictionary provides a clear indication that the embedded pointer is
838+
# indeed avalid Julia object.
839+
gv_to_value = Dict{String, Ptr{Cvoid}}()
840+
841+
# On certain version of Julia we have no reliable way to match the `gvs` to their initializers `inits`.
842+
if gvs === nothing
843+
# global variables here properly.
844+
for gv in globals(llvm_mod)
845+
if !haskey(metadata(gv), "julia.constgv")
846+
continue
847+
end
848+
gv_to_value[LLVM.name(gv)] = C_NULL
849+
end
850+
else
851+
@assert inits !== nothing
804852
for (gv_ref, init) in zip(gvs, inits)
805853
gv = GlobalVariable(gv_ref)
806-
val = const_inttoptr(ConstantInt(Int64(init)), LLVM.PointerType())
807-
initializer!(gv, val)
854+
gv_to_value[LLVM.name(gv)] = init
855+
# set the initializer
856+
# TODO(vc): To enable full relocation we should actually strip out the initializers here.
857+
if LLVM.isnull(initializer(gv))
858+
val = const_inttoptr(ConstantInt(Int64(init)), LLVM.PointerType())
859+
initializer!(gv, val)
860+
end
808861
end
809862
end
810863

@@ -918,7 +971,7 @@ function compile_method_instance(@nospecialize(job::CompilerJob))
918971
# ensure that the requested method instance was compiled
919972
@assert haskey(compiled, job.source)
920973

921-
return llvm_mod, compiled
974+
return llvm_mod, compiled, gv_to_value
922975
end
923976

924977
# partially revert JuliaLangjulia#49391

src/utils.jl

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,59 @@ function kernels(mod::LLVM.Module)
182182
end
183183
return vals
184184
end
185+
186+
@static if VERSION < v"1.13.0-DEV.623"
187+
import Libdl
188+
189+
const HAS_LLVM_GVS_GLOBALS = Libdl.dlsym(
190+
unsafe_load(cglobal(:jl_libjulia_handle, Ptr{Cvoid})), :jl_get_llvm_gvs_globals, throw_error=false) !== nothing
191+
192+
const AL_N_INLINE = 29
193+
194+
# Mirrors arraylist_t
195+
mutable struct ArrayList
196+
len::Csize_t
197+
max::Csize_t
198+
items::Ptr{Ptr{Cvoid}}
199+
_space::NTuple{AL_N_INLINE, Ptr{Cvoid}}
200+
201+
function ArrayList()
202+
list = new(0, AL_N_INLINE, Ptr{Ptr{Cvoid}}(C_NULL), ntuple(_ -> Ptr{Cvoid}(C_NULL), AL_N_INLINE))
203+
list.items = Base.pointer_from_objref(list) + fieldoffset(typeof(list), 4)
204+
205+
finalizer(list) do list
206+
if list.items != Base.pointer_from_objref(list) + fieldoffset(typeof(list), 4)
207+
Libc.free(list.items)
208+
end
209+
end
210+
return list
211+
end
212+
end
213+
214+
function get_llvm_global_vars(native_code::Ptr{Cvoid})
215+
gvs_list = ArrayList()
216+
GC.@preserve gvs_list begin
217+
p_gvs = Base.pointer_from_objref(gvs_list)
218+
@ccall jl_get_llvm_gvs_globals(native_code::Ptr{Cvoid}, p_gvs::Ptr{Cvoid})::Nothing
219+
gvs = Vector{Ptr{LLVM.API.LLVMOpaqueValue}}(undef, gvs_list.len)
220+
items = Base.unsafe_convert(Ptr{Ptr{LLVM.API.LLVMOpaqueValue}}, gvs_list.items)
221+
for i in 1:gvs_list.len
222+
gvs[i] = unsafe_load(items, i)
223+
end
224+
end
225+
return gvs
226+
end
227+
228+
function get_llvm_global_inits(native_code::Ptr{Cvoid})
229+
inits_list = ArrayList()
230+
GC.@preserve inits_list begin
231+
p_inits = Base.pointer_from_objref(inits_list)
232+
@ccall jl_get_llvm_gvs(native_code::Ptr{Cvoid}, p_inits::Ptr{Cvoid})::Nothing
233+
inits = Vector{Ptr{Cvoid}}(undef, inits_list.len)
234+
for i in 1:inits_list.len
235+
inits[i] = unsafe_load(inits_list.items, i)
236+
end
237+
end
238+
return inits
239+
end
240+
end

test/helpers/native.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ struct CompilerParams <: AbstractCompilerParams
1414
new(entry_safepoint, method_table)
1515
end
1616

17+
module Runtime end
18+
1719
NativeCompilerJob = CompilerJob{NativeCompilerTarget,CompilerParams}
18-
GPUCompiler.runtime_module(::NativeCompilerJob) = TestRuntime
20+
GPUCompiler.runtime_module(::NativeCompilerJob) = Runtime
1921

2022
GPUCompiler.method_table(@nospecialize(job::NativeCompilerJob)) = job.config.params.method_table
2123
GPUCompiler.can_safepoint(@nospecialize(job::NativeCompilerJob)) = job.config.params.entry_safepoint
@@ -24,7 +26,7 @@ function create_job(@nospecialize(func), @nospecialize(types);
2426
entry_safepoint::Bool=false, method_table=test_method_table, kwargs...)
2527
config_kwargs, kwargs = split_kwargs(kwargs, GPUCompiler.CONFIG_KWARGS)
2628
source = methodinstance(typeof(func), Base.to_tuple_type(types), Base.get_world_counter())
27-
target = NativeCompilerTarget()
29+
target = NativeCompilerTarget(;jlruntime=true)
2830
params = CompilerParams(entry_safepoint, method_table)
2931
config = CompilerConfig(target, params; kernel=false, config_kwargs...)
3032
CompilerJob(source, config), kwargs

test/native.jl

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,36 @@ end
3636
@testset "compilation database" begin
3737
mod = @eval module $(gensym())
3838
@noinline inner(x) = x+1
39-
function outer(x)
40-
return inner(x)
39+
function outer(x, sym)
40+
if sym == :a
41+
return inner(x)
42+
end
43+
return x
4144
end
4245
end
4346

44-
job, _ = Native.create_job(mod.outer, (Int,))
47+
job, _ = Native.create_job(mod.outer, (Int, Symbol))
4548
JuliaContext() do ctx
46-
ir, meta = GPUCompiler.compile(:llvm, job)
49+
ir, meta = GPUCompiler.compile(:llvm, job; validate=false)
4750

48-
meth = only(methods(mod.outer, (Int,)))
51+
meth = only(methods(mod.outer, (Int, Symbol)))
4952

5053
mis = filter(mi->mi.def == meth, keys(meta.compiled))
5154
@test length(mis) == 1
5255

5356
other_mis = filter(mi->mi.def != meth, keys(meta.compiled))
5457
@test length(other_mis) == 1
5558
@test only(other_mis).def in methods(mod.inner)
59+
60+
if VERSION >= v"1.12"
61+
@test length(meta.gv_to_value) == 1
62+
end
63+
# TODO: Global values get privatized, so we can't find them by name anymore.
64+
# %.not = icmp eq ptr %"sym::Symbol", inttoptr (i64 140096668482288 to ptr), !dbg !38
65+
# for (name, v) in meta.gv_to_value
66+
# gv = globals(ir)[name]
67+
# @test LLVM.initializer(gv) === v
68+
# end
5669
end
5770
end
5871

test/native/precompile.jl

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,34 @@ precompile_test_harness("Inference caching") do load_path
1313
A[1] = x
1414
return
1515
end
16+
17+
function kernel_w_global(A, x, sym)
18+
if sym == :A
19+
A[1] = x
20+
end
21+
return
22+
end
23+
24+
function square(x)
25+
return x*x
26+
end
1627

1728
let
1829
job, _ = NativeCompiler.Native.create_job(kernel, (Vector{Int}, Int))
1930
precompile(job)
2031
end
2132

33+
let
34+
job, _ = NativeCompiler.Native.create_job(kernel_w_global, (Vector{Int}, Int, Symbol))
35+
precompile(job)
36+
end
37+
38+
let
39+
# Emit the func abi to box the return
40+
job, _ = NativeCompiler.Native.create_job(square, (Float64,), entry_abi=:func)
41+
precompile(job)
42+
end
43+
2244
# identity is foreign
2345
@setup_workload begin
2446
job, _ = NativeCompiler.Native.create_job(identity, (Int,))
@@ -28,7 +50,7 @@ precompile_test_harness("Inference caching") do load_path
2850
end
2951
end) |> string)
3052

31-
Base.compilecache(Base.PkgId("NativeBackend"))
53+
Base.compilecache(Base.PkgId("NativeBackend"), stderr, stdout)
3254
@eval let
3355
import NativeCompiler
3456

@@ -47,6 +69,12 @@ precompile_test_harness("Inference caching") do load_path
4769
kernel_mi = GPUCompiler.methodinstance(typeof(NativeBackend.kernel), Tuple{Vector{Int}, Int})
4870
@test check_presence(kernel_mi, token)
4971

72+
kernel_w_global_mi = GPUCompiler.methodinstance(typeof(NativeBackend.kernel_w_global), Tuple{Vector{Int}, Int, Symbol})
73+
@test check_presence(kernel_w_global_mi, token)
74+
75+
square_mi = GPUCompiler.methodinstance(typeof(NativeBackend.square), Tuple{Float64})
76+
@test check_presence(square_mi, token)
77+
5078
# check that identity survived
5179
@test check_presence(identity_mi, token) broken=VERSION>=v"1.12.0-DEV.1268"
5280

test/ptx/precompile.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ precompile_test_harness("Inference caching") do load_path
2525
end
2626
end) |> string)
2727

28-
Base.compilecache(Base.PkgId("PTXBackend"))
28+
Base.compilecache(Base.PkgId("PTXBackend"), stderr, stdout)
2929
@eval let
3030
import PTXCompiler
3131

0 commit comments

Comments
 (0)