API

Kernel language

KernelAbstractions.@kernelMacro
@kernel function f(args) end

Takes a function definition and generates a Kernel constructor from it. The enclosed function is allowed to contain kernel language constructs. In order to call it the kernel has first to be specialized on the backend and then invoked on the arguments.

Kernel language

Kernel constructor

After defining a kernel function f, call f(backend[, workgroupsize[, ndrange]]) to obtain a Kernel specialized for that backend. Workgroup size and ndrange can be fixed at construction time (enabling size-specific compile-time optimizations and fewer runtime checks, at the cost of recompilation when the sizes change) or supplied at launch:

f(backend)                    # dynamic workgroup size and ndrange
f(backend, 64)                # static workgroup size of 64
f(backend, 64, 1024)          # static workgroup size and ndrange
f(backend, 64, (128, 128))    # multi-dimensional ndrange

Example

using KernelAbstractions

@kernel function vecadd(A, @Const(B))
    I = @index(Global)
    @inbounds A[I] += B[I]
end

dev = CPU()
A = ones(1024)
B = rand(1024)
vecadd(dev, 64)(A, B, ndrange=length(A))
synchronize(dev)
source
@kernel config function f(args) end

This allows for two different configurations:

  1. cpu={true, false}: Disables code-generation of the CPU function. This relaxes semantics such that KernelAbstractions primitives can be used in non-kernel functions.
  2. inbounds={false, true}: Enables a forced @inbounds macro around the function definition in the case the user is using too many @inbounds already in their kernel. Note that this can lead to incorrect results, crashes, etc and is fundamentally unsafe. Be careful!
  3. unsafe_indices={false, true}: Disables the implicit validation of indices, users must avoid @index(Global).
Warning

This is an experimental feature.

Note

cpu={true, false} is deprecated for KernelAbstractions 1.0

source
KernelAbstractions.@ConstMacro
@Const(A)

@Const is an argument annotiation that asserts that the memory reference by A is both not written to as part of the kernel and that it does not alias any other memory in the kernel.

Danger

Violating those constraints will lead to arbitrary behaviour.

As an example given a kernel signature kernel(A, @Const(B)), you are not allowed to call the kernel with kernel(A, A) or kernel(A, view(A, :)).

source
KernelAbstractions.@indexMacro
@index

The @index macro can be used to give you the index of a workitem within a kernel function. It supports both the production of a linear index or a cartesian index. A cartesian index is a general N-dimensional index that is derived from the iteration space.

Index granularity

  • Global: Used to access global memory.
  • Group: The index of the workgroup.
  • Local: The within workgroup index.

Index kind

  • Linear: Produces an Int64 that can be used to linearly index into memory.
  • Cartesian: Produces a CartesianIndex{N} that can be used to index into memory.
  • NTuple: Produces a NTuple{N} that can be used to index into memory.

If the index kind is not provided it defaults to Linear, this is subject to change.

Examples

@index(Global, Linear)
@index(Global, Cartesian)
@index(Local, Cartesian)
@index(Group, Linear)
@index(Local, NTuple)
@index(Global)
source
KernelAbstractions.@privateMacro
@private T dims

Declare storage that is local to each item in the workgroup. This can be safely used across @synchronize statements. On a CPU, this will allocate additional implicit dimensions to ensure correct localization.

For storage that only persists between @synchronize statements, an MArray can be used instead.

See also @uniform.

source
@private mem = 1

Creates a private local of mem per item in the workgroup. This can be safely used across @synchronize statements.

source
KernelAbstractions.@synchronizeMacro
@synchronize()

After a @synchronize statement 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.

Note

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

source
@synchronize(cond)

After a @synchronize statement 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. cond is not allowed to have any visible sideffects.

Platform differences

  • GPU: This synchronization will only occur if the cond evaluates.
  • CPU: This synchronization will always occur.
Warning

This variant of the @synchronize macro violates the requirement that @synchronize must be encountered by all workitems of a work-group executing the kernel or by none at all. Since v0.9.34 this version of the macro is deprecated and lowers to @synchronize()

source
KernelAbstractions.@printMacro
@print(items...)

This is a unified print statement.

Platform differences

  • GPU: This will reorganize the items to print via @cuprintf
  • CPU: This will call print(items...)
source
KernelAbstractions.@uniformMacro
@uniform expr

expr is evaluated outside the workitem scope. This is useful for variable declarations that span workitems, or are reused across @synchronize statements.

source
KernelAbstractions.@groupsizeMacro
@groupsize()

Query the workgroupsize on the backend. This function returns a tuple corresponding to kernel configuration. In order to get the total size you can use prod(@groupsize()).

source

Host language

Note

The Backend type hierarchy and most of the host-side management functions below (get_backend, allocate, synchronize, device selection, …) are defined in the KernelInterface sibling package and re-exported by KernelAbstractions, so KernelAbstractions.allocate and KernelInterface.allocate are the same function. User code can keep calling them through KernelAbstractions as before. Note that this only applies to KernelAbstractions 0.10 and later: KernelAbstractions 0.9 defines its own versions of these functions, which are distinct from the KernelInterface ones.

Backends and arrays

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

Type alias for POCLBackend, the CPU execution backend.

Construct with CPU() (equivalent to POCLBackend()). Kernels run on the host via POCL/OpenCL using the same programming model as GPU backends, which is useful for debugging and for running kernel code without a GPU.

Example

A = ones(Float32, 1024)
mul2_kernel(CPU(), 64)(A, ndrange=length(A))
synchronize(CPU())
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
Adapt.adapt_storageMethod
adapt(backend::Backend, x)

Convert x such that its array storage lives on backend. This is an extension of Adapt.jl, and lets code move data to a backend without knowing the backend's array type:

using Adapt
x = adapt(CUDABackend(), rand(Float32, 8))  # a CuArray
y = adapt(CPU(), x)                         # an Array again
Note

Backend implementations must implement Adapt.adapt_storage(::NewBackend, x). Adapt.jl's fallback is the identity, so a backend that omits this method silently leaves data where it is. The recommended definition delegates to the backend's array type, so that adapt(backend, x) behaves exactly like adapt(BackendArray, x):

Adapt.adapt_storage(::CUDABackend, x) = adapt(CuArray, x)
KernelAbstractions 0.10

adapt(backend, x) has been supported by the GPU backends since KernelAbstractions 0.9, but is only documented, and required of every backend, since 0.10.

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

Devices and execution

KernelInterface.synchronizeFunction
synchronize(::Backend)

Synchronize the current backend: block the calling task until all work it has queued on backend has completed.

Note

Backend implementations must implement this function, and it must be cooperative: it may not block inside a driver call, but has to yield to the Julia scheduler while waiting. See the notes for backend implementations for why.

source
KernelAbstractions.@spawnMacro
@spawn [threadpool] backend [device=id] expr

Run expr on a new Julia task, like Threads.@spawn, and return the Task. Use it in place of Threads.@spawn to launch kernels from a task. It guarantees that

  • the task runs on the device that was active in the spawning task, or on device when that argument is given;
  • the work the task queues on backend runs after the work the spawning task had queued on backend before calling @spawn;
  • once wait(task) or fetch(task) returns, all work the task queued on backend has completed, so its results may be used from any task. fetch(task) returns the value of expr. If expr throws, the task is not synchronized: its queued work may still be running when the exception surfaces.

Everything else works as for Threads.@spawn: the optional threadpool argument (:default or :interactive) is forwarded, $x captures the value of x at spawn time, and an enclosing @sync waits for the task.

Example

A = KernelAbstractions.ones(backend, Float32, 1024)
mul2_kernel(backend, 64)(A, ndrange = length(A))   # queued by the current task

task = KernelAbstractions.@spawn backend begin
    mul2_kernel(backend, 64)(A, ndrange = length(A))  # ordered after the launch above
    sum(A)
end
fetch(task) == 4 * length(A)

Choosing the device

Backends keep the active device in task-local state, and Julia does not copy that state into a child task. A task started with plain Threads.@spawn therefore runs on the backend's default device, whichever device the spawning task was using. @spawn selects the device explicitly instead: by default the one active in the spawning task, or the one named by device, a 1-based index into 1:ndevices(backend):

task = KernelAbstractions.@spawn backend device=2 begin
    mul2_kernel(backend, 64)(B, ndrange = length(B))
end

The ordering guarantee holds across that switch: the task's work on device is still ordered after the work the spawning task had queued on its device. Backends that support more than one device implement this with a cross-device wait_event.

Note

expr should not rely on data that the spawning task queues after @spawn returns. Order later work by waiting on the task, or by spawning again.

