-
-
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
RFC/WIP: make setindex! not remove zeros from sparsity pattern #15568
Conversation
@@ -2676,7 +2610,7 @@ function setindex!{Tv,Ti}(A::SparseMatrixCSC{Tv,Ti}, x, I::AbstractMatrix{Bool}) | |||
(xidx > n) && break | |||
end # for col in 1:A.n | |||
|
|||
if (nadd != 0) || (ndel != 0) | |||
if (nadd != 0) | |||
n = length(nzvalB) | |||
if n > (bidx-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.
TODO: Can this happen anymore?
bcbb39b
to
adc7508
Compare
Right now I went with the hardcore option. Basically, always do the same no matter what the value is. The middle ground is to not introduce any new stored elements in the matrix when possible, e.g: A = spzeros(2,2)
A[1,1] = 0
nnz(A) == 0 |
+1 this seems like the sanest approach coupled with explicit removal of zeros with |
What does this do for e.g. |
Indexing with a sparse vector promotes it to a sparse matrix. Right now it currently removes stored elements in the matrix. Actually, Indexing with a dense matrix converts it to a sparse matrix and calls the same method. Should probably be changed for consistency. I got some more stuff for |
I think people might find it weird that |
Yeah, there's a bit of a distinction here between the scalar case where a stored zero makes sense, vs the vector/submatrix case where you probably do want two different syntaxes for either storing zeros from the input or not. |
Would it be better to just start off with making |
Does closing #9906 require anything beyond this pull request and #16947? This PR appears blocked on clarification of desired
(1) appears uncontroversial. Consensus that (2) appears to be the sticking point. Concrete proposal for discussion: Re. (1), Arguments: As long as these aspects of behavior are clearly conceptually separated, this proposal is simple and consistent; that these two aspects of behavior have not always been clearly separated in preceding discussion seems to have been part of the bottleneck. Having Having Easy incremental injection of zeros does not require having Thoughts? Thanks, and best! |
So, in short, |
Awesome rephrase! :) |
+1 for a clear summary and position statement. Re. 1, I agree, that's definitely the right behavior. Re. 2, I disagree. Instead, I argue the behavior of I also support creating an |
Hitting zero exactly is not that uncommon: multiplying by 0.0 does it. However, in sums hitting 0.0 is indeed not guaranteed. If the sparsity structure should depend on sum==0.0, then it is the coder's responsibility to set those values to 0.0 if close enough. So, I'm +1 for @Sacha0's proposal. |
@JaredCrean2 What's the problem with "an inconsistent sparsity structure"? I don't see any drawback to storing less zero entries when possible. Any correct algorithm will return the same result disregarding the sparsity structure, only faster if we store less zeros. |
One "problem" is that the time complexity to assign a nonzero to an index then depends on the floating point value you previously assigned to that index. |
Yes, but always choosing the slowest path doesn't sound worth it... |
The other problem is memory consumption. Adding new non-zeros requires dynamically growing the vectors occasionally. The inconsistent sparsity structure leads to the matrix sometimes requiring x Gb of memory, but sometimes requiring 1.5x (or whatever the growth factor is). The difference becomes more pronounced as the matrix gets larger too. |
How often do you really introduce a new stored value through indexing though? Isn't the most common way to use |
I agree this should be quite rare, and my preferred implementation of Either way, this PR is definitely a step in the right direction, and once |
I appreciate and sympathize with the principle underlying this argument. Practical considerations should temper principle, however, and when weighing the tradeoffs the scales fall heavily in favor of (2): Relative to the 'hardcore' option, under (2) assignment values impact
(To differentiate the preceding two cases from behaviors (1) and (2) at issue, I will call these (A) and (B).) Moreover, consider that in the rare case where (B) does lead to a difference in a Additionally, consider that the One should also flip the structural consistency argument on its head: In the generic operations where these concerns are relevant, (2) should rarely lead to deviation from strict logical structure (and marginal deviation then), whereas the 'hardcore' option should almost always lead to loss of underlying structure (and frequently complete loss). In summary, on one side of the tradeoff scales we have a rare edge case in which results remain correct but a marginal and largely invisible underlying structure difference may exist. On the other side we lose generic programming, lose both time and memory efficiency, operations that should work turn into slow death by disk thrashing on accidental densification of large Concerning specific points:
The former case decomposes into two subcases: (1) the typical case of multiplying by a logical zero, which you would hope to preserve (as behavior (2) does); and (2) the rare case of multiplying by a miraculous zero, rare by nature of only occuring when miraculous cancellation occurs upstream in the calculation, itself rare. So perhaps a more precise statement would be: Hitting zero exactly when computing with logical zeros is common and the resulting zeros are logical and should be preserved, while hitting zero exactly outside of that case is rare.
Echoing
Thanks for reading! :) Best! |
Interesting. Of the arguments (and the are all quite good), the one I find most compelling is the impact on generic programming. The purpose of sparse matrices, broadly speaking, is to take operations that are O(N^2) or slower and try to make them O(nonzeros). The purpose of generic programming, in the particular case of sparse and dense matrices, is to facilitate applying algorithms with the wrong time complexity. I would rather have sparse matrices with a more limited set of operations defined on them, all of which have the right time complexity, than have more operations, some of which take an order of magnitude more time than they should. Generic programming is valuable for providing a unified interface to sets of similar objects, and Julia is a particularly good language for doing so, but unifying the interfaces to dissimilar objects decreases usability. However, if supporting generic programming between dense arrays and sparse arrays is the goal, then your original proposal for 2 is necessary to avoid densification.
Ah, this gets to the other half of the question. I've always though that Edit: fix bad grammar |
Even if only indexing actual non-zeros, |
I disagree, assembly into a global stiffness matrix from local smaller matrices is typically done with normal |
You aren't doing assembly in terms of index lists? |
I remember the previous linear index computed so I can half the search range in case I am in the same column (probably the same as what doing it with an index list does). |
This use falls under 'high-level convenience': You could implement the same assembly approach via the low-level interface, doing so would only require more development time. Moreover, a specialized implementation via the low-level interface could achieve higher efficiency via elimination or streamlining of some of the logic in
See e.g. Duff, Erisman, and Reid's book on sparse direct methods, or skim the HSL catalogue and archive for methods relevant to finite element matrices. I am only aware of the assembly approach you describe being used in high-level languages / where assembly is not a computational bottleneck --- which it usually isn't, so trading some computational efficiency for development time is indeed the right approach there. That statement provides a nice segue to...
This is really nicely stated! I appreciate and sympathize with the principle underlying this argument as well. Here I would again argue practical considerations motivate temperance: Suppose you need to perform an operation on a In situations like the above, your time is more valuable than the machine's time. The operation need not execute with optimal complexity and efficiency, it just needs to work. Were analogs of the above situation not common in practice, languages like Julia, Python, Mathematica, MATLAB, etcetera would not have the appeal they do. Concern with optimal complexity and efficiency (in preference to human time) should be overriding only where suboptimal complexity and inefficiency make a task computationally intractable or otherwise prohibitively expensive. Impeding the language's ability to work with
.
I share these questions, and hope that the Julia community will develop answers to them. Re. cartesian indexing adaptation, Thanks again for reading! Best! Edits: Improved poor sentence structure that led to unintended hyperbole. Removed some off topic bits. Made more concise. |
Here is where I distinguish between slow languages like Python and Matlab and fast languages like Julia and C. In the slow languages, there is a (well founded) assumption that speed is not important to users. For fast languages, this is not so, and that puts much more stringent requirements on the design of the standard library. For a performance analysis, I see two main axes: expense of calculating the entries and sparsity of the matrix. Consider:
On this basis, I reject the
argument. In your original post (pre-edits), you make the argument that premature optimization is the root of all evil, and you are quite correct. Using sparse matrices is the optimization. For users who want things to just work, using dense matrices is the better option. Getting the benefits of sparsity requires taking advantage of sparsity at every stage of the computation. For the 4 cases there are only 2 outcomes, either sparsity is essential and a dense operation would be big problem, or the benefits of sparsity are marginal at best and the user would be better off with a dense matrix. This is why I don't think the interface design should prioritize generic algorithms between sparse and dense matrices, because sparsity should only be employed when optimal time complexity is required. |
Generality matters. Julia is both a fast language and a high-level convenient one, and we shouldn't be designing API's that look exactly like C. Often a generic algorithm written as if all matrices are dense will be correct but slow for sparse matrices, and that's not ideal but we can always specialize on the implementation to fix the performance in the sparse case. Correctness is easier to do first though. Writing generic algorithms that can also automatically be sparsity-aware will hopefully come with time. Exact equality with zero and approximate equality with zero are more noticeably different with sparse matrices than with dense ones when it comes to performance. I think that comes with the territory. I'm in favor of the "avoid changing the sparsity structure if you don't absolutely have to" criteria here. |
It seems majority opinion is in favor of sacha0s original criteria, so lets merge the PR. |
I am on vacation (currently outside Grand Canyon) and wont be at a computer for 10 days so easiest to get this through is if someone can rebase this / take over this PR. |
@tkelman, I assume this change would arrive too late to join the other indexing-semantics changes in 0.5? If not too late, I should be able to rework this PR in the next few days.
Enjoy! |
If you're quick, I don't think this would be disruptive enough that it would absolutely have to wait until after we branch, as long as it doesn't hurt performance (gut feeling says it should only make things better). |
Superseded by #17404 |
Brief response for posterity: Reducing runtime is not the only motivation for using sparse data structures; reducing memory use is another and equally important. If forced to use a dense data structure for some dataset, you may run out of memory well before the runtime of non-intensive generic operations becomes prohibitive. Best! |
Since
sparse
now keeps structural zeros it makes sense forsetindex
to also do this.I also removed
spdelete!
since it is no longer used but maybe it is preferable to keep it anyway?Previous discussion: #9906