Skip to content
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

Deprecate generic stride implementation (#17812), allow AbstractArrays to use BLAS/LAPACK routines (#25247) #25321

Merged
merged 16 commits into from
Jan 5, 2018

Conversation

dlfivefifty
Copy link
Contributor

This pull request resolves Issues #17812 and #25247:

  1. Deprecate stride(::AbstractArray, i::Int) and strides(::AbstractArray).
  2. Re-implement above for the special cases of StridedArray.
  3. Removes restrictions to StridedArray in BLAS routines, so that user types that are memory-equivalent to strided arrays can access these routines.
  4. Removes the restrictions to StridedArray in sparse/sparsevector.jl. I'm not sure why these were there to begin.

I still need to weaken the types for ther LAPack routines. This is straightforward but will take some time to get the tests implemented.

@stevengj
Copy link
Member

So you need a subtype of StridedArray to use BLAS? Wouldn't it be better to pull away from subtypes here and push towards traits?

@dlfivefifty
Copy link
Contributor Author

That’s exactly what this is doing: the strides matrix interface consists of overriding conversion to Ptr and implementing stride.

Down the line a trait could be added as well but this is the first step.

strides(A::AbstractArray) = size_to_strides(1, size(A)...)
function strides end

# the definition of strides for Array is the cumsum of the product of sizes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cumprod of the size? But it's not quite that, it's cumprod([1;collect(size(A))[1:end-1]])

Provided the element type of the array is compatible with BLAS, a strided array can utilize BLAS and LAPACK routines
for more efficient linear algebra routines.

A typical example of a user defined strided array is one that wraps a standard `Array`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hyphenate "user-defined" (hyphenate compound adjectives). Don't hyphenate "subtype".

@@ -251,8 +251,8 @@ julia> strides(A)
"""
function strides end

# the definition of strides for Array is the cumsum of the product of sizes
function _cumsumprodsizes(a::AbstractArray, i)
# the definition of strides for Array is cumprod([1;collect(size(A))[1:end-1]])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, technically cumprod would return an array, whereas strides returns a tuple

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's not quite right since:

julia> A = Array{Float64,0}()
0-dimensional Array{Float64,0}:
0.0

julia> strides(A)
()

How about saying:

the defition of strides for Array{T,N} is tuple() if N = 0, otherwise it is a tuple containing 1 and a cumulative product of the first N-1 sizes

Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good, thanks for tackling this.

@@ -3489,6 +3489,20 @@ end
@deprecate getq(F::Factorization) F.Q
end

# Issues #17812 Remove default stride implementation
function stride(a::AbstractArray, i::Integer)
depwarn("`stride(a::AbstractArray, i)` is deprecated for general arrays. Override `stride` for custom array types that implement the strided array interface.", :stride)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this is insufficiently cautionary. Maybe

Specialize `stride` for custom array types that have the appropriate representation in memory. Warning: inappropriately implementing this method for an array type that does not use strided storage can lead to incorrect results or segfaults.

@@ -216,9 +216,40 @@ end
# Check that stride of matrix/vector is 1
# Writing like this to avoid splatting penalty when called with multiple arguments,
# see PR 16416
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new IPO constant propagation do we need stride1? It seems that stride(x, dim) could check for dim == 1, and the branch may be elided now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens automatically at compile time? Cool!

I'm not sure how to test for performance regression or that it's doing the right behaviour, so I might hold off on that change (since it's independent of this PR).

base/subarray.jl Outdated
@@ -255,11 +255,11 @@ IndexStyle(::Type{<:SubArray}) = IndexCartesian()
# so they are well-defined even for non-linear memory layouts
strides(V::SubArray) = substrides(V.parent, V.indices)

substrides(parent, I::Tuple) = substrides(1, parent, 1, I)
substrides(parent, I::Tuple) = substrides(stride(parent, 1), parent, 1, I)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine that many implementations of stride(x, dim) will involve a loop over d = 1:dim; since you call stride once per dimension, this is now O(N^2) where N = ndims(parent). It might be more efficient to write this in terms of strides(parent) which can be implemented with a single loop and thus be O(N).

Copy link
Contributor Author

@dlfivefifty dlfivefifty Dec 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Maybe something like:

substrides(parent, I) = substrides(parent, strides(parent), I)
substrides(parent, strds, ::Tuple{}) = ()
substrides(parent, strds, I::Tuple{ScalarIndex, Vararg{Any}}) = (substrides(parent, tail(strds), tail(I))...,)
substrides(parent, strds, I::Tuple{Slice, Vararg{Any}}) = (first(strds), substrides(parent, tail(strds), tail(I))...)
substrides(parent, strds, I::Tuple{AbstractRange, Vararg{Any}}) = (first(strds)*step(I[1]), substrides(parent, tail(strds), tail(I))...)
substrides(parent, strds, I::Tuple{Any, Vararg{Any}}) = throw(ArgumentError("strides is invalid for SubArrays with indices of type $(typeof(I[1]))"))

Does substrides have to go through a deprecation process? It's not exported.

A typical example of a user-defined strided array is one that wraps a standard `Array`
with additional structure.


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a warning here too about how dangerous it is to implement these methods if the underlying storage is not actually strided. Otherwise the following might happen:

  • user calls a function. Gets a MethodError due to missing implementation of strides
  • "helpfully" implements strides to fix the method error

It might be useful to give some concrete examples of arrays and categorize them as to whether they have strided memory representation. Examples:

1:5   # not strided (there is no storage associated with this array, could segfault if implemented `strides`)
collect(1:5)  # is strided
A = [1 5; 2 6; 3 7; 4 8]  # is strided
V = view(A, 1:2, :)   # is strided
V = view(A, 1:2:4, :)   # is strided
V = view(A, [1,2,4], :)   # is not strided

@dlfivefifty dlfivefifty changed the title WIP: Deprecate generic stride implementation (#17812), allow AbstractArrays to use BLAS routines (#25247) Deprecate generic stride implementation (#17812), allow AbstractArrays to use BLAS routines (#25247) Dec 30, 2017
@dlfivefifty dlfivefifty changed the title Deprecate generic stride implementation (#17812), allow AbstractArrays to use BLAS routines (#25247) Deprecate generic stride implementation (#17812), allow AbstractArrays to use BLAS/LAPACK routines (#25247) Dec 30, 2017
@dlfivefifty
Copy link
Contributor Author

I've changed this a bit so that the default override is strides(A) and so one does not need to override stride(A,i). This is consistent with the abstract array interface, where one overrides size(A), not size(A,i).

I've also removed the restriction to StridedArray in the LAPACK routines, in the process I found and fixed some missing chkstride1 calls, and a bug in tzrzf! which used size instead of stride.

@ararslan ararslan added deprecation This change introduces or involves a deprecation linear algebra Linear algebra labels Dec 30, 2017
@dlfivefifty
Copy link
Contributor Author

There is an issue with type-inferrence with the new substrides implementation:

julia> versioninfo()
Julia Version 0.7.0-DEV.3204
Commit d1086e0091* (2017-12-30 21:09 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin17.3.0)
  CPU: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
  WORD_SIZE: 64
  BLAS: libopenblas (USE64BITINT DYNAMIC_ARCH NO_AFFINITY Haswell)
  LAPACK: libopenblas64_
  LIBM: libopenlibm
  LLVM: libLLVM-3.9.1 (ORCJIT, skylake)
Environment:
  JULIA_VERSION = 0.6

julia> A = rand(5,5,5,5);

julia> V = view(A, 1:1 ,:, 1:3, :);

julia> using Test

julia> @inferred strides(A)
(1, 5, 25, 125)

julia> @inferred strides(V)
ERROR: return type NTuple{4,Int64} does not match inferred return type Tuple{Int64,Int64,Int64,Any}
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] top-level scope

This seems to be a limitation on type inference and I'm not sure how to resolve it.

The relevant function is

substrides(parent, I::Tuple) = substrides(parent, strides(parent), I)
substrides(parent, strds, ::Tuple{}) = ()
substrides(parent, strds, I::Tuple{ScalarIndex, Vararg{Any}}) = (substrides(parent, tail(strds), tail(I))...,)
substrides(parent, strds, I::Tuple{Slice, Vararg{Any}}) = (first(strds), substrides(parent, tail(strds), tail(I))...)
substrides(parent, strds, I::Tuple{AbstractRange, Vararg{Any}}) = (first(strds)*step(I[1]), substrides(parent, tail(strds), tail(I))...)
substrides(parent, strds, I::Tuple{Any, Vararg{Any}}) = throw(ArgumentError("strides is invalid for SubArrays with indices of type $(typeof(I[1]))"))

@dlfivefifty
Copy link
Contributor Author

I've worked around the issue with substrides by adding an ::Int assertion.

I think this PR is ready to be merged, though I'm happy to make any other changes.

There is one test failure but it appears to be a download failure, which shouldn't be caused by this PR. It doesn't look like I have the ability to restart the test.

@stevengj
Copy link
Member

stevengj commented Jan 3, 2018

An ::Int assertion may just be hiding the underlying problem; I'm worried that an Any variable is still heap-allocated somewhere and then converted to Int?

@dlfivefifty
Copy link
Contributor Author

dlfivefifty commented Jan 3, 2018

Good point. I'm not sure how to proceed as it appears to be a breakdown of type inference.

Perhaps putting the type assertion in the arguments is better? That is, for example:

substrides(parent, strds::NTuple{N,Int}, I::Tuple{Slice, Vararg{Any}}) where N = (first(strds), substrides(parent, tail(strds), tail(I))...)

Edit: "assertion" isn't the right word here.

@timholy
Copy link
Member

timholy commented Jan 4, 2018

Yeah, this does seem to be an inference bug. I get different answers depending on sequence of operations. For example:

@code_warntype substrides(A, (1, 5, 25, 125), (1:1, Base.Slice(Base.OneTo(5)), 1:3, Base.Slice(Base.OneTo(5))))  # "fails"
@code_warntype substrides(A, (5, 25, 125), (Base.Slice(Base.OneTo(5)), 1:3, Base.Slice(Base.OneTo(5))))  # "succeeds"
@code_warntype substrides(A, (1, 5, 25, 125), (1:1, Base.Slice(Base.OneTo(5)), 1:3, Base.Slice(Base.OneTo(5)))) # "succeeds"

Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presuming tests pass, one tiny tweak and I think this is done. Many, many thanks for seeing this through!

Base.setindex!(A::WrappedArray, v, i::Int) = setindex!(A.A, v, i)
Base.setindex!(A::WrappedArray{T, N}, v, I::Vararg{Int, N}) where {T, N} = setindex!(A.A, v, I...)
Base.unsafe_convert(::Type{Ptr{T}}, A::WrappedArray{T}) where T = Base.unsafe_convert(Ptr{T}, A.A)
Base.stride(A::WrappedArray, i::Int) = stride(A.A, i)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps before this line you can test that strides(A::WrappedArray) and stride(A::WrappedArray, i) both throw an error, and that afterward it does not? In a sense, the main contribution of this PR is removing a fallback method, so we should test that the fallback doesn't accidentally get reintroduced.

Or you could define WrappedArray2 and test for the error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume by "throws error" you mean @test_deprecated since the fallback is still there?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right indeed. 👍

@dlfivefifty
Copy link
Contributor Author

I think it’s ready to be merged now (again). The only failed test was a download error, which I don’t think this PR could cause.

@mbauman
Copy link
Member

mbauman commented Jan 5, 2018

This is really nice! 👍

@timholy
Copy link
Member

timholy commented Jan 8, 2018

I belatedly realized that I should have asked you to add the deprecation to NEWS.md. Any chance you could do that now?

@dlfivefifty
Copy link
Contributor Author

Yes I’ll try to do this soon.

mbauman added a commit that referenced this pull request Mar 8, 2018
Just like the deprecation of strides (#25321), the default implementation for `elsize` is generally not true. This deprecates the fallback, restructures it to be defined on the type (and not the value), and implements it where possible for the builtin array types and wrappers.
mbauman added a commit that referenced this pull request Mar 12, 2018
Just like the deprecation of strides (#25321), the default implementation for `elsize` is generally not true. This deprecates the fallback, restructures it to be defined on the type (and not the value), and implements it where possible for the builtin array types and wrappers.
@Nosferican
Copy link
Contributor

The deprecation method for this ain't very helpful on 0.7, given this message what should the appropriate method to update code be?

 Warning: The default `strides(a::AbstractArray)` implementation is deprecated for general arrays.
│ Specialize `strides(::Adjoint)` if `Adjoint` indeed uses a strided representation in memory.
│ Warning: inappropriately implementing this method for an array type that does not use strided
│ storage may lead to incorrect results or segfaults.
│   caller = stride at abstractarray.jl:350 [inlined]
└ @ Core ./abstractarray.jl:350

@timholy
Copy link
Member

timholy commented Sep 14, 2018

The alternative is to use cartesian indexing, i.e., A[i,j,k] rather than A[i] for a 3d array.

@dlfivefifty
Copy link
Contributor Author

If your custom array is strided in memory, then it should implement the interface and override strides(::MyArray).

If it's not strided in memory, you probably actually mean to use size(A,1), not stride(A,2).

@mbauman
Copy link
Member

mbauman commented Sep 14, 2018

If you're not using strides as a computation for pointer-loading (or passing to a C-API), then ideally you'd try to use eachindex or CartesianIndices to generate the loops you want. That requires a refactor, though. In the meantime, the operation strides is performing for Array is cumprod([1, size(A)...])[1:end-1]. It might be worth implementing cumprod(::Tuple) to make this easier and faster.

@timholy
Copy link
Member

timholy commented Sep 14, 2018

The xref above (added after my response) is from PyPlot, which probably means it is a pointer operation. Tread carefully in terms of following the advice above; I see there's an Adjoint in the message which means that the fallback implementation of cumprod([1, size(A)...])[1:end-1] may give the wrong answer.

The best advice might be to read up on strided arrays; once you understand the concept you will know if your array is strided and how to best implement the fix.

@mbauman
Copy link
Member

mbauman commented Sep 14, 2018

It may be that #29135 will solve the issue. A second thumbs up there would be good and then it can be merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
deprecation This change introduces or involves a deprecation linear algebra Linear algebra
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants