LU rewrite, second draft#142
Conversation
|
These initial numbers look very promising! Edit: emphasis. |
This commit adds a (for now private) testsupport module. Eventually this module may be made available to the public under a feature flag (e.g. `testsupport`).
For now it only wraps the existing lup_decomp function, without making any invasive changes.
Moved reproducible_random_matrix into this new module (from svd) so that it can be used in other benchmarks.
|
Recent developments: I first implemented the functionality from draft 1 ( #94 ) in this PR, using Here are the benchmark results before replacing I then rewrote these methods and structs to use As we can see, the numbers are better across the board except for |
|
I just realized that currently the |
|
Actually, upon re-running the benchmarks a few times, there seems to be somewhat negligible difference in the performance of |
Also add some inline notes on performance potential for inverse()
|
Another update: I've rewritten the LU code such that it stores both I think what remains now is first and foremost replacing the LU-related functionality in |
|
@AtheMathmo: I just had an idea. Replacing I'm otherwise pretty much done now, I think. There can still be more tests, but I think I'll tackle that in a later PR. The decomposition itself should at least work as well as the existing one. |
|
I think deprecating and removing later is a good idea! I should have some time to review the changes here tomorrow or Tuesday. The improvements here are huge, thanks! |
Andlon
left a comment
There was a problem hiding this comment.
Reminder to fix typo
| //! Since the right-hand side `b` has no bearing on the LU decomposition, | ||
| //! it follows that one can efficiently solve any such system for any `b`. | ||
| //! | ||
| //! It turns out that the matrices `L` and `U` can be stored compact |
There was a problem hiding this comment.
typo: compact -> compactly
Also switched internal use of lup_decomp with PartialPivLu.
|
I deprecated let lu = PartialPivLu::decompose(x);both clean, explicit and clear in terms of intent. It also works well once we have e.g. rook-pivoting or full pivoting implementations in place later on, because there are never any doubts as to what e.g. |
|
I think this does make sense. One of the only reasons I had for preferring a function on |
AtheMathmo
left a comment
There was a problem hiding this comment.
Only some minor comments and questions.
For the most part this looks really, really good!
| } | ||
| } | ||
|
|
||
| // TODO: assert is_lower_triangular |
There was a problem hiding this comment.
Are these TODOs still pending as part of this PR?
There was a problem hiding this comment.
I was a bit unsure how to handle it. Currently I put the is_lower_triangular and is_upper_triangular functions in the internal testsupport module, so they're not currently accessible from benches. My plan for testsupport was eventually to make it a public module under a "testsupport" feature flag, but this maybe needs some discussion. I was planning to let it house e.g. Arbitrary implementations of matrix types in the future.
However, perhaps they're
There was a problem hiding this comment.
I think this comment got cut off.
There was a problem hiding this comment.
Yeah. Hmm, I have no idea what I was planning to say.
In any case, it's not ... essential, but it would be a good idea to be able to double-check that the matrices generated for the benchmarks are actually triangular. It really depends on what we decide to do with the is_*_triangular functions (see my main response outside of individual commit comments).
|
|
||
| let b = try!(forward_substitution(&l, p * y)); | ||
| back_substitution(&u, b) | ||
| PartialPivLu::decompose(self)?.solve(y) |
There was a problem hiding this comment.
I just wanted to point out that I haven't used the ? operator anywhere else in rulinalg.
I actually quite like it but am wary of getting into a situation in which we have the two syntax heavily mixed throughout. Do you have any thoughts? I'm happy to leave the ? operator here and consider ways to resolve this in the future (probably switching all try macro invocations to ?).
There was a problem hiding this comment.
I actually kinda like them both, for different purposes. I think ? is superior for chaining (like in the above example), but try! is more explicit when you want to store the result of the computation, whereas in this case the question mark is easy to miss. E.g:
let lu = try!(PartialPivLu::decompose(self));
// Do something with lu
// vs
let lu = PartialPivLu::decompose(self)?;
// Do something with luIn any case, I don't see anything particularly troublesome with having both operators in the code base. I do have the impression that the community tends to prefer ? though, could that be right...?
There was a problem hiding this comment.
I also get the impression that most people prefer ?.
I actually agree with you that they work well in different situations. I would say that for now we avoid having any strong opinions on this :)
| pub mod ulp; | ||
| pub mod norm; | ||
|
|
||
| mod testsupport; |
There was a problem hiding this comment.
Perhaps it is the name that is throwing me off - wouldn't we prefer this to be under the test feature flag?
Otherwise I'm wondering why we want this to be so distinct from the BaseMatrix trait? Should we not add these as functions of this trait - or perhaps a new trait which contains these algebraic condition tests? (is_positive_definite, is_upper_triangular, is_singular, etc.).
There was a problem hiding this comment.
Well, my idea was actually to make this a pub module, put it under a feature flag (e.g. "testsupport") and populate it with things that are useful not just for ourselves, but also for users of rulinalg. For instance, someone writing applications involving linear algebra might want to use the Arbitrary implementations that we were talking about in the other thread to test their own code.
Currently, there's not much sense to it as it is though. I suppose it could be put under cfg(test) for now...? See also the above issue with the TODOs in benches.
There was a problem hiding this comment.
We might want to add a new trait, yeah. Although this would technically be a breaking change, I think, although a rather minor one (it only breaks if someone has implemented a trait for Matrix, MatrixSlice or MatrixSliceMut which has the same method names as the new trait).
I'd much rather make it a new trait than add it to the base matrix trait, though. Perhaps mainly for checking structural constraints, because checking for e.g. positive definiteness or singularity is very expensive, being O(n3) operations. However, O(n2) checks like upper/lower triangularity, diagonality (there is already is_diag() I think. We could deprecate it and create is_diagonal() in this new trait) could definitely be useful.
| /// Returns true if the matrix is lower triangular, otherwise false. | ||
| /// This generalizes to rectangular matrices, in which case | ||
| /// it returns true if the matrix is lower trapezoidal. | ||
| #[allow(dead_code)] |
There was a problem hiding this comment.
Would you mind explaining why we need this attribute? I'm not against it being here but would like to follow the motivation.
There was a problem hiding this comment.
Only because currently the functions here are only used in test code, so when you compile without test, it warns about unused functions. Depending on the outcome of the two other issues related to testsupport, this can probably be removed!
| //! the system `Ax = b` can be computed in O(n<sup>2</sup>) floating | ||
| //! point operations if the LU decomposition has already been obtained. | ||
| //! Since the right-hand side `b` has no bearing on the LU decomposition, | ||
| //! it follows that one can efficiently solve any such system for any `b`. |
There was a problem hiding this comment.
Minor language point. I think this reads better as: it follows that one can efficiently solve this system for any b. I think?
There was a problem hiding this comment.
Definitely agree :)
| } | ||
| } | ||
|
|
||
| // TODO: Remove Any bound (cannot for the time being, since |
There was a problem hiding this comment.
I think this Any bound is propagated from our implementation of matrix multiplication. We need to do some dynamic type comparison to call the correct matrixmultiply function.
We might be able to trim the bound in some places, for example back_substitution seems unlikely to need it.
There was a problem hiding this comment.
Ah, yes. Because of some changes to TypeId in Rust ... 1.8 or 1.9 maybe?, it only needs to be 'static now, you don't need Any anymore, so we could probably make this a crate-wide change. See here: https://doc.rust-lang.org/std/any/struct.TypeId.html . The example is actually out-of-date, but if you look at the signature it's only 'static and not Any for the trait bound.
There was a problem hiding this comment.
Oh nice spot! Yes, I'll write up an issue to remove the Any trait bounds.
| assert!(b.size() == self.lu.rows(), | ||
| "Right-hand side vector must have compatible size."); | ||
| // Note that applying p here implicitly incurs a clone. | ||
| // TODO: Is it possible to avoid the clone somehow? |
There was a problem hiding this comment.
I don't see where the clone is here. I'm guessing you mean as part of &self.p * b?
If so, this seems fairly minor to me but it is worth keeping the note here.
There was a problem hiding this comment.
You're right, that's it.
I agree it's minor, and we shouldn't worry about it now. However, solving linear systems is the typical kind of thing which happens in some kind of loop (say, for every time step), and so cloning something on every iteration is unnecessary. For a desktop computer, this isn't really a problem, but for e.g. resource constrained environments or real-time applications you'd definitely want to avoid this.
Anyway, this is low priority, but we should think about in the future at some point. I was actually considering adding a solve_into method which would take a buffer to store the solution in, which would avoid the implicit clone.
There was a problem hiding this comment.
I see your point. A solve_into function sounds like a pretty reasonable solution (if I understand correctly).
There was a problem hiding this comment.
I'll mock it up and see how it looks. The existing solve can be implemented by a single call to solve_into, which is convenient.
There was a problem hiding this comment.
Because backward substitution takes ownership, it cannot be easily implemented at the moment if one wishes to take a mutable reference to the buffer. I think I'll just leave it as it is for now. We can revisit in the future. It's just a minor thing at this point anyway :)
| impl<T: 'static + Float> PartialPivLu<T> { | ||
| /// Performs the decomposition. | ||
| /// | ||
| /// ### Panics |
There was a problem hiding this comment.
Elsewhere in rulinalg we've used h1 headers here (# Panics instead of ### Panics). It's fairly trivial but we should probably be consistent.
There was a problem hiding this comment.
Yeah, I agree. I'll change it to make it consistent.
I think I was toying around with header sizes to see what looked better. I think unfortunately there are some visual design issues with rustdoc. When using H1 headers, the headers are very distracting because you get visual spacing in the wrong places. This is however a design problem that needs to be solved at the rustdoc level, and not at the local crate level, so I will definitely change this back to H1.
There was a problem hiding this comment.
I agree that it does look a little weird in some places but as you point out consistency is at the local crate level is more important.
| // Note that this is not optimal in terms of performance, | ||
| // and there is likely significant potential for improvement. | ||
| // | ||
| // A more performant technique is usually to compute the |
There was a problem hiding this comment.
Thank you for pointing this out!
| //! | ||
| //! 3. [Computation of the SVD] | ||
| //! (http://www.cs.utexas.edu/users/inderjit/public_papers/HLA_SVD.pdf) | ||
| //! Decompositions in `rulinalg` are in general modeled after |
|
Thanks for the comments! I think the main issue to sort out is what to do with the pub trait MatrixInvariants : BaseMatrix {
fn is_lower_triangular(&self) { ... }
fn is_upper_triangular(&self) { ... }
fn is_diagonal(&self) { ... }
fn is_symmetric(&self) { ... }
}Like I said in the other comment, this is technically a breaking change, but it is a very mild one, in that it only breaks if someone implements a trait for Alternatively we could just put What do you think? EDIT: drop |
|
I'm a little unsure how to tackle the Right now my decision would be to tackle the new trait in a new PR. For this PR I think it is best to flag Oh and I'm not sure about the name |
|
All right, sounds good! I'll get it done right away. Huh, I've never come across that term. TIL :) |
Allow using deprecated lup_decomp in tests for lup_decomp
|
OK, so I think I've taken care of the things you pointed out. Let me know if I missed anything. The only thing is that I've left the TODO in the benchmark utility functions for now. They should ideally assert that they are triangular, but that would mean exposing |
|
Looks like I just gave this a conflict - I think it is from the column iterator reformatting. |
|
No worries, I'll take care of it next chance I get! |
Currently doesn't contain any code pertaining to LU whatsoever, but rather some benchmarks for multiplication of a vector by a permutation matrix. The benchmarks are intended to compare performance between using the new
PermutationMatrixstruct and a full matrix representation of the permutation, and further motivate the use ofPermutationMatrixin the LU decomposition. I will continue to develop the LU decomposition changes in this PR. Please see #94 for the first (unfinished) draft.PermutationMatrixis implemented such that it can be applied in linear time to a vector, whereas full matrix-vector multiplication is quadratic in the size of the vector. The benchmarks support the expectation that thePermutationMatrixapproach vastly outperforms the full matrix approach presumably for alln(size of theVector).Because the cost of applying a
PermutationMatrixis dependent on the specific permutation matrix, there are two kinds of benchmarks: one which applies the identity permutation (which is presumably the cheapest to apply) and one which applies a "perfect shuffle", which one would expect is not overly cache friendly (although perhaps also not the worst example either).Benchmark results: