From d3cf365efc8619ffdeb7ccbf69be7be22408cd31 Mon Sep 17 00:00:00 2001 From: Christian <28689358+christiangnrd@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:55:21 -0300 Subject: [PATCH 1/7] [JLArrays] Support KA 0.10 With KernelAbstractions 0.10, JLArray kernels run on its POCL back-end. Drop the KA 0.9 code paths, including JLDeviceArray, and convert JLArrays to POCL device arrays. Leave `Ref`s to POCL's conversion after converting their contents, so that `Ref`s of types, as passed by `convert.(T, A)`, are handled too. POCL's SPIR-V back-end cannot lower 128-bit integers, so stop testing Int128 with JLArrays. --- docs/src/index.md | 4 +- lib/JLArrays/Project.toml | 2 +- lib/JLArrays/src/JLArrays.jl | 6 +-- lib/JLArrays/src/JLKernels.jl | 20 ++------ lib/JLArrays/src/array.jl | 11 ++--- lib/JLArrays/src/device/array.jl | 84 ++------------------------------ lib/JLArrays/src/sparse.jl | 18 ++++--- test/runtests.jl | 4 ++ test/testsuite/broadcasting.jl | 3 ++ 9 files changed, 32 insertions(+), 120 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 8cb100cac..5716ca840 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -14,6 +14,6 @@ that builds on GPUArrays.jl such as [CUDA](https://github.com/JuliaGPU/CUDA.jl), This documentation is meant for users who might wish to implement a version of GPUArrays for another GPU backend and will cover the features you will need to implement, the functionality you gain by doing so, and the test suite that is available to verify your implementation. GPUArrays.jl also provides a reference implementation of -these interfaces on the CPU: The `JLArray` array type uses Julia's parallel programming -functionality to simulate GPU execution, and will be used throughout this documentation to +these interfaces on the CPU: The `JLArray` array type runs its kernels on the CPU through +KernelAbstractions' POCL back-end, and will be used throughout this documentation to illustrate functionality. diff --git a/lib/JLArrays/Project.toml b/lib/JLArrays/Project.toml index 698fb8ebd..5640a826e 100644 --- a/lib/JLArrays/Project.toml +++ b/lib/JLArrays/Project.toml @@ -14,7 +14,7 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] Adapt = "2.0, 3.0, 4.0" GPUArrays = "12.1" -KernelAbstractions = "0.9" +KernelAbstractions = "0.10.5" LinearAlgebra = "1" Random = "1" SparseArrays = "1" diff --git a/lib/JLArrays/src/JLArrays.jl b/lib/JLArrays/src/JLArrays.jl index ff9d7c3ec..997aaecd2 100644 --- a/lib/JLArrays/src/JLArrays.jl +++ b/lib/JLArrays/src/JLArrays.jl @@ -16,11 +16,7 @@ using Random import GPUArrays: dense_array_type -import KernelAbstractions - -@static if isdefined(JLArrays.KernelAbstractions, :POCL) # KA v0.10 - import KernelAbstractions: POCL -end +import KernelAbstractions: KernelAbstractions, POCL # the size of an element in an array, including padding up to the element's alignment @static if VERSION < v"1.11" diff --git a/lib/JLArrays/src/JLKernels.jl b/lib/JLArrays/src/JLKernels.jl index ca4d84d0f..483815b93 100644 --- a/lib/JLArrays/src/JLKernels.jl +++ b/lib/JLArrays/src/JLKernels.jl @@ -49,18 +49,8 @@ KernelAbstractions.allocate(::JLBackend, ::Type{T}, dims::Tuple) where T = JLArr return ndrange, workgroupsize, iterspace, dynamic end -@static if isdefined(KernelAbstractions, :isgpu) # KA v0.9 - KernelAbstractions.isgpu(b::JLBackend) = false -end - -@static if !isdefined(KernelAbstractions, :POCL) # KA v0.9 - function convert_to_cpu(obj::Kernel{JLBackend, W, N, F}) where {W, N, F} - return Kernel{typeof(KernelAbstractions.CPU(; static = obj.backend.static)), W, N, F}(KernelAbstractions.CPU(; static = obj.backend.static), obj.f) - end -else - function convert_to_cpu(obj::Kernel{JLBackend, W, N, F}) where {W, N, F} - return Kernel{typeof(KernelAbstractions.POCLBackend()), W, N, F}(KernelAbstractions.POCLBackend(), obj.f) - end +function convert_to_cpu(obj::Kernel{JLBackend, W, N, F}) where {W, N, F} + return Kernel{typeof(KernelAbstractions.POCLBackend()), W, N, F}(KernelAbstractions.POCLBackend(), obj.f) end function (obj::Kernel{JLBackend})(args...; ndrange=nothing, workgroupsize=nothing) @@ -76,10 +66,6 @@ end Adapt.adapt_storage(::JLBackend, a::Array) = Adapt.adapt(JLArrays.JLArray, a) Adapt.adapt_storage(::JLBackend, a::JLArrays.JLArray) = a -@static if !isdefined(KernelAbstractions, :POCL) # KA v0.9 - Adapt.adapt_storage(::KernelAbstractions.CPU, a::JLArrays.JLArray) = convert(Array, a) -else - Adapt.adapt_storage(::KernelAbstractions.POCLBackend, a::JLArrays.JLArray) = convert(Array, a) -end +Adapt.adapt_storage(::KernelAbstractions.POCLBackend, a::JLArrays.JLArray) = convert(Array, a) end diff --git a/lib/JLArrays/src/array.jl b/lib/JLArrays/src/array.jl index d3f708ae9..86271aa24 100644 --- a/lib/JLArrays/src/array.jl +++ b/lib/JLArrays/src/array.jl @@ -192,14 +192,9 @@ Adapt.adapt_storage(::Type{<:JLArray{T}}, xs::AbstractArray) where {T} = Adapt.adapt_storage(::Type{Array}, xs::JLArray) = convert(Array, xs) # adapt for the device -@static if !isdefined(JLArrays.KernelAbstractions, :POCL) # KA v0.9 - Adapt.adapt_storage(::Adaptor, x::JLArray{T,N}) where {T,N} = - JLDeviceArray{T,N}(x.data[], x.offset, x.tagoffset, x.dims) -else - function Adapt.adapt_storage(::Adaptor, x::JLArray{T,N}) where {T,N} - arr = typed_data(x) - Adapt.adapt_storage(POCL.KernelAdaptor([pointer(arr)]), arr) - end +function Adapt.adapt_storage(::Adaptor, x::JLArray{T,N}) where {T,N} + arr = typed_data(x) + Adapt.adapt_storage(POCL.KernelAdaptor([pointer(arr)]), arr) end ## conversions diff --git a/lib/JLArrays/src/device/array.jl b/lib/JLArrays/src/device/array.jl index f6fa65970..c5bcd210c 100644 --- a/lib/JLArrays/src/device/array.jl +++ b/lib/JLArrays/src/device/array.jl @@ -3,84 +3,6 @@ struct Adaptor end jlconvert(arg) = adapt(Adaptor(), arg) -# FIXME: add Ref to Adapt.jl (but make sure it doesn't cause ambiguities with CUDAnative's) -struct JlRefValue{T} <: Ref{T} - x::T -end -Base.getindex(r::JlRefValue) = r.x -Adapt.adapt_structure(to::Adaptor, r::Base.RefValue) = JlRefValue(adapt(to, r[])) - -## executed on-device - -# array type -@static if !isdefined(JLArrays.KernelAbstractions, :POCL) # KA v0.9 - struct JLDeviceArray{T, N} <: AbstractDeviceArray{T, N} - data::Vector{UInt8} - offset::Int - tagoffset::Int # see `JLArray` - dims::Dims{N} - end - - Base.elsize(::Type{<:JLDeviceArray{T}}) where {T} = aligned_sizeof(T) - - Base.size(x::JLDeviceArray) = x.dims - Base.sizeof(x::JLDeviceArray) = Base.elsize(x) * length(x) - - Base.unsafe_convert(::Type{Ptr{T}}, x::JLDeviceArray{T}) where {T} = - convert(Ptr{T}, pointer(x.data)) + x.offset - - # conversion of untyped data to a typed Array - function typed_data(x::JLDeviceArray{T}) where {T} - unsafe_wrap(Array, pointer(x), x.dims) - end - - @inline function Base.getindex(A::JLDeviceArray{T}, index::Integer) where {T} - if Base.isbitsunion(T) - @boundscheck checkbounds(A, index) - union_load(A, index) - else - getindex(typed_data(A), index) - end - end - @inline function Base.setindex!(A::JLDeviceArray{T}, x, index::Integer) where {T} - if Base.isbitsunion(T) - @boundscheck checkbounds(A, index) - union_store!(A, convert(T, x)::T, index) - return A - else - setindex!(typed_data(A), x, index) - end - end - - # elements of isbits unions are stored as the member type that their selector byte - # indexes in `Base.uniontypes(T)` - - @generated function union_load(A::JLDeviceArray{T}, index::Integer) where {T} - ex = :(error("invalid selector byte")) - for (sel, U) in Iterators.reverse(enumerate(Base.uniontypes(T))) - val = Base.issingletontype(U) ? U.instance : :(unsafe_load(convert(Ptr{$U}, ptr))) - ex = :(selector == $(UInt8(sel - 1)) ? $val : $ex) - end - quote - data = A.data - selector = @inbounds data[A.tagoffset + index] - GC.@preserve data begin - ptr = pointer(data) + A.offset + (index - 1) * $(Base.aligned_sizeof(T)) - $ex - end - end - end - - @generated function union_store!(A::JLDeviceArray{T}, x, index::Integer) where {T} - sel = findfirst(==(x), Base.uniontypes(T)) - quote - data = A.data - @inbounds data[A.tagoffset + index] = $(UInt8(sel - 1)) - GC.@preserve data begin - ptr = pointer(data) + A.offset + (index - 1) * $(Base.aligned_sizeof(T)) - $(Base.issingletontype(x) ? nothing : :(unsafe_store!(convert(Ptr{$x}, ptr), x))) - end - return - end - end -end +# convert what a `Ref` holds, but leave the `Ref` itself to POCL, which also handles `Ref`s +# of types (as passed by `convert.(T, A)`) +Adapt.adapt_structure(to::Adaptor, r::Base.RefValue) = Ref(adapt(to, r[])) diff --git a/lib/JLArrays/src/sparse.jl b/lib/JLArrays/src/sparse.jl index 032bd422f..c82c69b07 100644 --- a/lib/JLArrays/src/sparse.jl +++ b/lib/JLArrays/src/sparse.jl @@ -209,9 +209,15 @@ end ## adapt for the device -Adapt.adapt_structure(to::Adaptor, x::JLSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} = -GPUSparseDeviceMatrixCSC{Tv,Ti,JLDeviceArray{Ti, 1}, JLDeviceArray{Tv, 1}, AS.Generic}(adapt(to, x.colPtr), adapt(to, x.rowVal), adapt(to, x.nzVal), x.dims, x.nnz) -Adapt.adapt_structure(to::Adaptor, x::JLSparseMatrixCSR{Tv,Ti}) where {Tv,Ti} = -GPUSparseDeviceMatrixCSR{Tv,Ti,JLDeviceArray{Ti, 1}, JLDeviceArray{Tv, 1}, AS.Generic}(adapt(to, x.rowPtr), adapt(to, x.colVal), adapt(to, x.nzVal), x.dims, x.nnz) -Adapt.adapt_structure(to::Adaptor, x::JLSparseVector{Tv,Ti}) where {Tv,Ti} = -GPUSparseDeviceVector{Tv,Ti,JLDeviceArray{Ti, 1}, JLDeviceArray{Tv, 1}, AS.Generic}(adapt(to, x.iPtr), adapt(to, x.nzVal), x.len, x.nnz) +function Adapt.adapt_structure(to::Adaptor, x::JLSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} + colPtr, rowVal, nzVal = adapt(to, x.colPtr), adapt(to, x.rowVal), adapt(to, x.nzVal) + GPUSparseDeviceMatrixCSC{Tv,Ti,typeof(colPtr),typeof(nzVal),AS.Generic}(colPtr, rowVal, nzVal, x.dims, x.nnz) +end +function Adapt.adapt_structure(to::Adaptor, x::JLSparseMatrixCSR{Tv,Ti}) where {Tv,Ti} + rowPtr, colVal, nzVal = adapt(to, x.rowPtr), adapt(to, x.colVal), adapt(to, x.nzVal) + GPUSparseDeviceMatrixCSR{Tv,Ti,typeof(rowPtr),typeof(nzVal),AS.Generic}(rowPtr, colVal, nzVal, x.dims, x.nnz) +end +function Adapt.adapt_structure(to::Adaptor, x::JLSparseVector{Tv,Ti}) where {Tv,Ti} + iPtr, nzVal = adapt(to, x.iPtr), adapt(to, x.nzVal) + GPUSparseDeviceVector{Tv,Ti,typeof(iPtr),typeof(nzVal),AS.Generic}(iPtr, nzVal, x.len, x.nnz) +end diff --git a/test/runtests.jl b/test/runtests.jl index c2b9ea471..fc9d8f0c5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -11,6 +11,10 @@ const init_worker_code = quote TestSuite.sparse_types(::Type{<:JLArray}) = (JLSparseVector, JLSparseMatrixCSC, JLSparseMatrixCSR) TestSuite.sparse_types(::Type{<:Array}) = (SparseVector, SparseMatrixCSC) + + # JLArray kernels run on POCL, whose SPIR-V back-end lacks 128-bit integers + TestSuite.supported_eltypes(::Type{<:JLArray}) = + filter(!=(Int128), TestSuite.supported_eltypes()) end const init_code = quote diff --git a/test/testsuite/broadcasting.jl b/test/testsuite/broadcasting.jl index 19e8b1928..28d29412f 100644 --- a/test/testsuite/broadcasting.jl +++ b/test/testsuite/broadcasting.jl @@ -44,6 +44,9 @@ function broadcasting(AT, eltypes) gres .= test_idx.(gidx, Base.RefValue(gy)) cres .= test_idx.(cidx, Base.RefValue(cy)) @test Array(gres) == cres + + # a `Ref` of a type, as passed by `convert.(T, A)` + @test compare(a -> convert.(ET, a), AT, rand(ET, N)) end @testset "Tuple" begin From 9ba33eb99b607cb340cadafc833ef8942388aa80 Mon Sep 17 00:00:00 2001 From: Christian <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:41:14 -0300 Subject: [PATCH 2/7] [JLArrays] Fully support KI backend and test Make JLBackend a KernelInterface back-end, like the GPU back-ends that moved to KernelAbstractions 0.10: it converts arguments to POCL's and forwards compilation, launches and device queries to the POCL back-end. Run the KernelAbstractions and KernelInterface testsuites on it, skipping the dense-to-sparse test, which JLArrays don't support, and the tests that only exercise KernelAbstractions' own CPU back-end. KA 0.10's CPU back-end has no static scheduling anymore, so this removes the `static` option of JLBackend. --- lib/JLArrays/Project.toml | 2 + lib/JLArrays/src/JLKernels.jl | 93 ++++++++++++++++++++--------------- lib/JLArrays/src/sparse.jl | 1 + test/Project.toml | 4 ++ test/runtests.jl | 26 ++++++++++ 5 files changed, 85 insertions(+), 41 deletions(-) diff --git a/lib/JLArrays/Project.toml b/lib/JLArrays/Project.toml index 5640a826e..eda899d51 100644 --- a/lib/JLArrays/Project.toml +++ b/lib/JLArrays/Project.toml @@ -7,6 +7,7 @@ authors = ["Tim Besard "] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" +KernelInterface = "4ee993da-d684-4d17-a7dd-4e58e78d92bf" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" @@ -15,6 +16,7 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Adapt = "2.0, 3.0, 4.0" GPUArrays = "12.1" KernelAbstractions = "0.10.5" +KernelInterface = "0.4.2" LinearAlgebra = "1" Random = "1" SparseArrays = "1" diff --git a/lib/JLArrays/src/JLKernels.jl b/lib/JLArrays/src/JLKernels.jl index 483815b93..aae2cf7c1 100644 --- a/lib/JLArrays/src/JLKernels.jl +++ b/lib/JLArrays/src/JLKernels.jl @@ -3,8 +3,10 @@ module JLKernels using ..JLArrays using ..JLArrays: jlconvert -import KernelAbstractions -import KernelAbstractions: Kernel, StaticSize, DynamicSize, partition, launch_config +import KernelAbstractions: KernelAbstractions, POCLBackend +import KernelInterface as KI + +import GPUArrays import Adapt @@ -13,59 +15,68 @@ import Adapt export JLBackend -const MAXTHREADS = 256 +struct JLBackend <: KI.Backend end -struct JLBackend <: KernelAbstractions.GPU - static::Bool - JLBackend(;static::Bool=false) = new(static) -end +KI.get_backend(a::JLA) where JLA <: JLArray = JLBackend() +KI.get_backend(a::JLA) where JLA <: Union{JLSparseMatrixCSC, JLSparseMatrixCSR, JLSparseVector} = JLBackend() + +KI.allocate(::JLBackend, ::Type{T}, dims::Tuple) where T = JLArray{T}(undef, dims) + +KI.unsafe_free!(a::JLArray) = GPUArrays.unsafe_free!(a) -KernelAbstractions.get_backend(a::JLA) where JLA <: JLArray = JLBackend() -KernelAbstractions.get_backend(a::JLA) where JLA <: Union{JLSparseMatrixCSC, JLSparseMatrixCSR, JLSparseVector} = JLBackend() +## KernelInterface -function KernelAbstractions.mkcontext(kernel::Kernel{JLBackend}, I, _ndrange, iterspace, ::Dynamic) where Dynamic - return KernelAbstractions.CompilerMetadata{KernelAbstractions.ndrange(kernel), Dynamic}(I, _ndrange, iterspace) +# JLArrays are executed by POCL: convert arguments to their POCL-compatible (CPU) +# counterparts, and forward compilation and launches to the POCL backend. KernelAbstractions +# kernels are launched through these methods too. + +# POCL launches synchronously, and JLArrays live in host memory +KI.synchronize(::JLBackend) = nothing + +function KI.copyto!(::JLBackend, A, B) + length(A) == length(B) || + throw(ArgumentError("Arrays must match in length, got $(length(A)) and $(length(B))")) + Base.copyto!(A, B) + return A end -KernelAbstractions.allocate(::JLBackend, ::Type{T}, dims::Tuple) where T = JLArray{T}(undef, dims) - -@inline function launch_config(kernel::Kernel{JLBackend}, ndrange, workgroupsize) - if ndrange isa Integer - ndrange = (ndrange,) - end - if workgroupsize isa Integer - workgroupsize = (workgroupsize, ) - end - - if KernelAbstractions.workgroupsize(kernel) <: DynamicSize && workgroupsize === nothing - workgroupsize = (MAXTHREADS,) # Vectorization, 4x unrolling, minimal grain size - end - iterspace, dynamic = partition(kernel, ndrange, workgroupsize) - # partition checked that the ndrange's agreed - if KernelAbstractions.ndrange(kernel) <: StaticSize - ndrange = nothing - end - - return ndrange, workgroupsize, iterspace, dynamic +KI.argconvert(::JLBackend, arg) = KI.argconvert(POCLBackend(), jlconvert(arg)) + +# the POCL kernel, and the callable it was compiled from, which has to be kept alive since +# its converted form only holds pointers to the arrays it captures +struct JLKernel{K, F} + kernel::K + f::F end -function convert_to_cpu(obj::Kernel{JLBackend, W, N, F}) where {W, N, F} - return Kernel{typeof(KernelAbstractions.POCLBackend()), W, N, F}(KernelAbstractions.POCLBackend(), obj.f) +function KI.kernel_function(backend::JLBackend, f::F, tt::TT = Tuple{}; kwargs...) where {F, TT} + kernel = KI.kernel_function(POCLBackend(), jlconvert(f), tt; kwargs...) + kern = JLKernel(kernel, f) + return KI.Kernel{JLBackend, typeof(kern)}(backend, kern) end -function (obj::Kernel{JLBackend})(args...; ndrange=nothing, workgroupsize=nothing) - ndrange, workgroupsize, _, _ = launch_config(obj, ndrange, workgroupsize) +function KI.launch(obj::KI.Kernel{JLBackend}, groups::Dims{3}, items::Dims{3}, args::Tuple; kwargs...) # Use `map` rather than `jlconvert.(args)` to skip the broadcast # machinery (broadcasted/materialize/ntuple) that would otherwise # be specialized per unique arg-tuple type. - device_args = map(jlconvert, args) - new_obj = convert_to_cpu(obj) - new_obj(device_args...; ndrange, workgroupsize) + f = obj.kern.f + GC.@preserve f args KI.launch(obj.kern.kernel, groups, items, map(jlconvert, args); kwargs...) end -Adapt.adapt_storage(::JLBackend, a::Array) = Adapt.adapt(JLArrays.JLArray, a) -Adapt.adapt_storage(::JLBackend, a::JLArrays.JLArray) = a +KI.max_work_group_size(kernel::KI.Kernel{JLBackend}) = KI.max_work_group_size(kernel.kern.kernel) +KI.launch_configuration(kernel::KI.Kernel{JLBackend}; kwargs...) = + KI.launch_configuration(kernel.kern.kernel; kwargs...) +for f in (:functional, + :max_work_group_size, :max_work_group_dims, :max_num_groups, :sub_group_size, + :multiprocessor_count, :supports_subgroups, :supports_linear_subgroups, + :supports_independent_subgroups, :supports_partitionable_subgroups, + :supports_float64, :supports_atomics) + @eval KI.$f(::JLBackend) = KI.$f(POCLBackend()) +end +KI.supports_shuffle(::JLBackend, ::Type{T}) where {T} = KI.supports_shuffle(POCLBackend(), T) +KI.versioninfo(io::IO, ::JLBackend) = KI.versioninfo(io, POCLBackend()) -Adapt.adapt_storage(::KernelAbstractions.POCLBackend, a::JLArrays.JLArray) = convert(Array, a) +Adapt.adapt_storage(::JLBackend, a::Array) = Adapt.adapt(JLArray, a) +Adapt.adapt_storage(::JLBackend, a::JLArray) = a end diff --git a/lib/JLArrays/src/sparse.jl b/lib/JLArrays/src/sparse.jl index c82c69b07..2acca591c 100644 --- a/lib/JLArrays/src/sparse.jl +++ b/lib/JLArrays/src/sparse.jl @@ -153,6 +153,7 @@ function JLSparseMatrixCSC(xs::SparseMatrixCSC{Tv, Ti}) where {Ti, Tv} return JLSparseMatrixCSC{Tv, Ti}(colPtr, rowVal, nzVal, (xs.m, xs.n)) end JLSparseMatrixCSC(xs::SparseVector) = JLSparseMatrixCSC(SparseMatrixCSC(xs)) + Base.length(x::JLSparseMatrixCSC) = prod(x.dims) Base.size(x::JLSparseMatrixCSC) = x.dims diff --git a/test/Project.toml b/test/Project.toml index 7e816ed54..ab94d87b8 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,13 +1,17 @@ [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JLArrays = "27aeb0d3-9eb9-45fb-866b-73c2ecf80fcb" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" +KernelInterface = "4ee993da-d684-4d17-a7dd-4e58e78d92bf" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/runtests.jl b/test/runtests.jl index fc9d8f0c5..b43820e8d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -30,6 +30,32 @@ for AT in (:JLArray, :Array), name in keys(TestSuite.tests) testsuite["$(AT)/$name"] = :(TestSuite.tests[$name]($AT)) end +testsuite["JLArray/kernelabstractions"] = quote + import KernelAbstractions + import KernelAbstractions as KA + using JLArrays.JLKernels + + include(joinpath(dirname(pathof(KernelAbstractions)), "..", "test", "testsuite.jl")) + + # JLArrays don't convert dense arrays to sparse ones, and the other two tests only run + # kernels on KernelAbstractions' own CPU back end + skip_tests = Set(["sparse", "CPU synchronization", "fallback test: callable types"]) + # JLArray kernels are compiled by POCL, like the CPU back end's, so check them the same way + Testsuite.testsuite(JLBackend, "CPU", KernelAbstractions.POCL, JLArray, + KernelAbstractions.POCL.CLDeviceArray; skip_tests) +end + +testsuite["JLArray/kernelinterface"] = quote + import KernelInterface + import KernelInterface as KI + using JLArrays.JLKernels + import Adapt + + include(joinpath(dirname(pathof(KernelInterface)), "..", "test", "testsuite.jl")) + + Testsuite.testsuite(JLBackend(), JLArray) +end + # captured variables that get boxed (`Core.Box`) lose their type if isdefined(Test, :detect_closure_boxes) testsuite["closure boxes"] = quote From fe69160edaf33e0b3d952dce5ce5182427ea7d3e Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Tue, 27 Oct 2026 15:04:09 +0100 Subject: [PATCH 3/7] [JLArrays] Compute kernel indices in Int, like POCL KernelAbstractions narrows `@index` computations to Int32 on every back-end but POCL, because on POCL the narrowing keeps LLVM from vectorizing the loops over work-items. JLArray kernels run on POCL, but JLBackend isn't a POCLBackend, so they got Int32 indices. Using Int speeds up a 2048x2048 element-wise kernel by about 7%. --- lib/JLArrays/src/JLKernels.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/JLArrays/src/JLKernels.jl b/lib/JLArrays/src/JLKernels.jl index aae2cf7c1..44f68cbb7 100644 --- a/lib/JLArrays/src/JLKernels.jl +++ b/lib/JLArrays/src/JLKernels.jl @@ -76,6 +76,10 @@ end KI.supports_shuffle(::JLBackend, ::Type{T}) where {T} = KI.supports_shuffle(POCLBackend(), T) KI.versioninfo(io::IO, ::JLBackend) = KI.versioninfo(io, POCLBackend()) +# KernelAbstractions computes indices in `Int32` unless the back-end is POCL, where narrowing +# them keeps LLVM from vectorizing the loops over work-items. JLArray kernels run on POCL too. +KernelAbstractions.narrow_index_type(::JLBackend) = Int + Adapt.adapt_storage(::JLBackend, a::Array) = Adapt.adapt(JLArray, a) Adapt.adapt_storage(::JLBackend, a::JLArray) = a From 131017b69c883410bb8bab6d2b16fbd4409a440e Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Tue, 27 Oct 2026 15:04:53 +0100 Subject: [PATCH 4/7] [JLArrays] Convert kernel arguments without allocating A JLArray was converted to a POCL device array by wrapping its memory in an Array and adapting that with a fresh KernelAdaptor, allocating both on every conversion, which happens several times per argument per launch. Construct the device array from the JLArray's pointer instead. This reduces the allocations of `c .= a .+ b` from 1344 to 480 bytes. This also converts arrays of isbits unions, whose storage an `Array` can't wrap: POCL's device arrays find their selector bytes `maxsize` bytes past the data, so point that at the JLArray's own. --- lib/JLArrays/src/array.jl | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/JLArrays/src/array.jl b/lib/JLArrays/src/array.jl index 86271aa24..3a1b44e7a 100644 --- a/lib/JLArrays/src/array.jl +++ b/lib/JLArrays/src/array.jl @@ -191,10 +191,17 @@ Adapt.adapt_storage(::Type{<:JLArray{T}}, xs::AbstractArray) where {T} = # adapt back to the CPU Adapt.adapt_storage(::Type{Array}, xs::JLArray) = convert(Array, xs) -# adapt for the device +# adapt for the device, where JLArrays are POCL arrays (constructed directly, rather than +# by adapting a wrapping `Array`, to avoid allocating on every launch) function Adapt.adapt_storage(::Adaptor, x::JLArray{T,N}) where {T,N} - arr = typed_data(x) - Adapt.adapt_storage(POCL.KernelAdaptor([pointer(arr)]), arr) + AS = POCL.AS.CrossWorkgroup + ptr = reinterpret(Core.LLVMPtr{T,AS}, pointer(x)) + if Base.isbitsunion(T) + # POCL arrays of isbits unions find their selector bytes `maxsize` bytes past the data + POCL.CLDeviceArray{T,N,AS}(x.dims, ptr, x.tagoffset - x.offset) + else + POCL.CLDeviceArray{T,N,AS}(x.dims, ptr) + end end ## conversions From 9ac1c521c9727996b034f7056640fecd93b2c743 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Thu, 5 Nov 2026 11:51:00 +0100 Subject: [PATCH 5/7] [JLArrays] Run GPUArrays' AcceleratedKernels implementations JLArrays reduced, sorted, scanned and searched with Base on an `Array` wrapping its storage, because AcceleratedKernels' kernels did not run on its KA 0.9 back-end. On KA 0.10 they run through POCL like any other kernel, so JLArrays now takes the same code paths as the GPU back-ends instead of bypassing them. This also makes these operations work on arrays of isbits unions: an `Array` can't wrap their storage, whose selector bytes follow the data, while POCL arrays can. --- lib/JLArrays/src/JLArrays.jl | 4 --- lib/JLArrays/src/accumulate.jl | 20 ------------- lib/JLArrays/src/array.jl | 5 ---- lib/JLArrays/src/findall.jl | 14 ---------- lib/JLArrays/src/mapreduce.jl | 31 --------------------- lib/JLArrays/src/sorting.jl | 51 ---------------------------------- src/host/mapreduce.jl | 3 +- 7 files changed, 1 insertion(+), 127 deletions(-) delete mode 100644 lib/JLArrays/src/accumulate.jl delete mode 100644 lib/JLArrays/src/findall.jl delete mode 100644 lib/JLArrays/src/mapreduce.jl delete mode 100644 lib/JLArrays/src/sorting.jl diff --git a/lib/JLArrays/src/JLArrays.jl b/lib/JLArrays/src/JLArrays.jl index 997aaecd2..2d9fd9bde 100644 --- a/lib/JLArrays/src/JLArrays.jl +++ b/lib/JLArrays/src/JLArrays.jl @@ -38,10 +38,6 @@ include("device/array.jl") include("array.jl") include("sparse.jl") include("broadcast.jl") -include("mapreduce.jl") -include("sorting.jl") -include("accumulate.jl") -include("findall.jl") # KernelAbstractions include("JLKernels.jl") diff --git a/lib/JLArrays/src/accumulate.jl b/lib/JLArrays/src/accumulate.jl deleted file mode 100644 index 1bf87454d..000000000 --- a/lib/JLArrays/src/accumulate.jl +++ /dev/null @@ -1,20 +0,0 @@ -# scans - -# As for sorting: Base, applied to the storage, with the method shapes of GPUArrays. - -function Base._accumulate!(op, B::AnyJLArray, A::AnyJLVector, dims::Nothing, init::Nothing) - accumulate!(op, _host(B), _host(A)) - return B -end -function Base._accumulate!(op, B::AnyJLArray, A::AnyJLVector, dims::Nothing, init::Some) - accumulate!(op, _host(B), _host(A); init=something(init)) - return B -end -function Base._accumulate!(op, B::AnyJLArray, A::AnyJLArray, dims::Integer, init::Nothing) - accumulate!(op, _host(B), _host(A); dims) - return B -end -function Base._accumulate!(op, B::AnyJLArray, A::AnyJLArray, dims::Integer, init::Some) - accumulate!(op, _host(B), _host(A); dims, init=something(init)) - return B -end diff --git a/lib/JLArrays/src/array.jl b/lib/JLArrays/src/array.jl index 3a1b44e7a..d0cd68c1e 100644 --- a/lib/JLArrays/src/array.jl +++ b/lib/JLArrays/src/array.jl @@ -57,11 +57,6 @@ GPUArrays.dense_array_type(::Type{JLArray{T, N}}) where {T, N} = JLArray{T, N} GPUArrays.dense_vector_type(a::JLArray{T, N}) where {T, N} = JLArray{T, 1} GPUArrays.dense_vector_type(::Type{JLArray{T, N}}) where {T, N} = JLArray{T, 1} -# conversion of untyped data to a typed Array -function typed_data(x::JLArray{T}) where {T} - unsafe_wrap(Array, pointer(x), x.dims) -end - # the selector byte of element `i` of an array of isbits unions typetagdata(a::JLArray, i=1) = pointer(a.data[]) + a.tagoffset + i - 1 @static if VERSION >= v"1.11.0-DEV.753" diff --git a/lib/JLArrays/src/findall.jl b/lib/JLArrays/src/findall.jl deleted file mode 100644 index 11fdecbf0..000000000 --- a/lib/JLArrays/src/findall.jl +++ /dev/null @@ -1,14 +0,0 @@ -# findall and predicates - -# As for sorting: Base, applied to the storage, with the method shapes of GPUArrays. - -Base.findall(bools::AnyJLArray{Bool}) = JLArray(findall(_host(bools))) -Base.findall(f::Function, A::AnyJLArray) = JLArray(findall(f, _host(A))) -Base.findall(f::Base.Fix2{typeof(in)}, A::AnyJLArray) = JLArray(findall(f, _host(A))) -Base.getindex(A::JLArray, mask::AnyJLArray{Bool}) = JLArray(_host(A)[_host(mask)]) - -for fname in (:any, :all) - @eval Base.$fname(f::Function, A::AnyJLArray; dims=:) = - dims === Colon() ? $fname(f, _host(A)) : - invoke($fname, Tuple{Function, GPUArrays.AnyGPUArray}, f, A; dims) -end diff --git a/lib/JLArrays/src/mapreduce.jl b/lib/JLArrays/src/mapreduce.jl deleted file mode 100644 index c1725e1bb..000000000 --- a/lib/JLArrays/src/mapreduce.jl +++ /dev/null @@ -1,31 +0,0 @@ -# reductions - -struct ArrayNoCopy end -# (an `Array` can't wrap the storage of isbits unions, whose selector bytes are elsewhere) -Adapt.adapt_storage(::ArrayNoCopy, x::JLArray{T}) where {T} = - Base.isbitsunion(T) ? jlconvert(x) : typed_data(x) -@static if !isdefined(KernelAbstractions, :POCL) # KA v0.9 - # (AcceleratedKernels picks its host algorithms by the back-end) - KernelAbstractions.get_backend(::JLDeviceArray) = KernelAbstractions.CPU() -end - -# GPUArrays reduces with AcceleratedKernels, whose kernels do not run on JLArrays' back-end. The -# reference implementation runs AcceleratedKernels' host algorithms on the arrays' storage -# instead, through GPUArrays' internal indirection for this purpose. (The storage is wrapped -# without a copy, so the arrays must be preserved; a `Broadcasted` source is materialized on the -# host.) -_host_source(A::Broadcast.Broadcasted) = # (`copy` of a 0-dimensional one gives a scalar) - Array(copyto!(similar(A, Broadcast.combine_eltypes(A.f, A.args)), A)) -_host_source(A) = adapt(ArrayNoCopy(), A) - -GPUArrays._ak_mapreduce(f, op, A::Union{AnyJLArray, Broadcast.Broadcasted{<:JLArrayStyle}}; - backend=nothing, kwargs...) = - GC.@preserve A @allowscalar GPUArrays.AK.mapreduce(f, op, _host_source(A); kwargs...) - -function GPUArrays._ak_mapreducedim!(f, op, R::AnyJLArray, - A::Union{AbstractArray, Broadcast.Broadcasted}; - backend=nothing, kwargs...) - GC.@preserve R A @allowscalar GPUArrays.AK.mapreducedim!( - f, op, adapt(ArrayNoCopy(), R), _host_source(A); kwargs...) - return R -end diff --git a/lib/JLArrays/src/sorting.jl b/lib/JLArrays/src/sorting.jl deleted file mode 100644 index 07e26988e..000000000 --- a/lib/JLArrays/src/sorting.jl +++ /dev/null @@ -1,51 +0,0 @@ -# sorting and reversing - -# GPUArrays implements these with AcceleratedKernels, whose kernels do not run on JLArrays' -# back-end. The reference implementation applies Base to the arrays' storage instead, with the -# same method shapes, and accepts the same `alg` values as GPU arrays (`GPUArrays._akalg`). - -_host(A) = adapt(ArrayNoCopy(), A) - -function Base.sort!(v::AnyJLVector; alg=nothing, lt=isless, by=identity, rev=nothing, - order::Base.Order.Ordering=Base.Order.Forward, scratch=nothing) - GPUArrays._akalg(alg) - sort!(_host(v); lt, by, rev, order) - return v -end -function Base.sort!(A::AnyJLArray; dims::Integer, alg=nothing, scratch=nothing, kwargs...) - GPUArrays._akalg(alg) - sort!(_host(A); dims, kwargs...) - return A -end - -function Base.sortperm!(ix::AnyJLArray{<:Integer}, v::AnyJLVector; alg=nothing, - scratch=nothing, initialized::Bool=false, dims=nothing, kwargs...) - dims === nothing || throw(ArgumentError("sortperm! of a vector does not accept `dims`")) - axes(ix) == axes(v) || - throw(ArgumentError("index array must have the same axes as the source array")) - GPUArrays._akalg(alg) - sortperm!(_host(ix), _host(v); kwargs...) - return ix -end -function Base.sortperm!(ix::AnyJLArray{<:Integer}, A::AnyJLArray; dims::Integer, alg=nothing, - scratch=nothing, initialized::Bool=false, kwargs...) - axes(ix) == axes(A) || - throw(ArgumentError("index array must have the same axes as the source array")) - GPUArrays._akalg(alg) - sortperm!(_host(ix), _host(A); dims, kwargs...) - return ix -end - -function Base.reverse!(A::AnyJLArray; dims=:) - reverse!(_host(A); dims) - return A -end -Base.reverse(A::AnyJLArray; dims=:) = reverse!(copy(A); dims) -Base.reverse!(v::AnyJLVector; dims=:) = invoke(reverse!, Tuple{AbstractVector}, v; dims) -Base.reverse(v::AnyJLVector; dims=:) = invoke(reverse, Tuple{AbstractVector}, v; dims) -function Base.reverse!(v::AnyJLVector, start::Integer, stop::Integer=lastindex(v)) - reverse!(_host(v), start, stop) - return v -end -Base.reverse(v::AnyJLVector, start::Integer, stop::Integer=lastindex(v)) = - reverse!(copy(v), start, stop) diff --git a/src/host/mapreduce.jl b/src/host/mapreduce.jl index f6c469264..cd664c817 100644 --- a/src/host/mapreduce.jl +++ b/src/host/mapreduce.jl @@ -9,8 +9,7 @@ const AbstractArrayOrBroadcasted = Union{AbstractArray,Broadcast.Broadcasted} # AcceleratedKernels' primitives, on the backend `backend` (a `Broadcasted` source may hold arrays -# without one, such as `EachIndex`). The reference back-end (JLArrays) runs them on its storage -# instead; these are internal, not an extension point for back-ends. +# without one, such as `EachIndex`). These are internal, not an extension point for back-ends. # # Base's reductions combine the elements in their order, so `op` need not be commutative, while # AcceleratedKernels' faster kernels combine them in any order: they are only used for operators From 3215bcf6ebde91500b7e179c53c08603bd2e8b0a Mon Sep 17 00:00:00 2001 From: Christian <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:41:26 -0300 Subject: [PATCH 6/7] [JLArrays] Bump to 0.5 The port to KernelAbstractions 0.10 is breaking: `JLBackend(; static)` and `JLDeviceArray` are gone. --- lib/JLArrays/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/JLArrays/Project.toml b/lib/JLArrays/Project.toml index eda899d51..961210ca2 100644 --- a/lib/JLArrays/Project.toml +++ b/lib/JLArrays/Project.toml @@ -1,6 +1,6 @@ name = "JLArrays" uuid = "27aeb0d3-9eb9-45fb-866b-73c2ecf80fcb" -version = "0.4.2" +version = "0.5" authors = ["Tim Besard "] [deps] From 4de3fad283e85bbc7114f661231442c9380affc4 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:01:42 -0300 Subject: [PATCH 7/7] Increase the CI timeout to 90 minutes The KernelAbstractions and KernelInterface testsuites, and kernels for every reduction, sort and scan, make JLArrays' tests slower. The macOS runners, which run one test at a time, were already close to the 60-minute limit. --- .github/workflows/Test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index fe2d8021a..5bc921456 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -17,7 +17,7 @@ jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} - timeout-minutes: 60 + timeout-minutes: 90 strategy: fail-fast: false matrix: