From 82c9bcd55913621cf30d3ef2301890ce8eadbf84 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 5 Oct 2026 18:10:15 -0400 Subject: [PATCH 1/3] Distinguish disjoint JLArray byte ranges in mightalias --- lib/JLArrays/src/array.jl | 14 ++++++++++ test/jlarrays/aliasing.jl | 57 +++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 3 files changed, 72 insertions(+) create mode 100644 test/jlarrays/aliasing.jl diff --git a/lib/JLArrays/src/array.jl b/lib/JLArrays/src/array.jl index 530c7085..71e6948a 100644 --- a/lib/JLArrays/src/array.jl +++ b/lib/JLArrays/src/array.jl @@ -44,6 +44,20 @@ mutable struct JLArray{T, N} <: AbstractGPUArray{T, N} end GPUArrays.storage(a::JLArray) = a.data + +## alias detection + +# Keep the shared allocation's dataids for conservative alias detection involving +# wrappers, but distinguish disjoint byte ranges for dense arrays, as CUDA does. +function Base.mightalias(A::JLArray, B::JLArray) + nA, nB = sizeof(A), sizeof(B) + (iszero(nA) || iszero(nB)) && return false + GC.@preserve A B begin + pA, pB = UInt(pointer(A)), UInt(pointer(B)) + return pA <= pB < pA + nA || pB <= pA < pB + nB + end +end + GPUArrays.dense_array_type(a::JLArray{T, N}) where {T, N} = JLArray{T, N} 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} diff --git a/test/jlarrays/aliasing.jl b/test/jlarrays/aliasing.jl new file mode 100644 index 00000000..4a7941dd --- /dev/null +++ b/test/jlarrays/aliasing.jl @@ -0,0 +1,57 @@ +using Test, JLArrays, GPUArrays + +@testset "JLArray alias detection" begin + function check_alias(A, B, expected) + @test Base.mightalias(A, B) == expected + @test Base.mightalias(B, A) == expected + end + + buffer = JLArray{UInt8}(undef, 64) + left = view(buffer, 1:16) + right = view(buffer, 17:32) + overlap = view(buffer, 9:24) + inside = view(buffer, 5:8) + + # Contiguous views are dense JLArrays sharing one allocation. + @test left isa JLArray + @test right isa JLArray + @test Base.dataids(left) == Base.dataids(right) == Base.dataids(buffer) + check_alias(left, left, true) + check_alias(left, right, false) + check_alias(left, overlap, true) + check_alias(right, overlap, true) + check_alias(left, inside, true) + check_alias(right, inside, false) + check_alias(buffer, left, true) + check_alias(left, copy(left), false) + check_alias(left, reshape(left, 4, 4), true) + check_alias(right, reshape(left, 4, 4), false) + + # Compare bytes rather than element offsets or counts after reinterpretation. + words = reinterpret(UInt32, left) + check_alias(words, left, true) + check_alias(words, right, false) + check_alias(words, overlap, true) + check_alias(words, inside, true) + check_alias(words, view(buffer, 16:16), true) + + # Empty ranges can start inside a nonempty array, or at an allocation boundary. + for empty in (view(buffer, 9:8), view(buffer, 33:32), JLArray{UInt8}(undef, 0)) + check_alias(empty, buffer, false) + check_alias(empty, left, false) + check_alias(empty, empty, false) + end + check_alias(GPUArrays.derive(Nothing, buffer, (8,), 0), buffer, false) + + # Noncontiguous wrappers retain the conservative shared-storage detection. + strided = view(reshape(buffer, 8, 8), 1:2:8, :) + @test strided isa SubArray + check_alias(buffer, strided, true) + check_alias(left, strided, true) + check_alias(copy(buffer), strided, false) + + # The broadcast regression that motivated shared-storage dataids (#716). + A = JLArray(-ones(Float32, 3, 3)) + A .*= sign.(view(A, 1:4:9)) + @test Array(A) == ones(Float32, 3, 3) +end diff --git a/test/runtests.jl b/test/runtests.jl index 5c808d99..6bd912e9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,6 +21,7 @@ end args = parse_args(ARGS) testsuite = Dict{String, Expr}() +testsuite["JLArray/aliasing"] = :(include($(joinpath(@__DIR__, "jlarrays", "aliasing.jl")))) for AT in (:JLArray, :Array), name in keys(TestSuite.tests) testsuite["$(AT)/$name"] = :(TestSuite.tests[$name]($AT)) end From d979efbfb5ab513a729c19fab799c93c9186f16a Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 5 Oct 2026 18:31:46 -0400 Subject: [PATCH 2/3] Trim JLArray alias tests and cite CUDA implementation --- lib/JLArrays/src/array.jl | 5 +-- test/jlarrays/aliasing.jl | 64 +++++++++------------------------------ 2 files changed, 17 insertions(+), 52 deletions(-) diff --git a/lib/JLArrays/src/array.jl b/lib/JLArrays/src/array.jl index 71e6948a..11b1b3ab 100644 --- a/lib/JLArrays/src/array.jl +++ b/lib/JLArrays/src/array.jl @@ -47,8 +47,9 @@ GPUArrays.storage(a::JLArray) = a.data ## alias detection -# Keep the shared allocation's dataids for conservative alias detection involving -# wrappers, but distinguish disjoint byte ranges for dense arrays, as CUDA does. +# Byte-range overlap follows CUDA's Base.mightalias(::CuArray, ::CuArray): +# https://github.com/JuliaGPU/CUDA.jl/blob/main/CUDACore/src/array.jl#L113-L116 +# Keep shared-allocation dataids for conservative alias detection involving wrappers. function Base.mightalias(A::JLArray, B::JLArray) nA, nB = sizeof(A), sizeof(B) (iszero(nA) || iszero(nB)) && return false diff --git a/test/jlarrays/aliasing.jl b/test/jlarrays/aliasing.jl index 4a7941dd..7748e026 100644 --- a/test/jlarrays/aliasing.jl +++ b/test/jlarrays/aliasing.jl @@ -1,57 +1,21 @@ using Test, JLArrays, GPUArrays @testset "JLArray alias detection" begin - function check_alias(A, B, expected) - @test Base.mightalias(A, B) == expected - @test Base.mightalias(B, A) == expected - end - - buffer = JLArray{UInt8}(undef, 64) + buffer = JLArray{UInt8}(undef, 32) left = view(buffer, 1:16) right = view(buffer, 17:32) overlap = view(buffer, 9:24) - inside = view(buffer, 5:8) - - # Contiguous views are dense JLArrays sharing one allocation. - @test left isa JLArray - @test right isa JLArray - @test Base.dataids(left) == Base.dataids(right) == Base.dataids(buffer) - check_alias(left, left, true) - check_alias(left, right, false) - check_alias(left, overlap, true) - check_alias(right, overlap, true) - check_alias(left, inside, true) - check_alias(right, inside, false) - check_alias(buffer, left, true) - check_alias(left, copy(left), false) - check_alias(left, reshape(left, 4, 4), true) - check_alias(right, reshape(left, 4, 4), false) - - # Compare bytes rather than element offsets or counts after reinterpretation. - words = reinterpret(UInt32, left) - check_alias(words, left, true) - check_alias(words, right, false) - check_alias(words, overlap, true) - check_alias(words, inside, true) - check_alias(words, view(buffer, 16:16), true) - - # Empty ranges can start inside a nonempty array, or at an allocation boundary. - for empty in (view(buffer, 9:8), view(buffer, 33:32), JLArray{UInt8}(undef, 0)) - check_alias(empty, buffer, false) - check_alias(empty, left, false) - check_alias(empty, empty, false) - end - check_alias(GPUArrays.derive(Nothing, buffer, (8,), 0), buffer, false) - - # Noncontiguous wrappers retain the conservative shared-storage detection. - strided = view(reshape(buffer, 8, 8), 1:2:8, :) - @test strided isa SubArray - check_alias(buffer, strided, true) - check_alias(left, strided, true) - check_alias(copy(buffer), strided, false) - - # The broadcast regression that motivated shared-storage dataids (#716). - A = JLArray(-ones(Float32, 3, 3)) - A .*= sign.(view(A, 1:4:9)) - @test Array(A) == ones(Float32, 3, 3) + @test Base.dataids(left) == Base.dataids(right) + @test Base.mightalias(left, left) + @test !Base.mightalias(left, right) + @test !Base.mightalias(right, left) + @test Base.mightalias(left, overlap) + @test Base.mightalias(overlap, left) + @test !Base.mightalias(left, copy(left)) + # The last byte still overlaps after reinterpreting to a larger element type. + @test Base.mightalias(reinterpret(UInt32, left), view(buffer, 16:16)) + empty = view(buffer, 9:8) + @test !Base.mightalias(empty, left) + @test !Base.mightalias(left, empty) + @test !Base.mightalias(GPUArrays.derive(Nothing, buffer, (8,), 0), buffer) end From c0f9f21ef2699c500d592c3537982c1596946c2a Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 5 Oct 2026 18:34:31 -0400 Subject: [PATCH 3/3] Share aliasing regressions across GPU array backends --- test/runtests.jl | 1 - test/testsuite.jl | 1 + test/{jlarrays => testsuite}/aliasing.jl | 13 +++++-------- 3 files changed, 6 insertions(+), 9 deletions(-) rename test/{jlarrays => testsuite}/aliasing.jl (53%) diff --git a/test/runtests.jl b/test/runtests.jl index 6bd912e9..5c808d99 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,7 +21,6 @@ end args = parse_args(ARGS) testsuite = Dict{String, Expr}() -testsuite["JLArray/aliasing"] = :(include($(joinpath(@__DIR__, "jlarrays", "aliasing.jl")))) for AT in (:JLArray, :Array), name in keys(TestSuite.tests) testsuite["$(AT)/$name"] = :(TestSuite.tests[$name]($AT)) end diff --git a/test/testsuite.jl b/test/testsuite.jl index 6f8b49eb..86ccabff 100644 --- a/test/testsuite.jl +++ b/test/testsuite.jl @@ -125,6 +125,7 @@ end include("testsuite/construction.jl") include("testsuite/indexing.jl") include("testsuite/base.jl") +include("testsuite/aliasing.jl") include("testsuite/vector.jl") include("testsuite/reductions.jl") include("testsuite/sorting.jl") diff --git a/test/jlarrays/aliasing.jl b/test/testsuite/aliasing.jl similarity index 53% rename from test/jlarrays/aliasing.jl rename to test/testsuite/aliasing.jl index 7748e026..7b605441 100644 --- a/test/jlarrays/aliasing.jl +++ b/test/testsuite/aliasing.jl @@ -1,21 +1,18 @@ -using Test, JLArrays, GPUArrays - -@testset "JLArray alias detection" begin - buffer = JLArray{UInt8}(undef, 32) +@testsuite "aliasing" (AT, eltypes)->begin + T = Float32 in eltypes ? Float32 : first(eltypes) + buffer = AT{T}(undef, 32) left = view(buffer, 1:16) right = view(buffer, 17:32) overlap = view(buffer, 9:24) - @test Base.dataids(left) == Base.dataids(right) @test Base.mightalias(left, left) @test !Base.mightalias(left, right) @test !Base.mightalias(right, left) @test Base.mightalias(left, overlap) @test Base.mightalias(overlap, left) @test !Base.mightalias(left, copy(left)) - # The last byte still overlaps after reinterpreting to a larger element type. - @test Base.mightalias(reinterpret(UInt32, left), view(buffer, 16:16)) + # The last element still overlaps after reinterpreting to a different element size. + @test Base.mightalias(reinterpret(UInt8, left), view(buffer, 16:16)) empty = view(buffer, 9:8) @test !Base.mightalias(empty, left) @test !Base.mightalias(left, empty) - @test !Base.mightalias(GPUArrays.derive(Nothing, buffer, (8,), 0), buffer) end