Note

Prefer device= over calling device! inside expr. A device! in the body carries no ordering of its own, so work queued after it is ordered neither against the spawning task nor against what the body queued before the switch; you would have to bracket it with record_event and wait_event yourself.

Note

If expr throws the state of the device and the internal queue is unspecified.

Backend authors: see the notes for backend implementations for the protocol behind these guarantees, and for how to support it without a full synchronize.

source
KernelInterface.record_eventFunction
record_event(backend::Backend)

Capture the work the calling task has queued on backend's currently active device so far, and return a handle that wait_event can use to order later work after it, either from another task or from the same task after switching devices.

The handle is only meaningful for the pair record_event/wait_event; do not use it for anything else.

Note

The default implementation calls synchronize and returns nothing. Backends whose queue is task-local may override this to return an event recorded on the current task's queue instead, without blocking the host. Such a backend must then also implement wait_event for the returned type. See the notes for backend implementations.

source
KernelInterface.wait_eventFunction
wait_event(backend::Backend, event)

Order the work the calling task subsequently queues on backend's currently active device after the work captured by event, which was returned by record_event.

The dependency is queue-ordered rather than task-ordered: it applies to the device that is active when wait_event is called, and a later device! leaves the newly selected device unordered with respect to event. Select the device first and wait afterwards:

event = record_event(backend)   # captures work on the current device
device!(backend, 2)
wait_event(backend, event)      # device 2 now waits for that work
Note

wait_event(::Backend, ::Nothing) is a no-op, matching the default record_event. A backend that implements record_event must implement this for the event type it returns, either by enqueuing a dependency on the current task's queue, or by waiting cooperatively as synchronize does. A backend with more than one device must also accept an event that was recorded on a different device, by enqueuing the cross-device dependency if the driver supports one (CUDA's cuStreamWaitEvent does) and by waiting cooperatively otherwise. See the notes for backend implementations.

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

device! is not a synchronization point: work queued before the switch is not ordered with respect to work queued after it. To order across a switch, either synchronize beforehand, or bracket the switch with record_event and wait_event.

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

Kernel handles

KernelAbstractions.KernelType
Kernel{Backend, WorkgroupSize, NDRange, Func}

Host-side handle for a kernel specialized on a backend, workgroup size, and ndrange.

Kernels are created by calling a @kernel function on a backend, for example my_kernel(CUDABackend(), 256). The returned object is callable:

kernel = my_kernel(backend, 64)
kernel(A, B, ndrange=length(A))   # launch asynchronously
synchronize(backend)

Use workgroupsize, ndrange, and backend to inspect a kernel's static configuration.

Note

Backend implementations must implement:

(kernel::Kernel{<:NewBackend})(args...; ndrange=nothing, workgroupsize=nothing)

As well as the on-device functionality.

source

Reflection

To look at the code a backend actually generates, wrap a kernel launch in one of the @device_code_* macros below. They work the same on the CPU backend and on GPU backends, and they are public, but not exported, so you must call them qualified:

KernelAbstractions.@device_code_llvm mul2(backend, 64)(A, ndrange=length(A))
KernelAbstractions.@device_code_loweredMacro
KernelAbstractions.@device_code_lowered [kwargs...] ex

Evaluate ex and, for every device kernel compiled along the way, show the lowered IR.

This is GPUCompiler.@device_code_lowered, re-exposed for convenience; see its documentation for the supported keyword arguments. It applies to any GPUCompiler-based backend, so wrapping a kernel launch works on the CPU backend and on GPU backends alike. Note that ex is really evaluated: the kernels it launches are compiled and run.

Examples

KernelAbstractions.@device_code_lowered my_kernel(backend, 64)(A, ndrange=length(A))
source
KernelAbstractions.@device_code_typedMacro
KernelAbstractions.@device_code_typed [kwargs...] ex

Evaluate ex and, for every device kernel compiled along the way, show the type-inferred IR.

This is GPUCompiler.@device_code_typed, re-exposed for convenience; see its documentation for the supported keyword arguments. It applies to any GPUCompiler-based backend, so wrapping a kernel launch works on the CPU backend and on GPU backends alike. Note that ex is really evaluated: the kernels it launches are compiled and run.

Examples

KernelAbstractions.@device_code_typed my_kernel(backend, 64)(A, ndrange=length(A))
source
KernelAbstractions.@device_code_warntypeMacro
KernelAbstractions.@device_code_warntype [kwargs...] ex

Evaluate ex and, for every device kernel compiled along the way, show the type-inferred IR, highlighting type instabilities.

This is GPUCompiler.@device_code_warntype, re-exposed for convenience; see its documentation for the supported keyword arguments. It applies to any GPUCompiler-based backend, so wrapping a kernel launch works on the CPU backend and on GPU backends alike. Note that ex is really evaluated: the kernels it launches are compiled and run.

Examples

KernelAbstractions.@device_code_warntype my_kernel(backend, 64)(A, ndrange=length(A))
source
KernelAbstractions.@device_code_llvmMacro
KernelAbstractions.@device_code_llvm [kwargs...] ex

Evaluate ex and, for every device kernel compiled along the way, show the generated LLVM IR.

This is GPUCompiler.@device_code_llvm, re-exposed for convenience; see its documentation for the supported keyword arguments. It applies to any GPUCompiler-based backend, so wrapping a kernel launch works on the CPU backend and on GPU backends alike. Note that ex is really evaluated: the kernels it launches are compiled and run.

Examples

KernelAbstractions.@device_code_llvm my_kernel(backend, 64)(A, ndrange=length(A))
source
KernelAbstractions.@device_code_nativeMacro
KernelAbstractions.@device_code_native [kwargs...] ex

Evaluate ex and, for every device kernel compiled along the way, show the generated machine code.

This is GPUCompiler.@device_code_native, re-exposed for convenience; see its documentation for the supported keyword arguments. It applies to any GPUCompiler-based backend, so wrapping a kernel launch works on the CPU backend and on GPU backends alike. Note that ex is really evaluated: the kernels it launches are compiled and run.

Examples

KernelAbstractions.@device_code_native my_kernel(backend, 64)(A, ndrange=length(A))
source
KernelAbstractions.@device_codeMacro
KernelAbstractions.@device_code [dir=...] [...] ex

Evaluate ex and dump all forms of code generated for the device kernels it compiles to the directory dir, or to a temporary directory if none is given.

This is GPUCompiler.@device_code, re-exposed for convenience; see its documentation for the supported keyword arguments. Like the other @device_code_* macros it applies to any GPUCompiler-based backend, and really evaluates ex.

source

Internal

The functionalities in this section are considered internal and not part of the public API contract. They are only documented here for developers and contributors of KernelAbstractions.jl, but should not be used by end users (and if they do, they should expect breakage without notice).

KernelAbstractions.partitionFunction
partition(kernel, ndrange, workgroupsize)

Partition the iteration space of kernel into workgroups.

Returns the blocked iteration space and whether dynamic bounds-checking is required for the last (possibly partial) workgroup. Primarily used by backend implementations and tests.

source
KernelAbstractions.@contextMacro
@context()

Access the hidden context object used by KernelAbstractions.

Warning

Only valid to be used from a kernel with cpu=false.

Note

@context will be supported on all backends in KernelAbstractions 1.0

function f(@context, a)
    I = @index(Global, Linear)
    a[I]
end

@kernel cpu=false function my_kernel(a)
    f(@context, a)
end
source
KernelAbstractions.argconvertFunction
argconvert(kernel::Kernel, arg)

Convert arg to the device-side representation expected by kernel's backend.

Backend implementations define methods for their array and scalar types. This is called automatically when a kernel is launched.

source
KernelAbstractions.NDIteration.StaticSizeType
StaticSize{S}

Marker type encoding a compile-time workgroup size or ndrange as a tuple S. Each entry of S is an Int extent or, for an ndrange axis whose indices do not start at 1, a UnitRange{Int}.

source
KernelAbstractions.NDIteration.NDRangeType
NDRange

Encodes a blocked iteration space. The mapping field relates blocked indices to ndrange indices: nothing for the identity, or a StaticOffset/DynamicOffset for an ndrange whose indices do not start at 1.

Example

ndrange = NDRange{2, DynamicSize, DynamicSize}(CartesianIndices((256, 256)), CartesianIndices((32, 32)))
for block in ndrange
    for items in workitems(ndrange)
        I = expand(ndrange, block, items)
        checkbounds(Bool, A, I) || continue
        @inbounds A[I] = 2*A[I]
    end
end
source