Skip to content

KernelInterface 0.3: a tighter back-end contract - #800

Merged
maleadt merged 9 commits into
mainfrom
tb/ki-0.3
Sep 30, 2026
Merged

maleadt merged 9 commits into
mainfrom
tb/ki-0.3

Conversation

@maleadt

@maleadt maleadt commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

KernelInterface (KI) is the layer that backends (CUDA.jl, Metal.jl, oneAPI.jl, AMDGPU.jl, OpenCL.jl, and PoCL in this repository) implement, and that KernelAbstractions builds on. Porting those backends to KI 0.2.3 showed that the interface left too much to the individual backends: every backend validated and sized launches itself, and the copies had drifted apart; the same query meant different things on different backends; and the index and sub-group queries were underspecified exactly where the platforms disagree. A kernel written against KI could therefore behave differently depending on where it ran.

KI 0.3 fixes that before the ports merge. It moves launch validation into KI, and writes down the semantics that backends have to implement, so that the testsuite can check them. The design doesn't change: kernels are plain Julia functions, device functions are overlay stubs, and a backend value comes with a task-local device and queue. This is a breaking release without deprecations, since KI has no users outside the backends yet. Each commit is one of the changes below.

KernelInterface owns the launch

Backends used to implement the whole Kernel call, including argument validation and work-group sizing. Now KI implements it, and passes validated 3-D sizes to one method a backend implements:

# before, in every backend
function (obj::KI.Kernel{CUDABackend})(args...; numworkgroups=(), workgroupsize=(), ndrange=(), max_work_group_size=typemax(Int))
    KI.check_launch_args(numworkgroups, workgroupsize, ndrange)
    prod(ndrange) == 0 && return nothing
    blocks, threads = KI.auto_launch_sizes(obj, numworkgroups, workgroupsize, ndrange, max_work_group_size)
    obj.kern(args...; threads, blocks)
    return nothing
end

# after
KI.launch(k::KI.Kernel{CUDABackend}, groups::Dims{3}, items::Dims{3}, args::Vararg{Any,N}; kwargs...) where {N} =
    k.kern(args...; threads = items, blocks = groups, kwargs...)

The launch semantics are documented on KI.Kernel: an ndrange is rounded up to whole work-groups and not masked, a zero anywhere launches nothing, and work-group sizes are checked against the device and the compiled kernel. Invalid launches throw before the driver sees them (here on CUDA):

julia> k(c, a, b; workgroupsize = (1, 1, 128))
ERROR: ArgumentError: Work-group size (1, 1, 128) exceeds the maximum of (1024, 1024, 64) per dimension

julia> k(c, a, b; workgroupsize = (1, 0))
ERROR: ArgumentError: `workgroupsize` must be positive, got (1, 0)

julia> k(c, a, b; ndrange = typemax(Int), workgroupsize = 2)
ERROR: ArgumentError: Launch of (4611686018427387904, 1, 1) work-groups of (2, 1, 1) work-items has more than typemax(Int) work-items in a dimension

(The last one used to reach the backend as a negative global size.) Keywords that KI doesn't know are passed on to KI.launch, so backend options like CUDA's stream or shmem still reach the driver.

Two renames come with this. KI.@kernel is now KI.@launch, since it launches a call like @cuda does, while KA.@kernel defines a kernel. numworkgroups is now numgroups, matching get_num_groups and max_num_groups. KI.@launch also evaluates its backend expression once instead of once per argument, and passes keywords it doesn't know to kernel_function as compiler options.

The limit and the recommendation are separate queries

kernel_max_work_group_size returned an occupancy recommendation on CUDA, AMDGPU and oneAPI, and the hard limit on Metal, OpenCL and PoCL. On an RTX 5080 it returns 768 for a trivial kernel that can be launched with 1024 threads. Two queries replace it:

  • max_work_group_size(kernel) is the largest work-group the compiled kernel can be launched with. Launches are validated against it.
  • launch_configuration(kernel; nitems, max_work_group_size) is the recommended work-group size for a launch of nitems work-items. ndrange launches without a workgroupsize use it, and it falls back to the limit. The problem size is passed separately from the bound, so that heuristics like CUDA's prefer_blocks (more, smaller blocks) don't have to treat the bound as the problem size.

max_work_group_dims and max_num_groups are now required. Their typemax(Int) fallbacks meant both "unknown" and "unlimited", and #797 needs them to choose a launch.

Typed index queries wrap

