Skip to content

KernelInterface - #624

Closed
christiangnrd wants to merge 9 commits into
mainfrom
interface
Closed

christiangnrd wants to merge 9 commits into
mainfrom
interface

Conversation

@christiangnrd

@christiangnrd christiangnrd commented Aug 22, 2026 •

Copy link
Copy Markdown
Member

Do not merge until KernelInterface has been reviewed and interface fully decided

@christiangnrd
christiangnrd marked this pull request as draft August 22, 2026 18:35
@github-actions

github-actions Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Your PR requires formatting changes to meet the project's style guidelines.
Please consider running Runic (git runic main) to apply these changes.

Click here to view the suggested changes.
diff --git a/lib/level-zero/synchronization.jl b/lib/level-zero/synchronization.jl
index 809a03b..0143c93 100644
--- a/lib/level-zero/synchronization.jl
+++ b/lib/level-zero/synchronization.jl
@@ -28,22 +28,24 @@ Base.isdone(queue::ZeCommandQueue) =
 # the blocking synchronization, marked GC-safe so that it doesn't keep the GC from running
 gcsafe_synchronize(list::ZeImmediateCommandList) =
     @gcsafe_ccall libze_loader.zeCommandListHostSynchronize(
-        list::ze_command_list_handle_t, typemax(UInt64)::UInt64)::ze_result_t
+    list::ze_command_list_handle_t, typemax(UInt64)::UInt64
+)::ze_result_t
 gcsafe_synchronize(queue::ZeCommandQueue) =
     @gcsafe_ccall libze_loader.zeCommandQueueSynchronize(
-        queue::ze_command_queue_handle_t, typemax(UInt64)::UInt64)::ze_result_t
+    queue::ze_command_queue_handle_t, typemax(UInt64)::UInt64
+)::ze_result_t
 
 
 ## bidirectional channel
 
 # custom, unbuffered channel that supports returning a value to the sender
 # without the need for a second channel
-struct BidirectionalChannel{I,O} <: AbstractChannel{I}
+struct BidirectionalChannel{I, O} <: AbstractChannel{I}
     cond_take::Threads.Condition                 # waiting for data to become available
     cond_put::Threads.Condition                  # waiting for a writeable slot
     cond_ret::Threads.Condition                  # waiting for a data to be returned
 
-    function BidirectionalChannel{I,O}() where {I,O}
+    function BidirectionalChannel{I, O}() where {I, O}
         lock = ReentrantLock()
         cond_put = Threads.Condition(lock)
         cond_take = Threads.Condition(lock)
@@ -53,9 +55,9 @@ struct BidirectionalChannel{I,O} <: AbstractChannel{I}
 end
 
 Base.put!(c::BidirectionalChannel{I}, v) where {I} = put!(c, convert(I, v))
-function Base.put!(c::BidirectionalChannel{I,O}, v::I) where {I,O}
+function Base.put!(c::BidirectionalChannel{I, O}, v::I) where {I, O}
     lock(c)
-    try
+    return try
         # wait for a slot to be available
         while isempty(c.cond_take)
             Base.wait(c.cond_put)
@@ -71,9 +73,9 @@ function Base.put!(c::BidirectionalChannel{I,O}, v::I) where {I,O}
     end
 end
 
-function Base.take!(f::Base.Callable, c::BidirectionalChannel{I,O}) where {I,O}
+function Base.take!(f::Base.Callable, c::BidirectionalChannel{I, O}) where {I, O}
     lock(c)
-    try
+    return try
         # notify the producer that we're ready to accept a value
         notify(c.cond_put, nothing, false, false)
 
@@ -123,7 +125,7 @@ end
 ## slow path: synchronize on a separate thread
 
 const MAX_SYNC_THREADS = 4
-const sync_channels = Array{BidirectionalChannel{SyncObject,ze_result_t}}(undef, MAX_SYNC_THREADS)
+const sync_channels = Array{BidirectionalChannel{SyncObject, ze_result_t}}(undef, MAX_SYNC_THREADS)
 const sync_channel_cursor = Threads.Atomic{UInt32}(1)
 const sync_channel_lock = Base.ReentrantLock()
 
@@ -135,6 +137,7 @@ function synchronization_worker(data)
         # wait for work
         take!(gcsafe_synchronize, chan)
     end
+    return
 end
 
 @noinline function create_synchronization_worker(i)
@@ -146,7 +149,7 @@ end
 
         # should be safe to assign before threads are running;
         # any user will just submit work that makes it block
