Skip to content

Commit 19091ac

Browse files
committed
Make GPUCompiler IR relocatable across Julia sessions
1 parent 4a4be52 commit 19091ac

10 files changed

Lines changed: 548 additions & 157 deletions

File tree

src/driver.jl

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function compile_unhooked(output::Symbol, @nospecialize(job::CompilerJob))
9393
else
9494
error("Unknown assembly format $output")
9595
end
96-
asm, asm_meta = emit_asm(job, ir, format)
96+
asm, asm_meta = emit_asm(job, ir, ir_meta.host_references, format)
9797

9898
if output == :asm || output == :obj
9999
return asm, (; asm_meta..., ir_meta..., ir)
@@ -192,6 +192,7 @@ const __llvm_initialized = Ref(false)
192192
# finalize the current module. this needs to happen before linking deferred modules,
193193
# since those modules have been finalized themselves, and we don't want to re-finalize.
194194
entry = finish_module!(job, ir, entry)
195+
host_references = classify_gvs!(ir, gv_to_value)
195196

196197
# deferred code generation
197198
has_deferred_jobs = job.config.toplevel && !job.config.only_entry &&
@@ -245,11 +246,9 @@ const __llvm_initialized = Ref(false)
245246
dyn_ir, dyn_meta = @invokelatest deferred_codegen(dyn_job, job)
246247
dyn_entry_fn = LLVM.name(dyn_meta.entry)
247248
merge!(compiled, dyn_meta.compiled)
248-
if haskey(dyn_meta, :gv_to_value)
249-
merge!(gv_to_value, dyn_meta.gv_to_value)
250-
end
251249
@assert context(dyn_ir) == context(ir)
252-
link!(ir, dyn_ir)
250+
link_with_host_references!(ir, host_references, dyn_ir,
251+
dyn_meta.host_references)
253252
changed = true
254253
dyn_entry_fn
255254
end
@@ -292,7 +291,7 @@ const __llvm_initialized = Ref(false)
292291
if job.config.toplevel && job.config.libraries
293292
# load the runtime outside of a timing block (because it recurses into the compiler)
294293
if !uses_julia_runtime(job)
295-
runtime = load_runtime(job)
294+
runtime, runtime_references = load_runtime(job)
296295
end
297296

298297
@tracepoint "Library linking" begin
@@ -301,7 +300,8 @@ const __llvm_initialized = Ref(false)
301300

302301
# GPU run-time library
303302
if !uses_julia_runtime(job)
304-
@tracepoint "runtime library" link!(ir, runtime; only_needed=true)
303+
@tracepoint "runtime library" link_with_host_references!(
304+
ir, host_references, runtime, runtime_references; only_needed=true)
305305
end
306306
end
307307
end
@@ -334,13 +334,12 @@ const __llvm_initialized = Ref(false)
334334

335335
finish_linked_module!(job, ir)
336336

337-
# Materialize isbits and Bool boxes; bake addresses for other objects.
338-
portable = relocate_gvs!(ir, gv_to_value)
339-
portable || mark_session_dependent!(job)
337+
host_references.embedded_pointer |= !materialize_bool_singletons!(ir)
338+
host_references.embedded_pointer && mark_session_dependent!(job)
340339

341340
if job.config.optimize
342341
@tracepoint "optimization" begin
343-
optimize!(job, ir; job.config.opt_level)
342+
optimize!(job, ir, host_references; job.config.opt_level)
344343

345344
# deferred codegen has some special optimization requirements,
346345
# which also need to happen _after_ regular optimization.
@@ -405,26 +404,27 @@ const __llvm_initialized = Ref(false)
405404

406405
if job.config.toplevel && job.config.validate
407406
@tracepoint "validation" begin
408-
check_ir(job, ir)
407+
check_ir(job, ir, host_references)
409408
end
410409
end
411410

412411
if should_verify()
413412
@tracepoint "verification" verify(ir)
414413
end
415414

416-
return ir, (; entry, compiled, gv_to_value)
415+
return ir, (; entry, compiled, host_references)
417416
end
418417

419418
@locked function emit_asm(@nospecialize(job::CompilerJob), ir::LLVM.Module,
420-
format::LLVM.API.LLVMCodeGenFileType)
419+
refs::HostReferences, format::LLVM.API.LLVMCodeGenFileType)
421420
# NOTE: strip after validation to get better errors
422421
if job.config.strip
423422
@tracepoint "Debug info removal" strip_debuginfo!(ir)
424423
end
425424

426425
@tracepoint "LLVM back-end" begin
427-
@tracepoint "preparation" prepare_execution!(job, ir)
426+
@tracepoint "preparation" prepare_execution!(job, ir, refs)
427+
refs.embedded_pointer && mark_session_dependent!(job)
428428

429429
code = @tracepoint "machine-code generation" mcgen(job, ir, format)
430430
end

