-
-
Notifications
You must be signed in to change notification settings - Fork 187
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
Generalize matrix function signatures #1470
Comments
+1 for this. We also need to do this to allow support for sparse matrices. @SteveBronder has been working on that along with perfect forwarding so we can reduce copying and exploit move semantics wherever possible. |
We'd need to start coding a bit more defensively after implementing this, otherwise there's the risk of accidentally evaluating the expression multiple times. Take
If |
Right. I plan to do that in each function as I generalize its signature. At that time I can also make sure an expression is not used twice. Although I don't think just taking size of an expression evaluates it. I also agree with the need to document this. Any idea where the doc should be put? |
Absolutely. Is the pattern to do something like:
Eigen::Matrix<T::Scalar, -1, 1> x_eval = x.eval()
and then use that in the rest of the function?
Evaluating `.size()` shouldn't need any evaluation. The expression template will already be evaluated and should at least know its size. `.maxCoeff()` is another story. So we'll have to dig in and see what's costly to evaluate and what's not.
It would be a service to devs to update the developer-facing doc. I don't think this calls for any API-level doc changes other than of the acceptable input types for x here in log_sum_exp.
… On Nov 29, 2019, at 6:05 PM, Andrew Johnson ***@***.***> wrote:
We'd need to start coding a bit more defensively after implementing this, otherwise there's the risk of accidentally evaluating the expression multiple times. Take log_sum_exp for example:
template <typename T>
auto log_sum_exp(T& x) {
if (x.size() == 0) {
return -std::numeric_limits<double>::infinity();
}
const double max = x.maxCoeff();
if (!std::isfinite(max)) {
return max;
}
return max + std::log((x.array() - max).exp().sum());
}
If log_sum_exp is passed an expression, something like log_sum_exp(m1.array().exp().matrix() * m2), then that expression will get evaluated three times: x.size(), x.maxCoeff(), and the final return statement. So we'd need to start evaluating the expression at the beginning of the function, and probably also updating the documentation so that other/new developers know why
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
:-) I didn't see this before responding. We're on the same page w.r.t. size(). I think by "evaluate" we mean evaluate operator()(i, j) for every (i, j) pair. It always gets constructed.
… On Nov 30, 2019, at 4:00 AM, Tadej Ciglarič ***@***.***> wrote:
Right. I plan to do that in each function as I generalize its signature. At that time I can also make sure an expression is not used twice. Although I don't think just taking size of an expression evaluates it.
I also agree with the need to document this. Any idea where the doc should be put?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
I have a little info on adding new docs to the stan site here https://github.com/stan-dev/math/blob/develop/doxygen/howto_add_page.md I wonder if for some functions we use so much info about the matrices it would be better to leave their template arguments as |
If a function needs to evaluate an argument and the template params for that arguments are known (not templated) we can just leave that argument as is - |
You could also use the
Although some of the copies might be too expensive to be worth it |
First, I thought What are the rules for executing into temporaries? I wouldn't have thought any of those template expressions would cause an intermediate.
|
This page says:
|
Thanks---that really clarifies what's going on. I wish we'd figured this out a few years ago!
… On Dec 4, 2019, at 2:24 AM, Tadej Ciglarič ***@***.***> wrote:
This page says:
By default, a Ref can reference any dense vector expression of float having a contiguous memory layout. Likewise, a Ref can reference any column-major dense matrix expression of float whose column's elements are contiguously stored with the possibility to have a constant space in-between each column, i.e. the inner stride must be equal to 1, but the outer stride (or leading dimension) can be greater than the number of rows.
In the const case, if the input expression does not match the above requirement, then it is evaluated into a temporary before being passed to the function.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
I am running out of ideas how to group remaining functions into PRs. So I will just go in alphabetical order. |
Can we make a list of the remaining ones as an issue? I think you put one up somewhere a while ago though I couldn't find it. Would be good so we could divvy up work if anyone else is interested. I started picking apart some of the stuff for |
No, I never made such a list. If you have a list of all functions in Stan Math I am happy to check ones that I already completed. |
I think Steve is reffering to the list of functions we made for the kernel generator. |
Ah yeah nvm wrong one. Yeah I can look through and slam an issue up. We can use it as the issue for the remaining ones |
I don't think we need another issue for this as we have this one. Feel free to either post here or edit top level comment once you make the list. |
We are very close to having all functions accept expressions, so I am thinking about the details of returning expressions. Whenever a function returns an expression referencing a local variable it needs to use holder (see #1775). When a function returns an expression referencing a function argument it gets a bit less clear if a holder is necessary. In majority of use cases (including all possible uses from stan language) everything should work just fine without holder. However in c++ a function can be used in a way that the returned expression is not evaluated before its rvalue argument(s) get deleted. For example this happens if the resulting expression is stored in an
So the question is, should we use holder for such cases (to make code safer) or not (avoiding overhead of creating holder and simplifying code)? @SteveBronder @bbbales2 @andrjohns |
My first reaction is holders cause it's less to worry about then. How does this code compare to something written with Eigen? Like: #include <Eigen/Core>
#include <iostream>
int main() {
Eigen::MatrixXd a = Eigen::MatrixXd::Random(2, 2);
Eigen::MatrixXd b = Eigen::MatrixXd::Random(2, 2);
auto c = (a + b).array().abs().sqrt();
std::cout << c + 1.0 << std::endl;
return 0;
} Is Eigen having to allocate holder things along the way? |
This example would work with eigen. However if you replaced addition with a function that returns a plain matrix (not expression) it would have the same issue. |
Also eigen does not use anything like holder. In functions in Eigen, where such dangers would happen they usually evaluate the result of the function instead of returning an expression. |
Oh interesting. Adding an And what is the overhead of allocating a holder? Is it a malloc and a free, basically? Or is it something more? |
Oh, I overlooked eval in your example. Yeah that is why it fails too. About overhead: more or less just malloc and free. |
Nah nah, I added the |
So using a holder in this case means our softmax would allocate a holder for its argument? Or where would the holder appear in the code? |
Yes softmax would allocate it (if the argument is rvalue). You can look at implementation of |
I've been thinking about this some more. If we use holder for arguments, we need to do perfect forwarding on these arguments, as On the other hand avoiding auto is not that hard, so I am leaning towards not using holder for arguments. |
My cargo culting was not all wrong! 😆 If we really wanted to go this route I think I could come up with a regex that searches over function signatures and replaces Or alt, could we have a It feels like kind of a bummer to ban |
I am still trying to avoid littering all the code with calls to That second paragraph is more or less how Eigen expressions work. That works in most cases but fails as soon as it gets a rvalue plain type.
It would not get completely banned. Just banned unless you know what you are doing. :D
No. Temporaries are destructed after everything in a line of code gets executed. If expression referencing temporaries is evaluated in the same line as these temporaries are created everything works fine. |
Could we have our
Like if you're using stan::math and you're assigning to an I guess that means the difference between us and Eigen then is that we can't have expressions as l-values and Eigen can? |
It can be used like this if you want.
Nope. We are using Eigen expression anyway. |
Well Eigen can do:
And we cannot do:
|
@t4c1 can we start removing the code like return ret_type(ret); In functions that use https://github.com/stan-dev/math/blob/develop/stan/math/rev/fun/elt_multiply.hpp#L42 If it's safe to pass around arena matrices now I can also put up a lil PR in stanc3 that tests making stuff in the parameters block into arena matrices |
Actually wrt stanc3 here's a lil brutal version that uses stan-dev/stanc3@master...SteveBronder:feature/arena-var-matrix We could take off the |
I'm still a bit nervous about this unless something has happened and we're talking about Check this code out:
In both cases the function sets the result value to 1, but this is the output:
The |
Oof yeah now that I think about this, any |
The difference is that
Yeah, Ben is right. We can do it for var. For mat we need the copy. But mat will eventually be gone anyway. |
Also I am assuming you are ok with not using holder for arguments. |
Yeah and is that not true for all our functions? I think what we'd write to document this is:
Now that I write that it doesn't sound that clear, but that's what I'm thinking. I feel like Eigen must have something special going on that we don't cause its expressions merge into bigger and bigger expressions that don't necessarily go away. The reason I'm making the direct comparison to Eigen is it would be cool if we could say "Stan expressions have the same limitations as Eigen expressions", which I don't think is the case cause our functions don't build giant expression types in the same way.
|
Yeah I'm cool with this. It's the same with any oddity. It's fine for the language since we can hide it, and it needs doc'ed for Math so someone doing C++ doesn't get confused and mad. |
Not for all ,but for many it is true. What I am trying to say is it is not that we are doing things things in different way in our functions - we are doing different things.
Actually we are better - we have less limitations! In cases such as softmax that need internal matrices, we will use holder, so that will not be an issue anymore. Eigen's way of doing it would be to simply give up on expressions and eval the result. I guess softmax was not the best example to start this discussion with, as it will need holder anyway. |
Oh okay so we are going to add some holders, just not holders everywhere. Yeah @ me when you put up this pull so I can stare at it a bit. |
It will be up in 5 min :) |
Description
Matrix functions can be generalized to work with Eigen expressions not just matrices. By propagating matrix expressions around we can avoid creating temporary matrices, improving performance. This could also make adding support for sparse matrices easier.
There was some discoussion about this on discourse. Also an abandoned PR #1309. Contrary to this PR I intend to split the this into smaller chunks:
apply_scalar_unary
). Prim implementation of these can in many cases use Eigen implementations that can be faster than std.Example
Instead of :
We can do:
Expected Output
More general and possibly faster code.
Current Version:
v3.0.0
The text was updated successfully, but these errors were encountered: