From 8909f8cb649131dc52e14155b7dd29a4d8b5a058 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Thu, 1 Oct 2026 12:03:28 +0200 Subject: [PATCH 1/4] Port to LLVM.jl 10 Import the IR and Build vocabularies next to LLVM.Interop, since `using LLVM` no longer exports the API, and use properties instead of the removed accessor functions (parameters, users, operands, value types, names, the module's functions and metadata, ...). Named metadata is no longer created by indexing, so the OpenCL and SPIR-V version nodes are looked up with `get!`. The users of the kernel state are collected before the heap is inserted, as the users view is live and would otherwise include the new insertvalue. The heap arena is inserted at the beginning of the entry block with an explicit insertion point. --- Project.toml | 2 +- src/compiler/compilation.jl | 65 +++++++++++++++++++------------------ src/oneAPI.jl | 3 +- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/Project.toml b/Project.toml index 2a992247..0e15ba26 100644 --- a/Project.toml +++ b/Project.toml @@ -42,7 +42,7 @@ GPUArrays = "11.5.14" GPUCompiler = "2.9" GPUToolbox = "3.3.1" KernelAbstractions = "0.9.39" -LLVM = "6, 7, 8, 9" +LLVM = "10" NEO_jll = "=26.18.38308" PrecompileTools = "1" Preferences = "1" diff --git a/src/compiler/compilation.jl b/src/compiler/compilation.jl index 8f463103..42041233 100644 --- a/src/compiler/compilation.jl +++ b/src/compiler/compilation.jl @@ -59,12 +59,14 @@ function GPUCompiler.finish_module!(job::oneAPICompilerJob, mod::LLVM.Module, job, mod, entry) # OpenCL 2.0 - push!(metadata(mod)["opencl.ocl.version"], + push!( + get!(mod.metadata, "opencl.ocl.version").operands, MDNode([ConstantInt(Int32(2)), ConstantInt(Int32(0))])) # SPIR-V 1.5 - push!(metadata(mod)["opencl.spirv.version"], + push!( + get!(mod.metadata, "opencl.spirv.version").operands, MDNode([ConstantInt(Int32(1)), ConstantInt(Int32(5))])) @@ -105,21 +107,22 @@ function add_heap!(mod::LLVM.Module, entry::LLVM.Function) heap_field = Base.fieldindex(KernelState, :heap) - 1 uses_heap(mod, T_state, heap_field) || return false - params = parameters(entry) - if isempty(params) || value_type(params[1]) != T_state - error("kernel `$(LLVM.name(entry))` allocates but has no kernel state to hold the heap") + params = entry.parameters + if isempty(params) || params[1].value_type != T_state + error("kernel `$(entry.name)` allocates but has no kernel state to hold the heap") end state = params[1] - users = LLVM.Value[user(use) for use in uses(state)] + # collect the users now, before the insertvalue below becomes one of them + users = collect(state.users) T_size = convert(LLVMType, Csize_t) T_heap = LLVM.StructType([T_size, T_size, LLVM.ArrayType(LLVM.Int8Type(), HEAP_SIZE)]) T_ptr = convert(LLVMType, fieldtype(KernelState, :heap)) @dispose builder = IRBuilder() begin - position!(builder, first(instructions(first(blocks(entry))))) + position!(builder, LLVM.at_begin(entry.entry)) heap = alloca!(builder, T_heap, "heap") - alignment!(heap, HEAP_ALIGNMENT) + heap.alignment = HEAP_ALIGNMENT store!(builder, ConstantInt(T_size, 0), struct_gep!(builder, T_heap, heap, 0)) store!(builder, ConstantInt(T_size, HEAP_SIZE), struct_gep!(builder, T_heap, heap, 1)) @@ -127,7 +130,7 @@ function add_heap!(mod::LLVM.Module, entry::LLVM.Function) ptr = pointercast!(builder, heap, T_ptr) new_state = insert_value!(builder, state, ptr, heap_field, "state") for u in users - ops = operands(u) + ops = u.operands for i in 1:length(ops) ops[i] == state && (ops[i] = new_state) end @@ -139,9 +142,9 @@ end # Inspect field reads rather than calls to malloc, which may already have been inlined. function uses_heap(mod::LLVM.Module, T_state::LLVMType, heap_field::Integer) - for f in functions(mod), bb in blocks(f), inst in instructions(bb) + for f in mod.functions, bb in f.blocks, inst in bb.instructions inst isa LLVM.ExtractValueInst || continue - value_type(operands(inst)[1]) == T_state || continue + inst.operands[1].value_type == T_state || continue unsafe_load(LLVM.API.LLVMGetIndices(inst)) == heap_field && return true end return false @@ -165,30 +168,30 @@ function lower_bfloat_to_i16!(mod::LLVM.Module) eliminate_bf16_bitcasts!(mod, T_bf16, T_i16) # Phase 2: Replace remaining bfloat GEPs, loads, and stores with i16 equivalents. - for f in functions(mod) - isempty(blocks(f)) && continue - for bb in blocks(f) + for f in mod.functions + isempty(f.blocks) && continue + for bb in f.blocks to_replace = LLVM.Instruction[] - for inst in instructions(bb) + for inst in bb.instructions opcode = LLVM.API.LLVMGetInstructionOpcode(inst) if opcode == LLVM.API.LLVMGetElementPtr src_ty = LLVMType(LLVM.API.LLVMGetGEPSourceElementType(inst)) src_ty == T_bf16 && push!(to_replace, inst) elseif opcode == LLVM.API.LLVMLoad - value_type(inst) == T_bf16 && push!(to_replace, inst) + inst.value_type == T_bf16 && push!(to_replace, inst) elseif opcode == LLVM.API.LLVMStore - value_type(LLVM.operands(inst)[1]) == T_bf16 && push!(to_replace, inst) + inst.operands[1].value_type == T_bf16 && push!(to_replace, inst) end end for inst in to_replace opcode = LLVM.API.LLVMGetInstructionOpcode(inst) builder = LLVM.IRBuilder() - LLVM.position!(builder, inst) + position!(builder, LLVM.before(inst)) if opcode == LLVM.API.LLVMGetElementPtr - ptr = LLVM.operands(inst)[1] - indices = LLVM.Value[LLVM.operands(inst)[i] for i in 2:length(LLVM.operands(inst))] + ptr = inst.operands[1] + indices = LLVM.Value[inst.operands[i] for i in 2:length(inst.operands)] new_gep = if LLVM.API.LLVMIsInBounds(inst) != 0 LLVM.inbounds_gep!(builder, T_i16, ptr, indices) else @@ -196,12 +199,12 @@ function lower_bfloat_to_i16!(mod::LLVM.Module) end LLVM.replace_uses!(inst, new_gep) elseif opcode == LLVM.API.LLVMLoad - ptr = LLVM.operands(inst)[1] + ptr = inst.operands[1] new_load = LLVM.load!(builder, T_i16, ptr) LLVM.replace_uses!(inst, new_load) elseif opcode == LLVM.API.LLVMStore - val = LLVM.operands(inst)[1] - ptr = LLVM.operands(inst)[2] + val = inst.operands[1] + ptr = inst.operands[2] LLVM.store!(builder, val, ptr) end @@ -219,15 +222,15 @@ function eliminate_bf16_bitcasts!(mod::LLVM.Module, T_bf16::LLVMType, T_i16::LLV changed = true while changed changed = false - for f in functions(mod) - isempty(blocks(f)) && continue - for bb in blocks(f) + for f in mod.functions + isempty(f.blocks) && continue + for bb in f.blocks to_delete = LLVM.Instruction[] - for inst in instructions(bb) + for inst in bb.instructions if LLVM.API.LLVMGetInstructionOpcode(inst) == LLVM.API.LLVMBitCast - src = LLVM.operands(inst)[1] - src_ty = value_type(src) - dst_ty = value_type(inst) + src = inst.operands[1] + src_ty = src.value_type + dst_ty = inst.value_type if (src_ty == T_i16 && dst_ty == T_bf16) || (src_ty == T_bf16 && dst_ty == T_i16) || (src_ty == dst_ty) @@ -323,7 +326,7 @@ function compile_to_obj(@nospecialize(job::CompilerJob)) GPUCompiler.compile(:obj, job) end - (image=asm, entry=LLVM.name(meta.entry)) + (image=asm, entry=meta.entry.name) end # link the SPIR-V bytes into a session-local `ZeKernel` on the given context and device. diff --git a/src/oneAPI.jl b/src/oneAPI.jl index 6b6db42c..d402308e 100644 --- a/src/oneAPI.jl +++ b/src/oneAPI.jl @@ -14,8 +14,7 @@ import Preferences import KernelAbstractions: KernelAbstractions -using LLVM -using LLVM.Interop +using LLVM, LLVM.IR, LLVM.Build, LLVM.Interop using Core: LLVMPtr import Libdl From 9404b5e9661a76ad48a6732ff4666acb74ee34e2 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Thu, 1 Oct 2026 12:05:33 +0200 Subject: [PATCH 2/4] Use LLVM.jl's API instead of the C API Dispatch on the instruction types rather than on opcodes from the C API, and use the source_element_type, inbounds and indices properties and erase! instead of the corresponding C functions. --- src/compiler/compilation.jl | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/compiler/compilation.jl b/src/compiler/compilation.jl index 42041233..107c42aa 100644 --- a/src/compiler/compilation.jl +++ b/src/compiler/compilation.jl @@ -145,7 +145,7 @@ function uses_heap(mod::LLVM.Module, T_state::LLVMType, heap_field::Integer) for f in mod.functions, bb in f.blocks, inst in bb.instructions inst isa LLVM.ExtractValueInst || continue inst.operands[1].value_type == T_state || continue - unsafe_load(LLVM.API.LLVMGetIndices(inst)) == heap_field && return true + first(inst.indices) == heap_field && return true end return false end @@ -173,42 +173,39 @@ function lower_bfloat_to_i16!(mod::LLVM.Module) for bb in f.blocks to_replace = LLVM.Instruction[] for inst in bb.instructions - opcode = LLVM.API.LLVMGetInstructionOpcode(inst) - if opcode == LLVM.API.LLVMGetElementPtr - src_ty = LLVMType(LLVM.API.LLVMGetGEPSourceElementType(inst)) - src_ty == T_bf16 && push!(to_replace, inst) - elseif opcode == LLVM.API.LLVMLoad + if inst isa LLVM.GetElementPtrInst + inst.source_element_type == T_bf16 && push!(to_replace, inst) + elseif inst isa LLVM.LoadInst inst.value_type == T_bf16 && push!(to_replace, inst) - elseif opcode == LLVM.API.LLVMStore + elseif inst isa LLVM.StoreInst inst.operands[1].value_type == T_bf16 && push!(to_replace, inst) end end for inst in to_replace - opcode = LLVM.API.LLVMGetInstructionOpcode(inst) builder = LLVM.IRBuilder() position!(builder, LLVM.before(inst)) - if opcode == LLVM.API.LLVMGetElementPtr + if inst isa LLVM.GetElementPtrInst ptr = inst.operands[1] indices = LLVM.Value[inst.operands[i] for i in 2:length(inst.operands)] - new_gep = if LLVM.API.LLVMIsInBounds(inst) != 0 + new_gep = if inst.inbounds LLVM.inbounds_gep!(builder, T_i16, ptr, indices) else LLVM.gep!(builder, T_i16, ptr, indices) end LLVM.replace_uses!(inst, new_gep) - elseif opcode == LLVM.API.LLVMLoad + elseif inst isa LLVM.LoadInst ptr = inst.operands[1] new_load = LLVM.load!(builder, T_i16, ptr) LLVM.replace_uses!(inst, new_load) - elseif opcode == LLVM.API.LLVMStore + elseif inst isa LLVM.StoreInst val = inst.operands[1] ptr = inst.operands[2] LLVM.store!(builder, val, ptr) end - LLVM.API.LLVMInstructionEraseFromParent(inst) + erase!(inst) LLVM.dispose(builder) end end @@ -227,7 +224,7 @@ function eliminate_bf16_bitcasts!(mod::LLVM.Module, T_bf16::LLVMType, T_i16::LLV for bb in f.blocks to_delete = LLVM.Instruction[] for inst in bb.instructions - if LLVM.API.LLVMGetInstructionOpcode(inst) == LLVM.API.LLVMBitCast + if inst isa LLVM.BitCastInst src = inst.operands[1] src_ty = src.value_type dst_ty = inst.value_type @@ -241,7 +238,7 @@ function eliminate_bf16_bitcasts!(mod::LLVM.Module, T_bf16::LLVMType, T_i16::LLV end end for inst in to_delete - LLVM.API.LLVMInstructionEraseFromParent(inst) + erase!(inst) end end end From 60fa25327e07061b6e8ff089ef2391d7b8058386 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Thu, 1 Oct 2026 12:08:36 +0200 Subject: [PATCH 3/4] Simplify the heap and bfloat IR rewrites with LLVM.jl 10 functionality Pass the heap's alignment to alloca!, replace the kernel state in the operands of its users with replace!, and refer to the operands of loads, stores and GEPs by name. The bfloat lowering now uses a single builder (with @dispose) that is repositioned before every instruction it rewrites, instead of creating and disposing of one per instruction, and it skips declarations with isdeclaration. LLVM.jl 10 only supports LLVM 15 and later, which all have BFloatType, so drop that check. --- src/compiler/compilation.jl | 79 +++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 43 deletions(-) diff --git a/src/compiler/compilation.jl b/src/compiler/compilation.jl index 107c42aa..925cdd7e 100644 --- a/src/compiler/compilation.jl +++ b/src/compiler/compilation.jl @@ -92,7 +92,7 @@ function GPUCompiler.finish_ir!(job::oneAPICompilerJob, mod::LLVM.Module, # (`supports_bfloat16 = false`, see `_compiler_config`), so this pass never # runs there. target = job.config.target - if @static(isdefined(Core, :BFloat16) && isdefined(LLVM, :BFloatType)) && + if isdefined(Core, :BFloat16) && target.supports_bfloat16 && !occursin("SPV_KHR_bfloat16", target.extensions) lower_bfloat_to_i16!(mod) end @@ -121,8 +121,7 @@ function add_heap!(mod::LLVM.Module, entry::LLVM.Function) @dispose builder = IRBuilder() begin position!(builder, LLVM.at_begin(entry.entry)) - heap = alloca!(builder, T_heap, "heap") - heap.alignment = HEAP_ALIGNMENT + heap = alloca!(builder, T_heap, "heap"; align = HEAP_ALIGNMENT) store!(builder, ConstantInt(T_size, 0), struct_gep!(builder, T_heap, heap, 0)) store!(builder, ConstantInt(T_size, HEAP_SIZE), struct_gep!(builder, T_heap, heap, 1)) @@ -130,10 +129,7 @@ function add_heap!(mod::LLVM.Module, entry::LLVM.Function) ptr = pointercast!(builder, heap, T_ptr) new_state = insert_value!(builder, state, ptr, heap_field, "state") for u in users - ops = u.operands - for i in 1:length(ops) - ops[i] == state && (ops[i] = new_state) - end + replace!(u.operands, state => new_state) end end @@ -168,45 +164,42 @@ function lower_bfloat_to_i16!(mod::LLVM.Module) eliminate_bf16_bitcasts!(mod, T_bf16, T_i16) # Phase 2: Replace remaining bfloat GEPs, loads, and stores with i16 equivalents. - for f in mod.functions - isempty(f.blocks) && continue - for bb in f.blocks - to_replace = LLVM.Instruction[] - for inst in bb.instructions - if inst isa LLVM.GetElementPtrInst - inst.source_element_type == T_bf16 && push!(to_replace, inst) - elseif inst isa LLVM.LoadInst - inst.value_type == T_bf16 && push!(to_replace, inst) - elseif inst isa LLVM.StoreInst - inst.operands[1].value_type == T_bf16 && push!(to_replace, inst) + @dispose builder = IRBuilder() begin + for f in mod.functions + isdeclaration(f) && continue + for bb in f.blocks + to_replace = LLVM.Instruction[] + for inst in bb.instructions + if inst isa LLVM.GetElementPtrInst + inst.source_element_type == T_bf16 && push!(to_replace, inst) + elseif inst isa LLVM.LoadInst + inst.value_type == T_bf16 && push!(to_replace, inst) + elseif inst isa LLVM.StoreInst + inst.value_operand.value_type == T_bf16 && push!(to_replace, inst) + end end - end - for inst in to_replace - builder = LLVM.IRBuilder() - position!(builder, LLVM.before(inst)) - - if inst isa LLVM.GetElementPtrInst - ptr = inst.operands[1] - indices = LLVM.Value[inst.operands[i] for i in 2:length(inst.operands)] - new_gep = if inst.inbounds - LLVM.inbounds_gep!(builder, T_i16, ptr, indices) - else - LLVM.gep!(builder, T_i16, ptr, indices) + for inst in to_replace + position!(builder, LLVM.before(inst)) + + if inst isa LLVM.GetElementPtrInst + ptr = inst.pointer_operand + new_gep = if inst.inbounds + LLVM.inbounds_gep!(builder, T_i16, ptr, inst.indices) + else + LLVM.gep!(builder, T_i16, ptr, inst.indices) + end + LLVM.replace_uses!(inst, new_gep) + elseif inst isa LLVM.LoadInst + ptr = inst.pointer_operand + new_load = LLVM.load!(builder, T_i16, ptr) + LLVM.replace_uses!(inst, new_load) + elseif inst isa LLVM.StoreInst + LLVM.store!(builder, inst.value_operand, inst.pointer_operand) end - LLVM.replace_uses!(inst, new_gep) - elseif inst isa LLVM.LoadInst - ptr = inst.operands[1] - new_load = LLVM.load!(builder, T_i16, ptr) - LLVM.replace_uses!(inst, new_load) - elseif inst isa LLVM.StoreInst - val = inst.operands[1] - ptr = inst.operands[2] - LLVM.store!(builder, val, ptr) - end - erase!(inst) - LLVM.dispose(builder) + erase!(inst) + end end end end @@ -220,7 +213,7 @@ function eliminate_bf16_bitcasts!(mod::LLVM.Module, T_bf16::LLVMType, T_i16::LLV while changed changed = false for f in mod.functions - isempty(f.blocks) && continue + isdeclaration(f) && continue for bb in f.blocks to_delete = LLVM.Instruction[] for inst in bb.instructions From 1bd40b02271db92c1790b9801f800fe0e91335fe Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Thu, 1 Oct 2026 14:05:19 +0200 Subject: [PATCH 4/4] Dispose of the compiled IR before leaving the context GPUCompiler now hands the module that Julia's code generator produced to the caller of compile, so the IR of every kernel that compile_to_obj compiled was leaked, keeping its context alive (memcheck reports one undisposed module per compilation). compile_to_obj only needs the name of the entry point: read it and dispose of the IR inside the JuliaContext block. This also stops reading the entry function after the context has been exited, which only worked because the native code kept the context alive. --- src/compiler/compilation.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/compiler/compilation.jl b/src/compiler/compilation.jl index 925cdd7e..b1ce7533 100644 --- a/src/compiler/compilation.jl +++ b/src/compiler/compilation.jl @@ -312,11 +312,13 @@ end # session-portable so they survive precompilation when stored on a cached `CodeInstance`. function compile_to_obj(@nospecialize(job::CompilerJob)) # TODO: on 1.9, this actually creates a context. cache those. - asm, meta = JuliaContext() do ctx - GPUCompiler.compile(:obj, job) + return JuliaContext() do ctx + asm, meta = GPUCompiler.compile(:obj, job) + # the IR belongs to us: dispose of it, or it leaks along with the context + @dispose ir = meta.ir begin + (image = asm, entry = meta.entry.name) + end end - - (image=asm, entry=meta.entry.name) end # link the SPIR-V bytes into a session-local `ZeKernel` on the given context and device.