KernelInterface
KernelInterface (conventionally imported as KI) is the low-level API that backends implement, and that KernelAbstractions builds its higher-level kernel language on top of.
It ships as a standalone package under lib/KernelInterface with no dependencies outside the standard library, so a backend can implement the interface without taking on KernelAbstractions or its compiler stack:
using KernelInterface
const KI = KernelInterfaceKernelAbstractions re-exports it, so KernelAbstractions.KernelInterface and KernelAbstractions.KI refer to the same module. This includes the Backend type hierarchy and the host-side management API: these are defined here in KernelInterface, and KernelAbstractions.Backend, KernelAbstractions.allocate, KernelAbstractions.synchronize and so on are the same objects, so user code keeps using them through KernelAbstractions unchanged.
This only holds for KernelAbstractions 0.10 and later. KernelAbstractions 0.9 predates KernelInterface and defines its own Backend, allocate, synchronize, etc. — those are different functions and types from the KernelInterface ones. Methods added to one are not seen by the other, so a backend targeting both must implement both. KernelAbstractions 0.10 is based on KernelInterface, so any KernelInterface functionality does not need to be reimplemented for KernelAbstractions.
Most of the device-side functions below are stubs with no methods. They exist so that backends can add device-side implementations with GPUCompiler.@device_override, and so kernels can call them generically. Calling one without a backend that implements it is a MethodError.
KernelInterface.KernelInterface — Module
KernelInterface
The KernelInterface (or KI) module defines the API interface for backends to define various lower-level device and host-side functionality. The KI interface is used to define the higher-level device-side functionality in KernelAbstractions.
Both provide APIs for host and device-side functionality, but KI focuses on lower-level functionality that is shared amongst backends, while KernelAbstractions provides higher-level functionality such as writing kernels that work on arrays with an arbitrary number of dimensions, or convenience functions like allocating arrays on a backend.
Backend hierarchy
A backend package subtypes GPU (or Backend directly for non-GPU backends), and everything else in the interface dispatches on that type. These types and the host-side management functions below are re-exported by KernelAbstractions, so their canonical docstrings are on the API page.
KernelInterface.Backend — Type
BackendAbstract supertype for all KernelAbstractions backends.
Concrete backends (for example CUDABackend from CUDA.jl or CPU from KernelAbstractions) determine where arrays are allocated and where kernels execute. Use get_backend to obtain the backend for an array and allocate to create storage on a backend.
Example
backend = get_backend(A)
kernel = my_kernel(backend, 256)
kernel(A, ndrange=length(A))
synchronize(backend)KernelInterface.GPU — Type
Abstract type for all GPU based KernelAbstractions backends.
KernelInterface.get_backend — Function
get_backend(A::AbstractArray)::BackendGet a Backend instance suitable for array A.
Backend implementations must provide get_backend for their custom array type. It should be the same as the return type of allocate
Device-side API
These are called from inside a kernel. A backend provides each one with
@device_override KI.get_global_id() = ...along with the corresponding on-device functionality.
Indexing
All index queries are 1-based and return a named tuple of x, y and z components.
KernelInterface.get_global_size — Function
get_global_size()::@NamedTuple{x::Int, y::Int, z::Int}Return the number of global work-items specified.
KernelInterface.get_global_id — Function
get_global_id()::@NamedTuple{x::Int, y::Int, z::Int}Returns the unique global work-item ID.
KernelInterface.get_local_size — Function
get_local_size()::@NamedTuple{x::Int, y::Int, z::Int}Return the number of local work-items specified.
KernelInterface.get_local_id — Function
get_local_id()::@NamedTuple{x::Int, y::Int, z::Int}Returns the unique local work-item ID.
KernelInterface.get_num_groups — Function
get_num_groups()::@NamedTuple{x::Int, y::Int, z::Int}Returns the number of groups.
KernelInterface.get_group_id — Function
get_group_id()::@NamedTuple{x::Int, y::Int, z::Int}Returns the unique group ID.
Sub-groups
KernelInterface.get_sub_group_size — Function
get_sub_group_size()::UInt32Returns the number of work-items in the sub-group.
KernelInterface.get_max_sub_group_size — Function
get_max_sub_group_size()::UInt32Returns the maximum sub-group size for sub-groups in the current workgroup.
KernelInterface.get_num_sub_groups — Function
get_num_sub_groups()::UInt32Returns the number of sub-groups in the current workgroup.
KernelInterface.get_sub_group_id — Function
get_sub_group_id()::UInt32Returns the sub-group ID within the work-group.
KernelInterface.get_sub_group_local_id — Function
get_sub_group_local_id()::UInt32Returns the work-item ID within the current sub-group.
Barriers
KernelInterface.barrier — Function
barrier()After a barrier() call, all read and writes to global and local memory from each thread in the workgroup are visible in from all other threads in the workgroup.
This does not guarantee that a write from a thread in a certain workgroup will be visible to a thread in a different workgroup.
KernelInterface.sub_group_barrier — Function
sub_group_barrier()After a sub_group_barrier() call, all read and writes to global and local memory from each thread in the sub-group are visible in from all other threads in the sub-group.
This does not guarantee that a write from a thread in a certain sub-group will be visible to a thread in a different sub-group.
Memory
KernelInterface.localmemory — Function
localmemory(::Type{T}, dims)Declare memory that is local to a workgroup.
Communication
KernelInterface.shfl_down — Function
shfl_down(val::T, offset::Integer) where TRead val from a lane with higher id given by offset.
shfl_down must be encountered by all workitems of a sub-group executing the kernel or by none at all.
Backend implementations must implement:
@device_override shfl_down(val::T, offset::Integer) where TAs well as the on-device functionality.
This implementation must be synchronizing. That is, kernels using this function can safely assume that they do not need a sub_group_barrier before calling this function.
KernelInterface.shfl_down_types — Function
shfl_down_types(::Backend)::Vector{DataType}Returns a vector of DataTypes supported on backend
Printing
KernelInterface._print — Function
_print(args...)
Overloaded by backends to enable `KernelAbstractions.@print`
functionality.Backend implementations must implement:
@device_override _print(args...)If the backend does not support printing, define it to return nothing.
The generic fallback prints on the host, which keeps CPU backends working. Val arguments are unwrapped, since KernelAbstractions.@print uses them to pass literal strings through to backends that require compile-time format strings.
_print is the one device-side function with a working host fallback: it prints its arguments with Base.print, unwrapping any Val-wrapped literals. That is what makes KernelAbstractions.@print usable outside of a kernel.
Host-side API
Several of these have generic fallbacks. Each docstring notes which methods a backend must implement and which ones are optional.
Memory
KernelInterface.allocate — Function
allocate(::Backend, Type, dims...; unified=false)::AbstractArrayAllocate a storage array appropriate for the computational backend. unified=true allocates an array using unified memory if the backend supports it and throws otherwise. Use supports_unified to determine whether it is supported by a backend.
KernelInterface.zeros — Function
zeros(::Backend, Type, dims...; unified=false)::AbstractArrayAllocate a storage array appropriate for the computational backend filled with zeros. unified=true allocates an array using unified memory if the backend supports it and throws otherwise.
KernelInterface.ones — Function
ones(::Backend, Type, dims...; unified=false)::AbstractArrayAllocate a storage array appropriate for the computational backend filled with ones. unified=true allocates an array using unified memory if the backend supports it and throws otherwise.
KernelInterface.copyto! — Function
copyto!(::Backend, dest::AbstractArray, src::AbstractArray)Perform an asynchronous copyto! operation that is execution ordered with respect to the back-end.
For most users, Base.copyto! should suffice, performance a simple, synchronous copy. Only when you know you need asynchronicity w.r.t. the host, you should consider using this asynchronous version, which requires additional lifetime guarantees as documented below.
Because of the asynchronous nature of this operation, the user is required to guarantee that the lifetime of the source extends past the completion of the copy operation as to avoid a use-after-free. It is not sufficient to simply use GC.@preserve around the call to copyto!, because that only extends the lifetime past the operation getting queued. Instead, it may be required to synchronize(), or otherwise guarantee that the source will still be around when the copy is executed:
arr = zeros(64)
GC.@preserve arr begin
copyto!(backend, arr, ...)
# other operations
synchronize(backend)
endOn some back-ends it may be necessary to first call pagelock! on host memory to enable fully asynchronous behavior w.r.t to the host.
KernelInterface.pagelock! — Function
pagelock!(::Backend, dest::AbstractArray)::Union{Nothing, Missing}Pagelock (pin) a host memory buffer for a backend device. This may be necessary for copyto! to perform asynchronously w.r.t to the host/
This function should return nothing; or missing if not implemented.
KernelInterface.unsafe_free! — Function
unsafe_free!(x::AbstractArray)Release the memory of an array for reuse by future allocations and reduce pressure on the allocator. After releasing the memory of an array, it should no longer be accessed.
Execution
KernelInterface.synchronize — Function
synchronize(::Backend)Synchronize the current backend.
KernelInterface.priority! — Function
priority!(::Backend, prio::Symbol)::NothingSet the priority for the backend stream/queue. This is an optional feature that backends may or may not implement. If a backend shall support priorities it must accept :high, :normal, :low. Where :normal is the default.
Device management
KernelInterface.device — Function
device(backend::Backend)::IntReturn the 1-based index of the currently active device for backend.
KernelInterface.ndevices — Function
ndevices(backend::Backend)::IntReturn the number of devices available to backend.
KernelInterface.device! — Function
device!(backend::Backend, id::Int)::NothingSelect the active device for backend. id is a 1-based device index and must satisfy 1 <= id <= ndevices(backend).
Example
device!(CUDABackend(), 2) # use the second CUDA deviceCapability queries
KernelInterface.functional — Function
functional(::Backend)::Union{Bool, Missing}Queries if the provided backend is functional. This may mean different things for different backends, but generally should mean that the necessary drivers and a compute device are available.
This function should return a Bool or missing if not implemented.
KernelInterface.versioninfo — Function
versioninfo(io::IO=stdout, backend::Backend)::NothingPrint information about backend to io. It is up to the backends to determine what is relevant.
KernelInterface.supports_unified — Function
supports_unified(::Backend)::BoolReturns whether unified memory arrays are supported by the backend.
KernelInterface.supports_atomics — Function
supports_atomics(::Backend)::BoolReturns whether @atomic operations are supported by the backend.
KernelInterface.supports_float64 — Function
supports_float64(::Backend)::BoolReturns whether Float64 values are supported by the backend.
Backend queries
KernelInterface.max_work_group_size — Function
max_work_group_size(backend, kern; [max_work_items::Int])::IntThe maximum workgroup size limit for a kernel as reported by the backend. This function represents a theoretical maximum; kernel_max_work_group_size should be used before launching a kernel as some backends may error if kernel launch with too big a workgroup is attempted.
KernelInterface.sub_group_size — Function
sub_group_size(backend)::IntReturns a reasonable sub-group size supported by the currently active device for the specified backend. This would typically be 32, or 64 for devices that don't support 32.
KernelInterface.multiprocessor_count — Function
multiprocessor_count(backend::NewBackend)::IntThe multiprocessor count for the current device used by backend. Used for certain algorithm optimizations.
Compilation and launching
KernelInterface.Kernel — Type
Kernel{Backend, Kern}Kernel closure struct that is used to represent the backend kernel on the host.
Backend implementations must implement:
(kernel::Kernel{<:NewBackend})(args...; numworkgroups=(), workgroupsize=(), ndrange=(), max_work_group_size=typemax(Int))numworkgroups, workgroupsize, and ndrange must accept a scalar Integer, a 1, 2, or 3 Integer tuple, or an empty tuple. Otherwise, it must throw an ArgumentError. An ArgumentError must also be thrown if ndrange and numworkgroups are both specified. The helper function KI.check_launch_args(numworkgroups, workgroupsize, ndrange) can be used by the backend or a custom check can be implemented.
max_work_group_size is to allow algorithms to request a max workgroupsize with ndrange. This is a maximum value because a kernel's maximum workitems per workgroup may be lower than requested.
An ndrange with a zero-sized dimension, as when launching over an empty array, is not an error: the call must be a no-op and return nothing instead of launching.
By default, kernels must launch with 1 workgroup containing 1 workitem.
Backends must also implement the on-device kernel launch functionality.
KernelInterface.kernel_function — Function
KI.kernel_function(::NewBackend, f::F, tt::TT=Tuple{}; name=nothing, kwargs...) where {F,TT}Low-level interface to compile a function invocation for the currently-active GPU, returning a callable kernel object. For a higher-level interface, use KernelInterface.@kernel.
Currently, kernel_function only supports the name keyword argument as it is the only one by all backends.
Keyword arguments:
name: override the name that the kernel will have in the generated code
KernelInterface.kernel_max_work_group_size — Function
kernel_max_work_group_size(kern; [max_work_items::Int])::IntThe maximum workgroup size limit for a kernel as reported by the backend. This function should always be used to determine the workgroup size before launching a kernel.
KernelInterface.argconvert — Function
argconvert(::NewBackend, arg)This function is called for every argument to be passed to a kernel, converting them to their device side representation.
KernelInterface.@kernel — Macro
KI.@kernel backend [workgroupsize=... numworkgroups=... ndrange=...] [kwargs...] func(args...)High-level interface for executing code on a GPU.
The KI.@kernel macro should prefix a call, with func a callable function or object that should return nothing. It will be compiled to a function native to the specified backend upon first use, and to a certain extent arguments will be converted and managed automatically using argconvert. Finally, if launch=true, the newly created callable kernel object is called and launched according to the specified backend.
There are a few keyword arguments that influence the behavior of KI.@kernel:
launch: whether to launch this kernel, defaults totrue. Iffalse, the returned kernel object should be launched by calling it and passing arguments again.name: the name of the kernel in the generated code. Defaults to an automatically- generated name.
KI.@kernel differs from the KernelAbstractions macro in that this macro acts a wrapper around backend kernel compilation/launching (such as @cuda, @metal, etc.). It is used when calling a function to be run on a specific backend, while KernelAbstractions.@kernel is used kernel definition for use with the original higher-level KernelAbstractions API.
KI.@kernel is not KernelAbstractions.@kernel. KI.@kernel wraps a backend's own compile-and-launch path — the equivalent of @cuda or @metal — and prefixes a call. KernelAbstractions.@kernel prefixes a definition and produces a kernel written in the higher-level KernelAbstractions language.
Implementing a backend
A backend must, at minimum:
- Define a backend type subtyping
GPU(orBackendfor non-GPU backends), and implementget_backendfor its array type. - Implement the host-side management functions for that type:
allocate,copyto!,synchronizeandunsafe_free!are required; the remaining functions under Host-side API have fallbacks that only need overriding when the defaults don't apply. @device_overridethe device-side functions it supports. The indexing queries andbarrierare required; sub-group andshfl_downsupport is optional.- Implement
argconvertandkernel_functionfor its backend type, returning aKernel. - Make that
Kernelcallable, acceptingnumworkgroups,workgroupsizeandndrangeas a scalarIntegeror a 1-, 2- or 3-element tuple. UseKI.check_launch_argsto validate them, or check them directly. A zero-sizedndrange— launching over an empty array is not uncommon — must be a no-op returningnothing, not an error. - Report its limits through
kernel_max_work_group_sizeand, where applicable,max_work_group_size,sub_group_sizeandmultiprocessor_count.
The PoCL backend in src/pocl/backend.jl is a complete worked example.
See also the notes for backend implementations.