From 03c26db7c1fd91f09dd5f8371f948bcbf7d9cbe7 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 28 Oct 2026 08:37:40 +0100 Subject: [PATCH 1/2] SPIRVIntrinsics: define device functions with GPUToolbox `@device_function` defined its body as a consistent overlay, which lets inference constant-fold calls into the host method's error. Implement `@device_override` and `@device_function` with GPUToolbox's shared definitions instead, where the body of a device function is a plain overlay. The table still resolves in the caller's module, so back ends that use these macros keep defining methods in their own table. Co-Authored-By: Claude Opus 5.5 --- lib/intrinsics/Project.toml | 6 ++--- lib/intrinsics/src/SPIRVIntrinsics.jl | 3 +-- lib/intrinsics/src/utils.jl | 33 ++++----------------------- 3 files changed, 7 insertions(+), 35 deletions(-) diff --git a/lib/intrinsics/Project.toml b/lib/intrinsics/Project.toml index 098da6d1..6d15a660 100644 --- a/lib/intrinsics/Project.toml +++ b/lib/intrinsics/Project.toml @@ -1,10 +1,9 @@ name = "SPIRVIntrinsics" uuid = "71d1d633-e7e8-4a92-83a1-de8814b09ba8" authors = ["Tim Besard "] -version = "1.5.1" +version = "1.6.0" [deps] -ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" GPUToolbox = "096a3bc2-3ced-46d0-87f4-dd12716f4bfc" LLVM = "929cbde3-209d-540e-8aea-75f648917ca0" UnsafeAtomics = "013be700-e6cd-48c3-b4a1-df204f14c38f" @@ -22,9 +21,8 @@ SPIRVIntrinsicsSIMDSpecialFunctionsExt = ["SIMD", "SpecialFunctions"] SPIRVIntrinsicsSpecialFunctionsExt = "SpecialFunctions" [compat] -ExprTools = "0.1" GPUCompiler = "2.13" -GPUToolbox = "0.2, 0.3, 1, 2, 3" +GPUToolbox = "3.5" LLVM = "10" SIMD = "3.6" SpecialFunctions = "1.3, 2" diff --git a/lib/intrinsics/src/SPIRVIntrinsics.jl b/lib/intrinsics/src/SPIRVIntrinsics.jl index a59fd2e5..f9b5fae1 100644 --- a/lib/intrinsics/src/SPIRVIntrinsics.jl +++ b/lib/intrinsics/src/SPIRVIntrinsics.jl @@ -3,10 +3,9 @@ module SPIRVIntrinsics using LLVM, LLVM.IR, LLVM.Build, LLVM.Interop using Core: LLVMPtr -import ExprTools - import UnsafeAtomics +import GPUToolbox using GPUToolbox: u32 include("pointer.jl") diff --git a/lib/intrinsics/src/utils.jl b/lib/intrinsics/src/utils.jl index 0054ee4e..6b402012 100644 --- a/lib/intrinsics/src/utils.jl +++ b/lib/intrinsics/src/utils.jl @@ -103,37 +103,12 @@ Base.Experimental.@MethodTable(method_table) const HAS_CONSISTENT_OVERLAY = VERSION >= v"1.12.0-DEV.745" || v"1.11-rc1" <= VERSION < v"1.12-" +# `method_table` resolves in the caller's module, so that back ends that use these macros +# define their overrides in their own method table macro device_override(ex) - # `method_table` is not interpolated so that the local backend method_table is used - if HAS_CONSISTENT_OVERLAY - # this requires that the overlay method f′ is consistent with f, i.e., - # - if f(x) returns a value, f′(x) must return the identical value. - # - if f(x) throws an exception, f′(x) must also throw an exception - # (although the exceptions do not need to be identical). - # in return, calls that only reach overlays through their error paths (e.g. - # `checked_add` via `throw_overflowerr_binaryop`) remain eligible for concrete - # evaluation, which e.g. keyword-argument handling relies on. - esc(quote - Base.Experimental.@consistent_overlay(method_table, $ex) - end) - else - esc(quote - Base.Experimental.@overlay(method_table, $ex) - end) - end + esc(GPUToolbox.Overlays.device_override(__module__, GlobalRef(__module__, :method_table), ex)) end macro device_function(ex) - ex = macroexpand(__module__, ex) - def = ExprTools.splitdef(ex) - - # generate a function that errors - def[:body] = quote - error("This function is not intended for use on the CPU") - end - - esc(quote - $(ExprTools.combinedef(def)) - @device_override $ex - end) + esc(GPUToolbox.Overlays.device_function(__module__, GlobalRef(__module__, :method_table), ex)) end From 176227f37a7f7ce596b4e9447073e6132c9b05c7 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 28 Oct 2026 08:37:40 +0100 Subject: [PATCH 2/2] Define the device runtime as overlays, so sysimages load The exception mailbox, `kernel_state` and the RNG state accessors were ordinary methods whose bodies declare device intrinsics, like `julia.gpu.state_getter` and `julia.opencl.random_keys`. Sysimage builds compile such methods for the host, and a sysimage that refers to undefined symbols fails to load. Make them `@device_function`s, look up `initialize_rng_state` through the job's method tables and call it as a void function rather than inferring its return type natively, and test that no method reaches device code on the host with GPUToolbox's `host_leaks`. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 4 ++-- src/compiler/compilation.jl | 13 +++++++------ src/device/random.jl | 10 ++++++---- src/device/runtime.jl | 8 ++++---- test/Project.toml | 1 + test/runtests.jl | 7 +++++++ 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Project.toml b/Project.toml index f2a9d15c..9ca6735b 100644 --- a/Project.toml +++ b/Project.toml @@ -35,7 +35,7 @@ SPIRVIntrinsics = {path = "lib/intrinsics"} Adapt = "4" GPUArrays = "12.1" GPUCompiler = "2.18" -GPUToolbox = "3.4" +GPUToolbox = "3.5" KernelAbstractions = "0.10" KernelInterface = "0.4" LLVM = "10" @@ -48,7 +48,7 @@ Random = "1" Random123 = "1.7.1" RandomNumbers = "1.6.0" Reexport = "1" -SPIRVIntrinsics = "1.5.1" +SPIRVIntrinsics = "1.6" SPIRV_LLVM_Backend_jll = "23" SPIRV_Tools_jll = "2025.1" UnsafeAtomics = "0.4" diff --git a/src/compiler/compilation.jl b/src/compiler/compilation.jl index 983b8838..9e9e5874 100644 --- a/src/compiler/compilation.jl +++ b/src/compiler/compilation.jl @@ -95,8 +95,10 @@ function GPUCompiler.finish_module!(@nospecialize(job::OpenCLCompilerJob), ft = typeof(f) tt = Tuple{} - # create a deferred compilation job for `initialize_rng_state` - src = methodinstance(ft, tt, GPUCompiler.tls_world_age()) + # create a deferred compilation job for `initialize_rng_state`. it is an overlay, so + # look it up in the job's method tables. + src = GPUCompiler.generic_methodinstance( + ft, tt, job.world; method_table_view=GPUCompiler.method_table_view(job)) cfg = CompilerConfig(job.config; kernel=false, name=nothing) job = CompilerJob(src, cfg, job.world) id = length(GPUCompiler.deferred_codegen_jobs) + 1 @@ -123,10 +125,9 @@ function GPUCompiler.finish_module!(@nospecialize(job::OpenCLCompilerJob), end fptr = call!(builder, deferred_codegen_ft, deferred_codegen, [ConstantInt(id)]) - # call the `initialize_rng_state` function - rt = Core.Compiler.return_type(f, tt) - llvm_rt = convert(LLVMType, rt) - llvm_ft = LLVM.FunctionType(llvm_rt) + # call the `initialize_rng_state` function, which returns nothing. inferring its + # return type natively would leave host code for it in sysimages. + llvm_ft = LLVM.FunctionType(LLVM.VoidType()) fptr = inttoptr!(builder, fptr, LLVM.PointerType(llvm_ft)) call!(builder, llvm_ft, fptr) br!(builder, top_bb) diff --git a/src/device/random.jl b/src/device/random.jl index 3ce97e66..9ce37a6d 100644 --- a/src/device/random.jl +++ b/src/device/random.jl @@ -26,22 +26,24 @@ end end # local memory with the actual seed, set by `initialize_rng_state` or overridden by `seed!` -@inline function global_random_keys() +@device_function @inline function global_random_keys() ptr = random_keys()::LLVMPtr{UInt32, AS.Workgroup} return CLDeviceArray{UInt32, 1, AS.Workgroup}((rng_nslots(),), ptr) end # local memory with per-slot counters, incremented when generating numbers -@inline function global_random_counters() +@device_function @inline function global_random_counters() ptr = random_counters()::LLVMPtr{UInt32, AS.Workgroup} return CLDeviceArray{UInt32, 1, AS.Workgroup}((rng_nslots(),), ptr) end -# initialization function, called automatically at the start of each kernel -function initialize_rng_state() +# initialization function, called automatically at the start of each kernel. +# it must return nothing, as the kernel calls it as a void function. +@device_function function initialize_rng_state() slot = rng_slot() @inbounds global_random_keys()[slot] = kernel_state().random_seed @inbounds global_random_counters()[slot] = 0 + return end # generators diff --git a/src/device/runtime.jl b/src/device/runtime.jl index e99a3a48..e8468b98 100644 --- a/src/device/runtime.jl +++ b/src/device/runtime.jl @@ -66,7 +66,7 @@ const EXCEPTION_FRAME_SIZE = sizeof(ExceptionFrame_st) # the mailbox address as carried by the kernel state; null when the device has no # host-accessible memory to put a mailbox in (see `allocate_exception_mailbox`). -@inline exception_info() = reinterpret(ExceptionInfo, kernel_state().exception_info) +@device_function @inline exception_info() = reinterpret(ExceptionInfo, kernel_state().exception_info) @inline has_exception_info(info::ExceptionInfo) = info != reinterpret(ExceptionInfo, 0) # Claims must cover all work-groups and queues on the device. SPIRVIntrinsics' public @@ -176,7 +176,7 @@ macro gputhrow(name::String, reason::String) end end -function signal_exception() +@device_function function signal_exception() info = exception_info() # All faulting work-items signal, including level 0 kernels that do not claim a record. # Kernel completion makes both the status and any recorded details visible to the host. @@ -230,7 +230,7 @@ struct KernelState launch_id::UInt64 end -@inline @generated kernel_state() = GPUCompiler.kernel_state_value(KernelState) +@device_function @inline @generated kernel_state() = GPUCompiler.kernel_state_value(KernelState) ## intrinsics for adding and accessing additional kernel arguments @@ -255,5 +255,5 @@ end end for name in [:random_keys, :random_counters] - @eval @inline $name() = additional_arg_value(LLVMPtr{UInt32, AS.Workgroup}, Val($(QuoteNode(name)))) + @eval @device_function @inline $name() = additional_arg_value(LLVMPtr{UInt32, AS.Workgroup}, Val($(QuoteNode(name)))) end diff --git a/test/Project.toml b/test/Project.toml index 61736319..c42c3cf9 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -5,6 +5,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" GPUCompiler = "61eb1bfa-7361-4325-ad38-22787b887f55" +GPUToolbox = "096a3bc2-3ced-46d0-87f4-dd12716f4bfc" IOCapture = "b5f81e59-6552-4d32-b1f0-c071b021bf89" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" diff --git a/test/runtests.jl b/test/runtests.jl index f7be361b..c51e3bec 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -122,6 +122,13 @@ if isdefined(Test, :detect_closure_boxes) end end +# sysimage builds compile methods for the host, which must not reach device-only code +testsuite["host leaks"] = quote + import GPUToolbox, SPIRVIntrinsics + @test isempty(GPUToolbox.Overlays.host_leaks(OpenCL)) + @test isempty(GPUToolbox.Overlays.host_leaks(SPIRVIntrinsics)) +end + ## filter filter_tests!(testsuite, args)