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 b948da7e..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` @@ -287,9 +287,15 @@ 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) + # `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