From ff1519294c2d9c575975826b91f0b4e7817a1e85 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Fri, 23 Oct 2026 11:44:05 +0200 Subject: [PATCH 1/2] Fix device-side `reinterpret` of a oneDeviceArray `reinterpret` on a oneDeviceArray called `_reinterpret_exception`, which oneAPI.jl doesn't define, so any kernel using it failed to compile with an InvalidIRError. Call the GPUArrays function, as CUDA.jl and Metal.jl do. --- src/device/array.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/device/array.jl b/src/device/array.jl index b948da7e..148544f4 100644 --- a/src/device/array.jl +++ b/src/device/array.jl @@ -287,7 +287,7 @@ Base.show(io::IO, mime::MIME"text/plain", a::oneDeviceArray) = show(io, a) end function Base.reinterpret(::Type{T}, a::oneDeviceArray{S,N,A}) where {T,S,N,A} - err = _reinterpret_exception(T, a) + err = GPUArrays._reinterpret_exception(T, a) err === nothing || throw(err) if sizeof(T) == sizeof(S) # fast case From de005dca306a9913519212f2ba0ba3adfeead1eb Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Fri, 23 Oct 2026 11:44:13 +0200 Subject: [PATCH 2/2] Check the alignment of arrays passed to kernels Since oneArray offsets are stored in bytes (#570), `reinterpret` of a view can yield an array that isn't aligned for its element type, e.g. `reinterpret(UInt64, view(a::oneVector{UInt8}, 2:17))`. Copies handle that fine, but kernels access elements assuming the element type's alignment, and Intel GPUs then silently access the wrong memory: on an Arc A770, loads of such an array read from the address rounded down to 8 bytes, and stores were lost. Check the pointer when converting a oneArray to a device array and throw an ArgumentError instead. Device-side `reinterpret` to a type with a stronger alignment requirement checks the pointer too. x-ref JuliaGPU/CUDA.jl#3451 --- src/array.jl | 16 +++++++++++++++- src/compiler/execution.jl | 1 + src/device/array.jl | 16 +++++++++++----- test/array.jl | 9 +++++++++ test/execution.jl | 23 +++++++++++++++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/array.jl b/src/array.jl index f5b06381..57eb781c 100644 --- a/src/array.jl +++ b/src/array.jl @@ -419,10 +419,24 @@ end ## interop with GPU arrays function Base.unsafe_convert(::Type{oneDeviceArray{T,N,AS.CrossWorkgroup}}, a::oneArray{T,N}) where {T,N} - oneDeviceArray{T,N,AS.CrossWorkgroup}(size(a), reinterpret(LLVMPtr{T,AS.CrossWorkgroup}, pointer(a)), + ptr = pointer(a) + check_alignment(a, ptr) + oneDeviceArray{T,N,AS.CrossWorkgroup}(size(a), reinterpret(LLVMPtr{T,AS.CrossWorkgroup}, ptr), a.maxsize - a.offset) end +# a `oneArray` can point at memory that is not aligned for its element type, e.g., after +# `reinterpret` of a view. that is fine for copies, but element accesses in kernels assume +# the alignment of the element type (see `alignment`). +function check_alignment(a::oneArray{T}, ptr) where {T} + align = alignment(T) + if UInt(ptr) % align != 0 && !isempty(a) + alignment_error(T, ptr, align) + end +end +@noinline alignment_error(T, ptr, align) = + throw(ArgumentError("cannot use a $T array at $ptr in a kernel, which requires $align-byte alignment. Such arrays result from `reinterpret`ing a view that doesn't start at a multiple of the alignment; use `copy` to get an aligned array.")) + ## memory copying diff --git a/src/compiler/execution.jl b/src/compiler/execution.jl index 50bb77c0..f39bd4b7 100644 --- a/src/compiler/execution.jl +++ b/src/compiler/execution.jl @@ -167,6 +167,7 @@ Adapt.adapt_storage(to::KernelAdaptor, p::ZePtr{T}) where {T} = # convert oneAPI host arrays to device arrays function Adapt.adapt_storage(to::KernelAdaptor, xs::oneArray{T,N}) where {T,N} to.refs === nothing || push!(to.refs, xs.data) + check_alignment(xs, device_pointer(xs)) ptr = reinterpret(LLVMPtr{T,AS.CrossWorkgroup}, device_pointer(xs)) oneDeviceArray{T,N,AS.CrossWorkgroup}(size(xs), ptr, xs.maxsize - xs.offset) end diff --git a/src/device/array.jl b/src/device/array.jl index 148544f4..5d6d4afa 100644 --- a/src/device/array.jl +++ b/src/device/array.jl @@ -105,11 +105,10 @@ Base.unsafe_convert(::Type{LLVMPtr{T,A}}, x::oneDeviceArray{T,<:Any,A}) where {T ## indexing intrinsics -# TODO: how are allocations aligned by the level zero API? keep track of this -# because it enables optimizations like Load Store Vectorization -# (cfr. shared memory and its wider-than-datatype alignment) - -@generated function alignment(::oneDeviceArray{T}) where {T} +# the alignment that accesses to elements of type `T` assume. pointers that do not come from +# an allocation (`reinterpret` of a view) are checked when converting to a device array, see +# `check_alignment`. +@generated function alignment(::Type{T}) where {T} if Base.isbitsunion(T) _, sz, al = Base.uniontype_layout(T) al @@ -117,6 +116,7 @@ Base.unsafe_convert(::Type{LLVMPtr{T,A}}, x::oneDeviceArray{T,<:Any,A}) where {T Base.datatype_alignment(T) end end +alignment(::oneDeviceArray{T}) where {T} = alignment(T) @device_function @inline function arrayref(A::oneDeviceArray{T}, index::Integer) where {T} # simplified bounds check to avoid the OneTo construction, which calls `max` @@ -290,6 +290,12 @@ function Base.reinterpret(::Type{T}, a::oneDeviceArray{S,N,A}) where {T,S,N,A} err = GPUArrays._reinterpret_exception(T, a) err === nothing || throw(err) + # `T` may require a stronger alignment than `a` was checked for. this is not a bounds + # check: the alignment that element accesses assume depends on it. + if alignment(T) > alignment(S) && !isempty(a) && UInt(a.ptr) % alignment(T) != 0 + @gputhrow "ArgumentError" "cannot reinterpret an array whose pointer is not aligned for the new element type" + end + if sizeof(T) == sizeof(S) # fast case return oneDeviceArray{T,N,A}(size(a), reinterpret(LLVMPtr{T,A}, a.ptr), a.maxsize) end diff --git a/test/array.jl b/test/array.jl index b4c9aaa2..4db812dc 100644 --- a/test/array.jl +++ b/test/array.jl @@ -119,6 +119,15 @@ end v = view(a, 2:7) # offset of 1 Int32 = 4 bytes r = reinterpret(Int64, v) # Int64 = 8 bytes; 4 is not a multiple of 8 @test Array(r) == reinterpret(Int64, @view Array(a)[2:7]) + + # kernels require aligned elements + @test_throws ArgumentError r .+ 1 + @test Array(copy(r) .+ 1) == reinterpret(Int64, @view Array(a)[2:7]) .+ 1 + @test_throws ArgumentError Base.unsafe_convert(oneDeviceVector{Int64,oneAPI.AS.CrossWorkgroup}, r) + # empty arrays have no elements to access + e = reinterpret(Int64, view(a, 2:1)) + @test isempty(oneAPI.kernel_convert(e)) + @test isempty(Base.unsafe_convert(oneDeviceVector{Int64,oneAPI.AS.CrossWorkgroup}, e)) end @testset "aliasing" begin diff --git a/test/execution.jl b/test/execution.jl index a61be703..af6b627b 100644 --- a/test/execution.jl +++ b/test/execution.jl @@ -1648,3 +1648,26 @@ end @test Array(out) == fill(a[1], n) end end + +@testset "device-side reinterpret to a stronger alignment" begin + function kernel(y, a, offset) + ptr = reinterpret(Core.LLVMPtr{Int32,AS.CrossWorkgroup}, pointer(a)) + offset + x = oneDeviceArray{Int32,1,AS.CrossWorkgroup}((2,), ptr) + b = reinterpret(Int64, x) + @inbounds y[1] = b[1] + return + end + a = oneArray([1, 2]) + y = oneAPI.zeros(Int, 1) + @oneapi kernel(y, a, 8) + @test kernel_exception() === nothing + @test Array(y)[] == 2 + # the work-item that throws exits without storing + y = oneAPI.zeros(Int, 1) + @oneapi debug_level=1 kernel(y, a, 4) + err = kernel_exception() + @test err isa KernelException + @test err.name == "ArgumentError" + @test occursin("not aligned", err.reason) + @test Array(y)[] == 0 +end