src/interface.jl

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,82 @@ the check-and-compile sequence (all back-ends already do, e.g. `mtlfunction_lock
429429
"""
430430
function cached_results end
431431

432+
"""
433+
JuliaValueRef(value)
434+
435+
A Julia value used as the serializable identity of a host reference. Resolve it in the active
436+
session with [`resolve_host_reference`](@ref); a loader that writes the resulting address into
437+
device storage must keep `value` rooted for as long as that storage remains reachable.
438+
"""
439+
struct JuliaValueRef
440+
value::Any
441+
end
442+
443+
"""
444+
CGlobalRef(symbol)
445+
446+
A libjulia C data global. Resolving this reference calls `jl_cglobal` for `symbol` and loads
447+
the word stored at that address.
448+
"""
449+
struct CGlobalRef
450+
symbol::Symbol
451+
end
452+
453+
"""
454+
HostReference
455+
456+
A serializable source for a host-derived word: either a [`JuliaValueRef`](@ref) or a
457+
[`CGlobalRef`](@ref).
458+
"""
459+
const HostReference = Union{JuliaValueRef,CGlobalRef}
460+
461+
"""
462+
HostReferences(slots, embedded_pointer)
463+
464+
Host-reference metadata accompanying a module. `slots` maps each object-code symbol to the
465+
source of the word that must be written there. `embedded_pointer` conservatively records that
466+
an unavoidable session pointer has been embedded, so the artifact cannot be serialized into a
467+
package image even if `slots` is empty.
468+
"""
469+
mutable struct HostReferences
470+
slots::Dict{String,HostReference}
471+
embedded_pointer::Bool
472+
end
473+
474+
HostReferences() = HostReferences(Dict{String,HostReference}(), false)
475+
copy_host_references(refs::HostReferences) =
476+
HostReferences(copy(refs.slots), refs.embedded_pointer)
477+
478+
same_host_reference(a::JuliaValueRef, b::JuliaValueRef) = a.value === b.value
479+
same_host_reference(a::CGlobalRef, b::CGlobalRef) = a.symbol === b.symbol
480+
same_host_reference(::HostReference, ::HostReference) = false
481+
482+
"""
483+
resolve_host_reference(ref) -> UInt
484+
485+
Resolve `ref` to its current-session word. Loader-based backends use this after loading the
486+
object and before exposing a callable function.
487+
"""
488+
resolve_host_reference(ref::JuliaValueRef) =
489+
UInt(ccall(:jl_value_ptr, Ptr{Cvoid}, (Any,), ref.value))
490+
function resolve_host_reference(ref::CGlobalRef)
491+
address = ccall(:jl_cglobal, Any, (Any, Any), String(ref.symbol), UInt)
492+
return unsafe_load(address)
493+
end
494+
495+
"""
496+
lower_host_references!(job, mod, refs)
497+
498+
Lower compiler-managed Julia value globals and Julia runtime globals for `job`'s final
499+
backend representation. The default lowering resolves every live reference in the current
500+
Julia session. Backends with a loader relocation mechanism may call
501+
[`emit_host_reference_slots!`](@ref) instead, then resolve and patch those slots after loading.
502+
"""
503+
function lower_host_references!(@nospecialize(job::CompilerJob), mod::LLVM.Module,
504+
refs::HostReferences)
505+
resolve_host_reference_slots!(mod, refs)
506+
end
507+
432508
@static if HAS_INTEGRATED_CACHE
433509

434510
"""
@@ -550,6 +626,8 @@ end
550626
end # HAS_INTEGRATED_CACHE
551627

552628
@public GPUCompilerCacheToken, cache_owner, cached_results
629+
@public JuliaValueRef, CGlobalRef, HostReference, HostReferences
630+
@public lower_host_references!, emit_host_reference_slots!, resolve_host_reference
553631

554632
# the method table to use
555633
#

src/irgen.jl

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,36 @@ function irgen(@nospecialize(job::CompilerJob))
4343
end
4444
end
4545

46-
# sanitize global values (Julia doesn't when using the external codegen policy)
47-
for val in [collect(globals(mod)); collect(functions(mod))]
48-
isdeclaration(val) && continue
46+
# Julia value globals can be declarations. Sanitize every definition as well as every
47+
# mapped global, but leave unrelated external declarations alone because their names are
48+
# part of their ABI.
49+
for val in collect(globals(mod))
4950
old_name = LLVM.name(val)
51+
mapped = haskey(gv_to_value, old_name)
52+
isdeclaration(val) && !mapped && continue
5053
new_name = safe_name(old_name)
5154
if old_name != new_name
5255
LLVM.name!(val, new_name)
53-
val = get(gv_to_value, old_name, nothing)
54-
if val !== nothing
55-
delete!(gv_to_value, old_name)
56-
gv_to_value[new_name] = val
56+
actual_name = LLVM.name(val)
57+
if mapped
58+
init = pop!(gv_to_value, old_name)
59+
haskey(gv_to_value, actual_name) &&
60+
error("Duplicate Julia value global '$actual_name'")
61+
gv_to_value[actual_name] = init
5762
end
5863
end
5964
end
6065

66+
# sanitize defined functions (Julia doesn't when using the external codegen policy)
67+
for val in collect(functions(mod))
68+
isdeclaration(val) && continue
69+
old_name = LLVM.name(val)
70+
new_name = safe_name(old_name)
71+
if old_name != new_name
72+
LLVM.name!(val, new_name)
73+
end
74+
end
75+
6176
# rename and process the entry point
6277
if job.config.name !== nothing
6378
LLVM.name!(entry, safe_name(job.config.name))

0 commit comments

Comments
 (0)