-        sync_channels[i] = BidirectionalChannel{SyncObject,ze_result_t}()
+        sync_channels[i] = BidirectionalChannel{SyncObject, ze_result_t}()
 
         # we don't know what the size of uv_thread_t is, so reserve enough space
         tid = Ref{NTuple{32, UInt8}}(ntuple(i -> 0, 32))
diff --git a/src/compiler/compilation.jl b/src/compiler/compilation.jl
index 7305e04..3a9808f 100644
--- a/src/compiler/compilation.jl
+++ b/src/compiler/compilation.jl
@@ -1,7 +1,7 @@
 ## gpucompiler interface implementation
 
 Base.@kwdef struct oneAPICompilerParams <: AbstractCompilerParams
-    sub_group_size::Union{Nothing,Int} = nothing
+    sub_group_size::Union{Nothing, Int} = nothing
 end
 
 const oneAPICompilerConfig = CompilerConfig{SPIRVCompilerTarget, oneAPICompilerParams}
@@ -281,7 +281,7 @@ function _driver_supports_bfloat16_spirv(dev=device())
     end
 end
 
-@noinline function _compiler_config(dev; kernel=true, name=nothing, always_inline=false, sub_group_size=32, kwargs...)
+@noinline function _compiler_config(dev; kernel = true, name = nothing, always_inline = false, sub_group_size = 32, kwargs...)
     properties = oneL0.module_properties(dev)
     supports_fp16 = properties.fp16flags & oneL0.ZE_DEVICE_MODULE_FLAG_FP16 == oneL0.ZE_DEVICE_MODULE_FLAG_FP16
     supports_fp64 = properties.fp64flags & oneL0.ZE_DEVICE_MODULE_FLAG_FP64 == oneL0.ZE_DEVICE_MODULE_FLAG_FP64
