Skip to content

Use GPUArrays 12 for sorting, scans, reductions and findall - #3356

Merged
maleadt merged 2 commits into
mainfrom
tb/gpuarrays-12
Oct 9, 2026
Merged

maleadt merged 2 commits into
mainfrom
tb/gpuarrays-12

Conversation

@maleadt

@maleadt maleadt commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Ports CUDA.jl to GPUArrays 12. GPUArrays 12 implements Base's sorting, scans, reductions, findall, logical indexing and reverse once for every GPU array, on AcceleratedKernels 0.5 (JuliaGPU/GPUArrays.jl#790), and no longer calls the GPUArrays.mapreducedim! hook. This deletes CUDA.jl's own implementations, about 2,000 lines:

  • indexing.jl, accumulate.jl (including the internal scan!), reverse.jl and sorting.jl (with CUDA.QuickSort and CUDA.BitonicSort) are removed from CUDACore/src. findall(f, ::AnyCuArray) was also ambiguous with GPUArrays' findall(::Fix2{typeof(in)}, …).
  • mapreduce.jl keeps only reduce_warp/reduce_block, which cuBLAS and cuSPARSE use, as reduction.jl.
  • Tests of the removed internals are dropped or moved to the Base API. The NEWS entry lists the technically breaking changes, for a minor release.

Tested on an RTX 5080 (Julia 1.13): all of core, cuBLAS, cuSPARSE and the GPUArrays testsuite pass, except cholesky(::Diagonal) (3 errors). That's a closure capturing a ComposedFunction that Adapt doesn't adapt inferably, fixed in JuliaGPU/Adapt.jl#124.

Timings, CUDA 6.4.2 vs this branch, in ms, best of 20 (sorts best of 10, including a copy):

6.4.2 branch
sum, 1e7 Float32 0.029 0.027
sum, 1000×10000, dims=1 0.116 0.043
sum, 1000×10000, dims=2 0.143 0.023
sum, 1000×10, dims=2 0.007 0.017
sort!, 1e7 Int32 27.7 4.8
sort!, 1e7 Float32 28.3 5.3
sortperm, 1e7 Float32 64.0 7.5
cumsum, 1e7 Float32 0.271 0.182
findall(>(0.5f0), x), 1e7 0.661 0.189
x[x .> 0.5f0], 1e7 0.784 0.187

Small reductions along a dimension are slower; that's tracked in JuliaGPU/AcceleratedKernels.jl#135.

@maleadt
maleadt marked this pull request as ready for review October 7, 2026 06:17
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.94%. Comparing base (3dbe079) to head (a4872f0).

Files with missing lines Patch % Lines
CUDACore/src/reduction.jl 48.78% 21 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3356      +/-   ##
==========================================
- Coverage   87.38%   84.94%   -2.44%     
==========================================
  Files         195      190       -5     
  Lines       20044    19147     -897     
==========================================
- Hits        17515    16264    -1251     
- Misses       2529     2883     +354     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@giordano

giordano commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

I don't have the time to look into this, but I'll mention that this seems to cause a large regression in Oceananigans, here's a reproducer without Oceananigans itself

# Full reduction of a lazy, device-adaptable `AbstractArray` into a one-element `CuArray` with
# `Base.mapreducedim!`, the pattern Oceananigans uses for dot products and norms of fields: the
# source computes `a[i, j, k] * b[i, j, k] * mask[i, j, k]` on the fly from halo-padded storage.
#
# usage: julia --project=<env> lazy_reduction.jl [N]
using CUDA, Adapt, Printf
using GPUArrays: GPUArrays

const N = length(ARGS) ≥ 1 ? parse(Int, ARGS[1]) : 64
const H = 4 # halo width

# Lazy elementwise product of two padded arrays, masked: interior index (i, j, k) reads
# the padded storage at (i + H, j + H, k + H). Neither dense nor strided, like a lazy field operation.
struct MaskedProduct{T, A, M} <: AbstractArray{T, 3}
    a :: A
    b :: A
    mask :: M
    size :: NTuple{3, Int}
end

MaskedProduct(a, b, mask, size) = MaskedProduct{eltype(a), typeof(a), typeof(mask)}(a, b, mask, size)
Base.size(p::MaskedProduct) = p.size
Base.@propagate_inbounds Base.getindex(p::MaskedProduct, i::Int, j::Int, k::Int) =
    p.a[i + H, j + H, k + H] * p.b[i + H, j + H, k + H] * p.mask[i, j, k]
Adapt.adapt_structure(to, p::MaskedProduct) =
    MaskedProduct(adapt(to, p.a), adapt(to, p.b), adapt(to, p.mask), p.size)

function setup(T=Float64)
    a = CuArray(rand(T, N + 2H, N + 2H, N + 2H))
    b = CuArray(rand(T, N + 2H, N + 2H, N + 2H))
    mask = CuArray(T[k > N ÷ 4 for i in 1:N, j in 1:N, k in 1:N]) # bottom quarter "immersed"
    lazy = MaskedProduct(a, b, mask, (N, N, N))
    dense = a[H+1:H+N, H+1:H+N, H+1:H+N] .* b[H+1:H+N, H+1:H+N, H+1:H+N] .* mask
    reference = sum(Array(dense))
    return (; lazy, dense, reference)
end

variants(s) = (
    "dense CuArray (control)" => s.dense,
    "lazy AbstractArray"      => s.lazy,
    "Broadcasted of the lazy" => Broadcast.instantiate(Broadcast.broadcasted(identity, s.lazy)),
)

function reduce_into!(r, src)
    fill!(r, 0)
    Base.mapreducedim!(identity, +, r, src)
    return r
end

function measure(name, src, reference; calls=100)
    r = CUDA.zeros(Float64, 1, 1, 1)
    reduce_into!(r, src); CUDA.synchronize() # compile
    ok = isapprox(Array(r)[1], reference; rtol=1e-12)

    bytes = @allocated (reduce_into!(r, src); CUDA.synchronize())
    host = @elapsed (for _ in 1:calls; reduce_into!(r, src); end) # host time to enqueue
    CUDA.synchronize()
    wall = @elapsed (for _ in 1:calls; reduce_into!(r, src); end; CUDA.synchronize())

    prof = CUDA.@profile trace=true reduce_into!(r, src)
    names = prof.host.name
    launches = count(n -> occursin("aunchKernel", n), names)
    allocs = count(n -> occursin("MemAlloc", n), names)
    syncs = count(n -> occursin("ynchronize", n), names) # includes the profiler's own
    gpu = sum(prof.device.stop .- prof.device.start) * 1e6
    kernels = unique(first.(prof.device.name, 40))

    @printf("%-25s correct=%-5s wall=%8.1f µs/call  host=%7.1f µs/call  GPU=%8.1f µs  host alloc=%6d B  launches=%d  device allocs=%d  syncs=%d\n",
            name, ok, wall / calls * 1e6, host / calls * 1e6, gpu, bytes, launches, allocs, syncs)
    println("    kernels: ", join(kernels, " | "))
end

function main()
    println("CUDA.jl ", pkgversion(CUDA), ", GPUArrays ", pkgversion(GPUArrays), ", ", CUDA.name(CUDA.device()), ", N = $N")
    s = setup()
    for (name, src) in variants(s)
        measure(name, src, s.reference)
    end
end

main()
===== env-current N=32
dense CuArray (control)   correct=true  wall=    34.8 µs/call  host=   34.1 µs/call  GPU=     8.6 µs  host alloc=  3664 B  launches=4  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu_broadcast_kernel_cartesian(CompilerM | partial_mapreduce_grid(identity, _, void
lazy AbstractArray        correct=true  wall=    28.3 µs/call  host=   27.3 µs/call  GPU=     9.8 µs  host alloc=  4128 B  launches=4  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu_broadcast_kernel_cartesian(CompilerM | partial_mapreduce_grid(identity, _, void
Broadcasted of the lazy   correct=true  wall=    44.5 µs/call  host=   43.2 µs/call  GPU=    10.0 µs  host alloc=  4128 B  launches=4  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu_broadcast_kernel_cartesian(CompilerM | partial_mapreduce_grid(identity, _, void
===== env-current N=64
dense CuArray (control)   correct=true  wall=    26.1 µs/call  host=   30.3 µs/call  GPU=    10.7 µs  host alloc=  3664 B  launches=4  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu_broadcast_kernel_cartesian(CompilerM | partial_mapreduce_grid(identity, _, void
lazy AbstractArray        correct=true  wall=    40.2 µs/call  host=   44.3 µs/call  GPU=    10.7 µs  host alloc=  4128 B  launches=4  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu_broadcast_kernel_cartesian(CompilerM | partial_mapreduce_grid(identity, _, void
Broadcasted of the lazy   correct=true  wall=    38.0 µs/call  host=   38.3 µs/call  GPU=    10.7 µs  host alloc=  4128 B  launches=4  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu_broadcast_kernel_cartesian(CompilerM | partial_mapreduce_grid(identity, _, void
===== env-current N=128
dense CuArray (control)   correct=true  wall=    35.8 µs/call  host=   39.2 µs/call  GPU=    15.7 µs  host alloc=  3664 B  launches=4  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu_broadcast_kernel_cartesian(CompilerM | partial_mapreduce_grid(identity, _, void
lazy AbstractArray        correct=true  wall=    34.1 µs/call  host=   35.5 µs/call  GPU=    18.8 µs  host alloc=  4128 B  launches=4  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu_broadcast_kernel_cartesian(CompilerM | partial_mapreduce_grid(identity, _, void
Broadcasted of the lazy   correct=true  wall=    36.4 µs/call  host=   44.1 µs/call  GPU=    17.6 µs  host alloc=  4128 B  launches=4  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu_broadcast_kernel_cartesian(CompilerM | partial_mapreduce_grid(identity, _, void
===== env-pr3356 N=32
dense CuArray (control)   correct=true  wall=    26.9 µs/call  host=   21.9 µs/call  GPU=     5.5 µs  host alloc=  3120 B  launches=3  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu__mapreduce_block_(
lazy AbstractArray        correct=true  wall=  4187.2 µs/call  host=   17.8 µs/call  GPU=  4180.9 µs  host alloc=  3008 B  launches=2  device allocs=1  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu__mapreduce_nd_generic_(CompilerMetad
Broadcasted of the lazy   correct=true  wall=    25.1 µs/call  host=   23.1 µs/call  GPU=     8.3 µs  host alloc=  3984 B  launches=3  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu__mapreduce_block_(
===== env-pr3356 N=64
dense CuArray (control)   correct=true  wall=    22.7 µs/call  host=   19.0 µs/call  GPU=     5.7 µs  host alloc=  3120 B  launches=3  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu__mapreduce_block_(
lazy AbstractArray        correct=true  wall= 33400.3 µs/call  host=   20.8 µs/call  GPU= 33395.8 µs  host alloc=  3008 B  launches=2  device allocs=1  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu__mapreduce_nd_generic_(CompilerMetad
Broadcasted of the lazy   correct=true  wall=    23.0 µs/call  host=   32.7 µs/call  GPU=     7.9 µs  host alloc=  3984 B  launches=3  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu__mapreduce_block_(
===== env-pr3356 N=128
dense CuArray (control)   correct=true  wall=    20.2 µs/call  host=   21.6 µs/call  GPU=     8.8 µs  host alloc=  3120 B  launches=3  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu__mapreduce_block_(
lazy AbstractArray        correct=true  wall=266956.0 µs/call  host=   13.1 µs/call  GPU=266947.3 µs  host alloc=  3008 B  launches=2  device allocs=1  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu__mapreduce_nd_generic_(CompilerMetad
Broadcasted of the lazy   correct=true  wall=    25.4 µs/call  host=   19.9 µs/call  GPU=    19.8 µs  host alloc=  3984 B  launches=3  device allocs=2  syncs=2
    kernels: [copy pageable to device memory] | gpu_fill_kernel_(| gpu__mapreduce_block_(

We may be able to change the source code not to depend on this, but I wanted to flag that there are workloads which may be negatively affected

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

CUDA.jl Benchmarks

Details
Benchmark suite Current: a4872f0 Previous: 3dbe079 Ratio
array/accumulate/Float32/1d 65078 ns 104051 ns 0.63
array/accumulate/Float32/dims=1 208594 ns 76447 ns 2.73
array/accumulate/Float32/dims=1L 230285 ns 1674763 ns 0.14
array/accumulate/Float32/dims=2 94365 ns 147115 ns 0.64
array/accumulate/Float32/dims=2L 3404908 ns 682749 ns 4.99
array/accumulate/Int64/1d 105534 ns 122510 ns 0.86
array/accumulate/Int64/dims=1 223823 ns 80763 ns 2.77
array/accumulate/Int64/dims=1L 403753 ns 1797055 ns 0.22
array/accumulate/Int64/dims=2 110746 ns 161993 ns 0.68
array/accumulate/Int64/dims=2L 4901273 ns 986680 ns 4.97
array/broadcast 16835 ns 16758 ns 1.00
array/broadcast launch 6910.2 ns 7368.5 ns 0.94
array/construct 954.7142857142857 ns 941.3636363636364 ns 1.01
array/copy 16871 ns 16704 ns 1.01
array/copyto!/cpu_to_gpu 206710 ns 206840 ns 1.00
array/copyto!/gpu_to_cpu 241031 ns 240156 ns 1.00
array/copyto!/gpu_to_gpu 8875 ns 8801.333333333334 ns 1.01
array/iteration/findall/bool 45573 ns 138253 ns 0.33
array/iteration/findall/int 69018 ns 148621 ns 0.46
array/iteration/findfirst/bool 113298 ns 73937 ns 1.53
array/iteration/findfirst/int 111544 ns 75602 ns 1.48
array/iteration/findmin/1d 66463 ns 60274 ns 1.10
array/iteration/findmin/2d 67960 ns 102852 ns 0.66
array/iteration/logical 49530 ns 191926 ns 0.26
array/iteration/scalar 60880 ns 58756 ns 1.04
array/permutedims/2d 49803 ns 49221 ns 1.01
array/permutedims/3d 50438 ns 50406 ns 1.00
array/permutedims/4d 52242 ns 51570 ns 1.01
array/random/rand/Float32 11069 ns 10717 ns 1.03
array/random/rand/Int64 18128 ns 17992 ns 1.01
array/random/rand!/Float32 8310 ns 8151.666666666667 ns 1.02
array/random/rand!/Int64 15766 ns 15858 ns 0.99
array/random/randn/Float32 34757 ns 34404 ns 1.01
array/random/randn!/Float32 25084 ns 24470 ns 1.03
array/reductions/mapreduce/Float32/1d 48406 ns 32125 ns 1.51
array/reductions/mapreduce/Float32/dims=1 42965 ns 39831 ns 1.08
array/reductions/mapreduce/Float32/dims=1L 72884 ns 53153 ns 1.37
array/reductions/mapreduce/Float32/dims=2 45087 ns 58358 ns 0.77
array/reductions/mapreduce/Float32/dims=2L 73882 ns 70257 ns 1.05
array/reductions/mapreduce/Int64/1d 49929 ns 39151 ns 1.28
array/reductions/mapreduce/Int64/dims=1 44885 ns 42584 ns 1.05
array/reductions/mapreduce/Int64/dims=1L 113342 ns 88871 ns 1.28
array/reductions/mapreduce/Int64/dims=2 46152 ns 59775 ns 0.77
array/reductions/mapreduce/Int64/dims=2L 100016 ns 86986 ns 1.15
array/reductions/reduce/Float32/1d 49308 ns 31785 ns 1.55
array/reductions/reduce/Float32/dims=1 43748 ns 39985 ns 1.09
array/reductions/reduce/Float32/dims=1L 73684 ns 52563 ns 1.40
array/reductions/reduce/Float32/dims=2 45107 ns 58425 ns 0.77
array/reductions/reduce/Float32/dims=2L 74610 ns 70049 ns 1.07
array/reductions/reduce/Int64/1d 50469 ns 38703 ns 1.30
array/reductions/reduce/Int64/dims=1 46131 ns 42714 ns 1.08
array/reductions/reduce/Int64/dims=1L 114052 ns 88972 ns 1.28
array/reductions/reduce/Int64/dims=2 46919 ns 59946 ns 0.78
array/reductions/reduce/Int64/dims=2L 101013 ns 86589 ns 1.17
array/reverse/1d 23741 ns 15834 ns 1.50
array/reverse/1dL 129466 ns 69704 ns 1.86
array/reverse/1dL_inplace 70758 ns 67342 ns 1.05
array/reverse/1d_inplace 12597 ns 8969 ns 1.40
array/reverse/2d 23392 ns 20119 ns 1.16
array/reverse/2dL 81802 ns 74156 ns 1.10
array/reverse/2dL_inplace 71154 ns 67439 ns 1.06
array/reverse/2d_inplace 15272 ns 10477 ns 1.46
array/sorting/1d 725459 ns 2839489 ns 0.26
array/sorting/2d 204028 ns 1087405 ns 0.19
array/sorting/by 905127 ns 3391939 ns 0.27
cuda/graph/capture 41557 ns 43488 ns 0.96
cuda/graph/eager 38250 ns 39629 ns 0.97
cuda/graph/launch 14334 ns 14304 ns 1.00
cuda/synchronization/context/auto 7844 ns 8124.333333333333 ns 0.97
cuda/synchronization/context/blocking 894.4651162790698 ns 861.9807692307693 ns 1.04
cuda/synchronization/context/nonblocking 7765.333333333333 ns 8195 ns 0.95
cuda/synchronization/stream/auto 875.6140350877193 ns 815.5243902439024 ns 1.07
cuda/synchronization/stream/blocking 760.4 ns 756.3652173913043 ns 1.01
cuda/synchronization/stream/nonblocking 7735.333333333333 ns 7812.333333333333 ns 0.99
integration/byval/reference 147585 ns 147572 ns 1.00
integration/byval/slices=1 148830 ns 148814 ns 1.00
integration/byval/slices=2 290244 ns 290452 ns 1.00
integration/byval/slices=3 432309 ns 432316 ns 1.00
integration/cudadevrt 104985 ns 104953 ns 1.00
integration/volumerhs 9610720 ns 9609671 ns 1.00
kernel/indexing 11658 ns 11473 ns 1.02
kernel/indexing_checked 12057 ns 11938 ns 1.01
kernel/launch 2326.777777777778 ns 2435.777777777778 ns 0.96
kernel/occupancy 842.5909090909091 ns 861.1333333333333 ns 0.98
kernel/rand 15051 ns 14950 ns 1.01
latency/import 4221748225 ns 4323642976 ns 0.98
latency/precompile 5153439736 ns 5110693071 ns 1.01
latency/ttfp 4913819347 ns 4831528462 ns 1.02

This comment was automatically generated by workflow using github-action-benchmark.

@maleadt

maleadt commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Fixed in JuliaGPU/AcceleratedKernels.jl#154 and JuliaGPU/AcceleratedKernels.jl#153

GPUArrays 12 implements these once for every back-end on AcceleratedKernels,
following Base's semantics, and no longer calls the GPUArrays.mapreducedim!
hook. Remove CUDA's own implementations, which would otherwise shadow the
generic ones (and in the case of findall, be ambiguous with them). The warp-
and block-level reduction helpers stay, as the library packages use them.

This drops CUDA.QuickSort and CUDA.BitonicSort and the internal scan!, which
is technically breaking but has no known users.
GPUArrays 12.1 defines `Base.dataids` and `Base.mightalias` for every GPU
array from where its elements live. Implement that hook instead of our own
methods. Empty arrays now alias nothing (the testsuite's empty-view check
failed), and Base's check for two SubArrays no longer converts their parents
to pointers, which took stream ownership of their memory.
@maleadt
maleadt merged commit 4a98b2a into main Oct 9, 2026
18 checks passed
@maleadt
maleadt deleted the tb/gpuarrays-12 branch October 9, 2026 17:06
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