From 70c2fadff6c28a07a9e305aeb556b4cd38a0aa6d Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 28 Oct 2026 13:58:01 +0100 Subject: [PATCH 1/2] Link kernels through a callable struct rather than a closure `clfunction` passed a closure to `GPUCompiler.cached_lookup` to link the kernel on a cache miss. The closure's type includes the kernel's function and argument types, so every new kernel compiled it again, together with the `methodinstance` call inlined into it. Use a callable struct holding these types as values instead, which is compiled once. This saves about 7 ms of host compilation for every new kernel (PoCL, Julia 1.13). --- src/compiler/execution.jl | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/compiler/execution.jl b/src/compiler/execution.jl index 4cc7a24d..deecde26 100644 --- a/src/compiler/execution.jl +++ b/src/compiler/execution.jl @@ -272,12 +272,21 @@ function clfunction(f::F, tt::TT=Tuple{}; source::S=f, kwargs...) where {F,TT,S} # copies of arrays, like `extensions`, which the caller may mutate. options = map(v -> v isa AbstractArray ? copy(v) : v, kw) key = (cl.context(), cl.device(), program_backend(), options) - kernel, device_rng = GPUCompiler.cached_lookup(Tuple{cl.Kernel,Bool}, F, tt, key) do - clfunction_link(methodinstance(F, tt); kw...) - end + kernel, device_rng = GPUCompiler.cached_lookup(KernelLinker(F, tt, kw), + Tuple{cl.Kernel,Bool}, F, tt, key) HostKernel{F,S,tt}(f, source, kernel, device_rng) end +# what `clfunction` calls when the kernel isn't cached: a callable struct rather than a +# closure, whose type would depend on the kernel's, compiling it (and the inlined +# `methodinstance`) again for every kernel. +struct KernelLinker{K} + ft::Type + tt::Type + kw::K +end +(l::KernelLinker)() = clfunction_link(methodinstance(l.ft, l.tt); l.kw...) + # the part of `clfunction` that doesn't depend on the kernel's function and argument types, # kept separate so that it's compiled once rather than for every kernel function clfunction_link(mi::Core.MethodInstance; kwargs...) From a65c54b4e0d0b84873af9ccb6952744010f65fea Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 28 Oct 2026 13:58:01 +0100 Subject: [PATCH 2/2] Precompile displaying and freeing arrays The first `show` of a `CLArray` compiled for about 400 ms, and on the REPL's `TTYTerminal` for about 950 ms, while the first garbage collection that frees an array compiled `free` for 30 ms. The workload doesn't use a device, so it can't do either; precompile them explicitly, displaying arrays in USM, the preferred memory back-end where it is supported. With PoCL and Julia 1.13, the first display of a `CLVector` takes 34 ms instead of 419 ms on an `IOBuffer`, and 46 ms instead of 969 ms on a `TTYTerminal`. The image grows from 9.6 to 13.1 MB, without a measurable change in load time. --- src/OpenCL.jl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/OpenCL.jl b/src/OpenCL.jl index 38a83779..345eb9f0 100644 --- a/src/OpenCL.jl +++ b/src/OpenCL.jl @@ -66,4 +66,20 @@ export OpenCLBackend eval(Expr(:public, :synchronize, Symbol("@sync"))) end +# displaying an array (in USM, the preferred memory back-end where supported), which the +# device-free workload can't do, compiles for 150 to 400 ms otherwise. the REPL displays on +# a `TTYTerminal`, which is part of Base on 1.12+. +let ios = @static isdefined(Base, :Terminals) ? (IOBuffer, Base.Terminals.TTYTerminal) : + (IOBuffer,) + for io in ios, N in (1, 2) + precompile(Tuple{typeof(show), IOContext{io}, MIME"text/plain", + CLArray{Float32,N,cl.UnifiedDeviceMemory}}) + end +end + +# freeing an array, which otherwise compiles in the first garbage collection that frees one +for M in (cl.UnifiedDeviceMemory, cl.SharedVirtualMemory, cl.Buffer) + precompile(Tuple{typeof(free), Managed{M}}) +end + end