-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
generic broadcast/broadcast! for sparse matrices, with #19372 semantics #19518
Conversation
let | ||
# broadcast! over a single sparse matrix falls back to map!, tested above | ||
N, M = 10, 12 | ||
# test broadcast! implementation specialized for a pair of (input) sparse matrices |
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.
Is there a similar test for combinations of sparse matrices and scalars, e.g. A .* α
where α
is a scalar?
I guess this PR doesn't handle combinations of sparse matrices and scalars at all? Ideally we'd have something that worked for any such combination. |
My thinking was that |
Correct, and agreed. The next step is a pull request including the work in this review thread, hopefully capable of handling combinations involving more than two
Yes, that's the approach in the thread mentioned above. Thanks for reviewing! |
function _broadcast_zeropres!{Tf}(f::Tf, C::SparseMatrixCSC, A::SparseMatrixCSC, B::SparseMatrixCSC) | ||
isempty(C) && (fill!(C.colptr, 1); return C) | ||
spaceC = min(length(C.rowval), length(C.nzval)) | ||
rowsentinelA = convert(eltype(A.rowval), C.m + 1) |
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.
probably just eltype(A.rowval)(C.m + 1)
, since that does a type assertion to ensure the compiler knows the type of the result, unlike convert
. Though I wonder if it is better to just use typemax(eltype(A.rowval))
as the sentinel?
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.
probably just eltype(A.rowval)(C.m + 1), since that does a type assertion to ensure the compiler knows the type of the result, unlike convert.
Does the implicit type assertion provide any benefit when type inference is happy with the explicit convert
s? Explicit convert
s seem easier to understand. I suppose rowsentinelA::eltype(A.rowval) = C.m + 1
might provide the best of both worlds?
Though I wonder if it is better to just use typemax(Int) as the sentinal in all cases?
Using typemax(Int)
as the sentinel might cause an issue if a rowval
vector's element type is narrower than Int
?
Thanks!
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.
I suppose rowsentinelA::eltype(A.rowval) = C.m + 1 might provide the best of both worlds?
A further thought: A few assertions like Ak::eltype(A.rowval)
might allow us to simplify some expressions, e.g. Ak += one(Ak)
-> Ak += 1
. So perhaps adopting that style uniformly in these methods would be advantageous?
It seems like you might need more tests to get coverage of all your branches, e.g. the cases where |
Do you mean in the result tests, or in the allocation tests? If the former, existing tests should thoroughly cover the code specialized for two If the latter, yes, for allocation I only cover the no-singleton-dims and second-dim-singleton code. Would you like me to expand the allocation tests to also include the first-dim-singleton code? Thanks! |
I meant in the results tests; I wasn't sure if we had |
I believe we do, distributed piecemeal over |
…ith new (JuliaLang#19372) semantics / less pessimistic allocation behavior and optimize a few inefficiently handled common cases. Also provide a broadcast/broadcast! implementation capable of (relatively) efficiently handling an arbitrary number of (input) sparse matrices. Integrate with the map/map! implementation, and touch up a few rough edges of the map/map! implementation. Test.
Hm, before: julia> eltype(sparse(zeros(Real, 0, 0)) + sparse(zeros(Real, 0, 0)))
Real After: julia> eltype(sparse(zeros(Real, 0, 0)) + sparse(zeros(Real, 0, 0)))
Any I found the old behavior rather handy, but there is no way of knowing that |
In theory that should work if all julia> f(x, y) = x
f (generic function with 1 method)
julia> Core.Inference.return_type(f, Tuple{Real, Real})
Real But for julia> last(Base.return_types(+, Tuple{Real, Real}))
Any I guess we should find it and fix it. But unfortunately any package adding a non-inferrable function will break this again. |
The offenders are
... or adding a |
Ah, but for sparse matrices, the problem is a bit more grave, as it also hits the empty as in "no stored elements" case, not only the empty as in "at least one dimension is zero" case: julia> spzeros(Real,1,1) + spzeros(Real,1,1)
1×1 sparse matrix with 0 Any nonzero entries The element type should probably be julia> eltype(broadcast(+, zeros(Real,1,1), zeros(Real,1,1)))
Int64
julia> eltype(broadcast(+, sparse(zeros(Real,1,1)), sparse(zeros(Real,1,1))))
Any @Sacha0, do you think we can update the sparse matrix code to behave like the dense array code? |
But: julia> zeros(Real,1,1) + zeros(Real,1,1)
1×1 Array{Real,2}:
0 For Also, |
IMHO, the non-broadcast
Probably, otherwise
But that does not necessarily imply that for any
Errr, |
Sure, but both choices have their drawbacks, since the stricter type means some
What do you mean? At least this gives the expected result: julia> f{T}(::Type{T}) = promote_type(Bool, T)
f (generic function with 1 method)
julia> Core.Inference.return_type(f, Tuple{Type{Real}})
Type{Real}
Indeed, that's the problematic definition. It seems that even when no promotion rule exists, inference is not able to figure out that the function cannot return: julia> f{S,T}(x::S, y::T) = (U=promote_type(S,T); convert(U, x) + convert(U, y))
f (generic function with 1 method)
julia> type Num <: Number
end
julia> @code_warntype f(Num(), 1)
Variables:
#self#::#f
x::Num
y::Int64
U::Type{T}
Body:
begin
$(Expr(:inbounds, false))
# meta: location promotion.jl promote_type 140
# meta: location promotion.jl promote_result 178
# meta: location promotion.jl promote_to_supertype 187
SSAValue(1) = Num
SSAValue(2) = Int64
# meta: pop location
# meta: pop location
# meta: pop location
$(Expr(:inbounds, :pop))
U::Type{T} = (Base.throw)((Base.ErrorException)(((Core.getfield)((Core.getfield)(Base.Main,:Base)::Any,:string)::Any)("no promotion exists for ",SSAValue(1)," and ",SSAValue(2))::Any)::ErrorException)::Union{}
return ((Main.convert)(U::Type{T},x::Num)::Any + (Main.convert)(U::Type{T},y::Int64)::Any)::Any
end::Any
Here it seems that |
Yes, my intention was to change the sparse broadcast promotion mechanism in this pull request to that provided in #19421 prior to merging. #19576 (thanks!) should have fixed this discrepancy? (Edit: i.e. the |
The former methods only supported We could continue to use |
@nalimilan, could you clarify this example? Thanks! |
@Sacha0 Good catch, I copied/pasted an incorrect output. This should have been
We should definitely get rid of any inconsistencies. I don't think there's a strong reason which prevents extending |
I think, in a nutshell, Please also see #19595 which I have opened to continue the general discussion. |
This pull request provides (1) a new
broadcast
/broadcast!
implementation specialized for a pair of (input) sparse matrices, and (2) a generalbroadcast
/broadcast!
implementation capable of efficiently handling an arbitrary number of sparse matrices (within compiler limits). This pull request also integrates thesebroadcast
/broadcast!
methods with the newmap
/map!
implementation specialized for sparse matrices, and touches up a few rough edges of the latter.The semantics of these
broadcast!
methods are those discussed in #19372. Notably thesebroadcast!
methods only allocate when strictly necessary, so for exampleAdditionally, where these
broadcast!
methods do allocate, they do so slightly less pessimistically than the existingbroadcast!
methods. [1](Edited) For operations that yield zero when at least one argument is zero (like
*
), the newbroadcast
/broadcast!
methods specialized for a pair of (input) sparse matrices achieve substantially better performance than corresponding existingbroadcast
/broadcast!
methods (despite performing slightly more work to enable more precise allocation behavior).(Edited) For operations that yield zero only when all arguments are zero (like
+
), the newbroadcast
/broadcast!
methods specialized for a pair of (input) sparse matrices achieve performance comparable to corresponding existingbroadcast
/broadcast!
methods. The former win in some cases and the latter in others, with runtime differences usually within ~30% in (hopefully realistic) tests.The general
broadcast
/broadcast!
methods achieve performance not too far behind the new methods specialized for pairs of (input) sparse matrices. The former take from 1% to 80% again the latter's runtime, with performance for small matrices falling in the middle to slower half of that range, and performance for large matrices falling in the faster quarter to faster half of that range.Relevant to #16285 and friends as a (near-final?) step in the sparse
broadcast
/broadcast!
overhaul. Best![1]
broadcast
/broadcast!
could allocate still less pessimistically, but whether the additional code complexity would be worth the improvement is not clear. An extreme example:broadcast[!]
operations that both allocate and involve one-by-one sparse matrices allocate enough storage for a dense result, though that often isn't necessary. Implementing smarter behavior for that case is straightforward but complicates the code somewhat, andbroadcast
ing with one-by-one sparse matrices seems like a marginal case.