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

source

Semantics

A few rules hold throughout the interface:

  • Execution is task-local. A backend value (e.g. CUDABackend()) identifies a backend and its configuration, such as compiler options. Each Julia task has an active device per backend (selected with device!) and a queue on it. Host-side queries and compilation use the active device, allocations go to it, and copies and launches go to the calling task's queue. synchronize waits for that queue, and record_event/wait_event order work across queues. Switching devices doesn't synchronize.
  • Compiled kernels belong to a device. Queries on a Kernel (max_work_group_size, launch_configuration) answer for the device it was compiled for. Launching it after switching to another device either works or throws, but never runs on the wrong device.
  • Indices are 1-based, and x is the fastest-varying dimension.
  • Capabilities default to "unsupported". A backend that doesn't implement a supports_* query never claims support.

Contract

What a backend implements, at a glance. The docstrings below have the details.

RequiredOptional (fallback)
Backendsubtype Backend; get_backend for its array type
Memoryallocate, copyto!allocate(...; unified=true) (throws), pagelock! (missing), unsafe_free! (no-op)
Executionsynchronize (cooperative)record_event/wait_event (synchronize), priority! (no-op)
Deviceswith more than one device: ndevices, device, device!, device(backend, A)all four (a single device)
Queriesmax_work_group_size (for the backend and for a kernel), max_work_group_dims, max_num_groupslaunch_configuration (the limit), multiprocessor_count (0), functional (missing), versioninfo
Capabilitiessupports_float64, supports_atomics, supports_unified, supports_subgroups, supports_linear_subgroups, supports_independent_subgroups, supports_partitionable_subgroups (all false), supports_shuffle (false unless derived from the natively supported types, see its docstring)
Compilationargconvert, kernel_function, launch
Deviceget_local_id, get_group_id, get_local_size, get_num_groups, localmemory, barrierget_global_id, get_global_size (derived from the primitive queries), _print (host print)
Sub-groupsif supports_subgroups: sub_group_size, the sub-group queries, sub_group_barrier, sub_group_any, sub_group_all, and sub_group_ballot for widths of at most 64; if supports_shuffle(backend, T): shfl, shfl_down, shfl_up, shfl_xor for the primitive T supported natively, including UInt32sub_group_reduce, sub_group_scan, sub_group_exclusive_scan (built on the shuffles); the functions with a width (built on those without, or on the ballot); shuffles of other primitive types (as UInt32 or UInt64 words) and of structs (packed into UInt32 words)

Everything else, such as zeros, ones, the launch-keyword handling of Kernel and @launch, is generic and not meant to be overridden.

Versioning

  • Required methods only change in breaking releases (0.x → 0.x+1).
  • Optional methods can be added in any release, with a fallback that is conservative: never claiming support, never wrong. Tests for them pass on the fallback, or are gated on a capability query.
  • A patch release may add tests of behavior that was already specified; tests for newly specified behavior are new obligations and wait for a breaking release.

Backends test themselves against the contract with the testsuite in lib/KernelInterface/test:

import KernelInterface
using Test
include(joinpath(pkgdir(KernelInterface), "test", "testsuite.jl"))
Testsuite.testsuite(MyBackend(), MyArray)

Backend hierarchy

Backends subtype Backend, and everything else in the interface dispatches on that type. It and the host-side management functions below are re-exported by KernelAbstractions, so their canonical docstrings are on the API page.

KernelInterface.Backend — Type
Backend

Abstract supertype for all KernelInterface backends. Backends subtype it directly.

A backend value identifies a backend and its configuration (e.g. compiler options). The device and the queue that operations use are task-local: each task selects its active device with device!. Host-side queries answer for the calling task's active device, and work is queued on the calling task's queue of that device. Use get_backend to obtain the backend of 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.get_backend — Function
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_local_id(::Type{T}) where {T} = ...

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. They take an optional integer type T for the components, defaulting to Int, so a kernel can request e.g. Int32 indices with KI.get_global_id(Int32). The operands are converted to T before any arithmetic, and the result is the exact value modulo T: a query never throws, and a value that doesn't fit wraps around, as with x % T.

Backends implement the four primitive queries. get_global_id and get_global_size have fallbacks derived from them, which backends with a native builtin (e.g. SPIR-V and Metal) should override.

KernelInterface.get_local_id — Function
get_local_id([::Type{T}=Int])::@NamedTuple{x::T, y::T, z::T}

The 1-based index of the work-item within its work-group, as integers of type T.

T is a fixed-width integer type of at most 64 bits (e.g. Int32 or UInt64); the result is the exact value modulo T, as if computed with x % T.

Note

Backend implementations must implement:

@device_override get_local_id(::Type{T})::@NamedTuple{x::T, y::T, z::T} where {T}

The zero-argument form forwards to get_local_id(Int).

source
KernelInterface.get_group_id — Function
get_group_id([::Type{T}=Int])::@NamedTuple{x::T, y::T, z::T}

The 1-based index of the work-group within the launch, as integers of type T.

See get_local_id for the supported types T.

Note

Backend implementations must implement:

@device_override get_group_id(::Type{T})::@NamedTuple{x::T, y::T, z::T} where {T}

The zero-argument form forwards to get_group_id(Int).

source
KernelInterface.get_local_size — Function
get_local_size([::Type{T}=Int])::@NamedTuple{x::T, y::T, z::T}

The number of work-items in a work-group, as integers of type T.

See get_local_id for the supported types T.

Note

Backend implementations must implement:

@device_override get_local_size(::Type{T})::@NamedTuple{x::T, y::T, z::T} where {T}

The zero-argument form forwards to get_local_size(Int).

source
KernelInterface.get_num_groups — Function
get_num_groups([::Type{T}=Int])::@NamedTuple{x::T, y::T, z::T}

The number of work-groups in the launch, as integers of type T.

See get_local_id for the supported types T.

Note

Backend implementations must implement:

@device_override get_num_groups(::Type{T})::@NamedTuple{x::T, y::T, z::T} where {T}

The zero-argument form forwards to get_num_groups(Int).

source
KernelInterface.get_global_id — Function
get_global_id([::Type{T}=Int])::@NamedTuple{x::T, y::T, z::T}

The 1-based index of the work-item within the launch, as integers of type T: (get_group_id(T) - 1) * get_local_size(T) + get_local_id(T) per dimension.

See get_local_id for the supported types T.

Note

The fallback derives this from the primitive queries. Backend implementations with a native builtin should override it, returning the same values:

@device_override get_global_id(::Type{T})::@NamedTuple{x::T, y::T, z::T} where {T}
source
KernelInterface.get_global_size — Function
get_global_size([::Type{T}=Int])::@NamedTuple{x::T, y::T, z::T}

The number of work-items in the launch, as integers of type T: get_local_size(T) * get_num_groups(T) per dimension. For an ndrange launch, this is the ndrange padded to whole work-groups.

See get_local_id for the supported types T.

Note

The fallback derives this from the primitive queries. Backend implementations with a native builtin should override it, returning the same values:

@device_override get_global_size(::Type{T})::@NamedTuple{x::T, y::T, z::T} where {T}
source

Sub-groups

Sub-groups are optional (supports_subgroups). A work-group is divided into sub-groups of at most W =sub_group_size(backend) work-items, the sub-group width. What KernelInterface guarantees, and backends that report sub-group support have to ensure:

  • every work-item has a unique (get_sub_group_id(), get_sub_group_local_id()) pair in its work-group, which doesn't change during the kernel;
  • the sub-group ids are 1:get_num_sub_groups(), and the lanes of a sub-group are 1:get_sub_group_size().

Which work-items form a sub-group is not specified: it differs between APIs, devices and work-group shapes. For example, Intel's CPU OpenCL runtime forms sub-groups per row of a multi-dimensional work-group, so that a 33×2 work-group consists of four sub-groups of 32, 1, 32 and 1 work-items. In particular, get_num_sub_groups can be larger than cld(prod(get_local_size()), W). Storage for a value per sub-group has to be sized for up to one sub-group per work-item, and code combining those values has to use get_num_sub_groups() rather than compute the count.

A sub-group is partial when it has fewer work-items than the width: the lanes get_sub_group_size()+1:W have no work-item. Shuffles from those lanes give unspecified values, and the votes, sub_group_reduce and the scans only take the work-items of the sub-group into account.

Backends that guarantee more say so with these queries:

  • supports_linear_subgroups: if the work-group is 1-D, or its x extent is a multiple of W, sub-groups are formed from consecutive work-items, x fastest: the work-item with the linear local index lin = x + (y - 1) * size.x + (z - 1) * size.x * size.y (for (; x, y, z) = get_local_id() and size = get_local_size()) is in sub-group fld(lin - 1, W) + 1, lane mod(lin - 1, W) + 1, and only the last sub-group can be partial. Code that relates local ids to lanes needs this, e.g. stencils exchanging neighbouring values with shuffles. Code that doesn't, like a work-group reduction that stores a result per sub-group, can index those results by get_sub_group_id() and combine them using get_num_sub_groups().
  • supports_independent_subgroups: the communication functions (shuffles, votes, collectives and sub_group_barrier) can be executed in control flow that is uniform over each sub-group, but differs between the sub-groups of a work-group, e.g. a loop whose trip count depends on the sub-group, or after an early return of whole sub-groups. Otherwise, all sub-groups of a work-group have to execute the same communication functions in the same order.
  • supports_partitionable_subgroups: a sub-group can be partitioned into segments that diverge independently. The communication functions with a width work on segments of width consecutive lanes; all work-items of a segment have to execute them together, but different segments of a sub-group may take different branches or loop a different number of times. Implies supports_independent_subgroups.
Backendsupports_linear_subgroupssupports_independent_subgroupssupports_partitionable_subgroups
CUDA, AMDGPUtruetruetrue
Metaltrue (not specified by Metal, but tested)truetrue: Apple GPUs execute a SIMD-group in lock-step, so the barrier for a segment is a SIMD-group-scope fence (not specified by Metal, but validated on M1 and M3)
oneAPItruetruetrue: Intel GPUs execute a sub-group as the SIMD lanes of one hardware thread, so the barrier for a segment is a sub-group-scope fence
OpenCL.jlon PoCL and Intel's runtimes, which form them linearlytrue, except on PoCL and Intel's CPU runtimeon Intel's GPU runtime only, as with oneAPI
KernelAbstractions' POCL backendtruefalse: sub-group operations are work-group barriersfalse

Unless sub-groups are partitionable, the functions with a width need the whole sub-group, like the others: all its work-items have to execute them together, not in a branch that only some of its lanes take. Without independent sub-groups, all sub-groups of the work-group have to execute the same communication functions in the same order. The id and size queries can be used anywhere.

Kernel languages

The sub-group functions are meant for kernels written against KernelInterface, and for kernel languages built on it. A kernel is either a KernelInterface kernel or a KernelAbstractions @kernel, never a mix: @kernel adds padding work-items to partial work-groups, which skip the kernel's body, so KernelInterface's sub-group functions called in a @kernel aren't reached by all work-items. A @kernel uses KernelAbstractions' collectives instead, e.g. @groupreduce. Host code may use the queries of both.

KernelInterface.get_sub_group_size — Function
get_sub_group_size([::Type{T}=Int])::T

The number of work-items in the sub-group, at most the sub-group width (get_max_sub_group_size). Which sub-groups have fewer work-items than the width is unspecified (unless the layout is linear, see supports_linear_subgroups): there can be more than one, e.g. one per row of a multi-dimensional work-group.

See get_local_id for the supported types T.

Note

Backend implementations that support sub-groups must implement:

@device_override get_sub_group_size(::Type{T})::T where {T}

The zero-argument form forwards to get_sub_group_size(Int).

source
KernelInterface.get_max_sub_group_size — Function
get_max_sub_group_size([::Type{T}=Int])::T

The sub-group width (the warp or wavefront size), sub_group_size(backend) on the host.

Backends should make it a constant of the generated code, so that code depending on it, e.g. a loop over the lanes or a shuffle butterfly, is specialized for it; on some, it is only folded late in compilation, or not at all. Don't rely on it being known during type inference: to pick types or Val parameters from the width, e.g. the integer type of a mask with a bit per lane, pass sub_group_size(backend) from the host.

See get_local_id for the supported types T.

Note

Backend implementations that support sub-groups must implement this, and should return the width the kernel is compiled for as a constant rather than query the device at run time, so that code depending on it is specialized for the width:

@device_override get_max_sub_group_size(::Type{T})::T where {T}

The zero-argument form forwards to get_max_sub_group_size(Int).

source
KernelInterface.get_num_sub_groups — Function
get_num_sub_groups([::Type{T}=Int])::T

The number of sub-groups in the work-group. It is at least cld(prod(get_local_size()), get_max_sub_group_size()), but can be larger, since more than one sub-group can be partial (unless the layout is linear, see supports_linear_subgroups). Size storage for a value per sub-group for up to one sub-group per work-item.

See get_local_id for the supported types T.

Note

Backend implementations that support sub-groups must implement:

@device_override get_num_sub_groups(::Type{T})::T where {T}

The zero-argument form forwards to get_num_sub_groups(Int).

source
KernelInterface.get_sub_group_id — Function
get_sub_group_id([::Type{T}=Int])::T

The 1-based index of the sub-group within the work-group, between 1 and get_num_sub_groups. How it relates to get_local_id is unspecified, unless the layout is linear (see supports_linear_subgroups, which only covers some work-group shapes).

See get_local_id for the supported types T.

Note

Backend implementations that support sub-groups must implement:

@device_override get_sub_group_id(::Type{T})::T where {T}

The zero-argument form forwards to get_sub_group_id(Int).

source
KernelInterface.get_sub_group_local_id — Function
get_sub_group_local_id([::Type{T}=Int])::T

The 1-based index of the work-item within its sub-group (its lane), between 1 and get_sub_group_size. It doesn't depend on which work-items of the sub-group are active, e.g. in a divergent branch. How it relates to get_local_id is unspecified, unless the layout is linear (see supports_linear_subgroups, which only covers some work-group shapes).

See get_local_id for the supported types T.

Note

Backend implementations that support sub-groups must implement:

@device_override get_sub_group_local_id(::Type{T})::T where {T}

The zero-argument form forwards to get_sub_group_local_id(Int).

source

Barriers

KernelInterface.barrier — Function
barrier()

Wait until all work-items of the work-group have reached the barrier. Afterwards, the writes to global and local memory that each work-item made before the barrier are visible to all work-items of the work-group.

This does not order memory between work-groups.

All work-items of a work-group have to reach the same barrier() (not in a divergent branch).

Note

Backend implementations must implement:

@device_override barrier()
source
KernelInterface.sub_group_barrier — Function
sub_group_barrier()

Like barrier, for the work-items of a sub-group: wait until all work-items of the sub-group have reached the barrier, and make their writes to global and local memory before it visible to the sub-group.

All work-items of a sub-group have to reach the same sub_group_barrier(); see shfl.

Note

Backend implementations that support sub-groups must implement:

@device_override sub_group_barrier()
source
sub_group_barrier(width::Integer)

Like sub_group_barrier, for the work-items of the caller's segment of width consecutive lanes of the sub-group, as for the shuffles with a width (see shfl): wait until they have reached the barrier, and make their writes to global and local memory before it visible to the segment. It doesn't order memory with other segments.

Note

Backends may implement this. A backend that implements supports_partitionable_subgroups must implement it unless its sub_group_barrier can be executed by a segment alone. The fallback is sub_group_barrier.

source
KernelInterface.sub_group_barrier — Method
sub_group_barrier(width::Integer)

Like sub_group_barrier, for the work-items of the caller's segment of width consecutive lanes of the sub-group, as for the shuffles with a width (see shfl): wait until they have reached the barrier, and make their writes to global and local memory before it visible to the segment. It doesn't order memory with other segments.

Note

Backends may implement this. A backend that implements supports_partitionable_subgroups must implement it unless its sub_group_barrier can be executed by a segment alone. The fallback is sub_group_barrier.

source

Memory

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

Declare an array of element type T and size dims in memory that is local to a work-group. dims has to be known at compile time.

Every call site of localmemory in a kernel has its own memory, shared by all work-items of a work-group. It is uninitialized, and lives until the work-group finishes. Executing the same call site again, e.g. in a loop, returns the same memory. A function containing a call that is itself called from several places may get the same memory at each of them, or different memory, depending on whether it is inlined: don't rely on either. Use barrier to make writes visible to the other work-items.

Note

Backend implementations must implement:

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

Communication

The shuffles, votes and collectives below don't synchronize accesses to local or global memory. When a work-item reads memory that another one of its sub-group wrote, put a sub_group_barrier between the write and the read, or sub_group_barrier(width) within a segment.

KernelInterface.shfl — Function
shfl(val::T, lane::Integer)::T

Return val of the work-item with get_sub_group_local_id equal to lane in the sub-group. When there is no such work-item, the result is an unspecified value (of type T).

All work-items of the sub-group have to execute shfl together (not in a divergent branch), but they may read from different lanes. Unless supports_independent_subgroups, all sub-groups of the work-group have to execute it.

shfl doesn't synchronize memory accesses: use sub_group_barrier to communicate through memory.

Types for which supports_shuffle returns true are supported. Besides the types a backend supports natively, that includes other primitive types of 1, 2, 4, 8 or 16 bytes (e.g. Bool, Char or Int64) if the backend supports UInt32, and isbits structs and tuples of such types. Primitive types of up to 4 bytes are shuffled as a UInt32, and larger ones as UInt64 words, each of which is shuffled natively if the backend supports UInt64, and as two UInt32 words otherwise. Structs and tuples are packed into UInt32 words, with fields of less than 4 bytes sharing words, and shuffled word by word.

Note

Backends that support shuffles must implement this for the primitive types they support natively, which have to include UInt32, and only for those, so that other types reach the fallbacks:

@device_override shfl(val::T, lane::Integer) where {T <: Union{...}}
source
shfl(val::T, lane::Integer, width::Integer)::T
shfl_down(val::T, offset::Integer, width::Integer)::T
shfl_up(val::T, offset::Integer, width::Integer)::T
shfl_xor(val::T, mask::Integer, width::Integer)::T

Shuffles within segments of width consecutive lanes of the sub-group, with CUDA's semantics for a width:

  • shfl reads from lane mod(lane - 1, width) + 1 of the caller's segment;
  • shfl_down and shfl_up read from the lane offset lanes further or earlier, for an offset of at least 0, and return val of the work-item itself where that lane is outside of the caller's segment;
  • shfl_xor reads from the lane whose 0-based position in the segment is the caller's xor mask, which has to be between 0 and width - 1.

Reading from a lane of the segment that has no work-item (in a partial sub-group) gives an unspecified value. width has to be a power of two of at most the sub-group width get_max_sub_group_size, and the same for all work-items of the sub-group, so that the segments partition it. It should be a constant of the generated code.

If supports_partitionable_subgroups, only the work-items of the caller's segment have to execute them together: the segments of a sub-group can diverge, e.g. loop a different number of times. Otherwise, all work-items of the sub-group have to execute them together, as for the shuffles without a width. Either way, offset and mask have to be the same for the work-items that execute them together, while lane may differ.

Note

