Skip to content

KernelInterface: sub-group shuffles, votes and a constant width - #1133

Closed
vchuravy wants to merge 13 commits into
ka-0.10from
vc/ki-subgroup-ops
Closed

vchuravy wants to merge 13 commits into
ka-0.10from
vc/ki-subgroup-ops

Conversation

@vchuravy

@vchuravy vchuravy commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Implements the sub-group contract of KernelInterface from JuliaGPU/KernelAbstractions.jl#831 for ROCBackend: the shuffles shfl, shfl_down, shfl_up and shfl_xor, the votes sub_group_any, sub_group_all and sub_group_ballot, and a get_max_sub_group_size that is a constant of the generated code.

What's implemented

KernelInterface AMDGPU
shfl(val, lane) ds_bpermute (Device.bpermute) from hardware lane lane - 1
shfl_down(val, offset) ds_bpermute from hardware lane mbcnt + offset, or mbcnt past the wavefront
shfl_up(val, offset) ds_bpermute from hardware lane mbcnt - offset, or mbcnt if that is negative
shfl_xor(val, mask) ds_bpermute from hardware lane mbcnt ⊻ mask
sub_group_ballot(pred) Device.ballot(pred) (llvm.amdgcn.ballot.i32/.i64, widened to UInt64)
sub_group_any(pred) Device.ballot(pred) != 0
sub_group_all(pred) Device.ballot(!pred) == 0
get_max_sub_group_size() Device.wavefrontsize(), folded to a constant (unchanged)
  • The shuffles use the hardware lane (mbcnt), like get_sub_group_local_id, not activelane. They don't use Device.shfl*, which clamp out-of-range offsets to the own lane and work on activelane. ds_bpermute only uses the low bits of the address, so a lane out of range reads an unspecified value rather than trapping. Lanes and offsets are truncated with % Cint, so there is no checked conversion: the IR of a kernel using all shuffles and votes with runtime Int lanes/offsets has no throw/trap/unreachable.
  • The shuffles and supports_shuffle are now only defined for const ShuffleTypes = Union{Bool, Base.BitInteger, Base.IEEEFloat} (what Device._shfl decomposes into 32-bit bpermutes). The previous generic where {T} shfl_down override and the supports_shuffle that listed Complex are gone, so Complex and other isbits structs/tuples go through KernelInterface's fallback, as packed UInt32 words.
  • shfl_down/shfl_up return the work-item's own value where the source lane is past the wavefront (as KernelInterface now requires, like CUDA): one compare and select against the constant wavefront size before the ds_bpermute. shfl_xor needs no change, as KernelInterface requires mask below the width.
  • The shuffles with a width (KI.shfl(val, lane, width) etc.) use KernelInterface's fallbacks: one ds_bpermute from a lane computed with a few integer operations, the same as Device.shfl etc. DPP/ds_swizzle could be cheaper for constant offsets, but isn't used here.
  • KI.supports_linear_subgroups and KI.supports_independent_subgroups are true: wavefronts are formed from consecutive linear work-item ids, x fastest (KernelInterface's layout tests check this), and are independent. KernelInterface no longer implies either from supports_subgroups.
  • The votes rely on ballot setting only the bits of active lanes, i.e. of the work-items of the (possibly partial) sub-group. The result is uniform.
  • KI.supports_partitionable_subgroups is true: the functions with a width only need the lanes of the caller's segment. ds_bpermute only involves the lanes it reads from, llvm.amdgcn.ballot only reports the lanes that execute it, and the sub-group barrier is wavefront-scope fences around llvm.amdgcn.wave.barrier, which HIP's tile synchronization also uses in divergent code. KernelAbstractions can then pack several narrow tiles into a wavefront.

Constant width

This needed no new code. KI.kernel_function compiles for the device's wavefront size (and rejects a conflicting wavefrontsize64), and fold_wavefrontsize! in finish_module! already replaces llvm.amdgcn.wavefrontsize with the compiled-for size before optimization. I checked this with @device_code_llvm on a kernel that stores KI.get_max_sub_group_size(): it compiles to store i64 32 with wavefrontsize64=false and store i64 64 with wavefrontsize64=true, and the call to llvm.amdgcn.wavefrontsize is gone. I added a comment saying so.

[TEMP] commit

[TEMP] Test against KernelAbstractions' vc/ki-subgroup-ops branch points KernelAbstractions and KernelInterface at vc/ki-subgroup-ops. SPIRVIntrinsics 1.4 is registered, so the earlier pin to OpenCL.jl's vc/subgroup-votes is gone. Drop the commit once KernelAbstractions.jl#831 is merged.

Local testing

gfx1036 iGPU (wave32), Julia 1.12, on ka-0.10 rebased onto main, KernelAbstractions at the current #831: KernelInterface testsuite 6089/6089, KernelAbstractions testsuite 2639 passed, 4 broken, in three runs without hangs. Offsets up to typemax(Int64) give the own value past the wavefront.

maleadt and others added 12 commits October 10, 2026 20:36
`alloc_special` declared local memory as an external global named after its
`id`, so call sites using the same `id` shared one allocation. Define it with
internal linkage and an `undef` initializer instead, so that each call site
gets its own, as KernelInterface's `localmemory` requires: it has no `id`.

Co-authored-by: Christian Guinard <28689358+christiangnrd@users.noreply.github.com>
The maximum number of work-items along each dimension of a workgroup, from the
device's `maxThreadsDim`. It is cached in `HIPDevice`, since KernelInterface
queries it on every launch whose workgroup size it chooses.
KernelAbstractions 0.10 builds on KernelInterface, which defines what a back
end provides: memory and device management, compiling and launching kernels,
and the device-side intrinsics. KernelAbstractions then launches `@kernel`
kernels itself on any KernelInterface back end, which replaces AMDGPU's copy of
that launch path: partitioning the ndrange, building the kernel's context, and
tuning the workgroup size.

`ROCBackend` now implements KernelInterface, which AMDGPU depends on instead of
on KernelAbstractions. What KernelAbstractions still needs from a back end
moves to an extension: the `MArray` behind `@private`, the Adapt rule for
`@Const`, and the `maxthreads` hint for a static workgroup size.

`KI.launch` receives the kernel arguments as a tuple, and passes it on to the
`HIPKernel` without splatting it. It rejects `groupsize` and `gridsize`, which
would override the launch geometry that KernelInterface validated.
`KI.copyto!` accepts dense arrays and contiguous views of them.

`KI.kernel_function` receives the callable unconverted. It compiles its
converted form, and the kernel keeps the original alive, since the converted
form only holds pointers to the arrays a closure captures. `KI.launch` converts
it again for the launch's stream, like the arguments, so that those arrays are
made available to the stream too.

Co-authored-by: Christian Guinard <28689358+christiangnrd@users.noreply.github.com>
A barrier for the lanes of a wavefront, fenced at wavefront scope like
`sync_workgroup` is at workgroup scope, so that memory accesses before it are
visible to the other lanes afterwards. `llvm.amdgcn.wave.barrier` alone doesn't
generate any code; it only keeps the compiler from moving code across it.

Co-authored-by: Christian Guinard <28689358+christiangnrd@users.noreply.github.com>
Sub-groups are wavefronts: implement the sub-group queries,
`sub_group_barrier` with `sync_wavefront`, and `shfl_down`, and report their
support to KernelInterface. Kernels execute with the device's wavefront size,
which `KI.kernel_function` enforces.

KernelInterface leaves unspecified how work-items are grouped into sub-groups;
AMD GPUs form wavefronts from consecutive linear work-item indices, so the last
wavefront of a workgroup can be partial. The lane is the hardware lane
(`mbcnt`), which unlike `activelane` doesn't change in divergent code. Tests
check both.

`shfl_down` of a `Complex{Int32}` didn't compile, as `Int32` components had no
shuffle method.

Co-authored-by: Christian Guinard <28689358+christiangnrd@users.noreply.github.com>
Implement `KI.record_event` and `KI.wait_event` with a `HIPEvent` recorded on,
and waited for by, the task's stream. `KernelAbstractions.@spawn` uses them to
order a new task's work after the work its parent had queued, without
synchronizing the parent; the default is a full synchronization.

KernelInterface's testsuite checks them once `record_event` returns an event.

Co-authored-by: Christian Guinard <28689358+christiangnrd@users.noreply.github.com>
`KI.versioninfo(ROCBackend())` prints `AMDGPU.versioninfo()`.

Co-authored-by: Christian Guinard <28689358+christiangnrd@users.noreply.github.com>
KernelAbstractions 0.10 and KernelInterface 0.4 aren't registered yet.
KernelAbstractions is only a weak dependency of AMDGPU, so it is listed in
[extras] to be allowed in [sources].

Julia 1.10 doesn't support [sources], and without workspaces (Julia 1.11) only
the test project picks up KernelAbstractions from them, so Buildkite runs the
tests in the test project there, developing the packages from git on 1.10. The
GPU-less job moves to Julia 1.12.

Drop this commit once they are registered.
KernelAbstractions now backs @Private with a PrivateArray over
GPUCompiler.alloca, defined as an overlay in GPUCompiler's shared method
table, which GPUCompiler stacks under AMDGPU's own method table. Drop our
MArray-based Scratchpad override, which would otherwise take precedence,
along with the StaticArraysCore dependency. The shared table needs
GPUCompiler 2.12, which we already require.
Co-authored-by: Tim Besard <tim@juliahub.com>
Implement `shfl`, `shfl_down`, `shfl_up` and `shfl_xor` with `ds_bpermute` on the hardware
lane, for the primitive types it supports by decomposing them into 32-bit words; other types
use KernelInterface's fallbacks. Past the wavefront, `shfl_down` and `shfl_up` return the
work-item's own value, as on CUDA, comparing the offset before narrowing it. The votes use
the wavefront ballot, which only has bits for the work-items of the wavefront.

Wavefronts are formed from consecutive linear work-item indices and execute independently,
so report both `supports_linear_subgroups` and `supports_independent_subgroups`.

Co-authored-by: Tim Besard <tim@juliahub.com>
The device overrides of `Base.min` and `Base.max` for floats called OCML's
`__ocml_min`/`__ocml_max`, which, like C's `fmin`/`fmax`, return the other
argument for a NaN, while Julia's `min` and `max` return NaN. Drop them: Julia's
own definitions (`llvm.minimum`/`llvm.maximum` on Julia 1.12+, arithmetic before)
compile for AMDGPU.

Assisted-by: Claude Code (Opus 5.5)
Co-authored-by: Tim Besard <tim@juliahub.com>
@maleadt
maleadt force-pushed the vc/ki-subgroup-ops branch from c507ee3 to c3d2f0d Compare October 10, 2026 18:45
Report `KI.supports_partitionable_subgroups`: the functions of KernelInterface
with a `width` only need the lanes of the caller's segment on AMD GPUs:
`ds_bpermute` only involves the lanes it reads from, `llvm.amdgcn.ballot` only
reports the lanes that execute it, and the sub-group barrier is wavefront-scope
fences around `llvm.amdgcn.wave.barrier`, which HIP's tile synchronization also
uses in divergent code. KernelInterface's fallbacks for the width variants build
on those, so KernelAbstractions' tiles can now pack several narrow tiles into a
wavefront.
@maleadt
maleadt force-pushed the vc/ki-subgroup-ops branch from 6474af8 to b49da45 Compare October 12, 2026 09:08
@maleadt maleadt closed this Oct 13, 2026
An error occurred while trying to automatically change base from ka-0.10 to main October 13, 2026 09:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants