API
Kernel language
KernelAbstractions.@kernel — Macro
@kernel function f(args) endTakes 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 ndrangeExample
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)@kernel config function f(args) endThis allows for two different configurations:
cpu={true, false}: Disables code-generation of the CPU function. This relaxes semantics such that KernelAbstractions primitives can be used in non-kernel functions.inbounds={false, true}: Enables a forced@inboundsmacro around the function definition in the case the user is using too many@inboundsalready in their kernel. Note that this can lead to incorrect results, crashes, etc and is fundamentally unsafe. Be careful!unsafe_indices={false, true}: Disables the implicit validation of indices, users must avoid@index(Global).
KernelAbstractions.@Const — Macro
@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.
KernelAbstractions.@index — Macro
@indexThe @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 theworkgroup.Local: The withinworkgroupindex.
Index kind
Linear: Produces anInt64that can be used to linearly index into memory.Cartesian: Produces aCartesianIndex{N}that can be used to index into memory.NTuple: Produces aNTuple{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)KernelAbstractions.@localmem — Macro
@localmem T dimsDeclare storage that is local to a workgroup.
KernelAbstractions.@private — Macro
@private T dimsDeclare 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.
@private mem = 1Creates a private local of mem per item in the workgroup. This can be safely used across @synchronize statements.
KernelAbstractions.@synchronize — Macro
@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.
@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 thecondevaluates.CPU: This synchronization will always occur.
KernelAbstractions.@print — Macro
@print(items...)This is a unified print statement.
Platform differences
GPU: This will reorganize the items to print via@cuprintfCPU: This will callprint(items...)
KernelAbstractions.@uniform — Macro
@uniform exprexpr is evaluated outside the workitem scope. This is useful for variable declarations that span workitems, or are reused across @synchronize statements.
KernelAbstractions.@groupsize — Macro
@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()).
KernelAbstractions.@ndrange — Macro
@ndrange()Query the ndrange on the backend. This function returns a tuple corresponding to kernel configuration.
Host language
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.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.
KernelAbstractions.CPU — Type
CPUType 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())KernelAbstractions.POCL.POCLKernels.POCLBackend — Type
POCLBackend()CPU backend that compiles kernels to OpenCL via POCL and executes them on the host. This is the concrete type behind the CPU alias.
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
Adapt.adapt_storage — Method
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 againBackend 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)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.
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.
Devices and execution
KernelInterface.synchronize — Function
synchronize(::Backend)Synchronize the current backend: block the calling task until all work it has queued on backend has completed.
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.
KernelAbstractions.@spawn — Macro
@spawn [threadpool] backend [device=id] exprRun 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
devicewhen that argument is given; - the work the task queues on
backendruns after the work the spawning task had queued onbackendbefore calling@spawn; - once
wait(task)orfetch(task)returns, all work the task queued onbackendhas completed, so its results may be used from any task.fetch(task)returns the value ofexpr. Ifexprthrows, 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))
endThe 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.
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.
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.
Backend authors: see the notes for backend implementations for the protocol behind these guarantees, and for how to support it without a full synchronize.
KernelInterface.record_event — Function
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.
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.
KernelInterface.wait_event — Function
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 workwait_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.
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).
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 deviceKernelInterface.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.
Kernel handles
KernelAbstractions.Kernel — Type
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.
KernelAbstractions.workgroupsize — Function
workgroupsize(kernel::Kernel)Return the static workgroup size type parameter of kernel (StaticSize or DynamicSize).
KernelAbstractions.ndrange — Function
ndrange(ctx)Return the launch ndrange as a tuple.
KernelAbstractions.backend — Function
backend(kernel::Kernel)Return the Backend that kernel was constructed for.
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_lowered — Macro
KernelAbstractions.@device_code_lowered [kwargs...] exEvaluate 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))KernelAbstractions.@device_code_typed — Macro
KernelAbstractions.@device_code_typed [kwargs...] exEvaluate 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))KernelAbstractions.@device_code_warntype — Macro
KernelAbstractions.@device_code_warntype [kwargs...] exEvaluate 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))KernelAbstractions.@device_code_llvm — Macro
KernelAbstractions.@device_code_llvm [kwargs...] exEvaluate 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))KernelAbstractions.@device_code_native — Macro
KernelAbstractions.@device_code_native [kwargs...] exEvaluate 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))KernelAbstractions.@device_code — Macro
KernelAbstractions.@device_code [dir=...] [...] exEvaluate 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.
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.partition — Function
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.
KernelAbstractions.@context — Macro
@context()Access the hidden context object used by KernelAbstractions.
function f(@context, a)
I = @index(Global, Linear)
a[I]
end
@kernel cpu=false function my_kernel(a)
f(@context, a)
endKernelAbstractions.argconvert — Function
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.
KernelAbstractions.NDIteration.DynamicSize — Type
DynamicSizeMarker type indicating that a kernel's workgroup size or ndrange is chosen at launch time.
KernelAbstractions.NDIteration.StaticSize — Type
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}.
KernelAbstractions.NDIteration.NDRange — Type
NDRangeEncodes 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
endKernelAbstractions.NDIteration.StaticOffset — Type
StaticOffset{O}Compile-time offset O::NTuple{N, Int} added to the indices produced by an NDRange.
KernelAbstractions.NDIteration.DynamicOffset — Type
DynamicOffset{N}Runtime offset added to the indices produced by an NDRange.
KernelAbstractions.NDIteration.extents — Function
extents(ndrange)Number of indices along each axis of ndrange, given as a tuple of extents and/or ranges, a CartesianIndices, a single range, or an integer.
KernelAbstractions.NDIteration.offsets — Function
offsets(ndrange)Offset of the first index along each axis of ndrange relative to 1.
KernelAbstractions.NDIteration.linear_index — Function
linear_index(ndrange::CartesianIndices, I::CartesianIndex)Column-major position of I within ndrange, counted from 1.