-
-
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
MIT-licensed sparse() parent method and expert driver #14798
Conversation
This is great! Travis shows a whitespace failure.
|
Derp! Fixed, thanks @hayd! |
Very impressive work! |
sparse(I, J, V, [m, n, combine]) | ||
|
||
Create a sparse matrix `S` of dimensions `m` x `n` such that `S[I[k], J[k]] = V[k]`. The | ||
`combine` function is used to combine duplicates. If `m` and `n` are not specified, they |
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 think the common style does not add indentation here.
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.
Fixed, thanks!
This is excellent work. Let's review the technical aspects of this and not focus too much on cosmetic issues, which can be fixed after merging this. |
Well, these are clearly minor, but they are probably easier to correct while fixing more significant aspects than after merging (when nobody will care enough to make another PR). Great work, BTW, though I'm not in position of evaluating it! |
I just don't want to nitpick a great PR to death with cosmetic stuff, but yes, it can be easier to fix before merging. @Sacha0, if it's easy for you to fix the cosmetic stuff, great. If not, then we can just merge and fix after. |
Nicely done! |
There are test failures that obviously need fixing, so "just merge and fix cosmetic stuff later" is a bit premature. |
Yes, obviously the test failures need to get fixed before merging, but let's focus on those rather than a slew of indentation and formatting issues. |
I greatly appreciate the detailed review / feedback, cosmetic included :). Constructive criticism is a gift; the more and sharper, the more I learn, so much thanks @nalimilan! I will do my best to address the comments. CI is brilliant. I completely missed the arnoldi failure when testing locally! Thanks! |
I can reproduce the failure locally, but curiously |
The |
If you're using |
The issue was an out-of-bounds access. Running with Edit: For easy reference, the offending line: A = sparse([1, 1, 2, 3, 4], [2, 1, 1, 3, 1], [2.0, -1.0, 6.1, 7.0, 1.5]) |
|
"$(length(V)))"))) | ||
end | ||
|
||
if m == 0 || n == 0 || coolen == 0 |
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.
this should throw a BoundsError
ArgumentError
if elements of I
or J
are outside of m
-by-n
edit: my bad, sorry
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.
Fixed in lines 421 and 440 (ArgumentError
-> BoundsError
), 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.
No, if m == 0 || n == 0
this should not return successfully if the input indices are out of bounds (aka if there are any of them)
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.
Ah, I see, thanks! Fixing now.
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.
Fixed (I think), 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 the first check (!isempty(I)
) suffices given the earlier check length(I) == length(J) == length(V)
. But perhaps this is more clear. Thoughts?
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.
Sure, that works. I think the error message can be the same as the non-empty case though, row values I[k] must satisfy 1 <= I[k] <= m
row indices I[k] must satisfy 1 <= I[k] <= m
etc. Also always good to add more tests for this kind of corner case.
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.
After a little thought I might advocate for the distinct, explicit error message in now: The I[k]
-specific error message may be confusing where, for example, m == 2
, n == 0
, and (I,J,V)
= (2, 1, 1.0)
.
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.
The current version that starts "where ..., any entry is necessarily out-of-bounds," reads to me as too meandering for an error message. I'd do something like this
if coolen != 0
if n == 0
throw(BoundsError("column indices J[k] must satisfy 1 <= J[k] <= n"))
elseif m == 0
throw(BoundsError("row indices I[k] must satisfy 1 <= I[k] <= m"))
end
end
edit: except it should be ArgumentError
, whoops
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.
Beautiful. Copied verbatim. Thanks!
@inbounds for k in 1:coolen | ||
Ik, Jk = I[k], J[k] | ||
if 1 > Jk || n < Jk | ||
throw(BoundsError(string("row values I[k] must satisfy 1 <= I[k] <= m"))) |
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.
column indices J[k]…
, no? This also covers the TODO comment below, doesn't it?
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.
Fixed, 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.
Seems not? Both error messages still say "row values" where I agree with @mbauman that one should say "row indices" and the other should say "column indices"
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.
Good catch --- only the I
-> J
part of the comment registered. Thanks! Fixing now.
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.
Solid now? Thanks!
@mbauman I suspect you are correct, at least for most use cases. I would be happy going either way. My philosophy with the expert driver was to get out of the user's way as much as possible, checks included in case the user was doing something unanticipated, e.g. manipulating tiny sparse matrices in a tight loop. I've added a line to the documentation suggesting testing with |
Given that it's not exported, I'm not too worried here. I think it definitely needs those checks if it ever gets exported, but as it stands it's probably fine. |
`length(cscrowval) >= nnz(S)` and `length(cscnzval) >= nnz(S)`; hence, if `nnz(S)` is | ||
unknown at the outset, passing in empty vectors of the appropriate type (`Vector{Ti}()` | ||
and `Vector{Tv}()` respectively) suffices, or calling the `sparse!` method | ||
neglecting `cscrowval` and `cscnzval`. |
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.
These arrays are currently not optional.
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.
Fixed, thanks!
intermediate CSR forms and require `length(csrrowptr) >= m + 1`, | ||
`length(csrcolval) >= length(I)`, and `length(csrnzval >= length(I))`. Input | ||
array `klasttouch`, workspace for the second stage, requires `length(klasttouch) >= n`. | ||
Optional input arrays `csccolptr`, `cscrowval`, and `cscnzval` constitute storage for the |
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.
There's another optional here.
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.
The CSC arrays are currently optional; see the method definitions immediately following the main sparse!
definition. 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.
But there is a method argument style issue with the immediately following definitions. Fixing. Thanks!
@@ -303,7 +310,8 @@ mfe22 = eye(Float64, 2) | |||
@test_throws ArgumentError sparsevec([3,5,7],[0.1,0.0,3.2],4) | |||
|
|||
# issue #5169 | |||
@test nnz(sparse([1,1],[1,2],[0.0,-0.0])) == 0 | |||
# @test nnz(sparse([1,1],[1,2],[0.0,-0.0])) == 0 | |||
# commented following change to sparse() in #14798, also see #12605, #9928, #9906, #6769 |
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.
This PR seems to have fallen off the radar, I think the only thing we were missing was running some package tests and comparing performance on real use cases.
Also for these tests where you've commented them out, I think it would be preferable to change the tested-for value, maybe leaving a comment that points to these issues as being the reason for the value to be what it is after this change.
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.
This PR seems to have fallen off the radar, I think the only thing we were missing was running some package tests and comparing performance on real use cases.
Thanks for the bump! To clarify, was the request for package testing and benchmarking with e.g. Convex.jl directed at me? Apologies if so, I misinterpreted the request. Also if so, how should I go about package testing? Similarly, what benchmarking with Convex.jl would you like to see? Not being a Convex.jl user, I do not know what would constitute a meaningful benchmark.
Also for these tests where you've commented them out, I think it would be preferable to change the tested-for value, maybe leaving a comment that points to these issues as being the reason for the value to be what it is after this change.
Good call --- I will rebase, touch those tests up, and update the PR. Thanks again!
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 can try some benchmarks with my Finite Element code.
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 can try some benchmarks with my Finite Element code.
I would be delighted to see those, particularly if your code can benefit from using sparse!
in place of sparse
. I should have a new version up shortly --- in the process of building and testing. Thanks and best!
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.
No it shouldn't be required for you to run such benchmarks, and convex.jl in particular may have issues on nightly. More directed at other reviewers and people who want to see this merged, we can merge this without doing much benchmarking ahead of time but having some more testing of it in advance would be good to be better informed in case there are any situations where this might end up leading to regressions.
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.
In my code, master and this branch performed almost identically. I don't have time to test the sparse! driver now but it is great it is available!
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.
In my code, master and this branch performed almost identically.
Cheers, thanks!
5a8b334
to
c2fb43e
Compare
Rebased, touched up tests, and added a note to |
`combine` function is used to combine duplicates. If `m` and `n` are not specified, they | ||
are set to `maximum(I)` and `maximum(J)` respectively. If the `combine` function is not | ||
supplied, duplicates are added by default. All elements of `I` must satisfy | ||
`1 <= I[k] <= m`, and all elements of `J` must satisfy `1 <= J[k] <= n`. |
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.
there have been updates to this docstring that you need to incorporate.
make sure the signature is consistent between here and the rst docs and run make docs
to make sure you aren't undoing recent changes
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.
there have been updates to this docstring that you need to incorporate.
make sure the signature is consistent between here and the rst docs and run make docs to make sure you aren't undoing recent changes
First time playing in the doc
sandbox, so apologies in advance for inevitable mistakes. I updated the signature for sparse
in doc/stdlib/arrays.rst and issued make julia-genstdlib
. No complaints about sparse
, though complaints about other things:
UndefVarError(:build_sysimg)
WARNING: Mod Base build_sysimg
INFO: devdocs/sysimg.rst: no docs for build_sysimg(sysimg_path=default_sysimg_path, cpu_target="native", userimg_path=nothing; force=false) in Base
WARNING: Exported method missing doc for Base.StackTraces.StackTrace
WARNING: Exported method missing doc for StackFrame
WARNING: Exported method missing doc for Base.LibGit2.with
WARNING: Exported method missing doc for Base.Docs.doc
WARNING: Exported method missing doc for @threadcall
WARNING: Missing 5 exported doc strings
INFO: Missing 121 unexported doc strings
Guessing that constitutes success, there being no complaints about sparse
? Subsequently issued make docs
, and then make -C doc html
and make -C doc latex
for good measure; all seemed to complete fine. Should that do the trick? Will push after clarifying the sparse!
docs. Thanks again!
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.
check the diff on the rst and make sure you're incorporating rather than overwriting the recent updates to this docstring
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.
Thanks for the pointer! Diff seems solid?
diff --git a/doc/stdlib/arrays.rst b/doc/stdlib/arrays.rst
index 5847201..ee0c291 100644
--- a/doc/stdlib/arrays.rst
+++ b/doc/stdlib/arrays.rst
@@ -855,11 +855,13 @@ Sparse Vectors and Matrices
Sparse vectors and matrices largely support the same set of operations as their
dense counterparts. The following functions are specific to sparse arrays.
-.. function:: sparse(I,J,V,[m,n,combine])
+.. function:: sparse(I, J, V,[ m, n, combine])
.. Docstring generated from Julia source
- Create a sparse matrix ``S`` of dimensions ``m x n`` such that ``S[I[k], J[k]] = V[k]``\ . The ``combine`` function is used to combine duplicates. If ``m`` and ``n`` are not specified, they are set to ``maximum(I)`` and ``maximum(J)`` respectively. If the ``combine`` function is not supplied, ``combine`` defaults to ``+`` unless the elements of ``V`` are Booleans in which case ``combine`` defaults to ``|``\ . All elements of ``I`` must satisfy ``1 <= I[k] <= m``\ , and all elements of ``J`` must satisfy ``1 <= J[k] <= n``\ .
+ Create a sparse matrix ``S`` of dimensions ``m x n`` such that ``S[I[k], J[k]] = V[k]``\ . The ``combine`` function is used to combine duplicates. If ``m`` and ``n`` are not specified, they are set to ``maximum(I)`` and ``maximum(J)`` respectively. If the ``combine`` function is not supplied, ``combine`` defaults to ``+`` unless the elements of ``V`` are Booleans in which case ``combine`` defaults to ``|``\ . All elements of ``I`` must satisfy ``1 <= I[k] <= m``\ , and all elements of ``J`` must satisfy ``1 <= J[k] <= n``\ .
+
+ For additional documentation and an expert driver, see ``Base.SparseArrays.sparse!``\ .
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.
yep. looks like that isn't pushed yet?
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.
Added the doc modification below and pushed. Thanks!
Ah, we should really document the change in behavior regarding not dropping zero values any more. And maybe it would be a good idea to add new copies of those changed tests but exercising |
Sure, a line at the end of
Shall add these tomorrow. Best! |
Yeah, in sparse's documentation, and I think this is also worthy of mentioning in NEWS.md. |
Sorry, I accidentally closed this and pushed a revision prior to noticing. GH will not allow me to reopen this pull request post-push, and a brief search seems to indicate there is no recourse. How should I proceed? Concerning the push, I added a line to |
Yeah, looks like it can't be reopened by an admin either so you'll have to make a new PR :-\ |
Opened anew (#15242). Apologies for the glitch! |
Followup to #13001, #14631, and #14702. This pull request replaces the LGPL-licensed
sparse
parent method with an MIT-licensed version and introduces an expert driversparse!
underlying that method (see documentation for details on the expert driver).In accord with discussion in #12605, #9928, #9906, and #6769, this PR's
sparse
method does not automatically purge numerical zeros. Hence this pull request comments two tests of numerical-zero purging. Rather than simply being commented, perhaps those tests should remain withdropzeros!(sparse(...))
in place ofsparse(...)
and a reference to this PR?Hats off to those who contributed to the existing implementation! Matching its performance in all tested cases was challenging. Benchmarks results, benchmark code; the results should be comprehensible without reading the code. tl;dr: In these benchmarks, this PR's implementation ranges from matching the existing implementation's performance to reducing runtime by ~30%, and seems to allocate approximately
Vector{Ti}(m)
+Vector{Ti}(n)
less storage. Notably the sparser the matrix, the better this PR's implementation's relative performance. Though solving #13400 requires a fundamentally different algorithm, #13400's example serves as an extreme illustration of the preceding point:(with
mitsparse
this PR's implementation) yieldsWhen editing base/sparse/csparse.jl to remove the existing methods, I folded all blocks to avoid peaking at the LGPL code. So someone should check that the removal was graceful given it was blind but for method signatures.
Apologies for responding slowly elsewhere while working on this!