Backends may implement these for the types they support natively, e.g. if they have native shuffles with a width. A backend that implements supports_partitionable_subgroups must implement them unless its shfl from a lane can be executed by a segment alone. The fallbacks pack other types into UInt32 words, and shuffle those with shfl from a lane.

source
KernelInterface.shfl_down — Function
shfl_down(val::T, offset::Integer)::T

Return val of the work-item offset lanes further in the sub-group, i.e. with get_sub_group_local_id equal to get_sub_group_local_id() + offset, for an offset of at least 0. When that lane is past the sub-group width, i.e. get_sub_group_local_id() + offset > get_max_sub_group_size(), the result is val of the work-item itself, as on CUDA, HIP and Metal. When the lane is within the width but has no work-item, in a partial sub-group, the result is an unspecified value (of type T).

All work-items of the sub-group have to execute shfl_down together (not in a divergent branch), with the same offset; see shfl.

shfl_down doesn't synchronize memory accesses. See shfl for the supported types.

Note

Backend implementations must implement this like shfl:

@device_override shfl_down(val::T, offset::Integer) where {T <: Union{...}}
source
KernelInterface.shfl_up — Function
shfl_up(val::T, offset::Integer)::T

Return val of the work-item offset lanes earlier in the sub-group, i.e. with get_sub_group_local_id equal to get_sub_group_local_id() - offset, for an offset of at least 0. When there is no such lane, i.e. get_sub_group_local_id() <= offset, the result is val of the work-item itself, as on CUDA, HIP and Metal.

All work-items of the sub-group have to execute shfl_up together (not in a divergent branch), with the same offset; see shfl.

shfl_up doesn't synchronize memory accesses. See shfl for the supported types.

Note

Backend implementations must implement this like shfl:

@device_override shfl_up(val::T, offset::Integer) where {T <: Union{...}}
source
KernelInterface.shfl_xor — Function
shfl_xor(val::T, mask::Integer)::T

Return val of the work-item whose 0-based lane id is the 0-based lane id of this work-item xor mask, i.e. with get_sub_group_local_id equal to ((get_sub_group_local_id() - 1) ⊻ mask) + 1. mask has to be between 0 and get_max_sub_group_size() - 1. When that lane has no work-item, in a partial sub-group, the result is an unspecified value (of type T).

All work-items of the sub-group have to execute shfl_xor together (not in a divergent branch), with the same mask; see shfl. For a reduction over the sub-group, use sub_group_reduce, which also handles partial sub-groups.

shfl_xor doesn't synchronize memory accesses. See shfl for the supported types.

Note

Backend implementations must implement this like shfl:

@device_override shfl_xor(val::T, mask::Integer) where {T <: Union{...}}
source
KernelInterface.shfl — Method
shfl(val::T, lane::Integer, width::Integer)::T
shfl_down(val::T, offset::Integer, width::Integer)::T
shfl_up(val::T, offset::Integer, width::Integer)::T
shfl_xor(val::T, mask::Integer, width::Integer)::T

Shuffles within segments of width consecutive lanes of the sub-group, with CUDA's semantics for a width:

  • shfl reads from lane mod(lane - 1, width) + 1 of the caller's segment;
  • shfl_down and shfl_up read from the lane offset lanes further or earlier, for an offset of at least 0, and return val of the work-item itself where that lane is outside of the caller's segment;
  • shfl_xor reads from the lane whose 0-based position in the segment is the caller's xor mask, which has to be between 0 and width - 1.

Reading from a lane of the segment that has no work-item (in a partial sub-group) gives an unspecified value. width has to be a power of two of at most the sub-group width get_max_sub_group_size, and the same for all work-items of the sub-group, so that the segments partition it. It should be a constant of the generated code.

If supports_partitionable_subgroups, only the work-items of the caller's segment have to execute them together: the segments of a sub-group can diverge, e.g. loop a different number of times. Otherwise, all work-items of the sub-group have to execute them together, as for the shuffles without a width. Either way, offset and mask have to be the same for the work-items that execute them together, while lane may differ.

Note

Backends may implement these for the types they support natively, e.g. if they have native shuffles with a width. A backend that implements supports_partitionable_subgroups must implement them unless its shfl from a lane can be executed by a segment alone. The fallbacks pack other types into UInt32 words, and shuffle those with shfl from a lane.

source
KernelInterface.sub_group_any — Function
sub_group_any(pred::Bool)::Bool

Whether pred is true for any work-item of the sub-group. All work-items of the sub-group get the same result.

All work-items of the sub-group have to execute sub_group_any together (not in a divergent branch); see shfl.

It doesn't synchronize memory accesses, see sub_group_barrier.

Note

Backend implementations that support sub-groups must implement:

@device_override sub_group_any(pred::Bool)::Bool
source
KernelInterface.sub_group_all — Function
sub_group_all(pred::Bool)::Bool

Whether pred is true for all work-items of the sub-group. All work-items of the sub-group get the same result.

All work-items of the sub-group have to execute sub_group_all together (not in a divergent branch); see shfl.

It doesn't synchronize memory accesses, see sub_group_barrier.

Note

Backend implementations that support sub-groups must implement:

@device_override sub_group_all(pred::Bool)::Bool
source
KernelInterface.sub_group_ballot — Function
sub_group_ballot(pred::Bool)::UInt64

A mask of the work-items of the sub-group for which pred is true: bit i - 1 (counting from the least significant bit) is set if and only if the sub-group has a work-item with get_sub_group_local_id equal to i and its pred is true. All other bits, e.g. those of lanes without a work-item in a partial sub-group, are zero. All work-items of the sub-group get the same result. count_ones(mask) counts the work-items, and for a nonzero mask, trailing_zeros(mask) + 1 is the lane of the first one.

All work-items of the sub-group have to execute sub_group_ballot together (not in a divergent branch); see shfl. Only sub-groups of at most 64 work-items are supported.

It doesn't synchronize memory accesses, see sub_group_barrier.

Note

Backend implementations that support sub-groups with a width of at most 64 must implement:

@device_override sub_group_ballot(pred::Bool)::UInt64
source
KernelInterface.sub_group_ballot — Method
sub_group_any(pred::Bool, width::Integer)::Bool
sub_group_all(pred::Bool, width::Integer)::Bool
sub_group_ballot(pred::Bool, width::Integer)::UInt64

The votes within segments of width consecutive lanes of the sub-group, as for the shuffles with a width (see shfl): whether pred is true for any or all work-items of the caller's segment, or a mask of those for which it is, relative to the segment: bit i - 1 for the i-th lane of the segment. Lanes without a work-item don't count.

For a width smaller than the sub-group width, they need a sub-group width of at most 64.

Note

Backends may implement these. A backend that implements supports_partitionable_subgroups must implement them unless its sub_group_ballot can be executed by a segment alone and only reports the work-items that execute it. The fallbacks use the votes without a width if width is the sub-group width, and sub_group_ballot otherwise.

source
KernelInterface.sub_group_reduce — Function
sub_group_reduce(op, val::T)::T

Reduce val over the work-items of the sub-group with the binary operator op, in the order of the lanes: op(…op(op(v₁, v₂), v₃)…, vₙ) for the values vᵢ of lanes 1:n, with n = get_sub_group_size(), up to associativity. op has to be associative, but needn't be commutative. All work-items of the sub-group get the result.

All work-items of the sub-group have to execute sub_group_reduce together (not in a divergent branch), with the same op; see shfl. Values of the types that the shuffles support are supported, see supports_shuffle, and others for which the backend has a native reduction.

It doesn't synchronize memory accesses, see sub_group_barrier.

Note

Backends may implement this for operators and types with a native reduction (e.g. + on Float32), dispatching on typeof(op). The result has to be the one documented above, with Julia's semantics of op (e.g. NaN propagation and the sign of zero for min and max, wrap-around for integers), with one exception: for + and * on floating-point values, it may differ as if the values were combined in another order and grouping. The fallback combines ranges of doubling length with a butterfly of shfl_xor, and in a partial sub-group broadcasts the result of the first lane with shfl.

source
sub_group_reduce(op, val::T, width::Integer)::T
sub_group_scan(op, val::T, width::Integer)::T
sub_group_exclusive_scan(op, val::T, init::T, width::Integer)::T

The collectives within segments of width consecutive lanes of the sub-group, as for the shuffles with a width (see shfl): the reduction and scans of sub_group_reduce, sub_group_scan and sub_group_exclusive_scan, over the work-items of the caller's segment, in the order of its lanes. init has to be the same for the work-items of a segment.

Note

Backends may implement these, like the collectives without a width. The fallbacks use the collectives without a width if width is the sub-group width, and the shuffles with a width otherwise, like the fallbacks of the collectives without one.

source
KernelInterface.sub_group_scan — Function
sub_group_scan(op, val::T)::T

The inclusive scan of val over the work-items of the sub-group with the associative binary operator op, in the order of the lanes: the work-item in lane i gets the reduction of the values of lanes 1 to i, as with sub_group_reduce.

All work-items of the sub-group have to execute sub_group_scan together (not in a divergent branch), with the same op; see shfl. Values of the types that the shuffles support are supported, see supports_shuffle, and others for which the backend has a native scan.

It doesn't synchronize memory accesses, see sub_group_barrier.

Note

Backends may implement this for operators and types with a native scan (e.g. + on Float32), dispatching on typeof(op), with the results documented above, as for sub_group_reduce: for floating-point + and *, the values of each lane's prefix may be combined in another order and grouping. The fallback is a Hillis-Steele scan with shfl_up.

source
KernelInterface.sub_group_exclusive_scan — Function
sub_group_exclusive_scan(op, val::T, init::T)::T

The exclusive scan of val over the work-items of the sub-group with the associative binary operator op, in the order of the lanes, starting from init: the work-item in lane 1 gets init itself, and the one in lane i > 1 gets op(init, r) for the reduction r of the values of lanes 1 to i - 1, as with sub_group_scan. init has to be the same for all work-items of the sub-group, and of the type of val; it needn't be an identity of op.

All work-items of the sub-group have to execute sub_group_exclusive_scan together (not in a divergent branch), with the same op; see shfl. Values of the types that the shuffles support are supported, see supports_shuffle, and others for which the backend has a native scan.

It doesn't synchronize memory accesses, see sub_group_barrier.

Note

Backends may implement this like sub_group_scan. The fallback shifts the result of sub_group_scan up by a lane with shfl_up.

source
KernelInterface.sub_group_reduce — Method
sub_group_reduce(op, val::T, width::Integer)::T
sub_group_scan(op, val::T, width::Integer)::T
sub_group_exclusive_scan(op, val::T, init::T, width::Integer)::T

The collectives within segments of width consecutive lanes of the sub-group, as for the shuffles with a width (see shfl): the reduction and scans of sub_group_reduce, sub_group_scan and sub_group_exclusive_scan, over the work-items of the caller's segment, in the order of its lanes. init has to be the same for the work-items of a segment.

Note

Backends may implement these, like the collectives without a width. The fallbacks use the collectives without a width if width is the sub-group width, and the shuffles with a width otherwise, like the fallbacks of the collectives without one.

source

Printing

KernelInterface._print — Function
_print(args...)

Print args from a kernel; the backend hook behind KernelAbstractions.@print.

Note

Backend implementations should implement:

@device_override _print(args...)

A backend that can't print from a kernel defines it to return nothing, and documents that.

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

Memory

KernelInterface.allocate — Function
allocate(::Backend, Type, dims...; unified=false)::AbstractArray

Allocate an uninitialized array on the active device of the backend. unified=true allocates unified memory, accessible from the host and the device without explicit copies, 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.zeros — Function
zeros(::Backend, Type, dims...; unified=false)::AbstractArray

Allocate an array with allocate and fill it with zeros.

This is generic: backends implement allocate (and fill! for their array type).

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

Allocate an array with allocate and fill it with ones.

This is generic: backends implement allocate (and fill! for their array type).

source
KernelInterface.copyto! — Function
copyto!(::Backend, dest::AbstractArray, src::AbstractArray)::typeof(dest)

Copy the elements of src to dest, ordered with respect to the other work on the calling task's queue: after work queued before the copy, and before work queued after it. Returns dest.

Either array can be a host array or an array of backend. dest and src must have the same length, otherwise an ArgumentError is thrown. Backends only have to support dense (contiguous) arrays with the same element type.

The copy may be asynchronous with respect to the host, but doesn't have to be: it can also block until it has completed. For a simple, synchronous copy, use Base.copyto!.

Warning

Because the copy may be asynchronous, the caller has to keep both arrays alive, and not access them from the host, until the copy has completed, e.g. by calling synchronize before using them. A GC.@preserve around copyto! only keeps them alive until the copy is queued:

arr = zeros(64)
GC.@preserve arr begin
    copyto!(backend, arr, ...)
    # other operations
    synchronize(backend)
end
Note

On some backends 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, for host-to-device, device-to-host and device-to-device copies.

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 with respect to the host.

This function returns 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, reducing pressure on the allocator. The array may not be used afterwards.

