Rework the broadcast API and document it#23939
Conversation
|
Looks like this would also fix #22180 by the way? |
|
Regarding the Documenter error: Maybe you need to export |
3007181 to
4ac2424
Compare
4ac2424 to
85ec999
Compare
|
OK, I think this is done. @Sacha0, aside from the core API changes this pretty significantly reworks broadcasting esp for sparse arrays, so I'd love it if you'd take a look. The biggest change is that it no longer needs an ambiguity-resolving layer or "second pass" at computation of output type--- |
|
I was going to strike out the "docs don't build" comment above, since that works now too. (I'd just forgotten to add those functions to the stdlib section of the manual.) |
andyferris
left a comment
There was a problem hiding this comment.
Nice design, as usual @timholy !
E.g. I think I can see this working fine with StaticArrays given I implement a static unit range (I'd have to try).
| See [`Broadcast.Result`](@ref) and [`Broadcast.rule`](@ref). | ||
| """ | ||
| Base.similar(f, r::Result{BottomArray{N}}, As...) where N = similar(Array{eltype(r)}, indices(r)) | ||
| # In cases of conflict we fall back on Array |
base/broadcast.jl
Outdated
|
|
||
| # When ElType is not concrete, use narrowing. Use the first element of each input to determine | ||
| # the starting output eltype; the _broadcast! method will widen `dest` as needed to | ||
| # accomodate later values. |
There was a problem hiding this comment.
Silly question: Is it possible that this ends up in the situation where the element type is too narrow? E.g. For eltypes like Union{T,Null} might the final array end up with eltype T or Null? (This might prevent push! on a vector when you have broadcasted it - no problem in the functional case, though).
There was a problem hiding this comment.
If I understand your question correctly, you can definitely get that, but it's not unique to this PR. (I think this PR doesn't alter the element-type behavior whatsoever.) julia 0.6:
julia> a = Real[1, 2]
2-element Array{Real,1}:
1
2
julia> b = map(identity, a)
2-element Array{Int64,1}:
1
2
julia> push!(b, 3.2)
ERROR: InexactError()
Stacktrace:
[1] convert(::Type{Int64}, ::Float64) at ./float.jl:679
[2] push!(::Array{Int64,1}, ::Float64) at ./array.jl:617
julia> b = a .+ 1
2-element Array{Int64,1}:
2
3The reasoning here is that inference is unfortunately likely to give Any occasionally for something that doesn't have to be that wide. So it's good to narrow the outputs where possible to keep instability from propagating.
There was a problem hiding this comment.
Ok thanks for clarifying.
I'm wondering if we'll need to be "smarter" about union types in the future, now that they are faster.
There was a problem hiding this comment.
Yes, that's a common behavior already. That could be problematic if you expected to set a value to null in the resulting array. I'm not sure what could be done about this, as we can't know what are the possible types without relying on inference, which we want to avoid. And for things like map(isnull, x) we definitely don't want an Array{Union{T, Null}} result.
See also #23940.
There was a problem hiding this comment.
The good news is that there's broadcast!, so in the end you don't give anything up by forced narrowing.
doc/src/manual/interfaces.md
Outdated
| Broadcast.rule(::Type{<:AbstractArray{T,N}}) where {T,N} = BottomArray{N} | ||
| Broadcast.rule(::Type{A}, ::Type{BottomArray{N}}) where {A<:AbstractArray,N} = A | ||
|
|
||
| If you want to leverage this `AbstractArray` types, it makes sense to have the unary `rule` return an |
There was a problem hiding this comment.
Just noticed I left a sentence unfinished here. Will fix before merging, but I'll wait to see if there are any more requested changes, just to avoid jamming CI or erasing the record of the successful tests.
|
@nanosoldier |
|
Yes, completely orthogonal. |
|
Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @ararslan |
|
OK, looks likely that some of those |
Maybe to that PR, but I started another (currently secret) branch that is better than it and may depend quite heavily on this 🙂 (I don't mean to be cryptic, the branch just triggered some work on other issues first). |
| longest(V1::Val, V2::Val) = longest(ntuple(identity, V1), ntuple(identity, V2)) | ||
| longest(t1::Tuple, t2::Tuple) = (true, longest(Base.tail(t1), Base.tail(t2))...) | ||
| longest(::Tuple{}, t2::Tuple) = (true, longest((), Base.tail(t2))...) | ||
| longest(t1::Tuple, ::Tuple{}) = (true, longest(Base.tail(t1), ())...) |
There was a problem hiding this comment.
Can you explain? I need to check for Tuple{} anyway when one tuple is longer than the other. I could add nothing as a new sentinel, but I'm not sure I understand why.
base/broadcast.jl
Outdated
| # we defer to any case where the result of `rule` is known. | ||
| result_join(::Type{S}, ::Type{T}, ::Type{Unknown}, ::Type{Unknown}) where {S,T} = Unknown | ||
| result_join(::Type{S}, ::Type{T}, ::Type{Unknown}, ::Type{U}) where {S,T,U} = U | ||
| result_join(::Type{S}, ::Type{T}, ::Type{U}, ::Type{Unknown}) where {S,T,U} = U |
There was a problem hiding this comment.
result_join(::Type, ::Type, ::Type{U}, ::Type{Unknown}) where {U} = U and similarly for the ones above?
base/sparse/higherorderfns.jl
Outdated
| # dense vectors/matrices, and PromoteToSparse collections continue in the promote-to-sparse funnel | ||
| FunnelToSparseBC = Union{Type{Any},Type{Vector},Type{Matrix},Type{PromoteToSparse},Type{AbstractSparseArray}} | ||
| promote_spcontainertype(::FunnelToSparseBC, ::FunnelToSparseBC) = PromoteToSparse | ||
| Broadcast.rule(::Type{SPVM}, ::Type{Tuple}) = PromoteToSparse |
There was a problem hiding this comment.
Does this rule result in combinations involving tuples being sent to sparse broadcast?
There was a problem hiding this comment.
Yes, somehow I misinterpreted the current rules as diverting them to your "second stage" 1-2 dimensional analysis. Will fix (and add a test).
There was a problem hiding this comment.
In my view, sparse vectors should have precedence over tuples for broadcasting purposes. The problem IIUC is that the sparse broadcast implementations work only (for understandable practicality reasons) with combinations of sparse structures that share the same storage behavior. A possible way around this would be to create a wrapper for tuples that behaves as an SparseVector for sparse broadcasting purposes so this works.
Of course that can be done in another PR and with this new API in place, should much easier than before.
There was a problem hiding this comment.
... create a wrapper ... that behaves as an SparseVector for sparse broadcasting purposes so this works.
Yes, writing a common iteration interface for all types that participate in sparse broadcast is the direction I have in mind broadly. Such an interface would enable removal of the sparsifystructured layer and associated allocation. Very much a post-1.0 project though :).
The problem IIUC is that the sparse broadcast implementations work only (for understandable practicality reasons) with combinations of sparse structures that share the same storage behavior.
Rather, including tuples in sparse broadcast is as simple as adding an appropriate sparsifystructured method and changing the couple of rules that Tim inadvertently changed in this pull request. tuples do not presently participate because the utility is not clear. Though given Vectors participate, perhaps tuples should as well :).
There was a problem hiding this comment.
tuplesdo not presently participate because the utility is not clear. Though givenVectorsparticipate, perhaps tuples should as well
Given they "behave" as vectors in combination with arrays, I think they should behave as Vectors also in combination with sparse arrays. I imagine it would be unexpected for some people to see them not working with sparse structures.
There have been requests asking even for general iterables with shapes to participate broadcast and I don't see why general iterables with shape, including tuples, should be excluded (given a clear broadcasting precedence is well established).
base/sparse/higherorderfns.jl
Outdated
| Broadcast.rule(::Type{SPVM}, ::Type{Broadcast.BottomVector}) = PromoteToSparse | ||
| Broadcast.rule(::Type{SPVM}, ::Type{Broadcast.BottomMatrix}) = PromoteToSparse | ||
| Broadcast.rule(::Type{PromoteToSparse}, ::Type{Broadcast.Scalar}) = PromoteToSparse | ||
| Broadcast.rule(::Type{PromoteToSparse}, ::Type{Tuple}) = PromoteToSparse |
There was a problem hiding this comment.
Likewise here, does this rule result in combinations involving tuples being sent to sparse broadcast?
doc/src/manual/interfaces.md
Outdated
| Base.similar(f, r::Broadcast.Result{ContainerType}, As...) | ||
| ``` | ||
|
|
||
| `f` is the operation being performed, `ContainerType` signals the resulting container type |
There was a problem hiding this comment.
Should there be an "and" following the comma, or should the period terminating this sentence instead be a comma, or am I misreading these sentences? :)
85ec999 to
47a906c
Compare
|
@nanosoldier |
3ce4cb0 to
f875cf9
Compare
|
The problem turned out to be an unused type parameter in lines like Lines 176 to 177 in c94e502 I'm still a little reluctant to do the more general rename: what if it turns out that for purposes such as concatenation, etc, we want slightly different rules? I'd feel happier about generalizing the name if there were a proof-of-principle demonstrating that we can leverage this machinery for novel purposes without modification---but until then, I think it's better to use a too-narrow name than a too-broad one. However, I am willing to make the change if there is a strong consensus. In any event, I think this is ready. |
|
Build error on Travis is unrelated (libopenblas couldn't be loaded) |
|
The Appveyor failure also looks unrelated. I may merge this early next week in case it paves the way for #23692. |
doc/src/manual/interfaces.md
Outdated
| It is worth noting that you do not need to (and should not) define both argument orders | ||
| of this call; defining one is sufficient no matter what order the user supplies the arguments in. | ||
|
|
||
| For `AbstractArray` types, defining a BroadcastStyle supersedes the fallback choice, |
There was a problem hiding this comment.
"a BroadcastStyle" -> "a BroadcastStyle"?
| ``` | ||
|
|
||
| Whenever you subtype `AbstractArrayStyle`, you also need to define rules for combining | ||
| dimensionalities, by creating a constructor for your style that takes a `Val(N)` argument. |
test/broadcast.jl
Outdated
| Base.setindex!(A::ArrayData, v::Any, i::Integer...) = setindex!(A.data, v, i...) | ||
| Base.size(A::ArrayData) = size(A.data) | ||
| Base.broadcast_similar(f, ::Broadcast.ArrayStyle{A}, ::Type{T}, inds::Tuple, As...) where {A,T} = | ||
| A(Array{T}(length.(inds))) |
There was a problem hiding this comment.
Array{T}(length.(inds)) -> Array{T}(uninitialized, length.(inds)) in anticipation of conflict? :)
f875cf9 to
776139b
Compare
These broadcast styles were introduced in response to #23939 (comment) as a way to limit the "greediness" of Sparse's broadcasting implementation -- sparse only wanted to allow known combinations of array types (including Array but not any AbstractArray). The idea was to allow us to gradually improve the sparse broadcast implementation over 1.x in a non-breaking manner. Unfortunately, these special styles for Array make defining new styles in the heirarchy a bit of a pain (ref. #23939 (comment)), and it was making my life harder in getting the 1.0 breaking changes in. This commit removes these special broadcast styles in favor of just having Sparse identify the cases itself and re-dispatch back into the default implementation in the cases it doesn't know how to handle.
These broadcast styles were introduced in response to #23939 (comment) as a way to limit the "greediness" of Sparse's broadcasting implementation -- sparse only wanted to allow known combinations of array types (including Array but not any AbstractArray). The idea was to allow us to gradually improve the sparse broadcast implementation over 1.x in a non-breaking manner. Unfortunately, these special styles for Array make defining new styles in the heirarchy a bit of a pain (ref. #23939 (comment)), and it was making my life harder in getting the 1.0 breaking changes in. This commit removes these special broadcast styles in favor of just having Sparse identify the cases itself and re-dispatch back into the default implementation in the cases it doesn't know how to handle.
These broadcast styles were introduced in response to #23939 (comment) as a way to limit the "greediness" of Sparse's broadcasting implementation -- sparse only wanted to allow known combinations of array types (including Array but not any AbstractArray). The idea was to allow us to gradually improve the sparse broadcast implementation over 1.x in a non-breaking manner. Unfortunately, these special styles for Array make defining new styles in the heirarchy a bit of a pain (ref. #23939 (comment)), and it was making my life harder in getting the 1.0 breaking changes in. This commit removes these special broadcast styles in favor of just having Sparse identify the cases itself and re-dispatch back into the default implementation in the cases it doesn't know how to handle.
* Remove kludgy VectorStyle and MatrixStyle These broadcast styles were introduced in response to #23939 (comment) as a way to limit the "greediness" of Sparse's broadcasting implementation -- sparse only wanted to allow known combinations of array types (including Array but not any AbstractArray). The idea was to allow us to gradually improve the sparse broadcast implementation over 1.x in a non-breaking manner. Unfortunately, these special styles for Array make defining new styles in the heirarchy a bit of a pain (ref. #23939 (comment)), and it was making my life harder in getting the 1.0 breaking changes in. This commit removes these special broadcast styles in favor of just having Sparse identify the cases itself and re-dispatch back into the default implementation in the cases it doesn't know how to handle. * Add a graceful deprecation
* Remove kludgy VectorStyle and MatrixStyle These broadcast styles were introduced in response to JuliaLang/julia#23939 (comment) as a way to limit the "greediness" of Sparse's broadcasting implementation -- sparse only wanted to allow known combinations of array types (including Array but not any AbstractArray). The idea was to allow us to gradually improve the sparse broadcast implementation over 1.x in a non-breaking manner. Unfortunately, these special styles for Array make defining new styles in the heirarchy a bit of a pain (ref. JuliaLang/julia#23939 (comment)), and it was making my life harder in getting the 1.0 breaking changes in. This commit removes these special broadcast styles in favor of just having Sparse identify the cases itself and re-dispatch back into the default implementation in the cases it doesn't know how to handle. * Add a graceful deprecation
* Remove kludgy VectorStyle and MatrixStyle These broadcast styles were introduced in response to JuliaLang/julia#23939 (comment) as a way to limit the "greediness" of Sparse's broadcasting implementation -- sparse only wanted to allow known combinations of array types (including Array but not any AbstractArray). The idea was to allow us to gradually improve the sparse broadcast implementation over 1.x in a non-breaking manner. Unfortunately, these special styles for Array make defining new styles in the heirarchy a bit of a pain (ref. JuliaLang/julia#23939 (comment)), and it was making my life harder in getting the 1.0 breaking changes in. This commit removes these special broadcast styles in favor of just having Sparse identify the cases itself and re-dispatch back into the default implementation in the cases it doesn't know how to handle. * Add a graceful deprecation
This is an attempt towards developing an API for broadcasting that is relatively easy to extend. Overall I'm pretty happy with it. For most custom array types, only two methods (
Broadcast.ruleand a broadcast-specificsimilar) need to be defined. Probably the best place to start reviewing this is with the documentation, or to compare the new way against the old one.(I don't understand why the docs don't build locally, any ideas?)There are two jobs left to do:
sparse/higherorderfunctions.jlwith the new APII think it makes sense to first see whether people like this API before tackling either one. The existing sparse-broadcasting logic looks pretty complicated. I'd hope this may simplify it somewhat, but the proof will be in the pudding.
Fixes #20740, #22180 (edited).