Skip to content

Commit b6e602a

Browse files
authored
Add doc string to explain matrix-vector product's SSE code and a test (#3124)
* Add doc string to explain matrix-vector product's SSE code and a test * Increate tol * As aligned as possible (considering AVE and 512-bit instructions) * Fix alignment check * Address comments * Address comment
1 parent 55d911d commit b6e602a

File tree

4 files changed

+138
-3
lines changed

4 files changed

+138
-3
lines changed

src/Microsoft.ML.CpuMath/CpuMathUtils.netstandard.cs

+20-1
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@ internal static partial class CpuMathUtils
1919
public static int GetVectorAlignment()
2020
=> Vector128Alignment;
2121

22+
/// <summary>
23+
/// Check if <paramref name="a"/>'s alignment is suitable to SSE instructions. Returns <see langword="true"/>
24+
/// if <paramref name="a"/>'s alignment is ok and <see langword="false"/> otherwise.
25+
/// </summary>
26+
/// <param name="a">The vector being checked.</param>
27+
/// <returns>Whether <paramref name="a"/> is aligned well.</returns>
2228
private static bool Compat(AlignedArray a)
2329
{
2430
Contracts.AssertValue(a);
2531
Contracts.Assert(a.Size > 0);
26-
return a.CbAlign == Vector128Alignment;
32+
return a.CbAlign % Vector128Alignment == 0;
2733
}
2834

2935
private static unsafe float* Ptr(AlignedArray a, float* p)
@@ -34,6 +40,19 @@ private static bool Compat(AlignedArray a)
3440
return q;
3541
}
3642

43+
/// <summary>
44+
/// Compute the product of matrix <paramref name="mat"/> (the matrix is flattened because its type is <see cref="AlignedArray"/> instead of a matrix)
45+
/// and a vector <paramref name="src"/>.
46+
/// </summary>
47+
/// <param name="tran">Whether to transpose <paramref name="mat"/> before doing any computation.</param>
48+
/// <param name="mat">If <paramref name="tran"/> is <see langword="false"/>, <paramref name="mat"/> is a m-by-n matrix, and the value at the i-th row and the j-th column is indexed by i * n + j in <paramref name="mat"/>.
49+
/// If <paramref name="tran"/> is <see langword="true"/>, <paramref name="mat"/> would be viewed a n-by-m matrix, and the value at the i-th row and the j-th column in the transposed matrix is indexed by j * m + i in the
50+
/// original <paramref name="mat"/>.</param>
51+
/// <param name="src">A n-by-1 matrix, which is also a vector.</param>
52+
/// <param name="dst">A m-by-1 matrix, which is also a vector.</param>
53+
/// <param name="crun">The truncation level of <paramref name="dst"/>. For example, if <paramref name="crun"/> is 2, <paramref name="dst"/>
54+
/// will be considered as a 2-by-1 matrix and therefore elements after its 2nd element will be ignored. If no truncation should happen,
55+
/// set <paramref name="crun"/> to the length of <paramref name="dst"/>.</param>
3756
public static void MatrixTimesSource(bool tran, AlignedArray mat, AlignedArray src, AlignedArray dst, int crun)
3857
{
3958
Contracts.Assert(Compat(mat));

src/Microsoft.ML.CpuMath/Properties/AssemblyInfo.cs

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
[assembly: InternalsVisibleTo(assemblyName: "LibSvmWrapper" + InternalPublicKey.Value)]
1212
[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Runtime.NeuralNetworks" + InternalPublicKey.Value)]
1313
[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.RServerScoring.NeuralNetworks" + InternalPublicKey.Value)]
14+
[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Tests" + PublicKey.TestValue)]
1415
[assembly: InternalsVisibleTo(assemblyName: "RunTests" + InternalPublicKey.Value)]
1516
[assembly: InternalsVisibleTo(assemblyName: "SseTests" + InternalPublicKey.Value)]

test/Microsoft.ML.Tests/Microsoft.ML.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<ProjectReference Include="..\..\src\Microsoft.ML.TensorFlow.StaticPipe\Microsoft.ML.TensorFlow.StaticPipe.csproj" />
2727
<ProjectReference Include="..\..\src\Microsoft.ML.TensorFlow\Microsoft.ML.TensorFlow.csproj" />
2828
<ProjectReference Include="..\..\src\Microsoft.ML.TimeSeries\Microsoft.ML.TimeSeries.csproj" />
29+
<ProjectReference Include="..\..\src\Microsoft.ML.CpuMath\Microsoft.ML.CpuMath.csproj" />
2930
<ProjectReference Include="..\Microsoft.ML.Predictor.Tests\Microsoft.ML.Predictor.Tests.csproj" />
3031
<ProjectReference Include="..\Microsoft.ML.TestFramework\Microsoft.ML.TestFramework.csproj" />
3132
</ItemGroup>

test/Microsoft.ML.Tests/TrainerEstimators/MatrixFactorizationTests.cs

+116-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Linq;
99
using System.Runtime.InteropServices;
1010
using Microsoft.ML.Data;
11+
using Microsoft.ML.Internal.CpuMath;
1112
using Microsoft.ML.RunTests;
1213
using Microsoft.ML.TestFramework.Attributes;
1314
using Microsoft.ML.Trainers;
@@ -253,6 +254,29 @@ public void MatrixFactorizationInMemoryData()
253254
Assert.True(pred.Score != 0);
254255
}
255256

257+
internal class MatrixElementZeroBased256By256
258+
{
259+
// Matrix column index starts from 0 and is at most _synthesizedMatrixColumnCount.
260+
[KeyType(_matrixColumnCount)]
261+
public uint MatrixColumnIndex;
262+
// Matrix row index starts from 0 and is at most _synthesizedMatrixRowCount.
263+
[KeyType(_matrixRowCount)]
264+
public uint MatrixRowIndex;
265+
// The value at the MatrixColumnIndex-th column and the MatrixRowIndex-th row in the considered matrix.
266+
public float Value;
267+
}
268+
269+
internal class MatrixElementZeroBasedForScore256By256
270+
{
271+
// Matrix column index starts from 0 and is at most _synthesizedMatrixColumnCount.
272+
[KeyType(_matrixColumnCount)]
273+
public uint MatrixColumnIndex;
274+
// Matrix row index starts from 0 and is at most _synthesizedMatrixRowCount.
275+
[KeyType(_matrixRowCount)]
276+
public uint MatrixRowIndex;
277+
public float Score;
278+
}
279+
256280
internal class MatrixElementZeroBased
257281
{
258282
// Matrix column index starts from 0 and is at most _synthesizedMatrixColumnCount.
@@ -268,11 +292,9 @@ internal class MatrixElementZeroBased
268292
internal class MatrixElementZeroBasedForScore
269293
{
270294
// Matrix column index starts from 0 and is at most _synthesizedMatrixColumnCount.
271-
// Contieuous=true means that all values from 0 to _synthesizedMatrixColumnCount are allowed keys.
272295
[KeyType(_synthesizedMatrixColumnCount)]
273296
public uint MatrixColumnIndex;
274297
// Matrix row index starts from 0 and is at most _synthesizedMatrixRowCount.
275-
// Contieuous=true means that all values from 0 to _synthesizedMatrixRowCount are allowed keys.
276298
[KeyType(_synthesizedMatrixRowCount)]
277299
public uint MatrixRowIndex;
278300
public float Score;
@@ -603,5 +625,97 @@ public void OneClassMatrixFactorizationWithUnseenColumnAndRow()
603625
CompareNumbersWithTolerance(0.05511549, testResults[1].Score, digitsOfPrecision: 5);
604626
CompareNumbersWithTolerance(0.00316973357, testResults[2].Score, digitsOfPrecision: 5);
605627
}
628+
629+
const int _matrixColumnCount = 256;
630+
const int _matrixRowCount = 256;
631+
632+
[MatrixFactorizationFact]
633+
public void InspectMatrixFactorizationModel()
634+
{
635+
// Create an in-memory matrix as a list of tuples (column index, row index, value).
636+
// Iterators i and j are column and row indexes, respectively.
637+
var dataMatrix = new List<MatrixElementZeroBased256By256>();
638+
for (uint i = 0; i < _matrixColumnCount; ++i)
639+
for (uint j = 0; j < _matrixRowCount; ++j)
640+
dataMatrix.Add(new MatrixElementZeroBased256By256() { MatrixColumnIndex = i, MatrixRowIndex = j, Value = (i + j) % 5 });
641+
642+
// Convert the in-memory matrix into an IDataView so that ML.NET components can consume it.
643+
var dataView = ML.Data.LoadFromEnumerable(dataMatrix);
644+
645+
// Create a matrix factorization trainer which may consume "Value" as the training label, "MatrixColumnIndex" as the
646+
// matrix's column index, and "MatrixRowIndex" as the matrix's row index.
647+
var mlContext = new MLContext(seed: 1);
648+
649+
var options = new MatrixFactorizationTrainer.Options
650+
{
651+
MatrixColumnIndexColumnName = nameof(MatrixElement.MatrixColumnIndex),
652+
MatrixRowIndexColumnName = nameof(MatrixElement.MatrixRowIndex),
653+
LabelColumnName = nameof(MatrixElement.Value),
654+
NumberOfIterations = 100,
655+
NumberOfThreads = 1, // To eliminate randomness, # of threads must be 1.
656+
ApproximationRank = 64,
657+
LearningRate = 0.5,
658+
};
659+
660+
var pipeline = mlContext.Recommendation().Trainers.MatrixFactorization(options);
661+
662+
// Train a matrix factorization model.
663+
var model = pipeline.Fit(dataView);
664+
665+
// Check if the expected types in the trained model are expected.
666+
Assert.True(model.MatrixColumnIndexColumnName == nameof(MatrixElementZeroBased256By256.MatrixColumnIndex));
667+
Assert.True(model.MatrixRowIndexColumnName == nameof(MatrixElementZeroBased256By256.MatrixRowIndex));
668+
var matColKeyType = model.MatrixColumnIndexColumnType as KeyDataViewType;
669+
Assert.NotNull(matColKeyType);
670+
var matRowKeyType = model.MatrixRowIndexColumnType as KeyDataViewType;
671+
Assert.NotNull(matRowKeyType);
672+
Assert.True(matColKeyType.Count == _matrixColumnCount);
673+
Assert.True(matRowKeyType.Count == _matrixRowCount);
674+
675+
// Create a test set with assigning scores. It stands for the 2nd column of the training matrix.
676+
var testMatrix = new List<MatrixElementZeroBasedForScore256By256>();
677+
for (/* column index */ uint i = 1; i < 2; ++i)
678+
for (/* row index */ uint j = 0; j < _matrixRowCount; ++j)
679+
testMatrix.Add(new MatrixElementZeroBasedForScore256By256() { MatrixColumnIndex = i, MatrixRowIndex = j, Score = 0 });
680+
681+
// Load test set as IDataView.
682+
var testData = ML.Data.LoadFromEnumerable(testMatrix);
683+
684+
// Apply the trained model to the training set
685+
var transformedTestData = model.Transform(testData);
686+
687+
// Load back predictions on the 2nd column as IEnumerable<MatrixElementZeroBasedForScore>.
688+
var predictions = mlContext.Data.CreateEnumerable<MatrixElementZeroBasedForScore256By256>(transformedTestData, false).ToList();
689+
690+
// Inspect the trained model.
691+
int m = model.Model.NumberOfRows;
692+
int n = model.Model.NumberOfColumns;
693+
int k = model.Model.ApproximationRank;
694+
695+
// The training matrix is approximated by leftFactorMatrix * rightFactorMatrix^T, where "^T" means matrix transpose.
696+
// Thus, to compute the approximation of the 2nd column, we only need the whole leftFactorMatrix and the 2nd row in rightFactorMatrix.
697+
698+
// First copy the trained left factor matrix to an aligned for applying SSE code.
699+
var leftFactorMatrix = model.Model.LeftFactorMatrix;
700+
var leftFactorMatrixAligned = new AlignedArray(m * k, 64);
701+
for (int i = 0; i < leftFactorMatrix.Count; ++i)
702+
leftFactorMatrixAligned[i] = leftFactorMatrix[i];
703+
704+
// Second copy the trained right factor row to a k-by-1 aligned vector for applying SSE code.
705+
var rightFactorVectorAligned = new AlignedArray(k, 64);
706+
for (int i = 0; i < k; ++i)
707+
rightFactorVectorAligned[i] = model.Model.RightFactorMatrix[1 * k + i]; // value at the i-th row and j-th column is indexed by i * k + j.
708+
709+
// Prepare buffer to store result. The result will be a matrix-vector product, where the matrix is leftFactorMatrix
710+
// and the vector is the 2nd row of rightFactorMatrix.
711+
var valuesAtSecondColumn = new AlignedArray(m, 64);
712+
713+
// Compute leftFactorMatrixAligned (m-by-k) * rightFactorVectorAligned (k-by-1).
714+
CpuMathUtils.MatrixTimesSource(false, leftFactorMatrixAligned, rightFactorVectorAligned, valuesAtSecondColumn, m);
715+
716+
// Check if results computed by SSE code and MF predictor are the same.
717+
for (int i = 0; i < predictions.Count(); ++i)
718+
Assert.Equal(predictions[i].Score, valuesAtSecondColumn[i], 3);
719+
}
606720
}
607721
}

0 commit comments

Comments
 (0)