Skip to content

Look up the tuning hooks behind an inference barrier, so that loading a backend doesn't invalidate AK's operations - #189

Merged
maleadt merged 1 commit into
mainfrom
tb/tuning-invalidations
Oct 27, 2026
Merged

maleadt merged 1 commit into
mainfrom
tb/tuning-invalidations

Conversation

@maleadt

@maleadt maleadt commented Oct 27, 2026

Copy link
Copy Markdown
Member

AK's package extensions set the tuning of their backends (reduce_tuning(::MetalBackend, ::Type) etc.), so that it ships with AK rather than with the back ends. The back ends precompile AK's operations, e.g. Metal.jl's and CUDA.jl's reductions through GPUArrays, and that code calls the hooks directly, so it depends on their generic methods. Loading AK's extension for the backend then invalidates it. With the back ends defining their KernelAbstractions methods in src (JuliaGPU/Metal.jl#1118, JuliaGPU/CUDA.jl#3493) and JuliaGPU/KernelAbstractions.jl#937, which does the same for KernelAbstractions' own tuning, AK's reduce_tuning and scan_tuning are the largest invalidation trees when loading Metal, and reduce_tuning the largest one when loading CUDA.

This PR looks the tunings up through tuning(hook, backend, T), which calls the hook behind Base.inferencebarrier and asserts the result type that each hook declares with tuning_type. Every internal call goes through it, and the hooks and extensions are unchanged. Tunings can already differ per device, so no caller relied on their values being constants.

Unique method instances invalidated by using <backend>, KernelAbstractions, and the time a fresh process takes to load the back end and run its first broadcast, getindex, sum, sum(; dims), accumulate, sort, any and 4-d broadcast (Julia 1.13.2, with the PRs above):

main this PR first operations
Metal (M1) 1177 911 4.69–4.79 s → 3.93–4.00 s
CUDA (RTX 6000 Ada) 852 758 7.63 s → 7.49 s (median of 9)
oneAPI (Arc A770) 787 787 13.8–14.0 s either way

On Metal, the first broadcast takes 0.05 s instead of 0.72 s, as it no longer recompiles, and the first sum 0.13 s instead of 0.22 s. AK has no AMDGPU extension, and oneAPI.jl doesn't precompile AK's operations, so neither changes.

The values no longer constant-fold, so where a tuning selects between launches, such as reduce_then_scan and the sub-group width of scans, the host code for both is compiled. That makes the first accumulate 0.05–0.1 s slower (0.90 → 1.00 s on the M1). Each lookup also takes a dynamic dispatch, which is small next to the launches that follow it.

The tests pass on CUDA, AMDGPU and Metal.

Backend packages precompile AK's operations, e.g. Metal.jl's reductions
through GPUArrays, and that code depended on the generic methods of the
tuning hooks, so loading AK's extension for the backend invalidated it.
Look the tunings up through a `tuning` helper that hides the call from
inference. Tunings may already differ per device, so callers never
relied on their values being constants.
@maleadt
maleadt merged commit 605cc90 into main Oct 27, 2026
34 of 35 checks passed
@maleadt
maleadt deleted the tb/tuning-invalidations branch October 27, 2026 20:06
AzamatB added a commit to AzamatB/AcceleratedKernels.jl that referenced this pull request Oct 27, 2026
Since the tunings are looked up behind an inference barrier (JuliaGPU#189), `Auto()` picks a GPU's
predicate algorithm, `ConcurrentWrite` or `ViaReduce`, from a tuning that inference does not see.
The algorithm is then a union, `any_run` and `all_run` are dispatched at run time, and `any` and
`all` of a GPU array infer as `Any`, as does every caller of Base's `any` and `all` on one.

Assert that the result is a `Bool`, as `mapreduce` asserts its accumulator type, and test that
both infer as `Bool` with `Auto()`, with each algorithm and with an algorithm of abstract type.
maleadt pushed a commit to AzamatB/AcceleratedKernels.jl that referenced this pull request Oct 28, 2026
Since JuliaGPU#189 looks up the tunings behind an inference barrier, `Auto()`
picks a GPU's predicate algorithm (`ConcurrentWrite` or `ViaReduce`)
from a value that inference does not see. The plan then does not infer,
and neither does the result: `any` and `all` of a GPU array infer as
`Any`, and so do callers of Base's `any` and `all` on one that return
or use the result, e.g. `checkbounds(Bool, x, i::CuVector)`.

Every algorithm returns a `Bool`, so assert it.
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.

1 participant