The index queries take a result type, e.g. KI.get_global_id(Int32). The docs said the result is "computed in T" and undefined when it doesn't fit, but the backends implemented it as T(x): a checked conversion, which leaves a throw_inexacterror branch in every kernel that uses it. The result is now the exact value modulo T, as with x % T. That costs nothing, and it can be tested: a UInt8 query over 384 work-items has to wrap, where T(x) throws.

Backends implement the four primitive queries (get_local_id, get_group_id, get_local_size, get_num_groups). get_global_id and get_global_size have fallbacks derived from them for backends without a builtin (CUDA, HIP), and backends that have one (SPIR-V, Metal) should override them.

Sub-groups

  • Support is a capability: supports_subgroups(backend) and supports_shuffle(backend, T) replace shfl_down_types. The testsuite used to treat a non-empty type list as the flag for sub-group support.
  • sub_group_size(backend) is a guarantee instead of "a reasonable size": kernels from kernel_function run with exactly that width, so host code can pick a Val(N) for a warp-level reduction. A backend that can't guarantee it reports no sub-group support; PoCL now fixes the width at compile time.
  • Partial sub-groups follow OpenCL and SYCL: get_sub_group_size() counts the work-items that are present, and get_num_sub_groups() is cld(items, width). AMDGPU used ÷, reporting 0 sub-groups for a 32-item group on wave64.
  • How work-items map onto sub-groups is unspecified, but each work-item has a unique (sub-group id, lane) pair that doesn't change during the kernel. AMDGPU used activelane(), which renumbers lanes under divergence.
  • shfl_down from a lane that doesn't exist returns an unspecified value (CUDA returns the caller's own value, OpenCL leaves it undefined), and shuffles are not memory fences.
  • The sub-group queries take a result type like the index queries, defaulting to Int. They returned UInt32 on most backends and Int32 on CUDA.

Execution, devices and memory

  • Execution is task-local: each task has an active device per backend and a queue on it, which host queries, compilation, allocations, copies and launches use. kernel_function has to store the backend value it was given, so that its options apply (oneAPI dropped its compiler options, and OpenCL its platform, by constructing a new default). A kernel launched after switching devices either works or throws; it never runs on the wrong device.
  • New: device(backend, A), the device that owns A. The device functions are required for backends with more than one device, and the single-device fallbacks now throw when ndevices > 1 instead of answering for the wrong device.
  • Capability defaults are conservative: supports_float64 and supports_atomics now default to false, so that a missing method never claims support.
  • copyto!(backend, dst, src) copies in queue order, returns dst, and throws an ArgumentError for arrays of different lengths. CUDA's implementation copied length(dst) elements from pointer(src) without a check.
  • localmemory and barrier document what memory is shared and which writes become visible. unsafe_free! is an optional hint with a no-op fallback.

The contract

The docs now have a "Semantics" section and a contract table that lists, for each area, the required methods and the optional ones with their fallbacks. The public API is declared with public (Julia 1.11+). There is also a versioning rule: required methods only change in breaking releases, optional methods can be added in any release if their fallback is conservative, and patch releases only add tests for behavior that was already specified. (0.2.3 broke backend CI in a patch release by adding unconditional tests for new obligations.)

The testsuite's entry point is now Testsuite.testsuite(backend, AT), which takes a backend value, so that backends with options can be tested. It checks that the methods without a fallback are implemented, and covers launch validation, non-square launches, copyto!, local memory, barriers, typed-index wrapping, partial sub-groups and shuffles. Two existing launch tests had formulas that only worked because the group count equaled the group size; the tests now use different sizes in every dimension.

For KernelAbstractions users

KernelAbstractions.GPU is removed. It didn't mean GPU hardware: KA's CPU backend is PoCL and subtyped it, so CPU <: GPU. Backends subtype KernelAbstractions.Backend, and code that dispatched on ::GPU should dispatch on ::Backend, on concrete backend types, or on a capability query. This is in the 0.10 changelog.

Backend ports

Each backend's KI PR has commits porting it to 0.3 (they get KI 0.3 from this branch through [sources] until it's registered):

Backend PR KI testsuite
PoCL this PR passes
CUDA.jl JuliaGPU/CUDA.jl#3246 passes, RTX 5080 and CI
Metal.jl JuliaGPU/Metal.jl#917 passes, M1 (Julia 1.10 and 1.12)
oneAPI.jl JuliaGPU/oneAPI.jl#624 port in progress
AMDGPU.jl JuliaGPU/AMDGPU.jl#1047 port in progress
OpenCL.jl JuliaGPU/OpenCL.jl#474 passes, PoCL and NVIDIA (no sub-groups on NVIDIA: the width can't be fixed)

Host launch overhead on CUDA (RTX 5080, 1024-element kernel, per launch) is unchanged, except that launches with an explicit size now also query the kernel's thread limit (about 36 ns):

KI 0.2.3 KI 0.3
@cuda 1.68 µs, 256 B 1.68 µs, 256 B
KI.@launch, explicit sizes 1.85 µs, 416 B 1.95 µs, 416 B
KI.@launch, ndrange 2.03 µs, 416 B 2.04–2.11 µs, 416 B
compiled Kernel, explicit sizes 1.42 µs, 160 B 1.49–1.51 µs, 160 B
compiled Kernel, ndrange 1.66 µs, 160 B 1.70–1.74 µs, 160 B

KA's Buildkite jobs test the backends' KA 0.10 branches, which still require KI 0.2.3, so they can't be installed together with this PR and soft-fail until those branches are rebased onto the ported KI PRs. The ports above were tested against this branch by hand.

Not in this PR

Dynamic local memory, fences and barrier scopes, max_local_memory, more collectives and sub-group size requests can all be added in a minor release; launch options like dynamic local memory can become Kernel keywords without changing KI.launch. #797 and #801, stacked on this PR, use the new contract to launch @kernel kernels on N-d grids with 32-bit indices, and to replace the launch code that every backend copies with one generic implementation in KA.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Benchmark Results