diff --git a/src/context.jl b/src/context.jl
index 6c9e188..36f0cd0 100644
--- a/src/context.jl
+++ b/src/context.jl
@@ -401,7 +401,7 @@ println("GPU work completed")
 
 See also: [`global_stream`](@ref), [`context`](@ref), [`device`](@ref)
 """
-function oneL0.synchronize(s::oneStream; blocking::Bool=false)
+function oneL0.synchronize(s::oneStream; blocking::Bool = false)
     sync = blocking ? oneL0.synchronize : oneL0.nonblocking_synchronize
     sync(s.list)
     q = s.queue
@@ -412,8 +412,8 @@ function oneL0.synchronize(s::oneStream; blocking::Bool=false)
     return
 end
 
-function oneL0.synchronize(; blocking::Bool=false)
-    oneL0.synchronize(global_stream(context(), device()); blocking)
+function oneL0.synchronize(; blocking::Bool = false)
+    return oneL0.synchronize(global_stream(context(), device()); blocking)
 end
 
 # Julia → MKL ordering: everything Julia appended to the task's immediate list must be
diff --git a/src/oneAPIKernels.jl b/src/oneAPIKernels.jl
index 7346df2..4bd05f8 100644
--- a/src/oneAPIKernels.jl
+++ b/src/oneAPIKernels.jl
@@ -73,7 +73,7 @@ end
 
 KI.argconvert(::oneAPIBackend, arg) = kernel_convert(arg)
 
-function KI.kernel_function(backend::oneAPIBackend, f::F, tt::TT=Tuple{}; name = nothing, kwargs...) where {F,TT}
+function KI.kernel_function(backend::oneAPIBackend, f::F, tt::TT = Tuple{}; name = nothing, kwargs...) where {F, TT}
     # compile for the sub-group width that `KI.sub_group_size` promises
     sub_group_size = KI.sub_group_size(backend)
     kern = if sub_group_size > 0
@@ -81,7 +81,7 @@ function KI.kernel_function(backend::oneAPIBackend, f::F, tt::TT=Tuple{}; name =
     else
         zefunction(f, tt; name, backend.always_inline, kwargs...)
     end
-    KI.Kernel{oneAPIBackend, typeof(kern)}(backend, kern)
+    return KI.Kernel{oneAPIBackend, typeof(kern)}(backend, kern)
 end
 
 function KI.launch(obj::KI.Kernel{oneAPIBackend}, groups::Dims{3}, items::Dims{3}, args::Vararg{Any, N}; kwargs...) where {N}
@@ -114,19 +114,21 @@ function device_limits(dev::oneAPI.oneL0.ZeDevice = device())
     limits = get!(task_local_storage(), :oneAPIDeviceLimits) do
         Dict{oneAPI.oneL0.ZeDevice, DeviceLimits}()
     end::Dict{oneAPI.oneL0.ZeDevice, DeviceLimits}
-    get!(limits, dev) do
+    return get!(limits, dev) do
         props = oneAPI.oneL0.compute_properties(dev)
         module_props = oneAPI.oneL0.module_properties(dev)
         # the sub-group width that `kernel_function` compiles for: the width `@oneapi` defaults
         # to if the device supports it, and 0 if the device has no sub-groups
         sg_sizes = props.subGroupSizes
         sub_group_size = 32 in sg_sizes ? 32 : maximum(sg_sizes; init = 0)
-        (; max_work_group_size = props.maxTotalGroupSize,
-           max_work_group_dims = (props.maxGroupSizeX, props.maxGroupSizeY, props.maxGroupSizeZ),
-           max_num_groups = (props.maxGroupCountX, props.maxGroupCountY, props.maxGroupCountZ),
-           sub_group_size,
-           supports_float16 = module_props.flags & oneAPI.oneL0.ZE_DEVICE_MODULE_FLAG_FP16 != 0,
-           supports_float64 = module_props.flags & oneAPI.oneL0.ZE_DEVICE_MODULE_FLAG_FP64 != 0)
+        (;
+            max_work_group_size = props.maxTotalGroupSize,
+            max_work_group_dims = (props.maxGroupSizeX, props.maxGroupSizeY, props.maxGroupSizeZ),
+            max_num_groups = (props.maxGroupCountX, props.maxGroupCountY, props.maxGroupCountZ),
+            sub_group_size,
+            supports_float16 = module_props.flags & oneAPI.oneL0.ZE_DEVICE_MODULE_FLAG_FP16 != 0,
+            supports_float64 = module_props.flags & oneAPI.oneL0.ZE_DEVICE_MODULE_FLAG_FP64 != 0,
+        )
     end
 end
 KI.max_work_group_size(::oneAPIBackend)::Int = device_limits().max_work_group_size
@@ -134,7 +136,7 @@ KI.max_work_group_dims(::oneAPIBackend)::NTuple{3, Int} = device_limits().max_wo
 KI.max_num_groups(::oneAPIBackend)::NTuple{3, Int} = device_limits().max_num_groups
 KI.sub_group_size(::oneAPIBackend)::Int = device_limits().sub_group_size
 function KI.multiprocessor_count(::oneAPIBackend)::Int
-    oneAPI.oneL0.properties(device()).numSlices
+    return oneAPI.oneL0.properties(device()).numSlices
 end
 
 KI.supports_subgroups(::oneAPIBackend) = device_limits().sub_group_size > 0
@@ -199,7 +201,7 @@ end
     sub_group_barrier(SPIRVIntrinsics.LOCAL_MEM_FENCE | SPIRVIntrinsics.GLOBAL_MEM_FENCE)
 end
 
-@device_override function KI.shfl_down(val::T, offset::Integer) where T
+@device_override function KI.shfl_down(val::T, offset::Integer) where {T}
     sub_group_shuffle(val, get_sub_group_local_id() + offset)
 end
 
diff --git a/src/oneAPIKernelsOld.jl b/src/oneAPIKernelsOld.jl
index 854e8ab..a9ecfbb 100644
--- a/src/oneAPIKernelsOld.jl
+++ b/src/oneAPIKernelsOld.jl
@@ -49,7 +49,7 @@ end
 ## Memory Operations
 
 function KA.copyto!(::oneAPIBackend, A, B)
-    copyto!(A, B)
+    return copyto!(A, B)
     # TODO: Address device to host copies in jl being synchronizing
 end
 
@@ -75,11 +75,13 @@ end
 ## Kernel Launch
 
 function KA.mkcontext(kernel::KA.Kernel{oneAPIBackend}, _ndrange, iterspace)
-    KA.CompilerMetadata{KA.ndrange(kernel), KA.DynamicCheck}(_ndrange, iterspace)
+    return KA.CompilerMetadata{KA.ndrange(kernel), KA.DynamicCheck}(_ndrange, iterspace)
 end
-function KA.mkcontext(kernel::KA.Kernel{oneAPIBackend}, I, _ndrange, iterspace,
-                      ::Dynamic) where Dynamic
-    KA.CompilerMetadata{KA.ndrange(kernel), Dynamic}(I, _ndrange, iterspace)
+function KA.mkcontext(
+        kernel::KA.Kernel{oneAPIBackend}, I, _ndrange, iterspace,
+        ::Dynamic
+    ) where {Dynamic}
+    return KA.CompilerMetadata{KA.ndrange(kernel), Dynamic}(I, _ndrange, iterspace)
 end
 
 function KA.launch_config(kernel::KA.Kernel{oneAPIBackend}, ndrange, workgroupsize)
@@ -87,7 +89,7 @@ function KA.launch_config(kernel::KA.Kernel{oneAPIBackend}, ndrange, workgroupsi
         ndrange = (ndrange,)
     end
     if workgroupsize isa Integer
-        workgroupsize = (workgroupsize, )
+        workgroupsize = (workgroupsize,)
     end
 
     # partition checked that the ndrange's agreed
@@ -96,7 +98,7 @@ function KA.launch_config(kernel::KA.Kernel{oneAPIBackend}, ndrange, workgroupsi
     end
 
     iterspace, dynamic = if KA.workgroupsize(kernel) <: KA.DynamicSize &&
-        workgroupsize === nothing
+            workgroupsize === nothing
         # use ndrange as preliminary workgroupsize for autotuning
         # (clamped to 1, since an empty ndrange cannot serve as a workgroup size)
         KA.partition(kernel, ndrange, max.(ndrange, 1))
@@ -116,7 +118,7 @@ function threads_to_workgroupsize(threads, ndrange)
     end
 end
 
-function (obj::KA.Kernel{oneAPIBackend})(args...; ndrange=nothing, workgroupsize=nothing)
+function (obj::KA.Kernel{oneAPIBackend})(args...; ndrange = nothing, workgroupsize = nothing)
     backend = KA.backend(obj)
 
     ndrange, workgroupsize, iterspace, dynamic = KA.launch_config(obj, ndrange, workgroupsize)
diff --git a/src/utils.jl b/src/utils.jl
index 05bceb8..ce3dcac 100644
--- a/src/utils.jl
+++ b/src/utils.jl
@@ -31,13 +31,15 @@ function versioninfo(io::IO=stdout)
     get_module(name::Symbol) = (name, getfield(oneAPI, name))
     function get_module(pkg::Tuple{String, String})
         id = Base.PkgId(Base.UUID(pkg[1]), pkg[2])
-        (pkg[2], get(Base.loaded_modules, id, nothing))
+        return (pkg[2], get(Base.loaded_modules, id, nothing))
     end
 
     println(io, "Julia packages:")
     println(io, "- oneAPI.jl: $(Base.pkgversion(oneAPI))")
-    for pkg in [:GPUArrays, :GPUCompiler, ("63c18a36-062a-441e-b654-da1e3ab1ce7c", "KernelAbstractions"),
-                 :KernelInterface, :LLVM, :SPIRVIntrinsics]
+    for pkg in [
+            :GPUArrays, :GPUCompiler, ("63c18a36-062a-441e-b654-da1e3ab1ce7c", "KernelAbstractions"),
+            :KernelInterface, :LLVM, :SPIRVIntrinsics,
+        ]
         name, mod = get_module(pkg)
         isnothing(mod) || println(io, "- $(name): $(Base.pkgversion(mod))")
     end
diff --git a/test/execution.jl b/test/execution.jl
index 330a161..4df4759 100644
--- a/test/execution.jl
+++ b/test/execution.jl
@@ -756,7 +756,7 @@ end
 
 @testset "cooperative synchronize" begin
     a = oneArray{UInt32}(undef, 64)
-    slow(iters) = @oneapi items=64 slow_kernel(a, UInt32(iters))
+    slow(iters) = @oneapi items = 64 slow_kernel(a, UInt32(iters))
     slow(1)
     synchronize()
     # warm up the slow path of `synchronize`: compiling it would end the calibration early
@@ -785,7 +785,7 @@ end
 
     # blocking synchronization is still available
     slow(1)
-    @test synchronize(; blocking=true) === nothing
+    @test synchronize(; blocking = true) === nothing
 end
 
 ############################################################################################

@christiangnrd
christiangnrd force-pushed the interface branch 5 times, most recently from fa0662b to 8bb33ae Compare August 22, 2026 19:54
@codecov

codecov Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.58696% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.56%. Comparing base (3a233d2) to head (f9acd84).

Files with missing lines Patch % Lines
src/oneAPIKernelsOld.jl 90.35% 11 Missing ⚠️
src/oneAPIKernels.jl 83.63% 9 Missing ⚠️
src/compiler/compilation.jl 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #624      +/-   ##
==========================================
- Coverage   80.73%   80.56%   -0.18%     
==========================================
  Files          56       57       +1     
  Lines        4060     4147      +87     
==========================================
+ Hits         3278     3341      +63     
- Misses        782      806      +24     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@christiangnrd
christiangnrd force-pushed the interface branch 2 times, most recently from 261d87f to e6e9102 Compare September 3, 2026 19:12
@christiangnrd
christiangnrd force-pushed the interface branch 4 times, most recently from 19fba83 to b312ab0 Compare September 22, 2026 20:07
@christiangnrd
christiangnrd marked this pull request as ready for review September 22, 2026 20:09
@michel2323 michel2323 added the alcf-ci Run ALCF Aurora CI (GitLab) on this fork PR label Sep 24, 2026
@christiangnrd
christiangnrd force-pushed the interface branch 4 times, most recently from 4f40b47 to dd7431b Compare September 26, 2026 18:50
christiangnrd and others added 3 commits September 28, 2026 07:08
In preparation for KernelAbstractions becoming a weak dependency.
The `sub_group_size` compiler keyword sets the `intel_reqd_sub_group_size` metadata of the kernel, defaulting to 32.
Implement KernelInterface next to the KernelAbstractions back end, which
moves to oneAPIKernelsOld.jl until the port to KernelAbstractions 0.10.

The per-dimension launch limits (`max_work_group_dims`, `max_num_groups`)
come from the Level Zero compute properties. Every auto-sized launch
queries them, so they are cached per device.

The KernelInterface test suite runs as the `kernelinterface` test. Its
events test is skipped when every submission synchronizes (the Aurora LTS
workaround, ONEAPI_SYNC_EACH_SUBMISSION), as the ordering it checks is not
observable then.

Co-authored-by: Tim Besard <tim.besard@gmail.com>
Like the spill size, cache the kernel's `maxGroupSize` in the `ZeKernel`, so
that `launch_configuration` (and the KernelInterface launch validation, which
checks every explicit work-group size against it) don't query the kernel
properties, which allocates, on every launch.
- subtype `KI.Backend`, and implement `KI.launch` instead of the call method:
  KernelInterface now validates the launch geometry itself;
- implement the four primitive index queries with `% T`, and let
  KernelInterface derive the global ones; the sub-group queries take a type;
- `max_work_group_size(kernel)` is the kernel's legal maximum, and
  `launch_configuration` the recommended size;
- `supports_subgroups`/`supports_shuffle` replace `shfl_down_types`, and
  `supports_float64` and shuffles of `Float16`/`Float64` depend on the active
  device. `kernel_function` compiles for the sub-group width that
  `sub_group_size` reports, as KernelInterface now guarantees;
- `kernel_function` keeps the backend it was given, so `always_inline`
  applies;
- `copyto!` checks the lengths and returns the destination, `device!` returns
  `nothing`, and `device(backend, A)` and `unsafe_free!` are implemented;
- launching a kernel on another device than it was compiled for throws;
- drop `zeros`/`ones`, which KernelInterface implements generically.

The testsuite now skips its events test by itself for back ends that record
events by synchronizing, as this one does, so the explicit skip is gone.

The typed index queries need SPIRVIntrinsics 1.1.3, which truncates the 3-D
built-ins without producing illegal vector types.

KernelInterface 0.3 isn't registered yet, so take it from its branch with
`[sources]`, and develop it on Julia 1.10, which ignores `[sources]`.
`synchronize` blocked the calling thread in the driver until the work had
completed, so no other task could run on it in the meantime, and no other
thread could run the GC either. KernelInterface requires `synchronize` to
be cooperative.

Wait like CUDA.jl does: busy-wait on a non-blocking query first, which keeps
the latency of short operations low, and then block in the driver (GC-safe)
on one of a few dedicated threads, while the calling task waits for it
without blocking the scheduler.

This applies to `synchronize()` and `synchronize(::oneStream)`, i.e., to user
code, KernelAbstractions, KernelInterface and the synchronizing copies;
`synchronize(; blocking=true)` restores the old behavior. The command list
and queue methods, which also run from finalizers, keep blocking.
Julia doesn't specialize a method on `args...` that it only passes through, which
made every launch through KernelInterface's generic launch dispatch dynamically
(+1.2 µs and +1 kB per launch on CUDA).
KernelInterface 0.3 passes the number of work-items to launch as `nitems`,
separately from the bound on the work-group size.
With `ONEAPI_SYNC_EACH_SUBMISSION` set, as on Aurora's LTS stack, every
kernel launch blocked the thread in the driver until the kernel had
completed. `synchronize` then had nothing left to wait for, so no other
task ran in the meantime, which failed the cooperative synchronization
test there. Wait for the launch cooperatively, like `synchronize` does.

Also warm up the slow path of `synchronize` in that test. Otherwise, in a
fresh process, compiling it makes the first calibration step exceed its
time budget, and the kernel is then too short to observe other tasks.
@maleadt

maleadt commented Sep 30, 2026

Copy link
Copy Markdown
Member

#653

@maleadt maleadt closed this Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

alcf-ci Run ALCF Aurora CI (GitLab) on this fork PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants