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

Sparse Matrices #8

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
365 changes: 365 additions & 0 deletions designs/0004-sparse-matrices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
- Feature Name: sparse_matrices
- Start Date: August 1st, 2019
- RFC PR: (leave this empty)
- Stan Issue: (leave this empty)

# Summary
[summary]: #summary

Add a sparse matrix type to the Stan language and sparse matrix operations to Stan math to utilize operations that can take advantage of the matrices sparsity structure.

# Motivation
[motivation]: #motivation

Data for models such as [ICAR](https://mc-stan.org/users/documentation/case-studies/icar_stan.html) come in as sparse matrix. (i.e. a large number of elements in the matrix are zero). There are often methods for storing and computing these matrices which utilize the sparsity structure to give better performance and use less memory. Currently we have some Compressed Sparse Row (CSR) [methods](https://mc-stan.org/docs/2_19/functions-reference/sparse-matrices.html) for going from dense matrices to simpler representations that ignore zeros and vice versa. Though the only exposed operation is [`csr_matrix_times_vector`](https://mc-stan.org/docs/2_19/functions-reference/sparse-matrix-arithmetic.html). The CSR methods currently supported are limited and require a good deal of fan-dangling from the user.

A `sparse_matrix` type directly in the stan language would support all existing methods available for `matrix` types. From Dan and Aki's previous [proposal](https://aws1.discourse-cdn.com/standard14/uploads/mc_stan/original/2X/1/13fda4102c8f48e5aadbf0fbe75d3641a187d0a3.pdf) this would include full auto-diff and algebra support for:

- Addition (returns sparse matrix, possibly with different sparsity structure)
- Sparse matrix transpose (returns sparse matrix, different sparsity structure)
- Sparse matrix-vector multiplication (returns dense vector)

Choose a reason for hiding this comment

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

Later there is "Sparse matrix-dense matrix", should this be then also "Sparse matrix-dense vector" or do we assume that if sparse is not mentioned then it is dense.

- Sparse matrix-constant multiplication (returns sparse matrix, same sparsity)

Choose a reason for hiding this comment

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

Is "constant" correct work here? Should it be "real" or "scalar"?

- Sparse matrix-matrix multiplication (returns a sparse matrix that likely has a different sparsity structure)
- Sparse matrix-dense matrix multiplication (should return a dense matrix)
- Sparse matrix-diagonal matrix multiplication on the left and right (returns sparse matrix, same sparsity)
- Sparse inner product and quadratic form (returns scalars)
- Operations to move from sparse to dense matrices and vice versa.
- Fill-reducing reorderings
- A sparse Cholesky for a matrix of doubles.

Choose a reason for hiding this comment

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

Which models would benefit from additional sparse QR and sparse SVD?

- The computation of the log-determinant as the product of Cholesky diagonals
- Sparse linear solves and sparse triangular solves (for sampling from the marginalized out parameters in the generated quantities block)
- Sparse algorithms to compute the required elements of the inverse.
- An implementation of the reverse mode derivative of the log determinant
- Reverse-mode derivative of a Cholesky decomposition of a sparse matrix.
- This is the same algorithm that was initially used in the dense case before Rob implemented Iain Murray’s blocked version. But it will need to be implemented in a “data structure” aware way
- Specialization for Poisson, binomial and maybe gamma likelihoods.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Most of the below comes from the [functional-spec](https://github.com/stan-dev/stan/wiki/Functional-Spec:-Sparse-Matrix-Data-Types) for sparse matrices.

A sparse matrix/vector differs from a standard matrix/vector only in the underlying representation of the data.

Below we go over how sparse matrices can be constructed and operated on within each stan block.


## Data

Sparse matrix types in the Stan language can be constructed in the data block via [Coordinate List](https://en.wikipedia.org/wiki/Sparse_matrix#Coordinate_list_(COO)) notation using the row and column sizes, non-empty row/column indices, and values for those index positions. The non-zero (NZ) row and column indices are brought in as bounds in the `<>`. Setting these as bounds expands the definition of what can go into `<>` and should be discussed more.

```stan
data {
int N; // Rows
int M; // Cols
int K; // number non-empty
int nz_row_ind[K]; // Non-empty row positions
int nz_col_ind[K]; // Non-empty col positions

sparse_matrix<nz_rows=nz_row_ind nz_cols=nz_col_ind>[N, M] A
}
```

With proper IO the data sparsity pattern can be brought in automatically via a json or rdump list of lists.
```stan
data {
int N; // Rows
int M; // Cols
sparse_matrix[N, M] A

Choose a reason for hiding this comment

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

missing ;

}
```

Though not necessary, we can also add sparse vectors.

```stan
int N; // Rows
int K; // number non-empty
int nz_row_ind[K]; // Non-empty row positions
// Can we do this?
sparse_vector<nz_rows=nz_row_ind>[N] B;
```

The above sparse matrix example with values

```stan
N = 5
M = 5
K = 7
// Column major order
nonzero_row_index[K] = [ 2, 3, 1, 3, 5, 3, 2, 5]
nonzero_col_index[K] = [ 1, 1, 2, 2, 3, 4, 5, 5]
val[K] = [22, 7, 3, 5, 14, 1, 17, 8]
```

Would have the dense form of

| col/row | 1 | 2 | 3 | 4 | 5 |
|---------|----|---|----|---|----|
| 1 | 0 | 3 | 0 | 0 | 0 |
| 2 | 22 | 0 | 0 | 0 | 17 |
| 3 | 7 | 5 | 0 | 1 | 0 |
| 4 | 0 | 0 | 0 | 0 | 0 |
| 5 | 0 | 0 | 14 | 0 | 8 |

## Transformed Data

Sparse matrices in this block can be defined dynamically and declared such as

```stan
transformed data {
// Could construct here as well
sparse_matrix<nz_rows=nz_row_ind, nz_cols=nz_col_ind>[N, M] A =
to_sparse_matrix(N, M, nz_col_ind, nz_col_ind, vals);

// Linear Algebra is cool
sparse_matrix[N, N] C = A * A';
// For data this is fine
C[10, 10] = 100.0;
}
```

The assignment operation `C[10, 10] = 100.0;` works fine with Eigen as the implementation leaves room in the arrays for quick insertions. Though because the rest of Stan assumes the amount of coefficients are fixed this should be the only block where sparse matrix access to elements defined as zero valued in the bounds should be allowed.

Because the sparsity pattern is given in the bounds `<>` the above multiplication result `C` will have it's own sparsity pattern deduced from the result of `A * A'`. This is a concern of Eigen and Stan-math and I'm pretty sure would not need anything particular in the stan compiler.

## Parameters, Transformed Parameters, and Generated Quantities

Parameters can be defined as above for data or deduced from the output of other functions.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@seantalts

We don't have type inference yet in Stan - I think so far declarations will all need to be of the same form... not sure. Might want to make an open item of that in the design doc.

@SteveBronder

I think so far declarations will all need to be of the same form

One thing I'm a bit worried by is that users would then have to know the non-zero elements of the Cholesky or any sparse matrix output.

When I'm thinking about this, we need to specify the nonzero rows / columns when we are writing data into a sparse matrix via triplets. But the Eigen declaration for sparse matrices is really just Eigen::SparseMatrix, so it doesn't really need to know the nonzero elements. So doing something like

Eigen::SparseMatrix<double> A = inv_solver.solve(B);

Will not need to know the non-zero elements.

Does it make sense what I'm getting at here? i.e. that specifying the non-zero elements is much more for Eigen matrix initialization than it is for the math library.

@seantalts

Yeah, that makes sense. It's very good that code generation can support not knowing the sparsity ahead of time for Eigen in the cases where it is immediately assigned to something with Eigen sparse structure. It makes it a little more difficult for the compiler to check that this is true, though probably still mostly possible. We'd need to basically add sparsity to our type system somehow, possibly just as two new Eigen types, SparseMatrix and SparseVector (there are no SparseRowVectors that we care about, right?). And then we can annotate all of the Stan Lib functions with additional sparsity type signatures so we know when a return value will be sparse given the input types. And then the compiler can check that we don't need the sparsity on the declaration.

That brings up another awkward point - do we need to check the sparsity structure if someone creates a full declaration but then assigns to some function output? Or do we only allow the full sparsity declaration in the data and parameters blocks?

@dpsimpson

The sparsity pattern has to be a known-at-compile-time deterministic function of the data. Thankfully the Eigen sparse Cholesky (etc) does a two-phase sweep that can be separated out. Phase one is called a "symbolic factorization", which works out the sparsity pattern and does all the appropriate allocation. This is 100% a thing that can be used to infer that sparsity structure.

Some possible problems would be if someone wanted something like (apologies for the messy pseudocode)

sparse_matrix A; // structure given
sparse_matrix B; // structure given
sparse_matrix L; // structure to be inferred
real c; // data
L = chol(A + c*B);  // A problem if c = 0!

That brings up another awkward point - do we need to check the sparsity structure if someone creates a full declaration but then assigns to some function output? Or do we only allow the full sparsity declaration in the data and parameters blocks?

Yeah. This is the issue here. I had a shot at it here: https://aws1.discourse-cdn.com/standard14/uploads/mc_stan/original/2X/1/13fda4102c8f48e5aadbf0fbe75d3641a187d0a3.pdf

Essentially it would allow for two different declarations of sparse matrices:

sparse_matrix[N, M, nz_foo_row_index, nz_foo_col_index] A;
sparse_matrix[foo(A)] L;

where foo is a function that returns a sparse matrix. Steve's version doesn't have the explicit [foo(A)] bit and just has sparse_matrix L = foo(A) which is probably more in line with the idea of bringing declaration closer to use.

This will work if we have a requirement that every function foo_sparse in math that returns a sparse matrix has a foo_sparse_pattern_ variant that takes only data. (This is the same pattern Eigen uses for sparse matrix factorizations.)

@seantalts Would it be possible for the new compiler to find these things and just execute them once even though it would be in the model block? Because when this is declared in the parameter block, the pattern is data, while the "values" are vars (Does this question make sense?)

A thing that would need to be avoided is

// Declare B
sparse_matrix A = foo_sparse(B);
A = bar_sparse(B);

if foo and bar return different patterns. This means that the compound declaration has to be "special" (ie call the foo_sparse_pattern_ function to declare the sparse matrix and then call foo_sparse to get the values), while the ordinary bar_sparse call doesn't call bar_sparse_pattern_


```stan
parameters {

Choose a reason for hiding this comment

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

Add comment that currently we don't know a use case where sparse matrix would be defined as a parameter. It would be common to declare in transformed parameters block a sparse matrix which depends on few lower dimensional parameters.

// Defining sparse matrices in parameters needs the non-zero elements
sparse_matrix<nz_rows=nz_row_ind, nz_cols=nz_col_ind>[N, M] foo;
}
transformed parameters {
// Non-zero elements are deduced by the operation on x
sparse_matrix[N, N] K = cov_exp_quad(x, alpha, rho);

Choose a reason for hiding this comment

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

cov_exp_quad is a bad example as it will always produce a dense matrix. There are some covariance functions which produce sparse covariance, but they are not useful in Stan as the sparsity structure depends on length scale parameter. Better example could be e.g.

sparse_matrix[N, M] P = A * alpha;

}

```

The size and non-zero indices for sparse matrices in the parameter block must be from either the data block or transformed data block. This is because Stan's I/O and posterior analysis infrastructure assumes the same sparsity pattern in each iteration of the model.

## Helper Functions

We can also include helper functions to extract the sparsity pattern's row and column information.

```stan
int K = num_nz_elements(x);
// Extract effectively a tuple representation of the sparse matrix.
matrix[K, 3] = get_nz_elements(x);
```

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

## I/O

Data can be read in from json via a list of lists and the Rdump from the equivalent.
Copy link
Member

Choose a reason for hiding this comment

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

Should we also support that the input is in a dense matrix form in the input file but then the parser & data handler automatically constructs the sparse matrix, ignoring zeroes? We would need to somehow inform the parser to ignore zeroes during parsing, otherwise we would first create the dense matrix and then transform which defeats the purpose of introducing sparsity..


```js
{
...
{'col': 1, 'row': 20, 'val': 1.85},
{'col': 11, 'row': 3, 'val': 2.71},
...
}
Copy link
Member

@rok-cesnovar rok-cesnovar Aug 29, 2019

Choose a reason for hiding this comment

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

Not sure we need to be nit-picking here, but this should be written as

X = [
  ...
  {'col': 1, 'row': 20, 'val': 1.85},
  {'col': 11, 'row': 3, 'val': 2.71},
  ...
]

Currently JSON objects are only supported on the top level (Cmdstan at least), so we will need to remove that restriction.

```

```R
X <- list(..., list(1, 20, 1.85), list(11, 3, 2.71),...)
```

At the C++ level input data can be constructed to the sparse matrix through soemthing like what Ben did [here](https://github.com/stan-dev/rstanarm/blob/master/inst/include/csr_matrix_times_vector2.hpp#L18) for a revised `csr_matrix_times_vector`.

```cpp
int outerIndex[cols+1];
int innerIndices[nnz];
double values[nnz];
// read-write (parameters)
Map<SparseMatrix<double>> sm1(rows, cols, nnz, outerIndex, innerIndices, values);
// read only (data)
Map<const SparseMatrix<double>> sm2(rows, cols, nnz, outerIndex, innerIndices, values);
```

## Stan Math

### Templating

Sparse matrices can be supported in Stan-math by either moving to more thorough templating along with pseudo-concepts in the metaprogramming or by having separate methods for Sparse Matrices.

#### The Hard way


Let's look at primitive add for an example. One implementation of `add` in Stan math is

```cpp
template <typename T1, typename T2, int R, int C>
inline Eigen::Matrix<return_type_t<T1, T2>, R, C> add(
const Eigen::Matrix<T1, R, C>& m1, const Eigen::Matrix<T2, R, C>& m2) {
check_matching_dims("add", "m1", m1, "m2", m2);
return m1 + m2;
}
```

We can use a form of pseudo-concepts to write something like the following for functions which share the same Eigen dense and sparse methods.

```cpp
template <typename Mat1, typename Mat2, all_eigen_type<Mat1, Mat2>>
inline auto add(Mat1&& m1, Mat2&& m2) {
check_matching_dims("add", "m1", m1, "m2", m2);
return (m1 + m2).eval();
}
```

In the above, the return type without the `.eval()` will be something of the form `Eigen::CwiseBinaryOp<...>` which all of the other math functions would have to also take in. Adding a `.eval()` at the end will force the matrix to evaluate to either a sparse or dense matrix. Once more of Stan math can take in the Eigen expression types we can remove the `.eval()` at the end.

For places where the sparse and dense operations differ we can have dense/sparse matrix template specializations. There has been a lot of discussion on this refactor in the past (see [this](https://groups.google.com/forum/#!topic/stan-dev/ZKYCQ3Y7eY0) Google groups post and [this](https://github.com/stan-dev/math/issues/62) issue). Though looking at the two forms it seems like using `A.coeff()` for access instead of `operator()` would be sufficient to handle the coefficient access error Dan saw.


### The Simple Way

If we would rather not refactor the math library we can keep our current templates and have specializations for Sparse matrices.
```cpp
template <typename T1, typename T2>
inline Eigen::SparseMatrixBase<return_type_t<T1, T2>> add(
const Eigen::SparseMatrixBase<T1>& m1, const Eigen::SparseMatrixBase<T2>& m2) {
check_matching_dims("add", "m1", m1, "m2", m2);
return m1 + m2;
}
```

### Keeping Permutation Matrix from Cholesky

`SimplicalCholeskybase` keeps the permutation matrix, when the user does a cholesky decomposition we can pull out this permutation matrix and keep it to use in the next iteration. We do this through `EIGEN_SPARSEMATRIX_BASE_PLUGIN`, adding the permutation matrix to the input matrix. This adds a bit of state, but assuming the sparse matrices are fixed in size and sparsity this should be fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@seantalts

Simplical?
Also we might want to think about the Math library as being stateless and thread state through everywhere with the compiler going forward... can talk this over in person, not sure it's fully formed in my head yet.

@SteveBronder

Simplical?

That's the words Eigen uses

Also we might want to think about the Math library as being stateless and thread state through everywhere with the compiler going forward... can talk this over in person, not sure it's fully formed in my head yet.

Yeah I worry about adding state like this. Another options is to do the sorting immediately when a sparse matrix is created

@dpsimpson

We could assume that every square sparse matrix is going to be decomposed and just keep a permutation around at all times. Eigen's AMD re-ordering works regardless of symmetry so it's all good. (It's also cheap to compute relative to everything else.) This would make it a deterministic function of the data that is stored in the same object as the rest of the sparse matrix (assuming we make a simple container class around Eigen::SparseMatrix) so that should keep things stateless and thread safe

# Drawbacks
[drawbacks]: #drawbacks

Doing this improperly could lead to serious amounts of "code smell" aka duplicated code, confusing templates, etc.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

- Why is this design the best in the space of possible designs?

This is the most full fledged design that seems to have a reasonable consensus so far.

- What other designs have been considered and what is the rationale for not choosing them?

There are two other designs possible for the stan language. within the `[]` operator or as a new concept called an attribute.

```stan
// with operator[]
sparse_matrix[N, M, nz_rows_ind, nz_cols_ind, vals] A;

// With attribute
@sparse(nz_rows=nz_rows_ind, nz_cols=nz_cols_ind) Matrix[N, M] A;
```

The attribute method would require it's own design document, though it would allow a separation between computational and statistical concerns. Multiple attributes could also be placed into a tuple such as

```stan
(@sparse(nz_rows=nz_rows_ind, nz_cols=nz_cols_ind), @opencl) Matrix[N, M] A;
```

Another alternative would be to extend the `csr_*` functions, though this requires a lot of recomputation.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@dpsimpson

The csr_* functions are not good and should go away. Also it's inconsistent to have row oriented sparse-matrices and column oriented dense matrices.


- What is the impact of not doing this?

Kind of a bummer, it will be awkward for users to fit models whose data or parameters are sparsely defined.

# Prior art
[prior-art]: #prior-art

There has been a _lot_ of previous discussion about sparse matrices in Stan which are listed below in no particular order.

- [Sparse Matrix Roadmap](https://discourse.mc-stan.org/t/sparse-matrix-roadmap/5493/24)
- [Sparse Matrix Functionality without the Sparse Matrix](https://discourse.mc-stan.org/t/sparse-model-matrix-functionality-without-the-sparse-matrix/5759)
- [Spline Fitting Demo in Comparison of Sparse Vs. Non-Sparse](https://discourse.mc-stan.org/t/spline-fitting-demo-inc-comparison-of-sparse-vs-non-sparse/3287)
- [Should Sparse Matrices in Stan be Row Major or Column Major](https://discourse.mc-stan.org/t/should-sparse-matrices-in-stan-be-row-major-or-column-major/1563)
- [A Proposal for Sparse Matrices and GPs in Stan](https://discourse.mc-stan.org/t/a-proposal-for-sparse-matrices-and-gps-in-stan/2183)
- [Sparse Matrix Use Cases (Resolving Declaration Issues)](https://discourse.mc-stan.org/t/a-proposal-for-sparse-matrices-and-gps-in-stan/2183)
- [Sparse Matrices in Eigen with Autodiff](https://discourse.mc-stan.org/t/sparse-matrices-in-eigen-with-autodiff/3324/6)
- [Sparse Matrix Functional Spec](https://discourse.mc-stan.org/t/sparse-matrix-functional-spec/109)
- [Sparse Matrices in Stan](https://discourse.mc-stan.org/t/sparse-matrices-in-stan/3129)
- [Sparse Matrices discussion that took over anothe thread](https://discourse.mc-stan.org/t/stancon-cambridge-developer-meeting-opportunities/9743/46?u=stevo15025)

### Tensorflow

Tensorflow uses the same data storage schema inside of [`tf.sparse.SparseTensor`](https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/sparse/SparseTensor) with a limited amount of specific [methods](https://www.tensorflow.org/api_docs/python/tf/sparse). It does not seem that they have [Sparse Cholesky support](https://github.com/tensorflow/tensorflow/issues/15910).

It seems like OpenAI has methods like matrix multiplication for block sparse matrices (Sparse matrices with dense sub-blocks) in tensorflow available on [github](https://github.com/openai/blocksparse).
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@seantalts

Raises a question about our need (or lack thereof) for blocked operations too maybe?

@SteveBronder

yes @dpsimpson has brought this up as well as another person in my lab

@dpsimpson

We don't need blocked operations for sparse matrices. They're only efficient for sparse matrices with dense sub-blocks and we don't have any algorithms that take advantage of that structure. It's a real speed up on some sense (replace level 1 with level 2 and 3 BLAS operations lets you be more cache efficient for example) but it would be a weird thing to target on the first go

Choose a reason for hiding this comment

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

UGA animal breeding have a sparse-dense matrix package YAMS - it’s Fortran though. See slides at http://nce.ads.uga.edu/wiki/lib/exe/fetch.php?media=uga2018_yams.pdf

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Neat thanks! I'll try to find their code online


### Keras

Both Keras and Tensorflow Sparse support is very [low level](https://stackoverflow.com/a/43188970). Though it seems like work is going on for RaggedTensors and SparseTensors in [Keras](https://github.com/tensorflow/tensorflow/issues/27170#issuecomment-509296615).

### MxNet

MxNets [`portri`](https://mxnet.incubator.apache.org/api/python/symbol/linalg.html#mxnet.symbol.linalg.potrf) (Cholesky) function can handle sparse arrays, but there is not a lot of documentation saying that explicitly. Besides that they also store things in the compressed storage schema.

### PyTorch

Pytorch has an experimental section for limited sparse tensor operations in [`torch.sparse`](https://pytorch.org/docs/stable/sparse.html).


### Template Model Builder (TMB)

TMB uses a forked version of ADcpp for auto-diff and from their arvix paper seems to include sparse matrix types as well. Arvix [here](https://arxiv.org/pdf/1509.00660.pdf).

# Unresolved questions
[unresolved-questions]: #unresolved-questions

- What parts of the design do you expect to resolve through the RFC process before this gets merged?

- Language Design
- How this will work with the new Stan compiler?
- Right now it appears the need for the same number of coefficients in each iteration is because of historically how we have done our analysis stuff. Are there also statistical concerns?
- Will the cost of the refactor of Stan math to a more generic form be worth it?.
- Any prior art that I've missed?

# Appendix: (Eigen Sparse Matrix formats)

From the Eigen [Sparse Matrix Docs](https://eigen.tuxfamily.org/dox/group__TutorialSparse.html), Eigen matrices use a Compressed Column Scheme (CCS) to represent sparse matrices.
Copy link
Collaborator Author

@SteveBronder SteveBronder Aug 26, 2019

Choose a reason for hiding this comment

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

@dpsimpson

One very important but minimally documented part of Eigen's sparse libraries is that it tries to remove "lucky" zeros (aka the symbolic step computes the structural non-zeros of A*B, but some of the elements that could be non-zero end up being zero when the product is done. These are the "lucky" zeros that eigen removes). This is very bad for us as it changes the dimension of the resulting std::vector. The key function in Eigen is prune and a quick grep through the library looks like it's only used for matrix-matrix products. So this is a danger zone!

@SteveBronder

I'm looking at godbolt and it looks like Eigen keeps the sparse matrix values for multiplication even when they are zero?

https://godbolt.org/z/JJsyp0

Which is odd because looking at their unary_evaluator it looks like you are right and they do a pruning mechanism?

https://github.com/eigenteam/eigen-git-mirror/blob/c399f06d3f3a77b5bd2a74c11e635e4952b72a4b/Eigen/src/SparseCore/SparseProduct.h#L139

It looks like the pruning happens here. Maybe we can fiddle the value of epsilon to turn off pruning?


Sparse matrices in Eigen are stored using four compact arrays.

> Values: stores the coefficient values of the non-zeros.

> InnerIndices: stores the row (resp. column) indices of the non-zeros.

> OuterStarts: stores for each column (resp. row) the index of the first non-zero in the previous two arrays.

> InnerNNZs: stores the number of non-zeros of each column (resp. row). The word inner refers to an inner vector that is a column for a column-major matrix, or a row for a row-major matrix. The word outer refers to the other direction.

| row/col | 0 | 1 | 2 | 3 | 4 |
|---------|----|---|----|---|----|
| 0 | 0 | 3 | 0 | 0 | 0 |
| 1 | 22 | 0 | 0 | 0 | 17 |
| 2 | 7 | 5 | 0 | 1 | 0 |
| 3 | 0 | 0 | 0 | 0 | 0 |
| 4 | 0 | 0 | 14 | 0 | 8 |

| Inner Values: |----|---|---|---|---|----|---|---|---|---|----|---|
|----------------|----|---|---|---|---|----|---|---|---|---|----|---|
| Values: | 22 | 7 | _ | 3 | 5 | 14 | _ | _ | 1 | _ | 17 | 8 |
| InnerIndices: | 1 | 2 | _ | 0 | 2 | 4 | _ | _ | 2 | _ | 1 | 4 |

| Meta Info: |---|---|---|---|----|----|
|--------------|---|---|---|---|----|----|
| OuterStarts | 0 | 3 | 5 | 8 | 10 | 12 |
| InnerNNZs | 2 | 2 | 1 | 1 | 2 | |

The `_` indicates available free space to insert new elements. In the above example, 14 sits in the 4th column index (`InnerIndices`) while the 4th column index has 10 (`OuterStarts`) non-zero elements before it (there are 10 including the free space `_` for new elements) The 1st column in the above above has 2 (`InnerNNZs`) elements that are nonzero. The above allows for elements to be inserted inside of the sparse matrix, but can be compressed further with `makeCompressed()`

|Compressed Vals:|----|---|---|---|----|---|----|---|
|----------------|----|---|---|---|----|---|----|---|
| Values: | 22 | 7 | 3 | 5 | 14 | 1 | 17 | 8 |
| InnerIndices: | 1 | 2 | 0 | 2 | 4 | 2 | 1 | 4 |

|Meta Info: |---|---|---|---|---|---|
|--------------|---|---|---|---|---|---|
| OuterStarts: | 0 | 2 | 4 | 5 | 6 | 8 |

This is now in the Compressed Row Format (CRF) where value 14 sits in the 4th row index where the 4th row index has six non-zero elements before it.