From 82f0a3b70bed1de8e8b43239f25bf1f295163a64 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 28 Oct 2026 08:55:35 +0100 Subject: [PATCH] Define device-only functions as overlays, so they don't leak into host images Sysimage builds compile every method with a compileable signature for the host. The kernel state accessor, the dynamic allocator and the exception reporting functions call GPUCompiler's device intrinsics, so the image referenced `julia.gpu.state_getter` and `julia.gpu.debug_level` and failed to load. Mark them `@device_function`, which now comes from GPUToolbox through SPIRVIntrinsics 1.6: the host method throws, and the implementation lives in oneAPI's method table. Test that `GPUToolbox.Overlays.host_leaks` finds no other such functions. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 6 ++---- src/device/runtime.jl | 10 +++++----- src/oneAPI.jl | 2 -- test/Project.toml | 1 + test/runtests.jl | 6 ++++++ 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Project.toml b/Project.toml index 8b2f986c7..8d2cd51cd 100644 --- a/Project.toml +++ b/Project.toml @@ -8,7 +8,6 @@ AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" -ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" GPUCompiler = "61eb1bfa-7361-4325-ad38-22787b887f55" GPUToolbox = "096a3bc2-3ced-46d0-87f4-dd12716f4bfc" @@ -41,10 +40,9 @@ AbstractFFTs = "1.5.0" Adapt = "4" CEnum = "0.5, 1" Crayons = "4" -ExprTools = "0.1" GPUArrays = "12.1" GPUCompiler = "2.18" -GPUToolbox = "3.4" +GPUToolbox = "3.5" IntelITT = "0.2" KernelAbstractions = "0.10" KernelInterface = "0.4" @@ -54,7 +52,7 @@ PTI_jll = "1.2" PrecompileTools = "1" PrettyTables = "3" Preferences = "1" -SPIRVIntrinsics = "1.5.1" +SPIRVIntrinsics = "1.6" SPIRV_LLVM_Backend_jll = "23" SPIRV_LLVM_Translator_jll = "23" SPIRV_Tools_jll = "2025.4.0" diff --git a/src/device/runtime.jl b/src/device/runtime.jl index 3ea9be8ac..b8eb9db1b 100644 --- a/src/device/runtime.jl +++ b/src/device/runtime.jl @@ -19,7 +19,7 @@ KernelState(exception_report::UInt64=UInt64(0), exception_claim::UInt64=UInt64(0 KernelState(reinterpret(LLVMPtr{UInt8, AS.Function}, C_NULL), exception_report, exception_claim) -@inline @generated kernel_state() = GPUCompiler.kernel_state_value(KernelState) +@device_function @inline @generated kernel_state() = GPUCompiler.kernel_state_value(KernelState) ## dynamic memory allocation @@ -41,7 +41,7 @@ const HEAP_ALIGNMENT = 16 # Two 64-bit words keep the payload aligned to HEAP_ALIGNMENT. const HEAP_HEADER = 2 * sizeof(Csize_t) -function malloc(sz::Csize_t) +@device_function function malloc(sz::Csize_t) heap = kernel_state().heap heap == reinterpret(LLVMPtr{UInt8, AS.Function}, C_NULL) && return C_NULL @@ -157,7 +157,7 @@ end # Claim the report, returning whether this work-item owns it. Idempotent, so that every # reporting function can check ownership with it. The owner sets the report's status before # exiting, which keeps work-items with the same id in later kernels from claiming it too. -@inline function claim_report!() +@device_function @inline function claim_report!() claim = kernel_state().exception_claim claim == 0 && return false # A relaxed claim suffices: the host only reads the report once the kernel has completed. @@ -179,7 +179,7 @@ string_address(str::CString) = reinterpret(UInt64, str) # Record the exception's name and reason, unless one was recorded already: `@gputhrow` comes # first, and knows a more specific name than GPUCompiler derives from the throw. -@inline function record_exception!(name::CString, reason::Union{CString, Nothing}=nothing) +@device_function @inline function record_exception!(name::CString, reason::Union{CString, Nothing}=nothing) reporting_call(claim_report!) || return false name_ptr = report_field(Val(:name)) if unsafe_load(name_ptr) == 0 @@ -213,7 +213,7 @@ function report_exception_frame(idx, func, file, line) return end -function signal_exception() +@device_function function signal_exception() # At debug level 0 this is the only reporting function, and claims the report. reporting_call(claim_report!) && unsafe_store!(report_field(Val(:status)), 1 % Int32) return diff --git a/src/oneAPI.jl b/src/oneAPI.jl index 299361546..0afc5a32d 100644 --- a/src/oneAPI.jl +++ b/src/oneAPI.jl @@ -6,8 +6,6 @@ using Adapt using GPUCompiler import GPUToolbox -import ExprTools - using SpecialFunctions import Preferences diff --git a/test/Project.toml b/test/Project.toml index af23a69ba..ea679937d 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -6,6 +6,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" +GPUToolbox = "096a3bc2-3ced-46d0-87f4-dd12716f4bfc" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" diff --git a/test/runtests.jl b/test/runtests.jl index 89fb7dc04..ee064c1e2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,6 +22,12 @@ for name in keys(TestSuite.tests) testsuite["gpuarrays/$name"] = :(TestSuite.tests[$name](oneArray)) end +# sysimage builds compile methods for the host, which must not reach device-only code +testsuite["host leaks"] = quote + import GPUToolbox + @test isempty(GPUToolbox.Overlays.host_leaks(oneAPI)) +end + args = parse_args(ARGS) # Optional: spread test workers across all available GPUs, one worker per device