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 = KernelInterface

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

KernelAbstractions 0.10

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.

Note

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

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.

source

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.BackendType
Backend

Abstract 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)
source
KernelInterface.GPUType

Abstract type for all GPU based KernelAbstractions backends.

Note

New backend implementations must sub-type this abstract type.

Note

GPU will be removed in KernelAbstractions v1.0

source
KernelInterface.get_backendFunction
get_backend(A::AbstractArray)::Backend

Get a Backend instance suitable for array A.

Note

Backend implementations must provide get_backend for their custom array type. It should be the same as the return type of allocate

source

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_sizeFunction
get_global_size()::@NamedTuple{x::Int, y::Int, z::Int}

Return the number of global work-items specified.

Note

Backend implementations must implement:

@device_override get_global_size()::@NamedTuple{x::Int, y::Int, z::Int}
source
KernelInterface.get_global_idFunction
get_global_id()::@NamedTuple{x::Int, y::Int, z::Int}

Returns the unique global work-item ID.

Note

1-based.

Note

Backend implementations must implement:

@device_override get_global_id()::@NamedTuple{x::Int, y::Int, z::Int}
source
KernelInterface.get_local_sizeFunction
get_local_size()::@NamedTuple{x::Int, y::Int, z::Int}

Return the number of local work-items specified.

Note

Backend implementations must implement:

@device_override get_local_size()::@NamedTuple{x::Int, y::Int, z::Int}
source
KernelInterface.get_local_idFunction
get_local_id()::@NamedTuple{x::Int, y::Int, z::Int}

Returns the unique local work-item ID.

Note

1-based.

Note

Backend implementations must implement:

@device_override get_local_id()::@NamedTuple{x::Int, y::Int, z::Int}
source
KernelInterface.get_num_groupsFunction
get_num_groups()::@NamedTuple{x::Int, y::Int, z::Int}

Returns the number of groups.

Note

Backend implementations must implement:

@device_override get_num_groups()::@NamedTuple{x::Int, y::Int, z::Int}
source
KernelInterface.get_group_idFunction
get_group_id()::@NamedTuple{x::Int, y::Int, z::Int}

Returns the unique group ID.

Note

1-based.

Note

Backend implementations must implement:

@device_override get_group_id()::@NamedTuple{x::Int, y::Int, z::Int}
source

Sub-groups

KernelInterface.get_sub_group_sizeFunction
get_sub_group_size()::UInt32

Returns the number of work-items in the sub-group.

Note

Backend implementations must implement:

@device_override get_sub_group_size()::UInt32
source
KernelInterface.get_max_sub_group_sizeFunction
get_max_sub_group_size()::UInt32

Returns the maximum sub-group size for sub-groups in the current workgroup.

Note

Backend implementations must implement:

@device_override get_max_sub_group_size()::UInt32
source
KernelInterface.get_num_sub_groupsFunction
get_num_sub_groups()::UInt32

Returns the number of sub-groups in the current workgroup.

Note

Backend implementations must implement:

@device_override get_num_sub_groups()::UInt32
source
KernelInterface.get_sub_group_idFunction
get_sub_group_id()::UInt32

Returns the sub-group ID within the work-group.

Note

1-based.

Note

Backend implementations must implement:

@device_override get_sub_group_id()::UInt32
source
KernelInterface.get_sub_group_local_idFunction
get_sub_group_local_id()::UInt32

Returns the work-item ID within the current sub-group.

Note

1-based.

Note

Backend implementations must implement:

@device_override get_sub_group_local_id()::UInt32
source

Barriers

KernelInterface.barrierFunction
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.

Note

barrier() must be encountered by all workitems of a work-group executing the kernel or by none at all.

Note

Backend implementations must implement:

@device_override barrier()
source
KernelInterface.sub_group_barrierFunction
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.

Note

sub_group_barrier() must be encountered by all workitems of a sub-group executing the kernel or by none at all.

Note

Backend implementations must implement:

@device_override sub_group_barrier()
source

Memory

KernelInterface.localmemoryFunction
localmemory(::Type{T}, dims)

Declare memory that is local to a workgroup.

Note

Backend implementations must implement:

@device_override localmemory(::Type{T}, ::Val{Dims}) where {T, Dims}

As well as the on-device functionality.

source

Communication

KernelInterface.shfl_downFunction
shfl_down(val::T, offset::Integer) where T

Read val from a lane with higher id given by offset.

Note

shfl_down must be encountered by all workitems of a sub-group executing the kernel or by none at all.

Note

Backend implementations must implement:

@device_override shfl_down(val::T, offset::Integer) where T

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

source
KernelInterface.shfl_down_typesFunction
shfl_down_types(::Backend)::Vector{DataType}

Returns a vector of DataTypes supported on backend

Note

Backend implementations must implement this function only if they support shfl_down for any types.

source

Printing

KernelInterface._printFunction
_print(args...)

Overloaded by backends to enable `KernelAbstractions.@print`
functionality.
Note

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.

source

_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.allocateFunction
allocate(::Backend, Type, dims...; unified=false)::AbstractArray

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

Note

Backend implementations must implement allocate(::NewBackend, T, dims::Tuple) Backend implementations should implement allocate(::NewBackend, T, dims::Tuple; unified::Bool=false)

