diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayTensorExtensions.shared.cs b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayTensorExtensions.shared.cs index b26d215840..f6f57bd0b9 100644 --- a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayTensorExtensions.shared.cs +++ b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayTensorExtensions.shared.cs @@ -27,7 +27,12 @@ namespace Microsoft.ML.OnnxRuntime.Tensors /// A 1-dimensional DenseTensor<T> with the same length and content as . public static DenseTensor ToTensor(this T[] array) { - return new DenseTensor(array); + // DenseTensor(Array, ...) is not efficient so do the copy here. + var dimensions = new int[] { array.Length }; + T[] copy = new T[array.Length]; + array.CopyTo(copy, 0); + + return new DenseTensor(new Memory(copy), dimensions); } /// @@ -39,7 +44,25 @@ namespace Microsoft.ML.OnnxRuntime.Tensors /// A 2-dimensional DenseTensor<T> with the same dimensions and content as . public static DenseTensor ToTensor(this T[,] array, bool reverseStride = false) { - return new DenseTensor(array, reverseStride); + if (reverseStride) + { + // we need logic from the DenseTensor ctor to be applied during copying + return new DenseTensor(array, reverseStride); + } + else + { + // it's more efficient to copy and flatten to 1D T[] and construct DenseTensor with Memory + T[] copy = new T[array.Length]; + var dimensions = new int[] { array.GetLength(0), array.GetLength(1) }; + + long idx = 0; + foreach (var item in array) + { + copy[idx++] = item; + } + + return new DenseTensor(new Memory(copy), dimensions); + } } /// @@ -51,7 +74,56 @@ namespace Microsoft.ML.OnnxRuntime.Tensors /// A 3-dimensional DenseTensor<T> with the same dimensions and content as . public static DenseTensor ToTensor(this T[,,] array, bool reverseStride = false) { - return new DenseTensor(array, reverseStride); + if (reverseStride) + { + // we need logic from the DenseTensor ctor to be applied during copying + return new DenseTensor(array, reverseStride); + } + else + { + // it's more efficient to copy and flatten to 1D T[] and construct DenseTensor with Memory + T[] copy = new T[array.Length]; + var dimensions = new int[] { array.GetLength(0), array.GetLength(1), array.GetLength(2) }; + + long idx = 0; + foreach (var item in array) + { + copy[idx++] = item; + } + + return new DenseTensor(new Memory(copy), dimensions); + } + } + + /// + /// Creates a copy of this four-dimensional array as a DenseTensor<T> + /// + /// Type contained in the array to copy to the DenseTensor<T>. + /// The array to create a DenseTensor<T> from. + /// 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. + /// A 4-dimensional DenseTensor<T> with the same dimensions and content as . + public static DenseTensor ToTensor(this T[,,,] array, bool reverseStride = false) + { + if (reverseStride) + { + // we need logic from the DenseTensor ctor to be applied during copying + return new DenseTensor(array, reverseStride); + } + else + { + // it's more efficient to copy and flatten to 1D T[] and construct DenseTensor with Memory + T[] copy = new T[array.Length]; + var dimensions = new int[] { + array.GetLength(0), array.GetLength(1), array.GetLength(2), array.GetLength(3) }; + + long idx = 0; + foreach (var item in array) + { + copy[idx++] = item; + } + + return new DenseTensor(new Memory(copy), dimensions); + } } /// diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/DenseTensor.shared.cs b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/DenseTensor.shared.cs index 7dc3d6bd15..997b5eeb24 100644 --- a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/DenseTensor.shared.cs +++ b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/DenseTensor.shared.cs @@ -51,6 +51,7 @@ namespace Microsoft.ML.OnnxRuntime.Tensors backingArray[index++] = (T)item; } } + memory = backingArray; } @@ -66,26 +67,42 @@ namespace Microsoft.ML.OnnxRuntime.Tensors /// /// Initializes a rank-n Tensor using the dimensions specified in . /// - /// An span of integers that represent the size of each dimension of the DenseTensor 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. + /// + /// An span of integers that represent the size of each dimension of the DenseTensor 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. + /// public DenseTensor(ReadOnlySpan dimensions, bool reverseStride = false) : base(dimensions, reverseStride) { memory = new T[Length]; } /// - /// Constructs a new DenseTensor of the specifed dimensions, wrapping existing backing memory for the contents. + /// Constructs a new DenseTensor of the specified dimensions, wrapping existing backing memory for the contents. /// /// - /// An span of integers that represent the size of each dimension of the DenseTensor 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. - public DenseTensor(Memory memory, ReadOnlySpan dimensions, bool reverseStride = false) : base(dimensions, reverseStride) + /// + /// An span of integers that represent the size of each dimension of the DenseTensor 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. + /// + public DenseTensor(Memory memory, ReadOnlySpan dimensions, bool reverseStride = false) + : base(dimensions, reverseStride) { this.memory = memory; if (Length != memory.Length) { - throw new ArgumentException($"Length of {nameof(memory)} ({memory.Length}) must match product of {nameof(dimensions)} ({Length})."); + throw new ArgumentException( + $"Length of {nameof(memory)} ({memory.Length}) must match product of " + + $"{nameof(dimensions)} ({Length})."); } } @@ -95,8 +112,8 @@ namespace Microsoft.ML.OnnxRuntime.Tensors public Memory Buffer => memory; /// - /// 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 + /// Gets the value at the specified 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. @@ -106,8 +123,8 @@ 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 + /// Sets the value at the specified 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. @@ -130,7 +147,9 @@ namespace Microsoft.ML.OnnxRuntime.Tensors } if (array.Length < arrayIndex + Length) { - throw new ArgumentException("The number of elements in the Tensor is greater than the available space from index to the end of the destination array.", nameof(array)); + throw new ArgumentException( + "The number of elements in the Tensor is greater than the available space from index to " + + "the end of the destination array.", nameof(array)); } Buffer.Span.CopyTo(array.AsSpan(arrayIndex)); @@ -165,14 +184,17 @@ namespace Microsoft.ML.OnnxRuntime.Tensors /// A shallow copy of this tensor. public override Tensor Clone() { - return new DenseTensor(Buffer.ToArray(), dimensions, IsReversedStride); + // create copy + return new DenseTensor(new Memory(memory.ToArray()), dimensions, IsReversedStride); } /// - /// Creates a new Tensor of a different type with the specified dimensions and the same layout as this tensor with elements initialized to their default value. + /// Creates a new Tensor of a different type with the specified dimensions and the same layout as this tensor + /// with elements initialized to their default value. /// /// Type contained in the returned Tensor. - /// An span of integers that represent the size of each dimension of the DenseTensor to create. + /// + /// An span of integers that represent the size of each dimension of the DenseTensor to create. /// A new tensor with the same layout as this tensor but different type and dimensions. public override Tensor CloneEmpty(ReadOnlySpan dimensions) { @@ -182,7 +204,8 @@ namespace Microsoft.ML.OnnxRuntime.Tensors /// /// Reshapes the current tensor to new dimensions, using the same backing storage. /// - /// An span of integers that represent the size of each dimension of the DenseTensor to create. + /// + /// An span of integers that represent the size of each dimension of the DenseTensor to create. /// A new tensor that reinterprets backing Buffer of this tensor with different dimensions. public override Tensor Reshape(ReadOnlySpan dimensions) { @@ -191,7 +214,8 @@ namespace Microsoft.ML.OnnxRuntime.Tensors if (newSize != Length) { - throw new ArgumentException($"Cannot reshape array due to mismatch in lengths, currently {Length} would become {newSize}.", nameof(dimensions)); + throw new ArgumentException($"Cannot reshape array due to mismatch in lengths, " + + "currently {Length} would become {newSize}.", nameof(dimensions)); } return new DenseTensor(Buffer, dimensions, IsReversedStride); diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/Tensor.shared.cs b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/Tensor.shared.cs index 94b48d10c8..bb7eea2ad1 100644 --- a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/Tensor.shared.cs +++ b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/Tensor.shared.cs @@ -429,12 +429,16 @@ namespace Microsoft.ML.OnnxRuntime.Tensors } /// - /// Creates a n+1-dimension tensor using the specified n-dimension diagonal at the specified offset from the center. Values not on the diagonal will be filled with zeros. + /// Creates a n+1-dimension tensor using the specified n-dimension diagonal at the specified offset + /// from the center. Values not on the diagonal will be filled with zeros. /// - /// type contained within the Tensor. Typically a value type such as int, double, float, etc. + /// + /// type contained within the Tensor. Typically a value type such as int, double, float, etc. /// Tensor representing the diagonal to build the new tensor from. - /// Offset of diagonal to set in returned tensor. 0 for the main diagonal, less than zero for diagonals below, greater than zero from diagonals above. - /// A new tensor of the same layout and order as of one higher rank, with the values of along the specified diagonal and zeros elsewhere. + /// Offset of diagonal to set in returned tensor. 0 for the main diagonal, + /// less than zero for diagonals below, greater than zero from diagonals above. + /// A new tensor of the same layout and order as of one higher rank, + /// with the values of along the specified diagonal and zeros elsewhere. public static Tensor CreateFromDiagonal(Tensor diagonal, int offset) { if (diagonal.Rank < 1) @@ -678,10 +682,16 @@ 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 width 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. + /// + /// 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(Array fromArray, bool reverseStride) : base(typeof(T)) { if (fromArray == null) diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Microsoft.ML.OnnxRuntime.Tests.Common.csproj b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Microsoft.ML.OnnxRuntime.Tests.Common.csproj index 706920746b..853b3818ee 100644 --- a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Microsoft.ML.OnnxRuntime.Tests.Common.csproj +++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Microsoft.ML.OnnxRuntime.Tests.Common.csproj @@ -77,6 +77,7 @@ + diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Tensors/ArrayTensorExtensionsTests.cs b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Tensors/ArrayTensorExtensionsTests.cs new file mode 100644 index 0000000000..5e1e82f809 --- /dev/null +++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Tensors/ArrayTensorExtensionsTests.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Xunit; +using Microsoft.ML.OnnxRuntime.Tensors; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.ML.OnnxRuntime.Tests.ArrayTensorExtensions +{ + public class ArrayTensorExtensionsTests + { + static void CheckValues(IEnumerable expected, DenseTensor tensor) + { + foreach (var pair in expected.Zip(tensor.Buffer.ToArray(), Tuple.Create)) + { + Assert.Equal(pair.Item1, pair.Item2); + } + } + + [Fact] + public void ConstructFrom1D() + { + var array = new int[] { 1, 2, 3, 4 }; + var tensor = array.ToTensor(); + + var expectedDims = new int[] { 4 }; + Assert.Equal(tensor.Length, array.Length); + Assert.Equal(expectedDims, tensor.Dimensions.ToArray()); + CheckValues(array.Cast(), tensor); + } + + [Fact] + public void ConstructFrom2D() + { + var array = new int[,] { { 1, 2 } , { 3, 4 } }; + var tensor = array.ToTensor(); + + var expectedDims = new int[] { 2, 2 }; + Assert.Equal(tensor.Length, array.Length); + Assert.Equal(expectedDims, tensor.Dimensions.ToArray()); + CheckValues(array.Cast(), tensor); + } + + [Fact] + public void ConstructFrom3D() + { + var array = new int[,,] { { { 1, 2 }, { 3, 4 } }, + { { 5, 6 }, { 7, 8 } } }; + var tensor = array.ToTensor(); + + var expectedDims = new int[] { 2, 2, 2 }; + Assert.Equal(tensor.Length, array.Length); + Assert.Equal(expectedDims, tensor.Dimensions.ToArray()); + CheckValues(array.Cast(), tensor); + } + + [Fact] + public void ConstructFrom3DWithDim1() + { + var array = new int[,,] { { { 1, 2 } }, + { { 3, 4 } } }; + var tensor = array.ToTensor(); + + var expectedDims = new int[] { 2, 1, 2 }; + Assert.Equal(tensor.Length, array.Length); + Assert.Equal(expectedDims, tensor.Dimensions.ToArray()); + CheckValues(array.Cast(), tensor); + } + + [Fact] + public void ConstructFrom4D() + { + var array = new int[,,,] { + { { { 1, 2 }, { 3, 4 } }, + { { 5, 6 }, { 7, 8 } } } + }; + var tensor = array.ToTensor(); + + var expectedDims = new int[] { 1, 2, 2, 2 }; + Assert.Equal(tensor.Length, array.Length); + Assert.Equal(expectedDims, tensor.Dimensions.ToArray()); + CheckValues(array.Cast(), tensor); + } + + [Fact] + public void ConstructFrom5D() + { + var array = new int[,,,,] { + { { { { 1, 2 }, { 3, 4 } }, + { { 5, 6 }, { 7, 8 } } } } + }; + + // 5D requires cast to Array + Array a = (Array)array; + var tensor = a.ToTensor(); + + var expectedDims = new int[] { 1, 1, 2, 2, 2 }; + Assert.Equal(tensor.Length, array.Length); + Assert.Equal(expectedDims, tensor.Dimensions.ToArray()); + CheckValues(array.Cast(), tensor); + } + } +} diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.NetCoreApp/Microsoft.ML.OnnxRuntime.Tests.NetCoreApp.csproj b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.NetCoreApp/Microsoft.ML.OnnxRuntime.Tests.NetCoreApp.csproj index 6a3a7ed4c0..49ec3af9e9 100644 --- a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.NetCoreApp/Microsoft.ML.OnnxRuntime.Tests.NetCoreApp.csproj +++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.NetCoreApp/Microsoft.ML.OnnxRuntime.Tests.NetCoreApp.csproj @@ -89,6 +89,9 @@ TensorTests.cs + + ArrayTensorExtensionsTests.cs +