-
-
Notifications
You must be signed in to change notification settings - Fork 188
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
[WIP] Adds specializations for handling sparse matrix unary operations #2599
base: develop
Are you sure you want to change the base?
Changes from all commits
3c0b5ea
c647ad7
7589e59
d141664
27d2a7b
a7971f2
83eb6fd
e53238e
01f5ea7
6ca05af
b6144b2
03d446d
af21d26
787f3c2
30190e3
3723203
d7adc5e
dc4c178
729456d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,7 @@ | |
#define STAN_MATH_PRIM_FUNCTOR_APPLY_SCALAR_UNARY_HPP | ||
|
||
#include <stan/math/prim/fun/Eigen.hpp> | ||
#include <stan/math/prim/meta/is_eigen.hpp> | ||
#include <stan/math/prim/meta/is_complex.hpp> | ||
#include <stan/math/prim/meta/require_generics.hpp> | ||
#include <stan/math/prim/meta/is_vector.hpp> | ||
#include <stan/math/prim/meta/is_vector_like.hpp> | ||
#include <stan/math/prim/meta/plain_type.hpp> | ||
#include <stan/math/prim/meta.hpp> | ||
#include <utility> | ||
#include <vector> | ||
|
||
|
@@ -36,8 +31,12 @@ namespace math { | |
* | ||
* @tparam F Type of function to apply. | ||
* @tparam T Type of argument to which function is applied. | ||
* @tparam ApplyZero If true, the function applied is assumed to return zero for | ||
* inputs of zero and so sparse matrices will return sparse matrices. A value | ||
* of false will return a dense matrix for sparse matrices. | ||
*/ | ||
template <typename F, typename T, typename Enable = void> | ||
template <typename F, typename T, bool ApplyZero = false, | ||
typename Enable = void> | ||
struct apply_scalar_unary; | ||
|
||
/** | ||
|
@@ -48,8 +47,8 @@ struct apply_scalar_unary; | |
* @tparam F Type of function to apply. | ||
* @tparam T Type of argument to which function is applied. | ||
*/ | ||
template <typename F, typename T> | ||
struct apply_scalar_unary<F, T, require_eigen_t<T>> { | ||
template <typename F, typename T, bool ApplyZero> | ||
struct apply_scalar_unary<F, T, ApplyZero, require_eigen_t<T>> { | ||
/** | ||
* Type of underlying scalar for the matrix type T. | ||
*/ | ||
|
@@ -59,15 +58,82 @@ struct apply_scalar_unary<F, T, require_eigen_t<T>> { | |
* Return the result of applying the function defined by the | ||
* template parameter F to the specified matrix argument. | ||
* | ||
* @tparam DenseMat A type derived from `Eigen::DenseBase`. | ||
* @param x Matrix to which operation is applied. | ||
* @return Componentwise application of the function specified | ||
* by F to the specified matrix. | ||
*/ | ||
static inline auto apply(const T& x) { | ||
template <typename DenseMat, require_eigen_dense_base_t<DenseMat>* = nullptr> | ||
static inline auto apply(const DenseMat& x) { | ||
return x.unaryExpr( | ||
[](scalar_t x) { return apply_scalar_unary<F, scalar_t>::apply(x); }); | ||
} | ||
|
||
/** | ||
* Special case for `ApplyZero` set to true, returning a full sparse matrix. | ||
* Return the result of applying the function defined by the template | ||
* parameter F to the specified matrix argument. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The doc for both overloads state that a sparse matrix is returned, so might need to clarify which is which |
||
* | ||
* @param SparseMat A type derived from `Eigen::SparseMatrixBase` | ||
* @tparam NonZeroZero Shortcut trick for using class template for deduction, | ||
* should not be set manually. | ||
* @param x Matrix to which operation is applied. | ||
* @return Componentwise application of the function specified | ||
* by F to the specified matrix. | ||
*/ | ||
template <typename SparseMat, bool NonZeroZero = ApplyZero, | ||
require_t<bool_constant<NonZeroZero>>* = nullptr, | ||
require_eigen_sparse_base_t<SparseMat>* = nullptr> | ||
static inline auto apply(const SparseMat& x) { | ||
using val_t = value_type_t<SparseMat>; | ||
using triplet_t = Eigen::Triplet<val_t>; | ||
auto zeroed_val = apply_scalar_unary<F, scalar_t>::apply(val_t(0.0)); | ||
const auto x_size = x.size(); | ||
std::vector<triplet_t> triplet_list(x_size, triplet_t(0, 0, zeroed_val)); | ||
for (Eigen::Index i = 0; i < x.rows(); ++i) { | ||
for (Eigen::Index j = 0; j < x.cols(); ++j) { | ||
// Column major order | ||
triplet_list[i * x.cols() + j] = triplet_t(i, j, zeroed_val); | ||
} | ||
} | ||
for (Eigen::Index k = 0; k < x.outerSize(); ++k) { | ||
for (typename SparseMat::InnerIterator it(x, k); it; ++it) { | ||
triplet_list[it.row() * x.cols() + it.col()] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than iterating over each element, would it be more performant to Map the values as a vector and call the vectorised implementation? For example: Map<Eigen::VectorXd> mapSparse(x.valuePtr(), x.nonZeros());
apply_scalar_unary<F, Eigen::VectorXd>::apply(mapSparse); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would def be better, though I avoided it because I was a bit confused about the compressed sparse matrix scheme Eigen uses. If you look at their docs here under 'The SparseMatrix class' they sometimes allow empty values so its easier to write to the sparse matrix like
I was worried that directly grabbing the values could lead to some bad things since there may be places in the value pointer that are not initialized. Another issue is that our So I agree what your saying here is def faster, but imo for this first pass I'd rather do the safer path atm. Once we have everything working at the language level and know more about our constraints and assumptions we can make then we can come back and do these kinds of optimizations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, okay now I'm thinking about this more and I think that at the stan language level we can make it so that you can only assign to elements that are already non-zero. So I wonder if what we can do here is just assume that we are always working with compressed matrices, since then we don't have to worry about the uninitialized values and can do the fast thing safely |
||
= triplet_t(it.row(), it.col(), | ||
apply_scalar_unary<F, scalar_t>::apply(it.value())); | ||
} | ||
} | ||
plain_type_t<SparseMat> ret(x.rows(), x.cols()); | ||
ret.setFromTriplets(triplet_list.begin(), triplet_list.end()); | ||
return ret; | ||
} | ||
|
||
/** | ||
* Special case for `ApplyZero` set to false, returning a sparse matrix. | ||
* Return the result of applying the function defined by the template | ||
* parameter F to the specified matrix argument. | ||
* | ||
* @tparam SparseMat A type derived from `Eigen::SparseMatrixBase` | ||
* @tparam NonZeroZero Shortcut trick for using class template for deduction, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doc type name doesn't match function declaration. Also, neat trick! |
||
* should not be set manually. | ||
* @param x Matrix to which operation is applied. | ||
* @return Componentwise application of the function specified | ||
* by F to the specified matrix. | ||
*/ | ||
template <typename SparseMat, bool ReturnZeros = ApplyZero, | ||
require_t<bool_constant<!ReturnZeros>>* = nullptr, | ||
require_eigen_sparse_base_t<SparseMat>* = nullptr> | ||
static inline auto apply(const SparseMat& x) { | ||
auto ret = x.eval(); | ||
for (Eigen::Index k = 0; k < x.outerSize(); ++k) { | ||
for (typename SparseMat::InnerIterator it(x, k), ret_it(ret, k); it; | ||
++it, ++ret_it) { | ||
ret_it.valueRef() = apply_scalar_unary<F, scalar_t>::apply(it.value()); | ||
} | ||
} | ||
return ret; | ||
} | ||
|
||
/** | ||
* Return type for applying the function elementwise to a matrix | ||
* expression template of type T. | ||
|
@@ -82,8 +148,8 @@ struct apply_scalar_unary<F, T, require_eigen_t<T>> { | |
* | ||
* @tparam F Type of function defining static apply function. | ||
*/ | ||
template <typename F, typename T> | ||
struct apply_scalar_unary<F, T, require_floating_point_t<T>> { | ||
template <typename F, typename T, bool ApplyZero> | ||
struct apply_scalar_unary<F, T, ApplyZero, require_floating_point_t<T>> { | ||
/** | ||
* The return type, double. | ||
*/ | ||
|
@@ -107,8 +173,8 @@ struct apply_scalar_unary<F, T, require_floating_point_t<T>> { | |
* | ||
* @tparam F Type of function defining static apply function. | ||
*/ | ||
template <typename F, typename T> | ||
struct apply_scalar_unary<F, T, require_complex_t<T>> { | ||
template <typename F, typename T, bool ApplyZero> | ||
struct apply_scalar_unary<F, T, ApplyZero, require_complex_t<T>> { | ||
/** | ||
* The return type, double. | ||
*/ | ||
|
@@ -134,8 +200,8 @@ struct apply_scalar_unary<F, T, require_complex_t<T>> { | |
* | ||
* @tparam F Type of function defining static apply function. | ||
*/ | ||
template <typename F, typename T> | ||
struct apply_scalar_unary<F, T, require_integral_t<T>> { | ||
template <typename F, typename T, bool ApplyZero> | ||
struct apply_scalar_unary<F, T, ApplyZero, require_integral_t<T>> { | ||
/** | ||
* The return type, double. | ||
*/ | ||
|
@@ -162,8 +228,8 @@ struct apply_scalar_unary<F, T, require_integral_t<T>> { | |
* @tparam F Class defining a static apply function. | ||
* @tparam T Type of element contained in standard vector. | ||
*/ | ||
template <typename F, typename T> | ||
struct apply_scalar_unary<F, std::vector<T>> { | ||
template <typename F, typename T, bool ApplyZero> | ||
struct apply_scalar_unary<F, std::vector<T>, ApplyZero, void> { | ||
/** | ||
* Return type, which is calculated recursively as a standard | ||
* vector of the return type of the contained type T. | ||
|
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 this might be a bit clearer if the naming was
ReturnSparse
orReturnDense
(or something similar), so that it's explicit that the flag will affect the return typeThere 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.
Sorry I need to update the docs and this PRs summary. After discussion with @bob-carpenter, @nhuurre, and the Eigen dev team (mostly in #2597) I think it makes more sense to return a sparse matrix with all entries filled in. I do kind of wonder if we should have something like exp_nonzero(sparse_matrix) available at the Stan language level where it will only run over the nonzero entries. But we can sort that out at a later time