From b5b10d2a34e8999a9393c57372e43d55119ead25 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Sun, 25 Oct 2026 08:05:36 +0100 Subject: [PATCH 1/3] Wrap runtime functions that Julia returns through sret Julia returns values wider than a pointer, other than floating-point and vector ones, through an `sret` argument. On 32-bit hosts this includes the `Int64` and `UInt64` that `unbox_int64` and `unbox_uint64` return, so building the runtime library failed with "Invalid return type for runtime function 'unbox_int64'", breaking every compilation that links it (e.g. any KernelAbstractions kernel on the PoCL back-end on i686). Callers expect runtime functions to return by value, so bridge the two ABIs with a wrapper that calls the specfunc with a stack slot and returns the loaded value, as Julia does for `@cfunction`. The specfunc becomes internal, so the inliner folds it into its only caller. --- src/rtlib.jl | 39 ++++++++++++++++++++++++++++++++------- test/native.jl | 30 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/rtlib.jl b/src/rtlib.jl index dd389b84..600ee7b7 100644 --- a/src/rtlib.jl +++ b/src/rtlib.jl @@ -110,10 +110,10 @@ function emit_function!(mod, relocs::Relocations, config::CompilerConfig, # Keep this intermediate module relocatable even when the final back-end resolves # relocations eagerly. The caller links a fresh copy and lowers the merged sites. new_mod, meta = compile_unhooked(:llvm, rt_job; resolve_relocations=false) - ft = meta.entry.function_type + entry = meta.entry expected_ft = runtime_function_type(rt_job, method) - if ft.return_type != expected_ft.return_type - error("Invalid return type for runtime function '$(method.name)': expected $(expected_ft.return_type), got $(ft.return_type)") + if entry.function_type.return_type != expected_ft.return_type + entry = sret_wrapper!(new_mod, entry, expected_ft, method) end # recent Julia versions include prototypes for all runtime functions, even if unused @@ -125,12 +125,12 @@ function emit_function!(mod, relocs::Relocations, config::CompilerConfig, # rename to the final `gpu_*` name on the per-function module, so the cached bitcode # is immediately link-ready (no per-session rename pass on a cache hit). decl = get(new_mod.functions, name, nothing) - if decl !== nothing && decl != meta.entry - @assert decl.value_type == meta.entry.value_type - replace_uses!(decl, meta.entry) + if decl !== nothing && decl != entry + @assert decl.value_type == entry.value_type + replace_uses!(decl, entry) erase!(decl) end - meta.entry.name = name + entry.name = name io = IOBuffer() write(io, new_mod) @@ -145,6 +145,31 @@ function emit_function!(mod, relocs::Relocations, config::CompilerConfig, return ci::CodeInstance end +# Julia returns values wider than a pointer (e.g. `Int64` on 32-bit hosts) through an +# `sret` argument, while callers of runtime functions expect them to be returned by value. +# Bridge the two with a wrapper, like Julia does for `@cfunction`. +function sret_wrapper!(mod::LLVM.Module, f::LLVM.Function, ft::LLVM.FunctionType, method) + actual_ft = f.function_type + sret = isempty(actual_ft.parameters) ? nothing : + get(f.parameter_attributes[1], :sret, nothing) + T = ft.return_type + if actual_ft.return_type != LLVM.VoidType() || sret === nothing || sret.value != T || + actual_ft.parameters[2:end] != ft.parameters + error("Invalid signature for runtime function '$(method.name)': expected $ft, got $actual_ft") + end + + wrapper = LLVM.Function(mod, "", ft) + f.linkage = LLVM.Linkage.Internal + @dispose builder=IRBuilder() begin + position!(builder, LLVM.at_end(BasicBlock(wrapper, "top"))) + slot = alloca!(builder, T) + call = call!(builder, actual_ft, f, [slot, wrapper.parameters...]) + push!(LLVM.argument_attributes(call)[1], TypeAttribute(:sret, T)) + ret!(builder, load!(builder, T, slot)) + end + return wrapper +end + function runtime_results(::Type{V}, ci::CodeInstance, config::CompilerConfig) where {V} @static if HAS_INTEGRATED_CACHE if supports_relocatable_ir() diff --git a/test/native.jl b/test/native.jl index 8bf1008a..80a5700b 100644 --- a/test/native.jl +++ b/test/native.jl @@ -582,6 +582,36 @@ end end end + @testset "runtime functions returning through sret" begin + # Julia returns values wider than a pointer through an `sret` argument (on 32-bit + # hosts, this includes the `Int64` returned by `unbox_int64`), while callers expect + # runtime functions to return by value. (`@noinline` checks that the wrapped + # function keeps its attributes valid.) + mod = @eval module $(gensym()) + @noinline sret_function(x) = (x, x) + end + rt = GPUCompiler.Runtime.RuntimeMethodInstance( + mod.sret_function, NTuple{2,Int64}, (Int64,), :sret_function, + nothing, nothing, "gpu_sret_function") + job, _ = Native.create_job(mod.sret_function, (Int64,)) + config = GPUCompiler.runtime_config(job) + JuliaContext() do ctx + expected_ft = GPUCompiler.runtime_function_type(job, rt) + @test expected_ft.return_type isa LLVM.ArrayType + + # the second emission is served from the bitcode cache (when supported) + for i in 1:2 + ir = LLVM.Module("runtime") + GPUCompiler.emit_function!(ir, GPUCompiler.Relocations(), config, + job.source, rt, job.world) + f = ir.functions["gpu_sret_function"] + @test f.function_type == expected_ft + @test !isdeclaration(f) + verify(ir) + end + end + end + @testset "runtime relocations" begin # runtime functions like `box_bool` may reference Julia singletons through # `julia.constgv` globals. Keep their Julia identities with the cached bitcode From 96b72e17cca6bcd23877889e77f2fd11a31255e7 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Sun, 25 Oct 2026 08:55:59 +0100 Subject: [PATCH 2/3] Make the tests pass on 32-bit hosts Many tests assumed a 64-bit host: FileCheck patterns and error messages spelling `Int` as `i64`/`Int64`, hand-written IR with 64-bit relocation words (and no data layout, so LLVM's default 64-bit pointers applied), narrow-slot checks using `i32`, and JIT-executed kernels returning `UInt64`, which Julia returns through `sret` on 32-bit hosts while the tests `ccall`ed them as returning a `UInt`. Spell those in terms of the host's word size instead, through new `llvm_int` and `Native.datalayout()` helpers, and have the kernels compute host words. Metal and BPF have 64-bit pointers only, which a 32-bit host's Julia codegen cannot target, so skip their tests there (PTX and GCN are already skipped, as their back-end JLLs are only built for 64-bit hosts). --- test/helpers/native.jl | 4 + test/helpers/test.jl | 4 + test/native.jl | 176 ++++++++++++++++++++++------------------- test/runtests.jl | 8 ++ test/spirv/atomics.jl | 22 ++++-- test/utils.jl | 12 +-- 6 files changed, 131 insertions(+), 95 deletions(-) diff --git a/test/helpers/native.jl b/test/helpers/native.jl index d0f83c3b..1a595c53 100644 --- a/test/helpers/native.jl +++ b/test/helpers/native.jl @@ -20,6 +20,10 @@ end module Runtime end NativeCompilerJob = CompilerJob{NativeCompilerTarget,CompilerParams} + +# the host's data layout, for hand-written IR whose pointers must be as large as the host's +datalayout() = GPUCompiler.llvm_datalayout(NativeCompilerTarget()) + GPUCompiler.runtime_module(::NativeCompilerJob) = Runtime GPUCompiler.method_table(@nospecialize(job::NativeCompilerJob)) = job.config.params.method_table diff --git a/test/helpers/test.jl b/test/helpers/test.jl index 7222a3a7..66a16cd8 100644 --- a/test/helpers/test.jl +++ b/test/helpers/test.jl @@ -30,6 +30,10 @@ using LLVM, LLVM.IR, LLVM.Build, LLVM.Interop value end +# the LLVM type of `Int` and `UInt` (and thus of a relocation word), for checks of IR that +# depends on the host's word size +const llvm_int = "i$(8sizeof(Int))" + # typed/opaque pointer detection for conditional FileCheck checks const typed_ptrs = JuliaContext() do ctx diff --git a/test/native.jl b/test/native.jl index 80a5700b..7696aeaa 100644 --- a/test/native.jl +++ b/test/native.jl @@ -437,19 +437,19 @@ end end # smoke test - job, _ = Native.create_job(mod.kernel, (Int64,)) + job, _ = Native.create_job(mod.kernel, (Int,)) @test @filecheck begin - @check_label "define i64 @{{(julia|j)_kernel_[0-9]+}}" - @check "add i64 %{{[0-9]+}}, 1" + @check_label "define $llvm_int @{{(julia|j)_kernel_[0-9]+}}" + @check "add $llvm_int %{{[0-9]+}}, 1" GPUCompiler.code_llvm(job) end # basic redefinition @eval mod kernel(i) = child(i)+2 - job, _ = Native.create_job(mod.kernel, (Int64,)) + job, _ = Native.create_job(mod.kernel, (Int,)) @test @filecheck begin - @check_label "define i64 @{{(julia|j)_kernel_[0-9]+}}" - @check "add i64 %{{[0-9]+}}, 2" + @check_label "define $llvm_int @{{(julia|j)_kernel_[0-9]+}}" + @check "add $llvm_int %{{[0-9]+}}, 2" GPUCompiler.code_llvm(job) end end @@ -748,6 +748,7 @@ end ptrs = [ccall(:jl_value_ptr, Ptr{Cvoid}, (Any,), x) for x in objs] function slot_module(ptr::Ptr{Cvoid}) llvm_mod = LLVM.Module("test") + llvm_mod.datalayout = Native.datalayout() name = "jl_global#0" LLVM.GlobalVariable(llvm_mod, LLVM.PointerType(LLVM.Int8Type()), name) llvm_mod, Dict(name => ptr) @@ -835,7 +836,7 @@ end @dispose m=m begin relocs = collect!(m, map) rec = only(relocs.records) - @test rec.offset == 8 + @test rec.offset == 16 - sizeof(UInt) GPUCompiler.bake_relocations!(m, relocs) box = m.globals[rec.name] @test length(box.global_value_type.elements) == 3 @@ -845,6 +846,7 @@ end # 1.11, whose backported GV API does not deduplicate); their # content-derived names collide, so later slots must alias the first. @dispose m=LLVM.Module("duplicate slots") begin + m.datalayout = Native.datalayout() gvs = [LLVM.GlobalVariable(m, LLVM.PointerType(LLVM.Int8Type()), "jl_global#$i") for i in 1:2] relocs = collect!(m, Dict("jl_global#1" => ptrs[3], @@ -1007,8 +1009,8 @@ end end @test @filecheck begin - @check_label "define i64 @{{(julia|j)_parent_[0-9]+}}" - @check "call{{.*}} i64 @{{(julia|j)_child_[0-9]+}}" + @check_label "define $llvm_int @{{(julia|j)_parent_[0-9]+}}" + @check "call{{.*}} $llvm_int @{{(julia|j)_child_[0-9]+}}" Native.code_llvm(mod.parent, Tuple{Int}) end end @@ -1431,7 +1433,7 @@ end @noinline produce(cond::Bool, a::Int32) = cond ? a : 1.0 function f(cond::Bool) x = produce(cond, Int32(7)) - word = x isa Float64 ? reinterpret(UInt64, x) : UInt64(0) + word = x isa Float64 ? unsafe_trunc(UInt, x) : UInt(0) return word + UInt(pointer_from_objref(:applied_probe)) end end @@ -1458,7 +1460,7 @@ end @test !isempty(relocs) # the metadata is not consumed obj, _ = GPUCompiler.emit_asm(job, session_mod, LLVM.CodeGenFileType.Object) - expected = reinterpret(UInt64, 1.0) + + expected = UInt(1) + GPUCompiler.resolve_relocation_target( GPUCompiler.JuliaValueRef(:applied_probe)) fptr, lljit, _table = Native.load(Vector{UInt8}(codeunits(obj)), entry, @@ -1563,11 +1565,12 @@ end ref = GPUCompiler.JuliaValueRef(:shared_probe) function shared_object(entry) m = parse(LLVM.Module, """ - @shared_reloc = external global i64 + target datalayout = "$(Native.datalayout())" + @shared_reloc = external global $llvm_int - define i64 @$entry() { - %value = load i64, $(ptr("i64")) @shared_reloc - ret i64 %value + define $llvm_int @$entry() { + %value = load $llvm_int, $(ptr(llvm_int)) @shared_reloc + ret $llvm_int %value }""") relocs = GPUCompiler.Relocations( [GPUCompiler.Relocation(GPUCompiler.SlotSite, "shared_reloc", 0, ref)]) @@ -1609,7 +1612,7 @@ end @noinline produce(cond::Bool, a::Int32) = cond ? a : 2.0 function f(cond::Bool) x = produce(cond, Int32(7)) - word = x isa Float64 ? reinterpret(UInt64, x) : UInt64(0) + word = x isa Float64 ? unsafe_trunc(UInt, x) : UInt(0) return word + UInt(pointer_from_objref(:table_probe)) end end @@ -1628,7 +1631,7 @@ end # the words are read out of the table, not baked into the module @test haskey(meta.ir.globals, Native.RELOC_TABLE_BASE) - expected = reinterpret(UInt64, 2.0) + + expected = UInt(2) + GPUCompiler.resolve_relocation_target( GPUCompiler.JuliaValueRef(:table_probe)) fptr, lljit, table = Native.load(Vector{UInt8}(codeunits(obj)), @@ -1676,8 +1679,8 @@ end function f(cond::Bool) x = produce64(cond, Int32(7)) y = produce32(cond, Int32(8)) - wx = x isa Float64 ? reinterpret(UInt64, x) : UInt64(0) - wy = y isa Float32 ? UInt64(reinterpret(UInt32, y)) : UInt64(0) + wx = x isa Float64 ? unsafe_trunc(UInt, x) : UInt(0) + wy = y isa Float32 ? unsafe_trunc(UInt, y) : UInt(0) return wx + wy end end @@ -1718,60 +1721,61 @@ end GPUCompiler.resolve_relocation_target(GPUCompiler.JuliaValueRef(Symbol(name))) JuliaContext() do ctx m = parse(LLVM.Module, """ + target datalayout = "$(Native.datalayout())" @merged_a = external global ptr @merged_b = external global ptr @merged_c = external global ptr @merged_d = external addrspace(1) global ptr @jl_nothing = external global ptr - define i64 @pick(i64 %i) { + define $llvm_int @pick($llvm_int %i) { top: - switch i64 %i, label %other [ i64 1, label %one - i64 2, label %two ] + switch $llvm_int %i, label %other [ $llvm_int 1, label %one + $llvm_int 2, label %two ] one: br label %join two: br label %join other: - %large = icmp sgt i64 %i, 3 + %large = icmp sgt $llvm_int %i, 3 %other.addr = select i1 %large, ptr @jl_nothing, ptr @merged_c br label %join join: %addr = phi ptr [ @merged_a, %one ], [ @merged_b, %two ], [ %other.addr, %other ] - %word = load i64, ptr %addr, align 16 - ret i64 %word + %word = load $llvm_int, ptr %addr, align 16 + ret $llvm_int %word } - define i64 @constant_cast(i1 %cond) { + define $llvm_int @constant_cast(i1 %cond) { %addr = select i1 %cond, ptr @merged_a, ptr addrspacecast (ptr addrspace(1) @merged_d to ptr) - %word = load i64, ptr %addr - ret i64 %word + %word = load $llvm_int, ptr %addr + ret $llvm_int %word } - define i64 @flip(i64 %n) { + define $llvm_int @flip($llvm_int %n) { top: br label %loop loop: - %k = phi i64 [ 0, %top ], [ %k.next, %loop ] + %k = phi $llvm_int [ 0, %top ], [ %k.next, %loop ] %addr = phi ptr [ @merged_a, %top ], [ %addr.next, %loop ] - %flip = icmp eq i64 %k, 2 + %flip = icmp eq $llvm_int %k, 2 %addr.next = select i1 %flip, ptr @merged_b, ptr %addr - %k.next = add i64 %k, 1 - %done = icmp sge i64 %k.next, %n + %k.next = add $llvm_int %k, 1 + %done = icmp sge $llvm_int %k.next, %n br i1 %done, label %exit, label %loop exit: %word = load ptr, ptr %addr.next - %int = ptrtoint ptr %word to i64 - ret i64 %int + %int = ptrtoint ptr %word to $llvm_int + ret $llvm_int %int } - define i64 @cast(i1 %cond) { + define $llvm_int @cast(i1 %cond) { %addr = select i1 %cond, ptr @merged_a, ptr @merged_b %cast = addrspacecast ptr %addr to ptr addrspace(1) - %word = load i64, ptr addrspace(1) %cast - ret i64 %word + %word = load $llvm_int, ptr addrspace(1) %cast + ret $llvm_int %word }""") relocs = relocations() obj, _ = GPUCompiler.emit_asm(job, m, relocs, LLVM.CodeGenFileType.Object) @@ -1809,24 +1813,26 @@ end # Mixed addresses need not share an address space on the target. m = parse(LLVM.Module, """ + target datalayout = "$(Native.datalayout())" @merged_a = external global ptr - define i64 @mixed(i1 %cond, ptr %other) { + define $llvm_int @mixed(i1 %cond, ptr %other) { %addr = select i1 %cond, ptr @merged_a, ptr %other - %word = load i64, ptr %addr - ret i64 %word + %word = load $llvm_int, ptr %addr + ret $llvm_int %word }""") @test_throws "merged with unsupported address" GPUCompiler.emit_asm( job, m, relocations(), LLVM.CodeGenFileType.Object) # The table contains whole, read-only words. for (body, message) in ( - ("%word = load i32, ptr %addr; ret i32 %word", "Unsupported relocation slot load"), - ("store i64 0, ptr %addr; ret i32 0", "Unsupported use of relocation slot address")) + ("%word = load i16, ptr %addr; ret i16 %word", "Unsupported relocation slot load"), + ("store $llvm_int 0, ptr %addr; ret i16 0", "Unsupported use of relocation slot address")) m = parse(LLVM.Module, """ + target datalayout = "$(Native.datalayout())" @merged_a = external global ptr @merged_b = external global ptr - define i32 @unsupported(i1 %cond) { + define i16 @unsupported(i1 %cond) { %addr = select i1 %cond, ptr @merged_a, ptr @merged_b $(replace(body, "; " => "\n")) }""") @@ -1836,6 +1842,7 @@ end # Slot addresses are not exposed as general storage. m = parse(LLVM.Module, """ + target datalayout = "$(Native.datalayout())" @merged_a = external global ptr @merged_b = external global ptr @@ -1861,6 +1868,7 @@ end jlruntime=false) JuliaContext() do ctx m = parse(LLVM.Module, """ + target datalayout = "$(Native.datalayout())" @slot = external global ptr @jl_float32_type = external global ptr @@ -1869,11 +1877,11 @@ end ret ptr %value } - define i64 @header() { + define $llvm_int @header() { %value = load ptr, ptr @slot, align 8, !nonnull !0, !invariant.load !0 - %header = getelementptr i64, ptr %value, i64 -1 - %word = load i64, ptr %header - ret i64 %word + %header = getelementptr $llvm_int, ptr %value, $llvm_int -1 + %word = load $llvm_int, ptr %header + ret $llvm_int %word } define ptr @type() { @@ -1892,7 +1900,7 @@ end loads = [inst for f in ("value", "type") for inst in m.functions[f].entry.instructions if inst isa LLVM.LoadInst] - @test all(load -> load.value_type == LLVM.Int64Type(), loads) + @test all(load -> load.value_type == LLVM.IntType(8sizeof(UInt)), loads) @test !any(load -> haskey(load.metadata, LLVM.MD_nonnull), loads) @test any(load -> haskey(load.metadata, LLVM.MD_invariant_load), loads) end @@ -1966,7 +1974,7 @@ end # a slot must be word-sized mod = LLVM.Module("errors") - GlobalVariable(mod, LLVM.Int32Type(), "narrow") + GlobalVariable(mod, LLVM.Int16Type(), "narrow") @test_throws "has size" GPUCompiler.foreach_relocation(nop, mod, slot("narrow")) # an interior record must name a definition... @@ -2048,9 +2056,10 @@ end word_ptr = ptr("i8") word_ptr_ptr = ptr(word_ptr) function_word_ptr(name) = GPUCompiler.supports_typed_pointers(ctx) ? - "i64* bitcast (i64 ()* @$name to i64*)" : "ptr @$name" + "$llvm_int* bitcast ($llvm_int ()* @$name to $llvm_int*)" : "ptr @$name" mod = parse(LLVM.Module, """ + target datalayout = "$(Native.datalayout())" declare void @jl_get_pgcstack_resolved() define void @entry() { @@ -2061,6 +2070,7 @@ end @test haskey(mod.functions, "jl_get_pgcstack_resolved") mod = parse(LLVM.Module, """ + target datalayout = "$(Native.datalayout())" @jl_float32_type = external global $word_ptr define $word_ptr @entry() { @@ -2072,9 +2082,10 @@ end @test !occursin("load $word_ptr, $word_ptr_ptr @jl_float32_type", ir) expected = GPUCompiler.resolve_relocation_target( GPUCompiler.CGlobalRef(:jl_float32_type)) - @test occursin("inttoptr (i64 $expected to $word_ptr)", ir) + @test occursin("inttoptr ($llvm_int $(reinterpret(Int, expected)) to $word_ptr)", ir) mod = parse(LLVM.Module, """ + target datalayout = "$(Native.datalayout())" @jl_float32_type = external global $word_ptr define $word_ptr @entry() { @@ -2089,28 +2100,30 @@ end @test rec.offset == 0 # the slot is a symbol a loader addresses, so its name carries the job's namespace @test startswith(rec.name, GPUCompiler.relocation_namespace(job)) - @test occursin("@$(rec.name) = external global i64", string(mod)) + @test occursin("@$(rec.name) = external global $llvm_int", string(mod)) GPUCompiler.emit_patchable_relocations!(mod, relocs) - @test occursin("externally_initialized global i64 0", string(mod)) + @test occursin("externally_initialized global $llvm_int 0", string(mod)) mod = parse(LLVM.Module, """ - declare i64 @jl_float32_type() + target datalayout = "$(Native.datalayout())" + declare $llvm_int @jl_float32_type() - define i64 @entry() { - %value = load i64, $(function_word_ptr("jl_float32_type")) - ret i64 %value + define $llvm_int @entry() { + %value = load $llvm_int, $(function_word_ptr("jl_float32_type")) + ret $llvm_int %value }""") relocs = GPUCompiler.Relocations() @test GPUCompiler.collect_cglobal_relocations!(job, mod, relocs) rec = only(relocs.records) @test rec.target == GPUCompiler.CGlobalRef(:jl_float32_type) - @test occursin("@$(rec.name) = external global i64", string(mod)) + @test occursin("@$(rec.name) = external global $llvm_int", string(mod)) GPUCompiler.emit_patchable_relocations!(mod, relocs) - @test occursin("externally_initialized global i64 0", string(mod)) + @test occursin("externally_initialized global $llvm_int 0", string(mod)) # A global whose address is merged with others before being loaded from (e.g. # `jl_nothing` with the relocation slots of other singletons) takes its slot's address. merged_ir = """ + target datalayout = "$(Native.datalayout())" @jl_float32_type = external global $word_ptr @jl_float64_type = external global $word_ptr @@ -2151,18 +2164,19 @@ end expected = GPUCompiler.resolve_relocation_target( GPUCompiler.CGlobalRef(Symbol("jl_$(lowercase(string(T)))_type"))) @test expected == UInt(pointer_from_objref(T)) - @test occursin("inttoptr (i64 $expected to $word_ptr)", ir) + @test occursin("inttoptr ($llvm_int $(reinterpret(Int, expected)) to $word_ptr)", ir) end # a slot only holds a word, so anything else loaded is left, and reported, as is mod = parse(LLVM.Module, """ - @jl_float32_type = external global i32 - @jl_float64_type = external global i32 - - define i32 @entry(i1 %cond) { - %addr = select i1 %cond, $(ptr("i32")) @jl_float32_type, - $(ptr("i32")) @jl_float64_type - %value = load i32, $(ptr("i32")) %addr - ret i32 %value + target datalayout = "$(Native.datalayout())" + @jl_float32_type = external global i16 + @jl_float64_type = external global i16 + + define i16 @entry(i1 %cond) { + %addr = select i1 %cond, $(ptr("i16")) @jl_float32_type, + $(ptr("i16")) @jl_float64_type + %value = load i16, $(ptr("i16")) %addr + ret i16 %value }""") relocs = GPUCompiler.Relocations() @test !GPUCompiler.collect_cglobal_relocations!(job, mod, relocs) @@ -2705,14 +2719,14 @@ end @test_throws_message(InvalidIRError, Native.code_execution(mod.kernel, Tuple{Ptr{Int}}; jlruntime=false)) do msg - occursin(r"Julia code.*: .*\.g\(::Int64\), with .*\.g::Any", msg) && + occursin(Regex("Julia code.*: .*\\.g\\(::$Int\\), with .*\\.g::Any"), msg) && # Julia 1.14 types a type argument as `Core.TypeEgal{T}` - occursin(r"Julia code.*: convert\(::(Type|Core\.TypeEgal)\{Int64\}, ::Any\)", msg) + occursin(Regex("Julia code.*: convert\\(::(Type|Core\\.TypeEgal)\\{$Int\\}, ::Any\\)"), msg) end @test_throws_message(InvalidIRError, Native.code_execution(mod.typed_kernel, Tuple{Ptr{Int}}; jlruntime=false)) do msg - occursin(r"Julia code.*: .*\.typed\(::Int64\), with .*\.typed::Function", msg) + occursin(Regex("Julia code.*: .*\\.typed\\(::$Int\\), with .*\\.typed::Function"), msg) end # neither callee is known, and both calls are on the same line (so that they are reported @@ -2735,7 +2749,7 @@ end @test precision === :exact else @test precision === :ambiguous - @test any(contains(".g(::Int64)"), code) && any(contains(".h(::Float64)"), code) + @test any(contains(".g(::$Int)"), code) && any(contains(".h(::Float64)"), code) end end end @@ -2801,7 +2815,7 @@ end @test @filecheck begin @check_label "@julia_kernel" - @check "ret i64 0" + @check "ret $llvm_int 0" Native.code_llvm(mod.kernel, Tuple{}) end @@ -2818,7 +2832,7 @@ end @test @filecheck begin @check_label "@julia_kernel" - @check "ret i64 1" + @check "ret $llvm_int 1" Native.code_llvm(mod.kernel, Tuple{}; mod.method_table) end end @@ -2842,7 +2856,7 @@ end end # shared overlays apply to code for devices, but not to code for the host - if :NVPTX in LLVM.backends() + if :NVPTX in LLVM.backends() && Sys.WORD_SIZE == 64 @test @filecheck begin @check_label "@julia_shared_kernel" @check "ret i64 1" @@ -2851,7 +2865,7 @@ end end @test @filecheck begin @check_label "@julia_shared_kernel" - @check "ret i64 0" + @check "ret $llvm_int 0" Native.code_llvm(mod.shared_kernel, Tuple{}) end @test mod.shared_kernel() == 0 @@ -2859,18 +2873,18 @@ end # the tables from `method_tables` come first, in order @test @filecheck begin @check_label "@julia_stacked_kernel" - @check "ret i64 2" + @check "ret $llvm_int 2" Native.code_llvm(mod.stacked_kernel, Tuple{}; method_table=mod.other_method_table) end @test @filecheck begin @check_label "@julia_stacked_kernel" - @check "ret i64 3" + @check "ret $llvm_int 3" Native.code_llvm(mod.stacked_kernel, Tuple{}; method_table=(mod.method_table, mod.other_method_table)) end @test @filecheck begin @check_label "@julia_stacked_kernel" - @check "ret i64 2" + @check "ret $llvm_int 2" Native.code_llvm(mod.stacked_kernel, Tuple{}; method_table=(mod.other_method_table, mod.method_table)) end @@ -2883,7 +2897,7 @@ end kernel() = probe() Base.Experimental.@overlay GPUCompiler.SHARED_METHOD_TABLE flag() = false end - if :NVPTX in LLVM.backends() + if :NVPTX in LLVM.backends() && Sys.WORD_SIZE == 64 @test @filecheck begin @check_label "@julia_kernel" @check "ret i8 0" diff --git a/test/runtests.jl b/test/runtests.jl index 44fd5629..a6a1c784 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -68,6 +68,14 @@ if filter_tests!(testsuite, args) @warn "AMDGPU back-end not available; skipping GCN tests" delete!(testsuite, "gcn") end + if Sys.WORD_SIZE == 32 + # Metal and BPF targets are 64-bit only, and Julia's codegen assumes pointers as + # large as the host's + @warn "32-bit host; skipping Metal and BPF tests" + for key in collect(keys(testsuite)) + (startswith(key, "metal") || startswith(key, "bpf")) && delete!(testsuite, key) + end + end end runtests(GPUCompiler, args; testsuite, init_code) diff --git a/test/spirv/atomics.jl b/test/spirv/atomics.jl index f1ca7f37..1d41c7ac 100644 --- a/test/spirv/atomics.jl +++ b/test/spirv/atomics.jl @@ -157,7 +157,8 @@ end atomics) == [] for body in ["%r = atomicrmw add ptr addrspace(1) %g, i64 1 monotonic, align 8", "%r = cmpxchg ptr addrspace(3) %l, i64 0, i64 1 monotonic monotonic, align 8", - "%r = atomicrmw xchg ptr addrspace(1) %g, ptr null monotonic, align 8", + # (pointers are 64-bit on 64-bit hosts only) + (sizeof(Int) == 8 ? ["%r = atomicrmw xchg ptr addrspace(1) %g, ptr null monotonic, align 8"] : [])..., "%r = load atomic double, ptr addrspace(1) %g monotonic, align 8", "%r = atomicrmw fmax ptr addrspace(1) %g, double 1.0 monotonic, align 8", # (a generic pointer needs the native addition for both global and local memory) @@ -182,6 +183,11 @@ end const MEMORY = 0x380 # SubgroupMemory | WorkgroupMemory | CrossWorkgroupMemory +# pointers are as large as the host's (the target is spirv32 on 32-bit hosts), and their +# atomics are integer ones of that size +const IPTR = "i$(8sizeof(Int))" +const UPTR = sizeof(Int) == 8 ? "%ulong" : "%uint" + @testset "32-bit pointers" begin # pointers are as large as the data layout says, so with 32-bit pointers, their atomics # are 32-bit integer ones (and need no 64-bit integer atomics) @@ -339,10 +345,10 @@ end store ptr %old, ptr addrspace(1) %g """; backend) @test filecheck(ir, - "CHECK: [[NEW:%[0-9a-z]+]] = ptrtoint ptr %p to i64", - "CHECK: [[OLD:%[0-9a-z]+]] = call i64 @$(builtin("CompareExchange", "i64", 1; operands=2))(ptr addrspace(1) %g, i32 1, i32 $MEMORY, i32 $MEMORY, i64 [[NEW]], i64 0)", - "CHECK: inttoptr i64 {{%[0-9]+}} to ptr") - @test filecheck(asm, "CHECK: OpAtomicCompareExchange %ulong") + "CHECK: [[NEW:%[0-9a-z]+]] = ptrtoint ptr %p to $IPTR", + "CHECK: [[OLD:%[0-9a-z]+]] = call $IPTR @$(builtin("CompareExchange", IPTR, 1; operands=2))(ptr addrspace(1) %g, i32 1, i32 $MEMORY, i32 $MEMORY, $IPTR [[NEW]], $IPTR 0)", + "CHECK: inttoptr $IPTR {{%[0-9]+}} to ptr") + @test filecheck(asm, "CHECK: OpAtomicCompareExchange $UPTR") end @testset "floating-point and pointer accesses" begin @@ -357,13 +363,13 @@ end "CHECK: call i32 @$(builtin("Load", "i32", 1; operands=0))(ptr addrspace(1) %g, i32 1, i32 $(MEMORY | 0x2))", "CHECK: call void @$(builtin("Store", "i64", 3))(ptr addrspace(3) %l, i32 2, i32 $(MEMORY | 0x4), i64 4607182418800017408)", "CHECK: call i32 @$(builtin("Exchange", "i32", 1))(ptr addrspace(1) %g, i32 1, i32 $MEMORY, i32 %", - "CHECK: [[P:%[0-9a-z]+]] = ptrtoint ptr %p to i64", - "CHECK: call i64 @$(builtin("Exchange", "i64", 4))(ptr addrspace(4) %generic, i32 1, i32 $MEMORY, i64 [[P]])") + "CHECK: [[P:%[0-9a-z]+]] = ptrtoint ptr %p to $IPTR", + "CHECK: call $IPTR @$(builtin("Exchange", IPTR, 4))(ptr addrspace(4) %generic, i32 1, i32 $MEMORY, $IPTR [[P]])") @test filecheck(asm, "CHECK: OpAtomicLoad %uint %g 1 $(MEMORY | 0x2)", "CHECK: OpAtomicStore %l 2 $(MEMORY | 0x4) 4607182418800017408", "CHECK: OpAtomicExchange %uint %g 1 $MEMORY", - "CHECK: OpAtomicExchange %ulong {{%.+}} 1 $MEMORY") + "CHECK: OpAtomicExchange $UPTR {{%.+}} 1 $MEMORY") end @testset "floating-point addition" begin diff --git a/test/utils.jl b/test/utils.jl index c36f2c40..b73399e9 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -106,7 +106,7 @@ end # unions @test mangle(identity, Union{Int32, Int64}) == "identity(Union)" - @test mangle(identity, Union, Int, Union{Int64, Int32}) == "identity(Union, Int64, Union)" + @test mangle(identity, Union, Int64, Union{Int64, Int32}) == "identity(Union, Int64, Union)" # union alls @test mangle(identity, Array) == "identity(Array)" @@ -115,10 +115,10 @@ end @test mangle(identity, NTuple{2, I} where {I <: Integer}) == "identity(Tuple)" # Vararg - @test mangle(identity, Vararg{Int}) == "identity(Int64, ...)" - @test mangle(identity, Vararg{Int, 2}) == "identity(Int64, Int64)" + @test mangle(identity, Vararg{Int64}) == "identity(Int64, ...)" + @test mangle(identity, Vararg{Int64, 2}) == "identity(Int64, Int64)" @test mangle(identity, Tuple{1, 2}, Tuple{}, Tuple) == "identity(Tuple<1, 2>, Tuple<>, Tuple)" - @test mangle(identity, NTuple{2, Int}) == "identity(Tuple)" + @test mangle(identity, NTuple{2, Int64}) == "identity(Tuple)" @test mangle(identity, Tuple{Vararg{Int}}) == "identity(Tuple<>)" # A `Vararg` whose length is still a `TypeVar`, which is what unwrapping a `UnionAll` # leaves behind. It is the same type as the unbounded `Vararg` above, so it mangles alike. @@ -133,8 +133,8 @@ end # intertwined substitutions @test mangle( - identity, Val{1}, Ptr{Tuple{Ptr{Int}}}, Ptr{Int}, Val{1}, Val{2}, - Tuple{Ptr{Int}}, Tuple{Int8}, Int64, Int8 + identity, Val{1}, Ptr{Tuple{Ptr{Int64}}}, Ptr{Int64}, Val{1}, Val{2}, + Tuple{Ptr{Int64}}, Tuple{Int8}, Int64, Int8 ) == "identity(Val<1>, Tuple*, Int64*, Val<1>, Val<2>, Tuple, Tuple, Int64, Int8)" From edc4b6db5d58734153971119760151eeed990cde Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Sun, 25 Oct 2026 08:55:59 +0100 Subject: [PATCH 3/3] Test on 32-bit Linux Run the test suite with i686 Julia, for every tested version, so that 32-bit hosts (e.g. KernelAbstractions' PoCL back-end on i686) keep working. --- .github/workflows/Test.yml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index ca1ac8e8..f0222143 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -13,7 +13,7 @@ concurrency: jobs: release_test: - name: Julia ${{ matrix.version }} ${{ matrix.llvm_args }} - ${{ matrix.os }} + name: Julia ${{ matrix.version }} ${{ matrix.llvm_args }} - ${{ matrix.os }}${{ matrix.arch && format(' ({0})', matrix.arch) || '' }} runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: @@ -22,6 +22,9 @@ jobs: version: ['1.10', '1.11', '1.12', '1.13', 'nightly'] os: [ubuntu-24.04, ubuntu-24.04-arm, macOS-15, windows-2025] llvm_args: [''] + # the runner's architecture by default (a dimension, so that the 32-bit jobs below + # are added rather than merged into the existing ones) + arch: [''] include: # starting with Julia 1.10, we can enable opaque pointers # from Julia 1.12 on, this is the default. @@ -49,6 +52,22 @@ jobs: - version: '1.11' os: 'windows-2025' llvm_args: '--opaque-pointers' + # 32-bit hosts + - version: '1.10' + os: 'ubuntu-24.04' + arch: 'x86' + - version: '1.11' + os: 'ubuntu-24.04' + arch: 'x86' + - version: '1.12' + os: 'ubuntu-24.04' + arch: 'x86' + - version: '1.13' + os: 'ubuntu-24.04' + arch: 'x86' + - version: 'nightly' + os: 'ubuntu-24.04' + arch: 'x86' steps: - uses: actions/checkout@v7 @@ -56,6 +75,7 @@ jobs: uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.version }} + arch: ${{ matrix.arch || 'default' }} - name: Set-up cache uses: julia-actions/cache@v3