Show table
main 2e505ea... main / 2e505ea...
const/@Const/Float32/262144 0.341 ± 0.028 ms 0.344 ± 0.021 ms 0.992 ± 0.1
const/@Const/Float32/65536 0.131 ± 0.027 ms 0.146 ± 0.042 ms 0.897 ± 0.31
const/@Const/Float64/262144 0.433 ± 0.028 ms 0.454 ± 0.049 ms 0.952 ± 0.12
const/@Const/Float64/65536 0.166 ± 0.013 ms 0.166 ± 0.015 ms 0.999 ± 0.12
const/unmarked/Float32/262144 1.16 ± 0.037 ms 1.17 ± 0.039 ms 0.988 ± 0.045
const/unmarked/Float32/65536 0.325 ± 0.036 ms 0.326 ± 0.036 ms 0.998 ± 0.16
const/unmarked/Float64/262144 1.37 ± 0.035 ms 1.38 ± 0.034 ms 0.992 ± 0.035
const/unmarked/Float64/65536 0.395 ± 0.035 ms 0.392 ± 0.042 ms 1.01 ± 0.14
launch/3D static workgroup, dynamic ndrange 0.0712 ± 0.02 ms 0.0713 ± 0.023 ms 0.999 ± 0.43
launch/3D static workgroup, static ndrange 0.0717 ± 0.023 ms 0.072 ± 0.02 ms 0.996 ± 0.42
launch/dynamic workgroup, dynamic ndrange 0.0739 ± 0.029 ms 0.0734 ± 0.027 ms 1.01 ± 0.54
launch/dynamic workgroup, dynamic ndrange, workgroupsize given 0.064 ± 0.036 ms 0.0693 ± 0.032 ms 0.925 ± 0.68
launch/static workgroup, dynamic ndrange 0.0718 ± 0.018 ms 0.0713 ± 0.023 ms 1.01 ± 0.41
launch/static workgroup, static ndrange 0.0717 ± 0.028 ms 0.0718 ± 0.017 ms 1 ± 0.46
partition/dynamic workgroup, dynamic ndrange 0.0579 ± 0.001 μs 0.0618 ± 0.00087 μs 0.937 ± 0.021
partition/static workgroup, dynamic ndrange 0.0647 ± 0.012 μs 0.0649 ± 0.012 μs 0.997 ± 0.26
partition/static workgroup, static ndrange 1.55 ± 0.009 ns 1.55 ± 0.009 ns 1 ± 0.0082
saxpy/default/Float16/1024 0.0741 ± 0.026 ms 0.0736 ± 0.029 ms 1.01 ± 0.53
saxpy/default/Float16/1048576 0.8 ± 0.042 ms 0.795 ± 0.032 ms 1.01 ± 0.066
saxpy/default/Float16/16384 0.0634 ± 0.033 ms 0.064 ± 0.032 ms 0.99 ± 0.71
saxpy/default/Float16/2048 0.0738 ± 0.028 ms 0.0749 ± 0.028 ms 0.985 ± 0.53
saxpy/default/Float16/256 0.0741 ± 0.023 ms 0.0741 ± 0.028 ms 1 ± 0.5
saxpy/default/Float16/262144 0.252 ± 0.035 ms 0.254 ± 0.035 ms 0.991 ± 0.19
saxpy/default/Float16/32768 0.0735 ± 0.03 ms 0.0744 ± 0.031 ms 0.988 ± 0.58
saxpy/default/Float16/4096 0.0784 ± 0.031 ms 0.0766 ± 0.031 ms 1.02 ± 0.58
saxpy/default/Float16/512 0.0742 ± 0.024 ms 0.0743 ± 0.023 ms 0.998 ± 0.45
saxpy/default/Float16/64 0.0744 ± 0.019 ms 0.0741 ± 0.029 ms 1 ± 0.47
saxpy/default/Float16/65536 0.0959 ± 0.031 ms 0.0965 ± 0.03 ms 0.994 ± 0.44
saxpy/default/Float32/1024 0.0738 ± 0.021 ms 0.074 ± 0.024 ms 0.997 ± 0.43
saxpy/default/Float32/1048576 0.513 ± 0.057 ms 0.51 ± 0.038 ms 1.01 ± 0.14
saxpy/default/Float32/16384 0.0574 ± 0.03 ms 0.0595 ± 0.031 ms 0.965 ± 0.72
saxpy/default/Float32/2048 0.0742 ± 0.029 ms 0.0736 ± 0.029 ms 1.01 ± 0.56
saxpy/default/Float32/256 0.0743 ± 0.024 ms 0.0745 ± 0.024 ms 0.998 ± 0.45
saxpy/default/Float32/262144 0.177 ± 0.032 ms 0.181 ± 0.032 ms 0.98 ± 0.25
saxpy/default/Float32/32768 0.0632 ± 0.031 ms 0.0673 ± 0.032 ms 0.94 ± 0.64
saxpy/default/Float32/4096 0.0713 ± 0.031 ms 0.0719 ± 0.03 ms 0.992 ± 0.59
saxpy/default/Float32/512 0.0734 ± 0.021 ms 0.0733 ± 0.024 ms 1 ± 0.44
saxpy/default/Float32/64 0.074 ± 0.03 ms 0.0748 ± 0.021 ms 0.99 ± 0.48
saxpy/default/Float32/65536 0.0825 ± 0.032 ms 0.0874 ± 0.033 ms 0.943 ± 0.51
saxpy/default/Float64/1024 0.0735 ± 0.019 ms 0.0738 ± 0.021 ms 0.995 ± 0.38
saxpy/default/Float64/1048576 0.652 ± 0.081 ms 0.615 ± 0.094 ms 1.06 ± 0.21
saxpy/default/Float64/16384 0.0566 ± 0.029 ms 0.0606 ± 0.031 ms 0.934 ± 0.68
saxpy/default/Float64/2048 0.0706 ± 0.031 ms 0.0727 ± 0.03 ms 0.972 ± 0.59
saxpy/default/Float64/256 0.0732 ± 0.021 ms 0.0734 ± 0.023 ms 0.997 ± 0.43
saxpy/default/Float64/262144 0.205 ± 0.037 ms 0.192 ± 0.033 ms 1.07 ± 0.27
saxpy/default/Float64/32768 0.0735 ± 0.033 ms 0.0726 ± 0.033 ms 1.01 ± 0.64
saxpy/default/Float64/4096 0.0675 ± 0.029 ms 0.0686 ± 0.028 ms 0.984 ± 0.58
saxpy/default/Float64/512 0.0733 ± 0.017 ms 0.0731 ± 0.021 ms 1 ± 0.37
saxpy/default/Float64/64 0.0747 ± 0.013 ms 0.0749 ± 0.015 ms 0.998 ± 0.27
saxpy/default/Float64/65536 0.095 ± 0.033 ms 0.0983 ± 0.034 ms 0.967 ± 0.47
saxpy/static workgroup=(1024,)/Float16/1024 0.0704 ± 0.029 ms 0.0715 ± 0.028 ms 0.984 ± 0.56
saxpy/static workgroup=(1024,)/Float16/1048576 0.793 ± 0.041 ms 0.793 ± 0.033 ms 1 ± 0.067
saxpy/static workgroup=(1024,)/Float16/16384 0.0598 ± 0.03 ms 0.0612 ± 0.029 ms 0.977 ± 0.68
saxpy/static workgroup=(1024,)/Float16/2048 0.0719 ± 0.029 ms 0.072 ± 0.028 ms 0.998 ± 0.56
saxpy/static workgroup=(1024,)/Float16/256 0.0716 ± 0.026 ms 0.0715 ± 0.028 ms 1 ± 0.53
saxpy/static workgroup=(1024,)/Float16/262144 0.247 ± 0.035 ms 0.25 ± 0.036 ms 0.988 ± 0.2
saxpy/static workgroup=(1024,)/Float16/32768 0.0709 ± 0.031 ms 0.0726 ± 0.031 ms 0.977 ± 0.59
saxpy/static workgroup=(1024,)/Float16/4096 0.0686 ± 0.032 ms 0.0714 ± 0.032 ms 0.959 ± 0.62
saxpy/static workgroup=(1024,)/Float16/512 0.072 ± 0.023 ms 0.0723 ± 0.017 ms 0.996 ± 0.39
saxpy/static workgroup=(1024,)/Float16/64 0.0714 ± 0.027 ms 0.0718 ± 0.027 ms 0.996 ± 0.53
saxpy/static workgroup=(1024,)/Float16/65536 0.093 ± 0.028 ms 0.0964 ± 0.03 ms 0.965 ± 0.42
saxpy/static workgroup=(1024,)/Float32/1024 0.0712 ± 0.022 ms 0.0717 ± 0.024 ms 0.993 ± 0.45
saxpy/static workgroup=(1024,)/Float32/1048576 0.507 ± 0.045 ms 0.491 ± 0.032 ms 1.03 ± 0.11
saxpy/static workgroup=(1024,)/Float32/16384 0.0569 ± 0.029 ms 0.0599 ± 0.03 ms 0.95 ± 0.68
saxpy/static workgroup=(1024,)/Float32/2048 0.0707 ± 0.03 ms 0.071 ± 0.03 ms 0.995 ± 0.59
saxpy/static workgroup=(1024,)/Float32/256 0.0716 ± 0.025 ms 0.0722 ± 0.025 ms 0.992 ± 0.48
saxpy/static workgroup=(1024,)/Float32/262144 0.175 ± 0.03 ms 0.178 ± 0.03 ms 0.982 ± 0.24
saxpy/static workgroup=(1024,)/Float32/32768 0.0641 ± 0.029 ms 0.0689 ± 0.03 ms 0.931 ± 0.58
saxpy/static workgroup=(1024,)/Float32/4096 0.0606 ± 0.03 ms 0.0619 ± 0.031 ms 0.98 ± 0.69
saxpy/static workgroup=(1024,)/Float32/512 0.0708 ± 0.022 ms 0.072 ± 0.022 ms 0.983 ± 0.44
saxpy/static workgroup=(1024,)/Float32/64 0.0715 ± 0.025 ms 0.0722 ± 0.024 ms 0.99 ± 0.49
saxpy/static workgroup=(1024,)/Float32/65536 0.0779 ± 0.03 ms 0.0883 ± 0.03 ms 0.882 ± 0.45
saxpy/static workgroup=(1024,)/Float64/1024 0.0716 ± 0.013 ms 0.0718 ± 0.026 ms 0.996 ± 0.4
saxpy/static workgroup=(1024,)/Float64/1048576 0.661 ± 0.099 ms 0.598 ± 0.079 ms 1.11 ± 0.22
saxpy/static workgroup=(1024,)/Float64/16384 0.0595 ± 0.028 ms 0.0638 ± 0.03 ms 0.933 ± 0.63
saxpy/static workgroup=(1024,)/Float64/2048 0.0699 ± 0.031 ms 0.0723 ± 0.029 ms 0.967 ± 0.57
saxpy/static workgroup=(1024,)/Float64/256 0.0715 ± 0.024 ms 0.0721 ± 0.024 ms 0.991 ± 0.47
saxpy/static workgroup=(1024,)/Float64/262144 0.207 ± 0.036 ms 0.194 ± 0.031 ms 1.07 ± 0.25
saxpy/static workgroup=(1024,)/Float64/32768 0.0718 ± 0.03 ms 0.078 ± 0.032 ms 0.92 ± 0.54
saxpy/static workgroup=(1024,)/Float64/4096 0.0587 ± 0.028 ms 0.062 ± 0.029 ms 0.946 ± 0.64
saxpy/static workgroup=(1024,)/Float64/512 0.071 ± 0.03 ms 0.0717 ± 0.023 ms 0.99 ± 0.52
saxpy/static workgroup=(1024,)/Float64/64 0.0723 ± 0.021 ms 0.0725 ± 0.021 ms 0.997 ± 0.4
saxpy/static workgroup=(1024,)/Float64/65536 0.0961 ± 0.032 ms 0.0998 ± 0.031 ms 0.963 ± 0.44
time_to_load 0.804 ± 0.011 s 0.786 ± 0.0032 s 1.02 ± 0.014
main 2e505ea... main / 2e505ea...
const/@Const/Float32/262144 1 allocs: 16 B 1 allocs: 16 B 1
const/@Const/Float32/65536 1 allocs: 16 B 1 allocs: 16 B 1
const/@Const/Float64/262144 1 allocs: 16 B 1 allocs: 16 B 1
const/@Const/Float64/65536 1 allocs: 16 B 1 allocs: 16 B 1
const/unmarked/Float32/262144 1 allocs: 16 B 1 allocs: 16 B 1
const/unmarked/Float32/65536 1 allocs: 16 B 1 allocs: 16 B 1
const/unmarked/Float64/262144 1 allocs: 16 B 1 allocs: 16 B 1
const/unmarked/Float64/65536 1 allocs: 16 B 1 allocs: 16 B 1
launch/3D static workgroup, dynamic ndrange 5 allocs: 0.141 kB 5 allocs: 0.141 kB 1
launch/3D static workgroup, static ndrange 6 allocs: 0.172 kB 6 allocs: 0.172 kB 1
launch/dynamic workgroup, dynamic ndrange 15 allocs: 0.312 kB 15 allocs: 0.312 kB 1
launch/dynamic workgroup, dynamic ndrange, workgroupsize given 1 allocs: 16 B 1 allocs: 16 B 1
launch/static workgroup, dynamic ndrange 1 allocs: 16 B 1 allocs: 16 B 1
launch/static workgroup, static ndrange 2 allocs: 0.0469 kB 2 allocs: 0.0469 kB 1
partition/dynamic workgroup, dynamic ndrange 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
partition/static workgroup, dynamic ndrange 2 allocs: 32 B 2 allocs: 32 B 1
partition/static workgroup, static ndrange 0 allocs: 0 B 0 allocs: 0 B
saxpy/default/Float16/1024 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float16/1048576 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float16/16384 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float16/2048 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float16/256 16 allocs: 0.328 kB 16 allocs: 0.328 kB 1
saxpy/default/Float16/262144 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float16/32768 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float16/4096 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float16/512 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float16/64 16 allocs: 0.328 kB 16 allocs: 0.328 kB 1
saxpy/default/Float16/65536 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float32/1024 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float32/1048576 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float32/16384 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float32/2048 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float32/256 16 allocs: 0.328 kB 16 allocs: 0.328 kB 1
saxpy/default/Float32/262144 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float32/32768 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float32/4096 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float32/512 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float32/64 16 allocs: 0.328 kB 16 allocs: 0.328 kB 1
saxpy/default/Float32/65536 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float64/1024 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float64/1048576 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float64/16384 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float64/2048 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float64/256 16 allocs: 0.328 kB 16 allocs: 0.328 kB 1
saxpy/default/Float64/262144 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float64/32768 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float64/4096 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float64/512 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/default/Float64/64 16 allocs: 0.328 kB 16 allocs: 0.328 kB 1
saxpy/default/Float64/65536 24 allocs: 0.453 kB 24 allocs: 0.453 kB 1
saxpy/static workgroup=(1024,)/Float16/1024 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float16/1048576 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float16/16384 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float16/2048 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float16/256 1 allocs: 16 B 1 allocs: 16 B 1
saxpy/static workgroup=(1024,)/Float16/262144 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float16/32768 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float16/4096 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float16/512 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float16/64 1 allocs: 16 B 1 allocs: 16 B 1
saxpy/static workgroup=(1024,)/Float16/65536 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float32/1024 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float32/1048576 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float32/16384 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float32/2048 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float32/256 1 allocs: 16 B 1 allocs: 16 B 1
saxpy/static workgroup=(1024,)/Float32/262144 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float32/32768 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float32/4096 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float32/512 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float32/64 1 allocs: 16 B 1 allocs: 16 B 1
saxpy/static workgroup=(1024,)/Float32/65536 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float64/1024 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float64/1048576 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float64/16384 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float64/2048 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float64/256 1 allocs: 16 B 1 allocs: 16 B 1
saxpy/static workgroup=(1024,)/Float64/262144 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float64/32768 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float64/4096 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float64/512 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
saxpy/static workgroup=(1024,)/Float64/64 1 allocs: 16 B 1 allocs: 16 B 1
saxpy/static workgroup=(1024,)/Float64/65536 4 allocs: 0.0625 kB 4 allocs: 0.0625 kB 1
time_to_load 0.2 k allocs: 11.8 kB 0.2 k allocs: 11.8 kB 1