This is a hint: releasing the memory is allowed to do nothing.

Note

Backend implementations may implement this function for their array type, and should forward it to their own unsafe_free! if they have one. The fallback is a no-op.

source

Execution

KernelInterface.synchronize — Function
synchronize(::Backend)

Block the calling task until all work it has queued on the active device of backend has completed.

Note

Backend implementations must implement this function cooperatively, yielding to the Julia scheduler while waiting rather than blocking inside a driver call. It must not wait for work on other queues that the current queue is not ordered after, e.g., by wait_event. See the notes for backend implementations for why.

source
KernelInterface.record_event — Function
record_event(backend::Backend)

Capture the work the calling task has queued on backend's active device before this call, and return a handle for wait_event. Recording need not wait for that work to complete. The handle is only meant for wait_event.

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_event — Function
wait_event(backend::Backend, event)

Order the work the calling task subsequently queues on backend's active device after the work captured by event, which was returned by record_event. Returning does not mean that the captured work has completed.

The wait applies to the queue of the device that is active when wait_event is called; switching devices adds no ordering. To order work across a device switch, select the device first and wait afterwards:

event = record_event(backend)   # captures work on the current device
device!(backend, 2)
wait_event(backend, event)      # orders this task's work on device 2 after it
Note

wait_event(::Backend, ::Nothing) is a no-op, matching the default record_event. A backend that returns another event type must implement wait_event for it, either by adding a dependency to the current task's queue or by waiting cooperatively as synchronize does. A backend with more than one device must accept an event recorded on another device, waiting cooperatively if the driver cannot add a cross-device dependency. See the notes for backend implementations for how this ordering should interact with implicit synchronization.

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

Device management

KernelInterface.device — Function
device(backend::Backend)::Int

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

Note

Backends supporting multiple devices must implement device(backend::Backend)::Int, along with ndevices, device! and device(backend, A). The fallback only works for a single device, and throws if ndevices reports more.

source
device(backend::Backend, A::AbstractArray)::Int

Return the 1-based index of the device that owns the memory of A, independently of the currently active device.

Note

Backends supporting multiple devices must implement this for their array type. The fallback only works for a single device, and throws if ndevices reports more.

source
KernelInterface.ndevices — Function
ndevices(backend::Backend)::Int

Return the number of devices available to backend.

Note

Backends supporting multiple devices must implement ndevices(backend::Backend)::Int, along with device and device!. The fallback returns 1.

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

Select the active device for backend. id is a 1-based device index; an id outside 1:ndevices(backend) throws an ArgumentError.

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

Backends supporting multiple devices must implement device!(backend::Backend, id::Int), along with ndevices and device. The fallback only works for a single device, and throws if ndevices reports more.

source

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

KernelAbstractions v0.9.22

This function was added in KernelAbstractions v0.9.22

source
KernelInterface.versioninfo — Function
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_unified — Function
supports_unified(::Backend)::Bool

Whether allocate supports unified=true on the active device: memory that can be accessed from both the host and the device without explicit copies.

Note

Backend implementations must implement this function if they support unified memory. The fallback returns false.

source
KernelInterface.supports_atomics — Function
supports_atomics(::Backend)::Bool

Whether kernels on the active device support Atomix.jl's atomic operations: at least add and compare-and-swap on 32-bit integers and floats in global memory.

Note

Backend implementations must implement this function if they support atomics. The fallback returns false.

source
KernelInterface.supports_float64 — Function
supports_float64(::Backend)::Bool

Whether kernels on the active device support Float64 values.

Note

Backend implementations must implement this function if they support Float64. The fallback returns false.

source
KernelInterface.supports_subgroups — Function
supports_subgroups(::Backend)::Bool

Whether kernels on the active device support sub-groups with a fixed width sub_group_size: the sub-group queries (get_sub_group_size etc.), sub_group_barrier, and the votes sub_group_any, sub_group_all and, for widths of at most 64, sub_group_ballot. Which types the shuffles support is queried separately with supports_shuffle.

Which work-items form a sub-group is unspecified, unless supports_linear_subgroups returns true, and the communication functions have to be executed in control flow that is uniform over the work-group, unless supports_independent_subgroups returns true. See Sub-groups for the layout and participation guarantees.

Note

Backend implementations must implement this function if they support sub-groups. The fallback returns false.

source
KernelInterface.supports_linear_subgroups — Function
supports_linear_subgroups(::Backend)::Bool

Whether the active device forms sub-groups from consecutive work-items, for every kernel and launch with a 1-D work-group or one whose x extent is a multiple of the sub-group width W (sub_group_size): then the work-item with the linear local index lin (x fastest, see get_local_id) is in sub-group fld(lin - 1, W) + 1, at lane mod(lin - 1, W) + 1, the sub-group ids are 1:cld(n, W) for a work-group of n work-items, and only the last sub-group can be partial. In particular, a 1-D work-group of at most W work-items is a single sub-group. It promises nothing about other work-group shapes. Implies supports_subgroups.

Code needs this when it relates local ids to lanes: e.g. stencils that shuffle values between neighbouring work-items, transposes through shuffles, or work-group scans built from sub-group scans. Code that doesn't can index storage by the sub-group slot (get_sub_group_id() - 1) * W + get_sub_group_local_id(), which is unique within the work-group, but not dense: it can exceed the work-group size, so storage has to have room for get_num_sub_groups() * W values.

false means that the layout isn't guaranteed, not that it is different: OpenCL, Vulkan and other APIs leave it to the implementation.

Note

Backend implementations should implement this function if they guarantee the layout. The fallback returns false.

source
KernelInterface.supports_independent_subgroups — Function
supports_independent_subgroups(::Backend)::Bool

Whether the sub-group communication functions (shuffles, votes, collectives and sub_group_barrier) can be executed in control flow that is uniform over the sub-group, but not over the work-group: e.g. a loop whose trip count differs between sub-groups, or after an early return of whole sub-groups. Otherwise, all sub-groups of a work-group have to execute the same communication functions, in the same order.

This covers whole sub-groups, not the segments of the functions with a width (see supports_partitionable_subgroups), and doesn't promise forward progress: a sub-group spin-waiting for another one can still hang. Implies supports_subgroups.

Note

Backend implementations should implement this function if sub-groups are independent. The fallback returns false.

source
KernelInterface.supports_partitionable_subgroups — Function
supports_partitionable_subgroups(::Backend)::Bool

Whether a sub-group can be partitioned into segments of width consecutive lanes that diverge independently, for the sub-group communication functions with a width (the shuffles, votes, collectives and sub_group_barrier with a width, e.g. shfl): those only have to be executed by the work-items of the caller's segment, rather than by the whole sub-group, so the segments of a sub-group can take different branches, loop a different number of times, or skip the functions altogether.

This extends supports_independent_subgroups to segments, and implies it. Like that query, it doesn't promise forward progress between segments.

Note

Backend implementations should return true only if every function with a width, including the barrier, works when only one segment executes it. That can rest on the API, or on an execution model of the hardware that the backend relies on and validates, such as the lock-step execution of SIMD-groups on Apple GPUs; seeing lock-step execution in a test isn't enough. The fallback returns false.

source
KernelInterface.supports_shuffle — Function
supports_shuffle(::Backend, ::Type{T})::Bool

Whether kernels on the active device support the shuffles shfl, shfl_down, shfl_up and shfl_xor for values of type T.

Other primitive types of 1, 2, 4, 8 or 16 bytes are supported if UInt32 is, and so are isbits structs and tuples of such types (nested ones included). The fallbacks shuffle these as words; see shfl for how.

This reports whether values of type T can be moved between work-items, not whether the device can compute with them: e.g. Float64 values can be shuffled (as words) on a device without supports_float64, as long as they are only stored and loaded, e.g. through an array of UInt64.

Note

Backends that support shuffles must implement this function for the primitive types they support natively, which have to include UInt32, with a signature that only matches those, e.g. supports_shuffle(::NewBackend, ::Type{<:Union{UInt32, Float32}}). The fallback handles other types.

source

Limits

KernelInterface.max_work_group_size — Function
max_work_group_size(backend)::Int
max_work_group_size(kernel::Kernel)::Int