source
KernelInterface.zerosFunction
zeros(::Backend, Type, dims...; unified=false)::AbstractArray

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

source
KernelInterface.onesFunction
ones(::Backend, Type, dims...; unified=false)::AbstractArray

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

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

Warning

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)
end
Note

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

Note

Backends must implement this function.

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

Note

Backends may implement this function.

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

Note

On CPU backend this is always a no-op.

Note

Backend implementations may implement this function. If not implemented for a particular backend, default action is a no-op. Otherwise, it should be defined for backend's array type.

source

Execution

KernelInterface.priority!Function
priority!(::Backend, prio::Symbol)::Nothing

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

Note

Backend implementations may implement this function.

source

Device management

KernelInterface.deviceFunction
device(backend::Backend)::Int

Return the 1-based index of the currently active device for backend.

Note

The default implementation assumes a single device. Backends supporting multiple devices must implement device(backend::Backend)::Int, ndevices, and device!.

source
KernelInterface.ndevicesFunction
ndevices(backend::Backend)::Int

Return the number of devices available to backend.

Note

The default implementation assumes a single device. Backends supporting multiple devices must implement ndevices(backend::Backend)::Int, device, and device!.

source
KernelInterface.device!Function
device!(backend::Backend, id::Int)::Nothing

Select 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 device
Note

The default implementation assumes a single device. Backends supporting multiple devices must implement device!(backend::Backend, id::Int), ndevices, and device.

source

Capability queries

KernelInterface.functionalFunction
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.

KernelAbstractions v0.9.22

This function was added in KernelAbstractions v0.9.22

source
KernelInterface.versioninfoFunction
versioninfo(io::IO=stdout, backend::Backend)::Nothing

Print information about backend to io. It is up to the backends to determine what is relevant.

Note

Backend implementations may implement this function. If they do so, they should implement versioninfo(io::IO, ::Backend)::Nothing

source
KernelInterface.supports_unifiedFunction
supports_unified(::Backend)::Bool

Returns whether unified memory arrays are supported by the backend.

Note

Backend implementations should implement this function only if they do support unified memory.

source
KernelInterface.supports_atomicsFunction
supports_atomics(::Backend)::Bool

Returns whether @atomic operations are supported by the backend.

Note

Backend implementations must implement this function only if they do not support atomic operations with Atomix.

source
KernelInterface.supports_float64Function
supports_float64(::Backend)::Bool

Returns whether Float64 values are supported by the backend.

Note

Backend implementations must implement this function only if they do not support Float64.

source

Backend queries

KernelInterface.max_work_group_sizeFunction
max_work_group_size(backend, kern; [max_work_items::Int])::Int

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

Note

Backend implementations must implement:

max_work_group_size(backend::NewBackend)::Int

As well as the on-device functionality.

source
KernelInterface.sub_group_sizeFunction
sub_group_size(backend)::Int

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

Note

Backend implementations must implement:

sub_group_size(backend::NewBackend)::Int

As well as the on-device functionality.

source
KernelInterface.multiprocessor_countFunction
multiprocessor_count(backend::NewBackend)::Int

The multiprocessor count for the current device used by backend. Used for certain algorithm optimizations.

Note

Backend implementations may implement:

multiprocessor_count(backend::NewBackend)::Int

As well as the on-device functionality.

source

Compilation and launching

KernelInterface.KernelType
Kernel{Backend, Kern}

Kernel closure struct that is used to represent the backend kernel on the host.

Note

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.

source
KernelInterface.kernel_functionFunction
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
Note

Backend implementations must implement:

kernel_function(::NewBackend, f::F, tt::TT=Tuple{}; name=nothing, kwargs...) where {F,TT}
source
KernelInterface.kernel_max_work_group_sizeFunction
kernel_max_work_group_size(kern; [max_work_items::Int])::Int

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

Note

Backend implementations must implement:

kernel_max_work_group_size(kern::Kernel{<:NewBackend}; max_work_items::Int=typemax(Int))::Int

As well as the on-device functionality.

source
KernelInterface.argconvertFunction
argconvert(::NewBackend, arg)

This function is called for every argument to be passed to a kernel, converting them to their device side representation.

Note

Backend implementations must implement:

argconvert(::NewBackend, arg)
source
KernelInterface.@kernelMacro
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 to true. If false, 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.
Note

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.

source
Note

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:

  1. Define a backend type subtyping GPU (or Backend for non-GPU backends), and implement get_backend for its array type.
  2. Implement the host-side management functions for that type: allocate, copyto!, synchronize and unsafe_free! are required; the remaining functions under Host-side API have fallbacks that only need overriding when the defaults don't apply.
  3. @device_override the device-side functions it supports. The indexing queries and barrier are required; sub-group and shfl_down support is optional.
  4. Implement argconvert and kernel_function for its backend type, returning a Kernel.
  5. Make that Kernel callable, accepting numworkgroups, workgroupsize and ndrange as a scalar Integer or a 1-, 2- or 3-element tuple. Use KI.check_launch_args to validate them, or check them directly. A zero-sized ndrange — launching over an empty array is not uncommon — must be a no-op returning nothing, not an error.
  6. Report its limits through kernel_max_work_group_size and, where applicable, max_work_group_size, sub_group_size and multiprocessor_count.

The PoCL backend in src/pocl/backend.jl is a complete worked example.

See also the notes for backend implementations.