Skip to content

Commit 714dcab

Browse files
committed
Adapt to GPUCompiler 2
1 parent 4725303 commit 714dcab

7 files changed

Lines changed: 140 additions & 50 deletions

File tree

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ LLVM = "929cbde3-209d-540e-8aea-75f648917ca0"
1717
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
1818
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1919
NEO_jll = "700fe977-ac61-5f37-bbc8-c6c4b2b6a9fd"
20+
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
2021
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
2122
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
2223
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
@@ -37,11 +38,12 @@ Adapt = "4"
3738
CEnum = "0.4, 0.5"
3839
ExprTools = "0.1"
3940
GPUArrays = "11.2.1"
40-
GPUCompiler = "1.6"
41+
GPUCompiler = "2"
4142
GPUToolbox = "0.1, 0.2, 0.3, 1, 3"
4243
KernelAbstractions = "0.9.39"
4344
LLVM = "6, 7, 8, 9"
4445
NEO_jll = "=26.18.38308"
46+
PrecompileTools = "1"
4547
Preferences = "1"
4648
SPIRVIntrinsics = "1"
4749
SPIRV_LLVM_Backend_jll = "22"

src/array.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@ function contains_eltype(T, X)
2828
return false
2929
end
3030

31-
function _device_supports_bfloat16()
31+
function _device_supports_bfloat16(dev=device())
3232
# check the driver extension first
3333
if haskey(
34-
oneL0.extension_properties(driver()),
34+
oneL0.extension_properties(dev.driver),
3535
oneL0.ZE_BFLOAT16_CONVERSIONS_EXT_NAME
3636
)
3737
return true
3838
end
3939
# some drivers (e.g. older versions on PVC/Max) don't advertise the extension,
4040
# but the hardware supports BFloat16 natively. fall back to checking device ID.
41-
dev_id = oneL0.properties(device()).deviceId
41+
dev_id = oneL0.properties(dev).deviceId
4242
# Intel Data Center GPU Max (Ponte Vecchio): device IDs 0x0BD0-0x0BDB
4343
if 0x0BD0 <= dev_id <= 0x0BDB
4444
return true

src/compiler/compilation.jl

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,30 @@ struct oneAPICompilerParams <: AbstractCompilerParams end
44
const oneAPICompilerConfig = CompilerConfig{SPIRVCompilerTarget, oneAPICompilerParams}
55
const oneAPICompilerJob = CompilerJob{SPIRVCompilerTarget,oneAPICompilerParams}
66

7+
"""
8+
oneAPIResults
9+
10+
Cached compilation results for a oneAPI kernel job, managed by
11+
`GPUCompiler.cached_results`. Fields are populated through the compile pipeline:
12+
`image` (SPIR-V bytes) + `entry` after codegen, and `kernels` after the session-local
13+
link onto a Level Zero module. The first two are session-portable (cached through
14+
precompilation); `kernels` is session-local and never populated during precompilation.
15+
`image === nothing` identifies a job that has not been compiled yet.
16+
17+
`kernels` is a small linear cache of `(ZeContext, ZeDevice, ZeKernel)` tuples. The cache
18+
partition already covers everything that affects codegen via `GPUCompiler.cache_owner`, so
19+
the only runtime-visible dimensions left are the Level Zero context and device that own the
20+
linked `ZeKernel` (a `ZeModule` is built for a specific `(context, device)` pair). A linear
21+
scan with `===`/`==` is fastest in the common case (n=1) and stays cheap for the rare
22+
workload that bounces between a handful of contexts or devices.
23+
"""
24+
mutable struct oneAPIResults
25+
image::Union{Nothing, Vector{UInt8}} # SPIR-V binary
26+
entry::Union{Nothing, String}
27+
kernels::Vector{Tuple{ZeContext, ZeDevice, ZeKernel}} # session-local; linear-scanned
28+
oneAPIResults() = new(nothing, nothing, Tuple{ZeContext, ZeDevice, ZeKernel}[])
29+
end
30+
731
GPUCompiler.runtime_module(::oneAPICompilerJob) = oneAPI
832

933
GPUCompiler.method_table_view(job::oneAPICompilerJob) =
@@ -52,9 +76,13 @@ function GPUCompiler.finish_ir!(job::oneAPICompilerJob, mod::LLVM.Module,
5276

5377
# When the device supports BFloat16 but the SPIR-V runtime doesn't accept
5478
# SPV_KHR_bfloat16, lower all bfloat types to i16 so the translator can
55-
# handle the module without the extension.
79+
# handle the module without the extension. Both conditions are read from the
80+
# (device-independent) compiler config so this stays valid without a live
81+
# device: `_compiler_config` sets `supports_bfloat16` from the device and adds
82+
# the `SPV_KHR_bfloat16` extension iff the driver's SPIR-V runtime accepts it.
83+
target = job.config.target
5684
if @static(isdefined(Core, :BFloat16) && isdefined(LLVM, :BFloatType)) &&
57-
_device_supports_bfloat16() && !_driver_supports_bfloat16_spirv()
85+
target.supports_bfloat16 && !occursin("SPV_KHR_bfloat16", target.extensions)
5886
lower_bfloat_to_i16!(mod)
5987
end
6088

@@ -266,24 +294,13 @@ function eliminate_bf16_bitcasts!(mod::LLVM.Module, T_bf16::LLVMType, T_i16::LLV
266294
end
267295

268296

269-
## compiler implementation (cache, configure, compile, and link)
270-
271-
# cache of compilation caches, per device
272-
const _compiler_caches = Dict{ZeDevice, Dict{Any, Any}}()
273-
function compiler_cache(dev::ZeDevice)
274-
cache = get(_compiler_caches, dev, nothing)
275-
if cache === nothing
276-
cache = Dict{Any, Any}()
277-
_compiler_caches[dev] = cache
278-
end
279-
return cache
280-
end
297+
## compiler implementation (configure, compile, and link)
281298

282299
# cache of compiler configurations, per device (but additionally configurable via kwargs)
283300
const _toolchain = Ref{Any}()
284301
const _compiler_configs = Dict{UInt, oneAPICompilerConfig}()
285302
function compiler_config(dev; kwargs...)
286-
h = hash(dev, hash(kwargs))
303+
h = hash(dev.driver, hash(dev, hash(kwargs)))
287304
config = get(_compiler_configs, h, nothing)
288305
if config === nothing
289306
config = _compiler_config(dev; kwargs...)
@@ -292,10 +309,10 @@ function compiler_config(dev; kwargs...)
292309
return config
293310
end
294311
# Whether the driver's SPIR-V runtime accepts the SPV_KHR_bfloat16 extension.
295-
function _driver_supports_bfloat16_spirv()
312+
function _driver_supports_bfloat16_spirv(dev=device())
296313
return @static if isdefined(Core, :BFloat16)
297314
haskey(
298-
oneL0.extension_properties(driver()),
315+
oneL0.extension_properties(dev.driver),
299316
oneL0.ZE_BFLOAT16_CONVERSIONS_EXT_NAME
300317
)
301318
else
@@ -304,26 +321,29 @@ function _driver_supports_bfloat16_spirv()
304321
end
305322

306323
@noinline function _compiler_config(dev; kernel=true, name=nothing, always_inline=false, kwargs...)
307-
supports_fp16 = oneL0.module_properties(device()).fp16flags & oneL0.ZE_DEVICE_MODULE_FLAG_FP16 == oneL0.ZE_DEVICE_MODULE_FLAG_FP16
308-
supports_fp64 = oneL0.module_properties(device()).fp64flags & oneL0.ZE_DEVICE_MODULE_FLAG_FP64 == oneL0.ZE_DEVICE_MODULE_FLAG_FP64
324+
properties = oneL0.module_properties(dev)
325+
supports_fp16 = properties.fp16flags & oneL0.ZE_DEVICE_MODULE_FLAG_FP16 == oneL0.ZE_DEVICE_MODULE_FLAG_FP16
326+
supports_fp64 = properties.fp64flags & oneL0.ZE_DEVICE_MODULE_FLAG_FP64 == oneL0.ZE_DEVICE_MODULE_FLAG_FP64
309327
# Allow BFloat16 in IR if the device supports it (even if the SPIR-V runtime doesn't
310328
# advertise the extension). We lower bfloat→i16 in finish_ir! when needed.
311-
supports_bfloat16 = _device_supports_bfloat16()
329+
supports_bfloat16 = _device_supports_bfloat16(dev)
312330

313331
extensions = String[]
314332
# Only add the SPIR-V extension if the runtime actually supports it
315-
if _driver_supports_bfloat16_spirv()
333+
if _driver_supports_bfloat16_spirv(dev)
316334
push!(extensions, "SPV_KHR_bfloat16")
317335
end
336+
extensions_str = join(map(ext -> "+$ext", extensions), ",")
318337

319338
# create GPUCompiler objects
320-
target = SPIRVCompilerTarget(; extensions, supports_fp16, supports_fp64, supports_bfloat16, kwargs...)
339+
target = SPIRVCompilerTarget(; extensions=extensions_str, supports_fp16, supports_fp64, supports_bfloat16, kwargs...)
321340
params = oneAPICompilerParams()
322341
CompilerConfig(target, params; kernel, name, always_inline)
323342
end
324343

325-
# compile to executable machine code
326-
function compile(@nospecialize(job::CompilerJob))
344+
# run inference + LLVM codegen + SPIR-V emission. returns `(image, entry)`, both
345+
# session-portable so they survive precompilation when stored on a cached `CodeInstance`.
346+
function compile_to_obj(@nospecialize(job::CompilerJob))
327347
# TODO: on 1.9, this actually creates a context. cache those.
328348
asm, meta = JuliaContext() do ctx
329349
GPUCompiler.compile(:obj, job)
@@ -332,10 +352,8 @@ function compile(@nospecialize(job::CompilerJob))
332352
(image=asm, entry=LLVM.name(meta.entry))
333353
end
334354

335-
# link into an executable kernel
336-
function link(@nospecialize(job::CompilerJob), compiled)
337-
ctx = context()
338-
dev = device()
339-
mod = ZeModule(ctx, dev, compiled.image)
340-
kernels(mod)[compiled.entry]
355+
# link the SPIR-V bytes into a session-local `ZeKernel` on the given context and device.
356+
function link_kernel(image::Vector{UInt8}, entry::String, ctx::ZeContext, dev::ZeDevice)
357+
mod = ZeModule(ctx, dev, image)
358+
kernels(mod)[entry]
341359
end

src/compiler/execution.jl

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -241,26 +241,59 @@ end
241241
const zefunction_lock = ReentrantLock()
242242

243243
function zefunction(f::F, tt::TT=Tuple{}; kwargs...) where {F,TT}
244-
dev = device()
245-
246244
Base.@lock zefunction_lock begin
247-
# compile the function
248-
cache = compiler_cache(dev)
249-
source = methodinstance(F, tt)
245+
ctx = context()
246+
dev = device()
250247
config = compiler_config(dev; kwargs...)::oneAPICompilerConfig
251-
fun = GPUCompiler.cached_compilation(cache, source, config, compile, link)
248+
source = methodinstance(F, tt)
249+
job = CompilerJob(source, config)
250+
251+
res = compile_or_lookup(job)::oneAPIResults
252+
253+
# Resolve the ZeKernel for the active context and device. Linear scan over the
254+
# session-local cache; almost always n=1, so this is one `===`/`==` compare.
255+
fun = nothing
256+
@inbounds for (cached_ctx, cached_dev, cached_kernel) in res.kernels
257+
if cached_ctx == ctx && cached_dev == dev
258+
fun = cached_kernel
259+
break
260+
end
261+
end
262+
if fun === nothing
263+
fun = link_kernel(res.image::Vector{UInt8}, res.entry::String, ctx, dev)
264+
# Don't cache session-local kernel handles while precompiling: the results
265+
# struct is serialized into the package image along with its CodeInstance,
266+
# and the handles would come back dangling.
267+
if ccall(:jl_generating_output, Cint, ()) != 1
268+
push!(res.kernels, (ctx, dev, fun))
269+
end
270+
end
252271

253272
# create a callable object that captures the function instance. we don't need to think
254273
# about world age here, as GPUCompiler already does and will return a different object
255274
h = hash(fun, hash(f, hash(tt)))
256-
kernel = get(_kernel_instances, h, nothing)
257-
if kernel === nothing
258-
# create the kernel state object
259-
kernel = HostKernel{F,tt}(f, fun)
260-
_kernel_instances[h] = kernel
261-
end
262-
return kernel::HostKernel{F,tt}
275+
get!(_kernel_instances, h) do
276+
HostKernel{F,tt}(f, fun)
277+
end::HostKernel{F,tt}
278+
end
279+
end
280+
281+
# Look up cached compile artifacts for `job`, compiling on miss. Storage is managed
282+
# by `GPUCompiler.cached_results` (Julia's integrated code cache on 1.11+, which also
283+
# persists artifacts through precompilation; a session-local store on 1.10).
284+
#
285+
# `image === nothing` identifies a `oneAPIResults` that hasn't been compiled yet. The
286+
# `compile_hook` check additionally forces the compile path so reflection-style
287+
# consumers (`@device_code_*`) observe the compilation even on a cache hit.
288+
function compile_or_lookup(@nospecialize(job::CompilerJob))::oneAPIResults
289+
res = GPUCompiler.cached_results(oneAPIResults, job)
290+
if res === nothing || res.image === nothing || GPUCompiler.compile_hook[] !== nothing
291+
compiled = compile_to_obj(job)
292+
res = @something res GPUCompiler.cached_results(oneAPIResults, job)
293+
res.image = compiled.image
294+
res.entry = compiled.entry
263295
end
296+
return res
264297
end
265298

266299
# cache of kernel instances

src/compiler/precompile.jl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using PrecompileTools: @compile_workload
2+
3+
# Warm up the GPUCompiler -> SPIR-V pipeline during precompilation so the first real
4+
# `zefunction` call is cheap. This doesn't need a GPU: SPIR-V codegen is device-independent
5+
# (the target only carries version/extension/capability knobs), and the workload never
6+
# launches anything. It is gated on the SPIR-V LLVM back-end being available so the package
7+
# still precompiles on platforms/toolchains without it.
8+
if SPIRV_LLVM_Backend_jll.is_available()
9+
@compile_workload begin
10+
let
11+
function _precompile_kernel(a)
12+
@inbounds a[1] += 1.0f0
13+
return
14+
end
15+
16+
# Build a device-independent compiler config. `_compiler_config` normally derives
17+
# these knobs from the device; here we use conservative, portable defaults (the
18+
# workload only exercises the pipeline, it does not target a specific device).
19+
target = SPIRVCompilerTarget(; extensions="", supports_fp16=true,
20+
supports_fp64=true, supports_bfloat16=false)
21+
params = oneAPICompilerParams()
22+
config = CompilerConfig(target, params; kernel=true, name=nothing,
23+
always_inline=false)
24+
25+
tt = Tuple{oneDeviceArray{Float32,1,AS.CrossWorkgroup}}
26+
source = methodinstance(typeof(_precompile_kernel), tt)
27+
job = CompilerJob(source, config)
28+
29+
# On Julia < 1.12, driving GPU compilation during precompilation can leak foreign
30+
# MethodInstances into host native compilation; only run the full compile on 1.12+.
31+
@static if VERSION >= v"1.12-"
32+
# Exercise the launch-side cache path and serialize its portable SPIR-V image.
33+
compile_or_lookup(job)
34+
end
35+
end
36+
end
37+
end

src/device/runtime.jl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33

44
## Julia library
55

6-
# reset the runtime cache from global scope, so that any change triggers recompilation
7-
GPUCompiler.reset_runtime()
8-
96
function signal_exception()
107
return
118
end

src/oneAPI.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ include("sorting.jl")
7777
include("indexing.jl")
7878
export oneAPIBackend
7979

80+
# precompilation workload (warms up the SPIR-V compilation pipeline)
81+
include("compiler/precompile.jl")
82+
8083
# Work around a deadlock in Pkg's parallel precompilation on Julia 1.10, where it does
8184
# not pass `loadable_exts` to `Base.compilecache` (the kwarg is accidentally commented
8285
# out in Pkg's precompilation.jl), so a worker precompiling an extension freely loads

0 commit comments

Comments
 (0)