Repository navigation
Add StaticDimsArray: GPU arrays with a mix of static and dynamic dimensions #814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
vchuravy
wants to merge
2
commits into
main
Choose a base branch
from
vc/static-dims
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| module StaticArrayInterfaceExt | ||
|
|
||
| # Static sizes of arrays with a mix of static and dynamic dimensions, for packages that build | ||
| # on StaticArrayInterface.jl (e.g. StrideArraysCore.jl and LoopVectorization.jl). | ||
|
|
||
| using GPUArrays: GPUArrays, StaticDimsArray, StaticDimsDeviceArray | ||
| using StaticArrayInterface: StaticArrayInterface | ||
|
|
||
| const AnyStaticDimsArray = Union{StaticDimsArray, StaticDimsDeviceArray} | ||
|
|
||
| StaticArrayInterface.static_size(a::AnyStaticDimsArray) = GPUArrays.static_size(a) | ||
| StaticArrayInterface.known_size(::Type{A}) where {A <: AnyStaticDimsArray} = | ||
| GPUArrays.known_size(A) | ||
|
|
||
| end # module StaticArrayInterfaceExt |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # arrays with a mix of static and dynamic dimensions | ||
|
|
||
| export StaticDimsDeviceArray | ||
|
|
||
| # Each dimension is either a `StaticInt`, a compile-time constant that takes no space, or an | ||
| # `Int`. E.g. `Tuple{StaticInt{4}, Int}` for an array of size `(4, n)`. | ||
| const MixedDims{N} = NTuple{N, Union{Int, StaticInt}} | ||
|
|
||
| # Column-major linear index of the (in-bounds) indices `I`, as a sum of strided terms: | ||
| # `1 + (i1 - 1) + d1 * (i2 - 1) + (d1 * d2) * (i3 - 1)` for the dimensions `(d1, d2, d3)`. | ||
| # The strides are products of `StaticInt`s as long as the dimensions are, i.e. constants. | ||
| # (Unlike the Horner form `i1 + d1 * ((i2 - 1) + d2 * (i3 - 1))`, the terms of indices that | ||
| # don't change in a loop can be hoisted out of it when the dimensions are dynamic.) | ||
| # (Unrolled, as recursing over the dimensions with strides of growing types would not infer.) | ||
| @inline @generated function static_linear_index(dims::Tuple, I::NTuple{N, Int}) where {N} | ||
| ex = quote | ||
| stride_1 = StaticInt(1) | ||
| end | ||
| for k in 2:N | ||
| push!(ex.args, :($(Symbol(:stride_, k)) = $(Symbol(:stride_, k - 1)) * dims[$(k - 1)])) | ||
| end | ||
| terms = [:(dynamic($(Symbol(:stride_, k))) * (I[$k] - 1)) for k in 1:N] | ||
| push!(ex.args, :(+(1, $(terms...)))) | ||
| return ex | ||
| end | ||
|
|
||
| """ | ||
| StaticDimsDeviceArray{T,N,D,A} <: AbstractDeviceArray{T,N} | ||
|
|
||
| The device-side counterpart of [`StaticDimsArray`](@ref): a vector `parent::A` (typically a | ||
| device vector of the back-end) viewed as an `N`-dimensional array with the dimensions | ||
| `dims::D`, a tuple of `Int`s and `StaticInt`s. If the parent is `isbits`, so is the array. | ||
| """ | ||
| struct StaticDimsDeviceArray{T, N, D <: MixedDims{N}, A <: AbstractVector{T}} <: AbstractDeviceArray{T, N} | ||
| parent::A | ||
| dims::D | ||
| end | ||
|
|
||
| Base.parent(a::StaticDimsDeviceArray) = a.parent | ||
|
|
||
| # adapt the parent, e.g. so that KernelAbstractions' `@Const` reaches the back-end's read-only | ||
| # loads (`ld.global.nc` with CUDA) | ||
| Adapt.adapt_structure(to, a::StaticDimsDeviceArray) = | ||
| wrap_static_dims(adapt(to, parent(a)), a.dims) | ||
|
|
||
| Base.size(a::StaticDimsDeviceArray) = map(dynamic, a.dims) | ||
| Base.elsize(::Type{<:StaticDimsDeviceArray{T, N, D, A}}) where {T, N, D, A} = Base.elsize(A) | ||
|
|
||
| @inline function Base.getindex(a::StaticDimsDeviceArray, i::Int) | ||
| @boundscheck checkbounds(a, i) | ||
| @inbounds a.parent[i] | ||
| end | ||
| @inline function Base.getindex(a::StaticDimsDeviceArray{T, N}, I::Vararg{Int, N}) where {T, N} | ||
| @boundscheck checkbounds(a, I...) | ||
| @inbounds a.parent[static_linear_index(a.dims, I)] | ||
| end | ||
|
|
||
| @inline function Base.setindex!(a::StaticDimsDeviceArray, x, i::Int) | ||
| @boundscheck checkbounds(a, i) | ||
| @inbounds a.parent[i] = x | ||
| return a | ||
| end | ||
| @inline function Base.setindex!(a::StaticDimsDeviceArray{T, N}, x, I::Vararg{Int, N}) where {T, N} | ||
| @boundscheck checkbounds(a, I...) | ||
| @inbounds a.parent[static_linear_index(a.dims, I)] = x | ||
| return a | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # arrays with a mix of static and dynamic dimensions | ||
|
|
||
| export StaticDimsArray | ||
|
|
||
| """ | ||
| StaticDimsArray(parent::AbstractGPUArray, dims::Tuple) | ||
|
|
||
| An `N`-dimensional GPU array that views the memory of `parent` with the dimensions `dims`, a | ||
| tuple of `Int`s and `StaticInt`s from Static.jl, e.g. `(static(4), static(4), n)`. The static | ||
| dimensions are part of the type, so in kernels the linear indices, strides and bounds checks | ||
| involving them are computed from constants, while the other dimensions can change at runtime. | ||
|
|
||
| Like other GPU arrays, a `StaticDimsArray` supports broadcasting, reductions and the other | ||
| operations of GPUArrays; out-of-place operations return arrays of the back-end without the | ||
| static dimensions. In kernels it becomes a [`StaticDimsDeviceArray`](@ref). | ||
|
|
||
| The size information is available through `static_size` and `known_size` of | ||
| StaticArrayInterface.jl, when it is loaded. | ||
|
|
||
| ```julia | ||
| julia> a = StaticDimsArray(JLArray{Float32}(undef, 4 * 4 * 10), (static(4), static(4), 10)); | ||
|
|
||
| julia> size(a) | ||
| (4, 4, 10) | ||
|
|
||
| julia> typeof(a.dims) | ||
| Tuple{StaticInt{4}, StaticInt{4}, Int64} | ||
| ``` | ||
| """ | ||
| struct StaticDimsArray{T, N, D <: MixedDims{N}, A <: AbstractGPUArray{T, 1}} <: AbstractGPUArray{T, N} | ||
| parent::A | ||
| dims::D | ||
|
|
||
| function StaticDimsArray(parent::AbstractGPUArray{T, 1}, dims::MixedDims{N}) where {T, N} | ||
| if prod(map(dynamic, dims)) != length(parent) | ||
| throw(DimensionMismatch("parent of length $(length(parent)) cannot be viewed with dimensions $dims")) | ||
| end | ||
| new{T, N, typeof(dims), typeof(parent)}(parent, dims) | ||
| end | ||
| end | ||
|
|
||
| StaticDimsArray(parent::AbstractGPUArray, dims::Tuple{Vararg{Union{Integer, StaticInt}}}) = | ||
| StaticDimsArray(vec(parent), map(d -> d isa StaticInt ? d : Int(d), dims)) | ||
|
|
||
| # keep the dimensions of `a`, use different memory | ||
| StaticDimsArray(a::StaticDimsArray, parent::AbstractGPUArray) = StaticDimsArray(parent, a.dims) | ||
|
|
||
| """ | ||
| static_size(a) | ||
|
|
||
| The dimensions of a [`StaticDimsArray`](@ref) or [`StaticDimsDeviceArray`](@ref), as a tuple | ||
| of `StaticInt`s and `Int`s. | ||
| """ | ||
| static_size(a::Union{StaticDimsArray, StaticDimsDeviceArray}) = a.dims | ||
|
|
||
| """ | ||
| known_size(::Type{<:StaticDimsArray}) | ||
|
|
||
| The dimensions that are known from the type of a [`StaticDimsArray`](@ref) or | ||
| [`StaticDimsDeviceArray`](@ref), with `nothing` for the dynamic ones. | ||
| """ | ||
| known_size(::Type{<:Union{StaticDimsArray{T, N, D}, StaticDimsDeviceArray{T, N, D}}}) where {T, N, D} = | ||
| known(D) | ||
| known_size(a::Union{StaticDimsArray, StaticDimsDeviceArray}) = known_size(typeof(a)) | ||
|
|
||
| Base.parent(a::StaticDimsArray) = a.parent | ||
| Base.size(a::StaticDimsArray) = map(dynamic, a.dims) | ||
| Base.elsize(::Type{<:StaticDimsArray{T, N, D, A}}) where {T, N, D, A} = Base.elsize(A) | ||
|
|
||
| KernelAbstractions.get_backend(a::StaticDimsArray) = get_backend(parent(a)) | ||
|
|
||
| # the memory is the one of the parent, so is the alias detection | ||
| storage(a::StaticDimsArray) = storage(parent(a)) | ||
| memory_location(a::StaticDimsArray) = memory_location(parent(a)) | ||
|
|
||
| # views, reshapes and reinterpretations derive from the parent, and lose the static dimensions | ||
| derive(::Type{T}, a::StaticDimsArray, dims::Dims, offset::Int) where {T} = | ||
| derive(T, parent(a), dims, offset) | ||
|
|
||
| # broadcasting: the style of the parent, for `N` dimensions | ||
| function Broadcast.BroadcastStyle(::Type{<:StaticDimsArray{T, N, D, A}}) where {T, N, D, A} | ||
| style = Broadcast.BroadcastStyle(A) | ||
| typeof(style)(Val(N)) | ||
| end | ||
|
|
||
| # `similar`: keep the static dimensions when the size does not change | ||
| Base.similar(a::StaticDimsArray, ::Type{S}) where {S} = | ||
| StaticDimsArray(similar(parent(a), S), a.dims) | ||
| function Base.similar(a::StaticDimsArray, ::Type{S}, dims::Dims) where {S} | ||
| if dims == size(a) | ||
| similar(a, S) | ||
| else | ||
| similar(parent(a), S, dims) | ||
| end | ||
| end | ||
|
|
||
| Base.copy(a::StaticDimsArray) = StaticDimsArray(copy(parent(a)), a.dims) | ||
|
|
||
| # copies to and from the CPU go through the parent | ||
| function Base.copyto!(dest::Array{T}, dstart::Integer, src::StaticDimsArray{T}, | ||
| sstart::Integer, n::Integer) where {T} | ||
| copyto!(dest, dstart, parent(src), sstart, n) | ||
| end | ||
| function Base.copyto!(dest::StaticDimsArray{T}, dstart::Integer, src::Array{T}, | ||
| sstart::Integer, n::Integer) where {T} | ||
| copyto!(parent(dest), dstart, src, sstart, n) | ||
| return dest | ||
| end | ||
|
|
||
| # kernel arguments (and other conversions of the memory) | ||
| Adapt.adapt_structure(to, a::StaticDimsArray) = wrap_static_dims(adapt(to, parent(a)), a.dims) | ||
| Adapt.adapt_structure(::ToArray, a::StaticDimsArray) = convert(Array, a) | ||
|
|
||
| wrap_static_dims(parent::AbstractGPUArray, dims) = StaticDimsArray(parent, dims) | ||
| wrap_static_dims(parent::AbstractVector, dims) = StaticDimsDeviceArray(parent, dims) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| @kernel function staticdims_index_kernel!(dest, src) | ||
| i, j, k = @index(Global, NTuple) | ||
| @inbounds dest[i, j, k] = src[i, j, k] + i + 10j + 100k | ||
| end | ||
|
|
||
| @testsuite "staticdims" (AT, eltypes)->begin | ||
| # a `StaticDimsArray` wraps GPU memory | ||
| AT <: AbstractGPUArray || return | ||
| static = GPUArrays.Static.static | ||
| StaticInt = GPUArrays.Static.StaticInt | ||
|
|
||
| T = Float32 in eltypes ? Float32 : first(eltypes) | ||
| dims = (static(4), static(3), 5) | ||
| host = rand(T, 4, 3, 5) | ||
| a = StaticDimsArray(AT(vec(host)), dims) | ||
|
|
||
| @testset "size" begin | ||
| @test a isa AbstractGPUArray{T, 3} | ||
| @test size(a) == (4, 3, 5) | ||
| @test GPUArrays.static_size(a) === dims | ||
| @test GPUArrays.known_size(typeof(a)) === (4, 3, nothing) | ||
| @test Array(a) == host | ||
| @test_throws DimensionMismatch StaticDimsArray(AT{T}(undef, 10), dims) | ||
| # a multidimensional parent is viewed as a vector | ||
| @test StaticDimsArray(AT(host), dims) == a | ||
| end | ||
|
|
||
| @testset "kernel arguments" begin | ||
| backend = get_backend(a) | ||
| b = similar(a) | ||
| @test b isa StaticDimsArray{T, 3, typeof(dims)} | ||
| staticdims_index_kernel!(backend)(b, a; ndrange = size(a)) | ||
| expected = [host[i, j, k] + i + 10j + 100k for i in 1:4, j in 1:3, k in 1:5] | ||
| @test Array(b) ≈ expected | ||
| end | ||
|
|
||
| @testset "broadcasting" begin | ||
| b = similar(a) | ||
| b .= a .* 2 | ||
| @test Array(b) ≈ host .* 2 | ||
| @test b isa StaticDimsArray | ||
| # mixed with plain arrays of the back-end | ||
| b .= a .+ AT(host) | ||
| @test Array(b) ≈ host .+ host | ||
| # out of place: an array of the back-end | ||
| c = a .+ one(T) | ||
| @test Array(c) ≈ host .+ one(T) | ||
| fill!(b, zero(T)) | ||
| @test all(iszero, Array(b)) | ||
| end | ||
|
|
||
| @testset "reductions" begin | ||
| @test sum(a) ≈ sum(host) | ||
| @test Array(sum(a; dims = 3)) ≈ sum(host; dims = 3) | ||
| @test Array(maximum(a; dims = 1)) ≈ maximum(host; dims = 1) | ||
| end | ||
|
|
||
| @testset "linear indices" begin | ||
| # static and dynamic dimensions in any order | ||
| for dims in ((static(2), 3, static(4)), (2, static(3), 4), (2, 3, 4), | ||
| (static(2), static(3), static(4))) | ||
| d = StaticDimsDeviceArray(collect(1:24), dims) | ||
| @test all(d[I] == LinearIndices((2, 3, 4))[I] for I in CartesianIndices((2, 3, 4))) | ||
| end | ||
| end | ||
|
|
||
| @testset "indexing" begin | ||
| @test (@allowscalar a[2, 3, 4]) == host[2, 3, 4] | ||
| @test Array(a[:, 2, :]) == host[:, 2, :] | ||
| @test Array(view(a, :, :, 2)) == host[:, :, 2] | ||
| @test Array(reshape(a, 12, 5)) == reshape(host, 12, 5) | ||
| end | ||
|
|
||
| @testset "aliasing" begin | ||
| @test Base.mightalias(a, parent(a)) | ||
| @test !Base.mightalias(a, copy(a)) | ||
| end | ||
| end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have to think about this.