Benchmark Plots

A plot of the benchmark results have been uploaded as an artifact to the workflow run for this PR.
Go to "Actions"->"Benchmark a pull request"->[the most recent run]->"Artifacts" (at the bottom).

@christiangnrd christiangnrd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments.

Also, if it's not too much trouble, could you get claude to split off 7b0276c?

Comment thread docs/src/kernelinterface.md Outdated
Comment thread docs/src/kernelinterface.md Outdated
Comment thread docs/src/kernelinterface.md Outdated
Comment thread docs/src/kernelinterface.md
Comment thread docs/src/kernelinterface.md Outdated
Comment thread lib/KernelInterface/src/host.jl Outdated
Comment thread lib/KernelInterface/src/launch.jl
Comment thread lib/KernelInterface/src/launch.jl Outdated
Comment thread lib/KernelInterface/src/backend.jl Outdated
Comment thread lib/KernelInterface/test/interface.jl
@maleadt

maleadt commented Sep 29, 2026 •

Copy link
Copy Markdown
Member Author

Also, if it's not too much trouble, could you get claude to split off 7b0276c?

Split off into #807.

@maleadt
maleadt removed this pull request from stack #804 September 29, 2026 20:22
@maleadt
maleadt changed the base branch from tb/ci-reverse to tb/pocl-spirvintrinsics-1.1.3 September 29, 2026 20:22
@maleadt
maleadt added this pull request to stack #808 September 29, 2026 20:22
Base automatically changed from tb/pocl-spirvintrinsics-1.1.3 to main September 30, 2026 04:57
`GPU` didn't mean GPU hardware: the CPU backend is PoCL and subtyped it, so
`CPU <: GPU`. Backends now subtype `Backend` directly.

