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

Add path for DenseMatrix multiplication with SparseVector #933

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

Rob-Hague
Copy link

Currently when multiplying a DenseMatrix with a Vector there is a path to a ILinearAlgebraProvider when the multiplicand and the result are both DenseVector, otherwise we fall back to a base implementation:

protected override void DoMultiply(Vector<double> rightSide, Vector<double> result)
{
for (var i = 0; i < RowCount; i++)
{
var s = 0.0;
for (var j = 0; j < ColumnCount; j++)
{
s += At(i, j)*rightSide[j];
}
result[i] = s;
}
}

When rightSide is a SparseVector then rightSide[j] is a binary search on the sparse indices. We can instead just iterate the sparse indices as the column indices, similar to the DiagonalMatrix path here:

if (other.Storage is DiagonalMatrixStorage<double> diagonalOther)

This PR does that in the second commit. The first commit tries to increase test coverage to make sure this path is tested. I wanted to add some tests to check arithmetic results are equal across different storage types (a generalisation of #912), I didn't achieve that but I think the changes add a little extra robustness in that area.

I ran a rudimentary benchmark in a distinctly unscientific environment (third commit, you may not want to take that) just for fun:


BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1766 (21H2)
Intel Xeon CPU E5-2640 v4 2.40GHz, 2 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.302
  [Host]     : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
  DefaultJob : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
Method N SparseRatio Mean Error StdDev
DenseMatrixSparseVector (before) 100 0.01 216.2 μs 7.15 μs 20.86 μs
DenseMatrixSparseVector (after) 100 0.01 1.0 μs 0.03 μs 0.08 μs
DenseMatrixSparseVector (before) 100 0.1 280.8 μs 5.58 μs 14.50 μs
DenseMatrixSparseVector (after) 100 0.1 4.2 μs 0.13 μs 0.37 μs
DenseMatrixSparseVector (before) 100 0.5 356.9 μs 6.99 μs 11.87 μs
DenseMatrixSparseVector (after) 100 0.5 26.0 μs 0.79 μs 2.30 μs
DenseMatrixSparseVector (before) 1000 0.01 36,249.2 μs 942.81 μs 2,765.10 μs
DenseMatrixSparseVector (after) 1000 0.01 44.6 μs 1.50 μs 4.47 μs
DenseMatrixSparseVector (before) 1000 0.1 60,699.3 μs 1,585.09 μs 4,598.63 μs
DenseMatrixSparseVector (after) 1000 0.1 512.4 μs 15.27 μs 44.79 μs
DenseMatrixSparseVector (before) 1000 0.5 87,900.7 μs 1,782.12 μs 5,170.26 μs
DenseMatrixSparseVector (after) 1000 0.5 3,109.7 μs 104.21 μs 307.28 μs
DenseMatrixSparseVector (before) 10000 0.01 9,142,649.0 μs 176,397.41 μs 181,147.15 μs
DenseMatrixSparseVector (after) 10000 0.01 8,723.8 μs 375.52 μs 1,095.41 μs
DenseMatrixSparseVector (before) 10000 0.1 11,304,746.1 μs 253,185.38 μs 734,536.69 μs
DenseMatrixSparseVector (after) 10000 0.1 109,569.1 μs 3,059.45 μs 8,924.56 μs
DenseMatrixSparseVector (before) 10000 0.5 14,264,705.2 μs 363,221.20 μs 1,042,149.72 μs
DenseMatrixSparseVector (after) 10000 0.5 1,314,051.1 μs 40,127.83 μs 115,777.97 μs

@Rob-Hague
Copy link
Author

Actually I realise this breaks behaviour around double.NaN :( Closing

[Test]
public void DenseVsSparse()
{
    Matrix<double> m = Matrix<double>.Build.DenseOfArray(new double[,] { { double.NaN, 0 }, { 0, 0 } });

    double[] arr = new double[] { 0, 0 };

    Vector<double> sparseResult = m * Vector<double>.Build.SparseOfArray(arr);
    Vector<double> denseResult = m * Vector<double>.Build.DenseOfArray(arr);

    for (var i = 0; i < denseResult.Count; i++)
    {
        Assert.AreEqual(denseResult[i], sparseResult[i], $"Failed at index {i}");
        /* Message: 
                Failed at index 0
                Expected: NaN
                But was:  0.0d
         */
    }
}

@Rob-Hague Rob-Hague closed this Jul 20, 2022
@Rob-Hague
Copy link
Author

Rob-Hague commented Jul 20, 2022

SparseMatrix.DoMultiply also exhibits similar NaN behaviour. The test added in commit 4 has 6 (out of 324) failures on master:

Failures on master:

Test Error Message
MatrixVectorMultiplicationSameResult("Sparse","Sparse",0.0d,double.PositiveInfinity) Bad result index 0 Expected: NaN But was: 0.0d
MatrixVectorMultiplicationSameResult("Sparse","Dense",0.0d,double.NaN) Bad result index 0 Expected: NaN But was: 0.0d
MatrixVectorMultiplicationSameResult("Sparse","Dense",0.0d,double.NegativeInfinity) Bad result index 0 Expected: NaN But was: 0.0d
MatrixVectorMultiplicationSameResult("Sparse","Dense",0.0d,double.PositiveInfinity) Bad result index 0 Expected: NaN But was: 0.0d
MatrixVectorMultiplicationSameResult("Sparse","Sparse",0.0d,double.NaN) Bad result index 0 Expected: NaN But was: 0.0d
MatrixVectorMultiplicationSameResult("Sparse","Sparse",0.0d,double.NegativeInfinity) Bad result index 0 Expected: NaN But was: 0.0d

Failures on PR in addition to above:

Test Error Message
MatrixVectorMultiplicationSameResult("Dense","Sparse",double.NaN,0.0d) Bad result index 0 Expected: NaN But was: 0.0d
MatrixVectorMultiplicationSameResult("Dense","Sparse",double.NegativeInfinity,0.0d) Bad result index 0 Expected: NaN But was: 0.0d
MatrixVectorMultiplicationSameResult("Dense","Sparse",double.PositiveInfinity,0.0d) Bad result index 0 Expected: NaN But was: 0.0d

So it is not as cut-and-dried as I thought. I am reopening and will await your decision on what to do, if anything.

@Rob-Hague Rob-Hague reopened this Jul 20, 2022
Copy link
Member

@jkalias jkalias left a comment

Choose a reason for hiding this comment

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

CI Build fails

}
else

if (rightSide.Storage is SparseVectorStorage<Complex> sparseRight)
Copy link
Member

Choose a reason for hiding this comment

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

This logic appears 3 times in this file, only the matrix indices are swapped and/or its element is conjugated. I would extract the core part in one function, and call it 3 times; each time changing only the logic which manipulates the matrix value

}
else

if (rightSide.Storage is SparseVectorStorage<Complex32> sparseRight)
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as in the Complex/DenseMatrix.cs file.

}
else

if (rightSide.Storage is SparseVectorStorage<double> sparseRight)
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as in the Complex/DenseMatrix.cs file.

}
else

if (rightSide.Storage is SparseVectorStorage<float> sparseRight)
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as in the Complex/DenseMatrix.cs file.

@Rob-Hague
Copy link
Author

CI Build fails

It fails the same 9 tests as the comment above, relating to NaN/Inf behaviour differences between Sparse and Dense multiplication (AFAIR): those which exist already on master (exposed by 8d6317d) and those which this PR introduces more of.

There needs to be a decision as to what to do (if anything) about the behavioural differences before the PR goes anywhere. It is easy enough to use the fast logic in consuming code (that's what I ended up doing).

@jkalias
Copy link
Member

jkalias commented Mar 26, 2023

I don't really see where the master branch fails, can you please share a link?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants