From 26bd8c20854aafbc36d843892eb2eac2440b6065 Mon Sep 17 00:00:00 2001 From: Hariharan Seshadri Date: Tue, 25 Aug 2020 11:00:33 -0700 Subject: [PATCH] Support scalar tensors in c# (#4849) --- .../Tensors/ArrayUtilities.cs | 29 ++++-- .../Tensors/DenseTensor.cs | 8 +- .../Tensors/Tensor.cs | 13 +-- .../Tensors/TensorTests.cs | 88 +++++++++++++++++-- 4 files changed, 112 insertions(+), 26 deletions(-) diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayUtilities.cs b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayUtilities.cs index 827b838dd5..9955146f26 100644 --- a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayUtilities.cs +++ b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayUtilities.cs @@ -36,11 +36,6 @@ namespace Microsoft.ML.OnnxRuntime.Tensors public static long GetProduct(ReadOnlySpan dimensions, int startIndex = 0) { - if (dimensions.Length == 0) - { - return 0; - } - long product = 1; for (int i = startIndex; i < dimensions.Length; i++) { @@ -96,6 +91,11 @@ namespace Microsoft.ML.OnnxRuntime.Tensors { int[] strides = new int[dimensions.Length]; + if (dimensions.Length == 0) + { + return strides; + } + int stride = 1; if (reverseStride) { @@ -174,6 +174,12 @@ namespace Microsoft.ML.OnnxRuntime.Tensors Debug.Assert(reverseStride ? IsAscending(strides) : IsDescending(strides), "Index decomposition requires ordered strides"); Debug.Assert(strides.Length == indices.Length); + // scalar tensor - nothing to process + if (indices.Length == 0) + { + return; + } + int remainder = index; for (int i = startFromDimension; i < strides.Length; i++) { @@ -199,6 +205,12 @@ namespace Microsoft.ML.OnnxRuntime.Tensors Debug.Assert(reverseStride ? IsAscending(strides) : IsDescending(strides), "Index decomposition requires ordered strides"); Debug.Assert(strides.Length == indices.Length); + // scalar tensor - nothing to process + if (indices.Length == 0) + { + return; + } + int remainder = index; for (int i = startFromDimension; i < strides.Length; i++) { @@ -220,6 +232,13 @@ namespace Microsoft.ML.OnnxRuntime.Tensors Debug.Assert(sourceReverseStride ? IsAscending(sourceStrides) : IsDescending(sourceStrides), "Index decomposition requires ordered strides"); Debug.Assert(sourceStrides.Length == transformStrides.Length); + // scalar tensor + if (sourceStrides.Length == 0) + { + Debug.Assert(index == 0, "Index has to be zero for a scalar tensor"); + return 0; + } + int transformIndex = 0; int remainder = index; diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/DenseTensor.cs b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/DenseTensor.cs index efa193a42b..d71ff5ddca 100644 --- a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/DenseTensor.cs +++ b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/DenseTensor.cs @@ -93,6 +93,7 @@ namespace Microsoft.ML.OnnxRuntime.Tensors /// /// Gets the value at the specied index, where index is a linearized version of n-dimension indices using strides. + /// For a scalar, use index = 0 /// /// An integer index computed as a dot-product of indices. /// The value at the specified position in this Tensor. @@ -103,6 +104,7 @@ namespace Microsoft.ML.OnnxRuntime.Tensors /// /// Sets the value at the specied index, where index is a linearized version of n-dimension indices using strides. + /// For a scalar, use index = 0 /// /// An integer index computed as a dot-product of indices. /// The new value to set at the specified position in this Tensor. @@ -170,11 +172,7 @@ namespace Microsoft.ML.OnnxRuntime.Tensors /// A new tensor that reinterprets backing Buffer of this tensor with different dimensions. public override Tensor Reshape(ReadOnlySpan dimensions) { - if (dimensions.Length == 0) - { - throw new ArgumentException("Dimensions must contain elements.", nameof(dimensions)); - } - + var newSize = ArrayUtilities.GetProduct(dimensions); if (newSize != Length) diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/Tensor.cs b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/Tensor.cs index 1e3ba0baa2..62f4be6832 100644 --- a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/Tensor.cs +++ b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/Tensor.cs @@ -397,15 +397,15 @@ namespace Microsoft.ML.OnnxRuntime.Tensors } /// - /// Initialize an n-dimensional tensor with the specified dimensions and layout. ReverseStride=true gives a stride of 1-element witdth to the first dimension (0). ReverseStride=false gives a stride of 1-element width to the last dimension (n-1). + /// Initialize an n-dimensional tensor with the specified dimensions and layout. ReverseStride=true gives a stride of 1-element width to the first dimension (0). ReverseStride=false gives a stride of 1-element width to the last dimension (n-1). /// /// An span of integers that represent the size of each dimension of the Tensor to create. /// False (default) to indicate that the first dimension is most major (farthest apart) and the last dimension is most minor (closest together): akin to row-major in a rank-2 tensor. True to indicate that the last dimension is most major (farthest apart) and the first dimension is most minor (closest together): akin to column-major in a rank-2 tensor. protected Tensor(ReadOnlySpan dimensions, bool reverseStride) : base(typeof(T)) { - if (dimensions.Length == 0) + if (dimensions == null) { - throw new ArgumentException("Dimensions must contain elements.", nameof(dimensions)); + throw new ArgumentNullException(nameof(dimensions)); } this.dimensions = new int[dimensions.Length]; @@ -427,7 +427,7 @@ namespace Microsoft.ML.OnnxRuntime.Tensors } /// - /// Initializes tensor with same dimensions as array, content of array is ignored. ReverseStride=true gives a stride of 1-element witdth to the first dimension (0). ReverseStride=false gives a stride of 1-element width to the last dimension (n-1). + /// Initializes tensor with same dimensions as array, content of array is ignored. ReverseStride=true gives a stride of 1-element width to the first dimension (0). ReverseStride=false gives a stride of 1-element width to the last dimension (n-1). /// /// Array from which to derive dimensions. /// False (default) to indicate that the first dimension is most major (farthest apart) and the last dimension is most minor (closest together): akin to row-major in a rank-2 tensor. True to indicate that the last dimension is most major (farthest apart) and the first dimension is most minor (closest together): akin to column-major in a rank-2 tensor. @@ -438,11 +438,6 @@ namespace Microsoft.ML.OnnxRuntime.Tensors throw new ArgumentNullException(nameof(fromArray)); } - if (fromArray.Rank == 0) - { - throw new ArgumentException("Array must contain elements.", nameof(fromArray)); - } - dimensions = new int[fromArray.Rank]; long size = 1; for (int i = 0; i < dimensions.Length; i++) diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests/Tensors/TensorTests.cs b/csharp/test/Microsoft.ML.OnnxRuntime.Tests/Tensors/TensorTests.cs index 24a8cca5aa..14e7a9996f 100644 --- a/csharp/test/Microsoft.ML.OnnxRuntime.Tests/Tensors/TensorTests.cs +++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests/Tensors/TensorTests.cs @@ -159,9 +159,7 @@ namespace Microsoft.ML.OnnxRuntime.Tensors.Tests Assert.Equal(24, tensor.Length); Assert.Equal(tensorConstructor.IsReversedStride, tensor.IsReversedStride); - //Assert.Throws("dimensions", () => tensorConstructor.CreateFromDimensions(dimensions: null)); - Assert.Throws("dimensions", () => tensorConstructor.CreateFromDimensions(dimensions: new int[0])); - + Assert.Throws("dimensions", () => tensorConstructor.CreateFromDimensions(dimensions: null)); Assert.Throws("dimensions", () => tensorConstructor.CreateFromDimensions(dimensions: new[] { 1, -1 })); // ensure dimensions are immutable @@ -224,12 +222,88 @@ namespace Microsoft.ML.OnnxRuntime.Tensors.Tests Assert.True(StructuralComparisons.StructuralEqualityComparer.Equals(emptyTensor1, emptyTensor4)); Assert.True(StructuralComparisons.StructuralEqualityComparer.Equals(emptyTensor4, emptyTensor1)); - // create an empty DenseTensor + // create an empty DenseTensor from dimensions dimensions = new[] { 0, 2, 1 }; - var emptyDenseTensor = new DenseTensor(new Span(dimensions)); + var emptyDenseTensor1 = new DenseTensor(new Span(dimensions)); + Assert.Equal(0, emptyDenseTensor1.Length); // accessing any index in the underlying buffer should result in an IndexOutOfRangeException - Assert.Throws(() => emptyDenseTensor.GetValue(0)); - Assert.Throws(() => emptyDenseTensor.GetValue(5)); + Assert.Throws(() => emptyDenseTensor1.GetValue(0)); + Assert.Throws(() => emptyDenseTensor1.GetValue(5)); + + // create an empty DenseTensor from memory + var memory = new Memory(new int[] { }); + var emptyDenseTensor2 = new DenseTensor(memory, new int[] {2, 0, 2 }); + Assert.Equal(0, emptyDenseTensor2.Length); + // accessing any index in the underlying buffer should result in an IndexOutOfRangeException + Assert.Throws(() => emptyDenseTensor2.GetValue(0)); + Assert.Throws(() => emptyDenseTensor2.GetValue(5)); + + } + + [Theory()] + [MemberData(nameof(GetSingleTensorConstructors))] + public void ConstructScalarTensors(TensorConstructor tensorConstructor) + { + // tests associated with scalar tensors (i.e.) tensors with no dimensions + + // test creation of scalar tensors (from dimensions) + var dimensions = new int[] { }; + var scalarTensor1 = tensorConstructor.CreateFromDimensions(dimensions: dimensions); + Assert.Equal(0, scalarTensor1.Dimensions.Length); + Assert.Equal(0, scalarTensor1.Rank); + + dimensions = new int[0]; + var scalarTensor2 = tensorConstructor.CreateFromDimensions(dimensions: dimensions); + Assert.Equal(0, scalarTensor2.Dimensions.Length); + Assert.Equal(0, scalarTensor2.Rank); + + // TODO: Create from Array ? + + // ensure the lengths of the scalar tensors is 1 + Assert.Equal(1, scalarTensor1.Length); + Assert.Equal(1, scalarTensor2.Length); + + // equality comparison of scalar tensors with non-scalar tensors throws an ArgumentException + dimensions = new int[] { 2, 2 }; + var nonScalarTensor1 = tensorConstructor.CreateFromDimensions(dimensions: dimensions); + + dimensions = new int[] { 0, 2, 4 }; + var nonScalarTensor2 = tensorConstructor.CreateFromDimensions(dimensions: dimensions); + + Assert.Throws("other", () => StructuralComparisons.StructuralComparer.Compare(nonScalarTensor1, scalarTensor1)); + Assert.Throws("other", () => StructuralComparisons.StructuralComparer.Compare(scalarTensor1, nonScalarTensor1)); + Assert.Throws("other", () => StructuralComparisons.StructuralComparer.Compare(nonScalarTensor2, scalarTensor1)); + Assert.Throws("other", () => StructuralComparisons.StructuralComparer.Compare(scalarTensor1, nonScalarTensor2)); + + // equality comparison of scalar tensors is true + // equality comparison of a scalar tensor with itself + Assert.Equal(0, StructuralComparisons.StructuralComparer.Compare(scalarTensor1, scalarTensor1)); + Assert.True(StructuralComparisons.StructuralEqualityComparer.Equals(scalarTensor1, scalarTensor1)); + // equality comparison of a scalar tensor with another scalar tensor + Assert.Equal(0, StructuralComparisons.StructuralComparer.Compare(scalarTensor1, scalarTensor2)); + Assert.Equal(0, StructuralComparisons.StructuralComparer.Compare(scalarTensor2, scalarTensor1)); + Assert.True(StructuralComparisons.StructuralEqualityComparer.Equals(scalarTensor1, scalarTensor2)); + Assert.True(StructuralComparisons.StructuralEqualityComparer.Equals(scalarTensor2, scalarTensor1)); + + // create a scalar DenseTensor from dimensions + dimensions = new int[] { }; + var scalarDenseTensor1 = new DenseTensor(new Span(dimensions)); + Assert.Equal(1, scalarDenseTensor1.Length); + // set and get values in the scalar tensor + scalarDenseTensor1.SetValue(0, 100); + Assert.Equal(100, scalarDenseTensor1.GetValue(0)); + + // setting a non-zero in the underlying buffer should result in an IndexOutOfRangeException + Assert.Throws(() => scalarDenseTensor1.SetValue(6, 100)); + + // accessing a non-zero index in the underlying buffer should result in an IndexOutOfRangeException + Assert.Throws(() => scalarDenseTensor1.GetValue(5)); + + // create a scalar DenseTensor from memory + var memory = new Memory(new int[] { 1 }); + var scalarDenseTensor2 = new DenseTensor(memory, new int[] { }); + Assert.Equal(1, scalarDenseTensor2.Length); + Assert.Equal(1, scalarDenseTensor2.GetValue(0)); } [Theory()]