This is the first breaking change of KernelInterface 0.3.
`testsuite(backend, AT)` replaces `testsuite(Backend, name, mod, AT, DAT)`, whose
name, module and device array type were unused, and tests the backend value it
is given, e.g. one with non-default options.

The events tests now skip backends without events of their own (whose
`record_event` returns `nothing`), instead of them having to pass `skip_tests`.
…mgroups

`KI.@kernel` prefixes a call and launches it, like `@cuda` or `@metal`, while
`KA.@kernel` defines a kernel; sharing the name was a source of confusion.
`numgroups` matches `get_num_groups` and `max_num_groups`.

`KI.@launch` now also evaluates its backend expression once (it was evaluated
once per argument), and passes the keywords it doesn't know to
`kernel_function` as compiler options instead of rejecting them.
…dation

`kernel_max_work_group_size` returned an occupancy recommendation on CUDA,
AMDGPU and oneAPI, but the hard limit on Metal, OpenCL and PoCL: on an
RTX 5080, 768 for a trivial kernel that can be launched with 1024 threads. It
is replaced by two queries:

- `max_work_group_size(kernel)`, the largest work-group the compiled kernel
  can be launched with;
- `launch_configuration(kernel; nitems, max_work_group_size)`, the recommended
  work-group size for a launch of `nitems` work-items, which `ndrange` launches
  without a `workgroupsize` use. It falls back to the limit. The problem size
  is passed separately from the bound, so that heuristics like CUDA's
  `prefer_blocks` don't have to guess it.