The largest number of work-items a work-group can have: on the active device of backend, or for launches of the compiled kernel (which may be lower, e.g. because of the kernel's register use). Launching a larger work-group is an error.

The work-group size that performs best is often smaller; see launch_configuration.

Note

Backend implementations must implement both:

max_work_group_size(backend::NewBackend)::Int
max_work_group_size(kernel::Kernel{<:NewBackend})::Int

The kernel form answers for the device the kernel was compiled for.

source
KernelInterface.launch_configuration — Function
launch_configuration(kernel::Kernel; nitems=nothing, max_work_group_size=typemax(Int))::@NamedTuple{workgroupsize::Int}

The recommended number of work-items per work-group for launching kernel over nitems work-items in total (nothing if unknown), at most max_work_group_size. This is what an ndrange launch without a workgroupsize uses, passing the number of work-items in the ndrange (saturated at typemax(Int)). nitems and max_work_group_size are positive.

Unlike max_work_group_size, this is advice: backends may base it on occupancy or on the size of the launch, e.g. to prefer more work-groups over larger ones.

Note

Backend implementations may implement:

launch_configuration(kernel::Kernel{<:NewBackend}; nitems::Union{Int, Nothing}=nothing,
                     max_work_group_size::Int=typemax(Int))::@NamedTuple{workgroupsize::Int}

The result has to be positive and at most max_work_group_size and max_work_group_size(kernel). The fallback recommends the largest legal work-group size.

source
KernelInterface.max_work_group_dims — Function
max_work_group_dims(backend)::NTuple{3, Int}

The maximum number of work-items along each dimension of a work-group, for the active device of backend. max_work_group_size bounds their product.

Note

Backend implementations must implement:

max_work_group_dims(backend::NewBackend)::NTuple{3, Int}
source
KernelInterface.max_num_groups — Function
max_num_groups(backend)::NTuple{3, Int}

The maximum number of work-groups along each dimension of a launch, for the active device of backend.

This is conservative: a launch within these limits works for any work-group size (as long as the number of work-items in each dimension fits an Int), but some backends accept more work-groups for smaller work-groups (e.g. HIP bounds the number of work-items per dimension). The backend's validation at launch time is authoritative.

Note

Backend implementations must implement:

max_num_groups(backend::NewBackend)::NTuple{3, Int}
source
KernelInterface.sub_group_size — Function
sub_group_size(backend)::Int

The sub-group width of kernels compiled for the active device of backend.

Kernels compiled by kernel_function execute with exactly this width: on the device, get_max_sub_group_size returns it, and full sub-groups have this many work-items. Host code can rely on it, e.g. to pick a Val(N) for a warp-level reduction.

Note

Backend implementations must implement this if supports_subgroups returns true:

sub_group_size(backend::NewBackend)::Int

A backend that cannot guarantee the width for every kernel has to report supports_subgroups(backend) = false.

source
KernelInterface.multiprocessor_count — Function
multiprocessor_count(backend)::Int

The number of multiprocessors (CUDA SMs, AMD CUs, Intel Xe cores, ...) of the active device of backend, or 0 if unknown. The unit differs between backends, so this is only useful for heuristics, e.g. to choose how many work-groups a grid-stride loop launches.

Note

Backend implementations may implement:

multiprocessor_count(backend::NewBackend)::Int
source

Compilation and launching

KernelInterface.Kernel — Type
Kernel{Backend, Kern}

A kernel compiled by kernel_function for backend, wrapping the backend's own kernel object kern. kernel.backend is the backend value that was passed to kernel_function.

Calling a Kernel launches it:

(kernel::Kernel)(args...; numgroups=(), workgroupsize=(), ndrange=(),
                 max_work_group_size=typemax(Int), kwargs...)

args are the host-side arguments, e.g. a CuArray rather than a CuDeviceArray. The backend converts them with argconvert, and the converted types have to match the argument types the kernel was compiled for.

The launch geometry is given in one of three ways:

  • numgroups and workgroupsize: launch exactly that many work-groups of that many work-items. Either defaults to 1.
  • ndrange and workgroupsize: launch cld.(ndrange, workgroupsize) work-groups.
  • ndrange alone: the work-group size is chosen with launch_configuration, bounded by max_work_group_size and by max_work_group_dims, and filled first dimension first.

Each is an Integer or a tuple of up to 3 Integers; missing dimensions are 1. ndrange and numgroups are mutually exclusive.

ndrange is rounded up to whole work-groups and is not masked: the kernel runs for every work-item of every launched group, and get_global_size returns the padded size. Kernels have to check their own bounds.

A zero anywhere in ndrange or numgroups launches nothing. Work-group sizes must be positive and fit max_work_group_dims and max_work_group_size(kernel), and the number of work-items in each dimension must fit an Int; anything else throws an ArgumentError before the backend sees it. The number of work-groups is validated by the backend.

Other keyword arguments are passed to launch unchanged. They are backend-specific: a backend throws an error for keywords it doesn't support.

A launch is queued on the calling task's queue of the active device, and returns nothing.

source
KernelInterface.kernel_function — Function
kernel_function(backend, f::F, tt::TT=Tuple{}; name=nothing, kwargs...)::Kernel

Compile the callable f for arguments of the (device-side) types tt, for the active device of backend, returning a Kernel. For a higher-level interface, use KernelInterface.@launch.

f is the host-side callable, not converted with argconvert: the backend converts it. For a closure, that matters: it can capture arrays, which its converted form only holds pointers to.

Keyword arguments:

  • name: override the name that the kernel will have in the generated code.

Other keyword arguments are backend-specific compiler options (e.g. maxthreads for CUDA.jl); backends throw an error for options they don't support.

The returned kernel keeps f alive, but not the arguments: they are passed again at launch.

Note

Backend implementations must implement:

kernel_function(backend::NewBackend, f::F, tt::TT=Tuple{}; name=nothing, kwargs...) where {F,TT}

It converts f with argconvert to compile it, and the returned Kernel has to keep f itself alive for as long as it can be launched, since the converted f may only hold pointers to the arrays f captures. A backend that needs to know about those arrays at launch, e.g. to declare them to the device, can convert f again for every launch, as it does for the arguments.

The returned Kernel stores backend itself (not a new default backend), so that options it carries apply to the launch. Kernels must execute with sub-group width sub_group_size(backend) if the backend supports sub-groups.

Launching a kernel after device! switched to a device other than the one it was compiled for must either work, or throw an error: it may never run on the wrong device.

source
KernelInterface.argconvert — Function
argconvert(backend, arg)

Convert arg to its device-side representation, e.g. a CuArray to a CuDeviceArray. Called for every kernel argument, and for the kernel function itself.

It has to be pure: it may be called more than once for the same launch.

Note

Backend implementations must implement:

argconvert(::NewBackend, arg)
source
KernelInterface.launch — Function
launch(kernel::Kernel, groups::Dims{3}, items::Dims{3}, args::Tuple; kwargs...)

Launch kernel with groups work-groups of items work-items each, passing the host-side arguments args, a tuple. This is what calling a Kernel does after validating and normalizing the launch geometry; users call the kernel instead.

groups and items are positive, items fits max_work_group_dims and max_work_group_size(kernel), and groups .* items doesn't overflow Int. kwargs are the keyword arguments of the call that KernelInterface doesn't know.

Note

Backend implementations must implement:

launch(kernel::Kernel{<:NewBackend}, groups::Dims{3}, items::Dims{3}, args::Tuple; kwargs...)

It converts args with argconvert (or lets its native launcher do so), and queues the launch on the calling task's queue; it doesn't have to wait for the kernel to complete. To keep launches with many arguments cheap, it should pass args on as a tuple rather than splatting it: Julia doesn't turn a splat of more than 32 elements into a direct call. It must throw for keywords it does not support, and may throw for a number of work-groups the device cannot launch, or for a geometry that backend-specific compiler options of the kernel don't allow.

KernelInterface 0.4

Before KernelInterface 0.4, launch received the arguments as varargs, launch(kernel, groups, items, args...; kwargs...).

source
KernelInterface.@launch — Macro
KI.@launch backend [launch=true] [numgroups=...] [workgroupsize=...] [ndrange=...] [max_work_group_size=...] [kwargs...] f(args...)

Compile f(args...) for backend and launch it, like @cuda or @metal do.

f is compiled with kernel_function for the types of the arguments converted with argconvert, and the resulting Kernel is called with the launch keywords numgroups, workgroupsize, ndrange and max_work_group_size, whose meaning is documented there. The arguments are kept alive while the launch is being queued.

Other keyword arguments:

  • launch: whether to launch the kernel, defaults to true. With launch=false, the kernel is only compiled and returned, and the launch keywords can't be used: launch it by calling it with the arguments and the launch keywords.
  • name and any other keyword are passed to kernel_function as compiler options.

Launch options specific to a backend (such as a CUDA stream) can't be passed to @launch; use launch=false and pass them when calling the kernel.

backend is evaluated once. Returns the Kernel.

function vadd(c, a, b)
    i = KI.get_global_id().x
    if i <= length(c)
        @inbounds c[i] = a[i] + b[i]
    end
    return
end

KI.@launch backend ndrange=length(c) vadd(c, a, b)
source

Implementing a backend

A backend implements the required methods from the contract, and those optional methods where it can do better than the fallback. In particular:

  1. Define a backend type subtyping Backend, and implement get_backend for its array type.

  2. Extend Adapt.adapt_storage(::NewBackend, x) so that adapt(backend, x) moves data to the backend, preferably by delegating to its array type: Adapt.adapt_storage(::NewBackend, x) = adapt(NewArray, x).

  3. Implement kernel_function, which receives the unconverted callable, and returns a Kernel that holds the backend value it was given and keeps that callable alive. Also implement launch, which receives an already validated NTuple{3, Int} of work-groups and of work-items, and the arguments as a tuple. Pass that tuple on to the native launcher rather than splatting it: Julia doesn't turn a splat of more than 32 elements into a direct call, so kernels with many arguments would be slow to launch. For the PoCL backend, whose kernels hold the compiled kernel and the callable, launch is

    function KI.launch(k::KI.Kernel{POCLBackend}, groups::Dims{3}, items::Dims{3}, args::Tuple)
        f = k.kern.f
        GC.@preserve f POCL.launch_and_wait(
            k.kern.kernel, args; local_size = items, global_size = groups .* items
        )
        return nothing
    end
  4. Compute the typed index queries with % T, not T(x): a checked conversion leaves an error branch in every kernel.

  5. Sub-groups are optional. To support them, report supports_subgroups and sub_group_size, and implement the device functions the contract requires. If the backend has shuffles, implement them only for the primitive types the hardware shuffles natively, including UInt32, and report those with supports_shuffle; KernelInterface shuffles other types as words. It also provides the reductions, scans and functions with a width; override those where a native implementation helps, keeping their documented results.

    Report linear, independent and partitionable sub-groups only when the backend guarantees them. Partitionable sub-groups need every function with a width, including sub_group_barrier(width), to work when only that segment executes it. Without a barrier for part of a sub-group, as in OpenCL and Metal, that can rest on a hardware execution model, such as sub-groups that run in lock-step, combined with a fence. Base these guarantees on the API or the hardware, not on the testsuite passing: it checks the layout and divergent control flow, but can't prove them.

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

See also the notes for backend implementations.