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

MIT-licensed sparse() parent method and expert driver #14798

Closed
wants to merge 1 commit into from

Conversation

Sacha0
Copy link
Member

@Sacha0 Sacha0 commented Jan 26, 2016

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 driver sparse! 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 with dropzeros!(sparse(...)) in place of sparse(...) 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:

using Benchmarks
@benchmark sparse([1;100000000], [1;1], [1;1], 100000000, 1, Base.AddFun())
@benchmark mitsparse([1;100000000], [1;1], [1;1], 100000000, 1, Base.AddFun())

(with mitsparse this PR's implementation) yields

================ Benchmark Results ========================
     Time per evaluation: 1.94 s [1.90 s, 1.98 s]
Proportion of time in GC: 9.71% [9.01%, 10.40%]
        Memory allocated: 2.24 gb
   Number of allocations: 13 allocations
       Number of samples: 4
   Number of evaluations: 4
 Time spent benchmarking: 9.67 s

================ Benchmark Results ========================
     Time per evaluation: 735.76 ms [723.62 ms, 747.89 ms]
Proportion of time in GC: 6.50% [5.98%, 7.03%]
        Memory allocated: 762.94 mb
   Number of allocations: 11 allocations
       Number of samples: 12
   Number of evaluations: 12
 Time spent benchmarking: 9.73 s

When 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!

@hayd
Copy link
Member

hayd commented Jan 26, 2016

This is great!

Travis shows a whitespace failure.

git rebase --whitespace=fix HEAD~1

@Sacha0
Copy link
Member Author

Sacha0 commented Jan 26, 2016

Travis shows a whitespace failure.

Derp! Fixed, thanks @hayd!

@KristofferC
Copy link
Member

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
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed, thanks!

@StefanKarpinski
Copy link
Member

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.

@nalimilan
Copy link
Member

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!

@StefanKarpinski
Copy link
Member

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.

@ViralBShah
Copy link
Member

Nicely done!

@tkelman
Copy link
Contributor

tkelman commented Jan 26, 2016

There are test failures that obviously need fixing, so "just merge and fix cosmetic stuff later" is a bit premature.

@StefanKarpinski
Copy link
Member

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.

@Sacha0
Copy link
Member Author

Sacha0 commented Jan 26, 2016

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!

@Sacha0
Copy link
Member Author

Sacha0 commented Jan 26, 2016

I can reproduce the failure locally, but curiously Base.runtests("linalg/arnoldi") succeeds where Base.runtest("linalg") fails. Somehow the former call does not force bounds checking whereas the latter does?

@Sacha0
Copy link
Member Author

Sacha0 commented Jan 26, 2016

The linalg/arnoldi test failure should be fixed. I still wonder about the difference between Base.runtests("linalg/arnoldi") and Base.runtests("linalg") mentioned above. Thanks!

@tkelman
Copy link
Contributor

tkelman commented Jan 27, 2016

If you're using Base.runtests, then I don't think anything unusual happens with bounds checking. More likely, running different sets of tests in the same process results in different random numbers.

@Sacha0
Copy link
Member Author

Sacha0 commented Jan 27, 2016

If you're using Base.runtests, then I don't think anything unusual happens with bounds checking. More likely, running different sets of tests in the same process results in different random numbers.

The issue was an out-of-bounds access. Running with --check-bounds=yes enabled reproduction under all conditions (at the REPL, via Base.runtests("linalg/arnoldi"), and via Base.runtests("linalg")). Without --check-bounds=yes, the issue appeared only during Base.runtests("linalg"). So it seems bounds checking state must have been involved somehow? Moreover, there are no random numbers involved in the offending line? Thanks!

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])

@tkelman
Copy link
Contributor

tkelman commented Jan 27, 2016

Base.runtests("linalg") runs multiple processes, so it may be that the check-bounds flag only gets propagated to child processes when running multiple tests.

"$(length(V)))")))
end

if m == 0 || n == 0 || coolen == 0
Copy link
Contributor

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

Copy link
Member Author

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!

Copy link
Contributor

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)

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed (I think), thanks!

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member Author

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).

Copy link
Contributor

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

Copy link
Member Author

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")))
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed, thanks!

Copy link
Contributor

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"

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Solid now? Thanks!

@Sacha0
Copy link
Member Author

Sacha0 commented Jan 27, 2016

My hunch is that checking the lengths of the input vectors wouldn't be all that expensive compared to the actual work that the function does. But if you've tried that and it's too costly, then it may be a necessary tradeoff.

@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 --check-bounds=yes to partially address this concern. Thoughts? Thanks!

@mbauman
Copy link
Member

mbauman commented Jan 27, 2016

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`.
Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

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.

Copy link
Member Author

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!

Copy link
Member Author

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
Copy link
Contributor

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.

Copy link
Member Author

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!

Copy link
Member

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.

Copy link
Member Author

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!

Copy link
Contributor

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.

Copy link
Member

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!

Copy link
Member Author

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!

@Sacha0 Sacha0 force-pushed the mitsparse branch 2 times, most recently from 5a8b334 to c2fb43e Compare February 24, 2016 18:27
@Sacha0
Copy link
Member Author

Sacha0 commented Feb 24, 2016

Rebased, touched up tests, and added a note to sparse!'s documentation making it clear that output arrays csccolptr, cscrowval, and cscnzval can alias input arrays I, J, and V in space-constrained environments.

`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`.
Copy link
Contributor

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

Copy link
Member Author

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!

Copy link
Contributor

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

Copy link
Member Author

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!``\ .

Copy link
Contributor

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?

Copy link
Member Author

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!

@tkelman
Copy link
Contributor

tkelman commented Feb 25, 2016

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 dropzeros!.

@Sacha0
Copy link
Member Author

Sacha0 commented Feb 25, 2016

Ah, we should really document the change in behavior regarding not dropping zero values any more.

Sure, a line at the end of sparse's documentation noting it retains numerical zeros as structural nonzeros? Or something else?

And maybe it would be a good idea to add new copies of those changed tests but exercising dropzeros!

Shall add these tomorrow. Best!

@tkelman
Copy link
Contributor

tkelman commented Feb 25, 2016

Yeah, in sparse's documentation, and I think this is also worthy of mentioning in NEWS.md.

@Sacha0
Copy link
Member Author

Sacha0 commented Feb 25, 2016

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 sparse's documentation and a mention in NEWS.md regarding the numerical zero retention change. I also added the dropzeros!-exercising test versions mentioned above. Thanks, and best!

@StefanKarpinski
Copy link
Member

Yeah, looks like it can't be reopened by an admin either so you'll have to make a new PR :-\

@Sacha0
Copy link
Member Author

Sacha0 commented Feb 25, 2016

Opened anew (#15242). Apologies for the glitch!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
potential benchmark Could make a good benchmark in BaseBenchmarks sparse Sparse arrays
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants