From 9c0ba424c086a91c6e186a3e1c8d5374df21cbb5 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Sun, 5 Jul 2026 21:47:15 +0200 Subject: [PATCH 1/2] Add Atomix and UnsafeAtomics atomics tests Adds an Atomics testsuite exercising @atomic/@atomicswap/@atomicreplace (via Atomix) in kernels on all backends, plus UnsafeAtomics pointer-based atomics on the CPU backend. UnsafeAtomics becomes a direct test dependency. Co-Authored-By: Claude Fable 5 --- test/Project.toml | 1 + test/atomics.jl | 176 ++++++++++++++++++++++++++++++++++++++++++++++ test/testsuite.jl | 5 ++ 3 files changed, 182 insertions(+) create mode 100644 test/atomics.jl diff --git a/test/Project.toml b/test/Project.toml index 84411df74..53dee1575 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -9,6 +9,7 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +UnsafeAtomics = "013be700-e6cd-48c3-b4a1-df204f14c38f" pocl_standalone_jll = "54f56a70-6062-5590-a942-1226658f6c83" [sources] diff --git a/test/atomics.jl b/test/atomics.jl new file mode 100644 index 000000000..74aac36b9 --- /dev/null +++ b/test/atomics.jl @@ -0,0 +1,176 @@ +using KernelAbstractions +using KernelAbstractions: @atomic, @atomicswap, @atomicreplace +using Test + +import UnsafeAtomics + +# Atomix based kernels + +@kernel function atomix_add!(hist) + i = @index(Global, Linear) + j = (i - 1) % length(hist) + 1 + @inbounds @atomic hist[j] += one(eltype(hist)) +end + +@kernel function atomix_max!(A) + i = @index(Global, Linear) + @inbounds @atomic max(A[1], eltype(A)(i)) +end + +@kernel function atomix_min!(A) + i = @index(Global, Linear) + @inbounds @atomic min(A[1], eltype(A)(i)) +end + +@kernel function atomix_load_store!(A, B) + i = @index(Global, Linear) + @inbounds begin + v = @atomic B[i] + @atomic A[i] = v + end +end + +@kernel function atomix_swap!(A, B) + i = @index(Global, Linear) + @inbounds B[i] = @atomicswap A[i] = eltype(A)(i) +end + +@kernel function atomix_replace!(A, success) + i = @index(Global, Linear) + T = eltype(A) + @inbounds begin + # CAS that must succeed, followed by one that must fail + (_, ok1) = @atomicreplace A[i] zero(T) => T(i) + (_, ok2) = @atomicreplace A[i] zero(T) => T(-1) + success[i] = ok1 & !ok2 + end +end + +# UnsafeAtomics based kernels (CPU only, operating on raw pointers) + +@kernel function unsafe_atomics_add!(hist) + i = @index(Global, Linear) + j = (i - 1) % length(hist) + 1 + UnsafeAtomics.add!(pointer(hist, j), one(eltype(hist))) +end + +@kernel function unsafe_atomics_minmax!(A) + i = @index(Global, Linear) + T = eltype(A) + UnsafeAtomics.max!(pointer(A, 1), T(i)) + UnsafeAtomics.min!(pointer(A, 2), T(i)) +end + +@kernel function unsafe_atomics_ops!(A, B) + i = @index(Global, Linear) + T = eltype(A) + p = pointer(A, i) + UnsafeAtomics.store!(p, T(i)) + old, new = UnsafeAtomics.modify!(p, +, T(1)) + (; success) = UnsafeAtomics.cas!(p, new, T(2) * new) + if success + old = UnsafeAtomics.xchg!(p, old) + end + B[i] = UnsafeAtomics.load(p) +end + +@kernel function unsafe_atomics_ordering!(hist) + i = @index(Global, Linear) + j = (i - 1) % length(hist) + 1 + UnsafeAtomics.add!(pointer(hist, j), one(eltype(hist)), UnsafeAtomics.seq_cst) +end + +function atomics_testsuite(backend, ArrayT) + if !KernelAbstractions.supports_atomics(backend()) + @test_skip "Backend does not support atomics" + return + end + + @testset "Atomix" begin + # Float32 is excluded since atomic float add requires the SPIR-V + # extension SPV_EXT_shader_atomic_float_add, unavailable with PoCL. + @testset "atomic add ($T)" for T in (Int32, UInt32) + hist = ArrayT(zeros(T, 32)) + atomix_add!(backend())(hist, ndrange = 1024) + synchronize(backend()) + @test all(Array(hist) .== T(1024 ÷ 32)) + end + + @testset "atomic max/min" begin + A = ArrayT(zeros(Int32, 1)) + atomix_max!(backend())(A, ndrange = 1024) + synchronize(backend()) + @test Array(A)[1] == 1024 + + A = ArrayT(fill(typemax(Int32), 1)) + atomix_min!(backend())(A, ndrange = 1024) + synchronize(backend()) + @test Array(A)[1] == 1 + end + + @testset "atomic load/store" begin + A = ArrayT(zeros(Int32, 256)) + B = ArrayT{Int32}(collect(Int32, 1:256)) + atomix_load_store!(backend())(A, B, ndrange = 256) + synchronize(backend()) + @test Array(A) == 1:256 + end + + @testset "atomicswap" begin + A = ArrayT(fill(Int32(-1), 256)) + B = ArrayT(zeros(Int32, 256)) + atomix_swap!(backend())(A, B, ndrange = 256) + synchronize(backend()) + @test Array(A) == 1:256 + @test all(Array(B) .== -1) + end + + @testset "atomicreplace" begin + A = ArrayT(zeros(Int32, 256)) + success = ArrayT(zeros(Bool, 256)) + atomix_replace!(backend())(A, success, ndrange = 256) + synchronize(backend()) + @test Array(A) == 1:256 + @test all(Array(success)) + end + end + + @testset "UnsafeAtomics" begin + if !(backend() isa CPU) + @test_skip "UnsafeAtomics tests only run on the CPU backend" + return + end + + @testset "atomic add ($T)" for T in (Int32, UInt32) + hist = ArrayT(zeros(T, 32)) + unsafe_atomics_add!(backend())(hist, ndrange = 1024) + synchronize(backend()) + @test all(Array(hist) .== T(1024 ÷ 32)) + end + + @testset "atomic max/min" begin + A = ArrayT(Int32[0, typemax(Int32)]) + unsafe_atomics_minmax!(backend())(A, ndrange = 1024) + synchronize(backend()) + @test Array(A) == [1024, 1] + end + + @testset "store/modify/cas/xchg/load" begin + A = ArrayT(zeros(Int32, 256)) + B = ArrayT(zeros(Int32, 256)) + unsafe_atomics_ops!(backend())(A, B, ndrange = 256) + synchronize(backend()) + # store i, modify + 1, cas to 2(i + 1), xchg back to i + @test Array(A) == 1:256 + @test Array(B) == 1:256 + end + + @testset "explicit ordering" begin + hist = ArrayT(zeros(Int32, 32)) + unsafe_atomics_ordering!(backend())(hist, ndrange = 1024) + synchronize(backend()) + @test all(Array(hist) .== 32) + end + end + return +end diff --git a/test/testsuite.jl b/test/testsuite.jl index 69fc89f19..b286fb931 100644 --- a/test/testsuite.jl +++ b/test/testsuite.jl @@ -27,6 +27,7 @@ end include("test.jl") +include("atomics.jl") include("interface.jl") include("localmem.jl") include("private.jl") @@ -46,6 +47,10 @@ function testsuite(backend, backend_str, backend_mod, AT, DAT; skip_tests = Set{ unittest_testsuite(backend, backend_str, backend_mod, DAT; skip_tests) end + @conditional_testset "Atomics" skip_tests begin + atomics_testsuite(backend, AT) + end + @conditional_testset "SpecialFunctions" skip_tests begin specialfunctions_testsuite(backend) end From 291db1516f3f9033dd086f07a80802ada941da2f Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Thu, 9 Jul 2026 17:09:17 +0200 Subject: [PATCH 2/2] Address review: run UnsafeAtomics tests on all backends; test orderings, fences, and syncscopes Co-Authored-By: Claude Fable 5 --- test/atomics.jl | 103 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/test/atomics.jl b/test/atomics.jl index 74aac36b9..8d536e633 100644 --- a/test/atomics.jl +++ b/test/atomics.jl @@ -46,7 +46,20 @@ end end end -# UnsafeAtomics based kernels (CPU only, operating on raw pointers) +@kernel function atomix_ordered!(A, B) + i = @index(Global, Linear) + T = eltype(A) + @inbounds begin + @atomic :release A[i] = T(i) + v = @atomic :acquire A[i] + @atomic :monotonic A[i] += one(T) + @atomic :acquire_release A[i] += one(T) + @atomic :sequentially_consistent A[i] += one(T) + B[i] = @atomicswap :acquire_release A[i] = v + T(3) + end +end + +# UnsafeAtomics based kernels, operating on raw pointers @kernel function unsafe_atomics_add!(hist) i = @index(Global, Linear) @@ -74,10 +87,45 @@ end B[i] = UnsafeAtomics.load(p) end -@kernel function unsafe_atomics_ordering!(hist) +@kernel function unsafe_atomics_add_ordered!(hist, ordering) + i = @index(Global, Linear) + j = (i - 1) % length(hist) + 1 + UnsafeAtomics.add!(pointer(hist, j), one(eltype(hist)), ordering) +end + +@kernel function unsafe_atomics_load_store_ordered!(A, B) i = @index(Global, Linear) + v = UnsafeAtomics.load(pointer(B, i), UnsafeAtomics.acquire) + UnsafeAtomics.store!(pointer(A, i), v, UnsafeAtomics.release) +end + +# Non-blocking message passing: workitem 1 publishes data guarded by a flag +# with release/acquire fences; observers must see the data if they see the flag. +@kernel function unsafe_atomics_fence!(data, flag, observed) + i = @index(Global, Linear) + T = eltype(data) + @inbounds if i == 1 + data[1] = T(42) + UnsafeAtomics.fence(UnsafeAtomics.release) + UnsafeAtomics.store!(pointer(flag, 1), one(T), UnsafeAtomics.monotonic) + else + f = UnsafeAtomics.load(pointer(flag, 1), UnsafeAtomics.monotonic) + UnsafeAtomics.fence(UnsafeAtomics.acquire) + observed[i] = f == one(T) ? data[1] : T(-1) + end +end + +@kernel function unsafe_atomics_syncscope!(A, hist) + i = @index(Global, Linear) + T = eltype(A) + # contended, system scope j = (i - 1) % length(hist) + 1 - UnsafeAtomics.add!(pointer(hist, j), one(eltype(hist)), UnsafeAtomics.seq_cst) + UnsafeAtomics.add!(pointer(hist, j), one(T), UnsafeAtomics.seq_cst, UnsafeAtomics.none) + # uncontended, singlethread scope + p = pointer(A, i) + UnsafeAtomics.store!(p, T(i), UnsafeAtomics.monotonic, UnsafeAtomics.singlethread) + UnsafeAtomics.fence(UnsafeAtomics.seq_cst, UnsafeAtomics.singlethread) + UnsafeAtomics.add!(p, one(T), UnsafeAtomics.monotonic, UnsafeAtomics.singlethread) end function atomics_testsuite(backend, ArrayT) @@ -89,6 +137,7 @@ function atomics_testsuite(backend, ArrayT) @testset "Atomix" begin # Float32 is excluded since atomic float add requires the SPIR-V # extension SPV_EXT_shader_atomic_float_add, unavailable with PoCL. + # TODO: use CAS-based fallbacks, cf. JuliaGPU/GPUCompiler.jl#652 @testset "atomic add ($T)" for T in (Int32, UInt32) hist = ArrayT(zeros(T, 32)) atomix_add!(backend())(hist, ndrange = 1024) @@ -133,14 +182,18 @@ function atomics_testsuite(backend, ArrayT) @test Array(A) == 1:256 @test all(Array(success)) end - end - @testset "UnsafeAtomics" begin - if !(backend() isa CPU) - @test_skip "UnsafeAtomics tests only run on the CPU backend" - return + @testset "orderings" begin + A = ArrayT(zeros(Int32, 256)) + B = ArrayT(zeros(Int32, 256)) + atomix_ordered!(backend())(A, B, ndrange = 256) + synchronize(backend()) + @test Array(A) == (1:256) .+ 3 + @test Array(B) == (1:256) .+ 3 end + end + @testset "UnsafeAtomics" begin @testset "atomic add ($T)" for T in (Int32, UInt32) hist = ArrayT(zeros(T, 32)) unsafe_atomics_add!(backend())(hist, ndrange = 1024) @@ -165,10 +218,40 @@ function atomics_testsuite(backend, ArrayT) @test Array(B) == 1:256 end - @testset "explicit ordering" begin + @testset "ordering $ordering" for ordering in ( + UnsafeAtomics.monotonic, UnsafeAtomics.acquire, UnsafeAtomics.release, + UnsafeAtomics.acq_rel, UnsafeAtomics.seq_cst, + ) + hist = ArrayT(zeros(Int32, 32)) + unsafe_atomics_add_ordered!(backend())(hist, ordering, ndrange = 1024) + synchronize(backend()) + @test all(Array(hist) .== 32) + end + + @testset "ordered load/store" begin + A = ArrayT(zeros(Int32, 256)) + B = ArrayT(collect(Int32, 1:256)) + unsafe_atomics_load_store_ordered!(backend())(A, B, ndrange = 256) + synchronize(backend()) + @test Array(A) == 1:256 + end + + @testset "fences" begin + data = ArrayT(zeros(Int32, 1)) + flag = ArrayT(zeros(Int32, 1)) + observed = ArrayT(zeros(Int32, 1024)) + unsafe_atomics_fence!(backend())(data, flag, observed, ndrange = 1024) + synchronize(backend()) + # observers either did not see the flag (-1) or must see the data + @test all(x -> x == -1 || x == 42, Array(observed)[2:end]) + end + + @testset "syncscopes" begin + A = ArrayT(zeros(Int32, 1024)) hist = ArrayT(zeros(Int32, 32)) - unsafe_atomics_ordering!(backend())(hist, ndrange = 1024) + unsafe_atomics_syncscope!(backend())(A, hist, ndrange = 1024) synchronize(backend()) + @test Array(A) == (1:1024) .+ 1 @test all(Array(hist) .== 32) end end