`max_work_group_dims` and `max_num_groups` are now required: their
`typemax(Int)` fallbacks meant both "unknown" and "unlimited".
…ment KI.launch

Every backend repeated the launch validation and sizing in front of its native
launch, and the copies had drifted apart (return values, a `prod(ndrange) == 0`
check that can overflow). KernelInterface now implements the `Kernel` call and
passes validated 3-D sizes to one required method:

    KI.launch(kernel, groups::Dims{3}, items::Dims{3}, args...; kwargs...)

The launch semantics are documented on `Kernel`: `ndrange` is rounded up to
whole work-groups and not masked, a zero in `ndrange` or `numgroups` launches
nothing, work-group sizes are checked against `max_work_group_dims` and
`max_work_group_size(kernel)`, and the number of work-items per dimension has
to fit an `Int`. Invalid launches throw an `ArgumentError` before the backend
sees them. Unknown keywords are passed on to `KI.launch`, so options like CUDA's
`stream` still reach the driver.

The `Kernel` call and PoCL's `launch` declare their arguments as `Vararg{Any, N}`:
Julia doesn't specialize a method on `args...` that it only passes through, which
made every launch dispatch dynamically (on CUDA, 1.2 µs and 1 kB more per
`KI.@launch`). The `launch` docstring recommends the same to backends.

`check_launch_args` and `auto_launch_sizes` are removed. The launch tests now
use different group counts and sizes per dimension, which exposes two tests
whose formulas only worked because they were equal.
The 0.2 docs said that typed queries are "computed in `T`" and undefined when
the value doesn't fit, but backends implemented them as `T(x)`, a checked
conversion that leaves a `throw_inexacterror` branch in every kernel. The
result is now the exact value modulo `T`, as with `x % T`, which is cheap and
testable: a `UInt8` query over 384 work-items has to wrap, where `T(x)` throws.

Backends implement the four primitive queries (`get_local_id`, `get_group_id`,
`get_local_size`, `get_num_groups`); `get_global_id` and `get_global_size`
have fallbacks derived from them for backends without a builtin (CUDA, HIP),
which backends with one (SPIR-V, Metal) should override.
Sub-groups were underspecified exactly where backends disagree:

- Support is a capability: `supports_subgroups(backend)` and
  `supports_shuffle(backend, T)` replace `shfl_down_types`, whose non-empty
  result the testsuite used as the flag for sub-group support.
- `sub_group_size(backend)` is a guarantee instead of "a reasonable size":
  kernels from `kernel_function` run with exactly that width, so host code can
  pick a `Val(N)` for a warp-level reduction. A backend that can't guarantee it
  reports no sub-group support. PoCL fixes the width when compiling.
- Partial sub-groups follow OpenCL and SYCL: `get_sub_group_size()` counts the
  work-items that are present, and `get_num_sub_groups()` is
  `cld(items, width)`.
- How work-items are assigned to sub-groups is unspecified, but each has a
  unique `(sub-group id, lane)` pair that doesn't change during the kernel.
- A `shfl_down` from a lane that doesn't exist returns an unspecified value,
  and shuffles are not memory fences.
- The sub-group queries take a result type like the index queries, defaulting
  to `Int`; they returned `UInt32` on most backends but `Int32` on CUDA.

The testsuite checks partial sub-groups, lane uniqueness, `shfl_down` on every
lane, and that `sub_group_barrier` makes memory writes visible.
- Execution is task-local: each task has an active device per backend and a
  queue on it, which host queries, compilation, allocations, copies and
  launches use. `kernel_function` has to store the backend value it was given
  (oneAPI dropped its compiler options, and OpenCL its platform, by
  constructing a new default), and a compiled kernel launched after switching
  devices either works or throws, but never runs on the wrong device.
- New: `device(backend, A)`, the device that owns `A`. `device`, `device!`,
  `ndevices` and `device(backend, A)` are required for backends with more than
  one device; the single-device fallbacks now throw when `ndevices` reports
  more instead of answering for the wrong device.
- Capability defaults are conservative: `supports_float64` and
  `supports_atomics` default to `false`, so that a missing method never claims
  support. `supports_atomics` means Atomix add and CAS on 32-bit integers and
  floats in global memory.
- `copyto!(backend, dst, src)` copies in queue order and returns `dst`, and
  throws an `ArgumentError` for arrays of different lengths. CUDA's
  implementation copied `length(dst)` elements from `pointer(src)` without a
  check.
- `localmemory` and `barrier` say what is shared and what becomes visible, and
  `unsafe_free!` is an optional hint with a legal no-op fallback.

The testsuite covers devices, `copyto!`, local memory (two allocations don't
alias; contents are visible after a barrier) and global-memory barriers.
- The public API is declared with `public` (Julia 1.11+); nothing is exported,
  so `KI.` prefixes the interface everywhere.
- The docs get a contract table listing what a backend implements, which
  methods are optional and what their fallbacks do, and a versioning rule:
  required methods only change in breaking releases, optional methods can be
  added in any release if their fallback is conservative, and patch releases
  only add tests for behavior that was already specified. (0.2.3 broke backend
  CI in a patch release by adding tests for new obligations.)
- The testsuite checks that a backend implements the methods without a
  fallback.
@maleadt
maleadt merged commit 385b0ae into main Sep 30, 2026
69 of 71 checks passed
@maleadt
maleadt deleted the tb/ki-0.3 branch September 30, 2026 11:33
@codecov

codecov Bot commented Sep 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.55556% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.91%. Comparing base (557fc62) to head (2e505ea).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
ext/EnzymeCore07Ext.jl 0.00% 4 Missing ⚠️
ext/EnzymeCore08Ext.jl 0.00% 4 Missing ⚠️
src/pocl/backend.jl 91.42% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #800      +/-   ##
==========================================
+ Coverage   67.70%   67.91%   +0.21%     
==========================================
  Files          24       24              
  Lines        2031     2023       -8     
==========================================
- Hits         1375     1374       -1     
+ Misses        656      649       -7     

☔ 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants