C#: Avoid inefficient DenseTensor ctor in ToTensor extensions (#10240)

* Update extension helpers to avoid inefficient construction of DenseTensor.
Add tests for extension helpers.
This commit is contained in:
Scott McKay 2022-01-19 07:43:44 +10:00 committed by GitHub
parent 6ae22d562b
commit c1c9fa18bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 241 additions and 26 deletions

View file

@ -27,7 +27,12 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
/// <returns>A 1-dimensional DenseTensor&lt;T&gt; with the same length and content as <paramref name="array"/>.</returns>
public static DenseTensor<T> ToTensor<T>(this T[] array)
{
return new DenseTensor<T>(array);
// DenseTensor<T>(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<T>(new Memory<T>(copy), dimensions);
}
/// <summary>
@ -39,7 +44,25 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
/// <returns>A 2-dimensional DenseTensor&lt;T&gt; with the same dimensions and content as <paramref name="array"/>.</returns>
public static DenseTensor<T> ToTensor<T>(this T[,] array, bool reverseStride = false)
{
return new DenseTensor<T>(array, reverseStride);
if (reverseStride)
{
// we need logic from the DenseTensor ctor to be applied during copying
return new DenseTensor<T>(array, reverseStride);
}
else
{
// it's more efficient to copy and flatten to 1D T[] and construct DenseTensor with Memory<T>
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<T>(new Memory<T>(copy), dimensions);
}
}
/// <summary>
@ -51,7 +74,56 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
/// <returns>A 3-dimensional DenseTensor&lt;T&gt; with the same dimensions and content as <paramref name="array"/>.</returns>
public static DenseTensor<T> ToTensor<T>(this T[,,] array, bool reverseStride = false)
{
return new DenseTensor<T>(array, reverseStride);
if (reverseStride)
{
// we need logic from the DenseTensor ctor to be applied during copying
return new DenseTensor<T>(array, reverseStride);
}
else
{
// it's more efficient to copy and flatten to 1D T[] and construct DenseTensor with Memory<T>
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<T>(new Memory<T>(copy), dimensions);
}
}
/// <summary>
/// Creates a copy of this four-dimensional array as a DenseTensor&lt;T&gt;
/// </summary>
/// <typeparam name="T">Type contained in the array to copy to the DenseTensor&lt;T&gt;.</typeparam>
/// <param name="array">The array to create a DenseTensor&lt;T&gt; from.</param>
/// <param name="reverseStride">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.</param>
/// <returns>A 4-dimensional DenseTensor&lt;T&gt; with the same dimensions and content as <paramref name="array"/>.</returns>
public static DenseTensor<T> ToTensor<T>(this T[,,,] array, bool reverseStride = false)
{
if (reverseStride)
{
// we need logic from the DenseTensor ctor to be applied during copying
return new DenseTensor<T>(array, reverseStride);
}
else
{
// it's more efficient to copy and flatten to 1D T[] and construct DenseTensor with Memory<T>
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<T>(new Memory<T>(copy), dimensions);
}
}
/// <summary>

View file

@ -51,6 +51,7 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
backingArray[index++] = (T)item;
}
}
memory = backingArray;
}
@ -66,26 +67,42 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
/// <summary>
/// Initializes a rank-n Tensor using the dimensions specified in <paramref name="dimensions"/>.
/// </summary>
/// <param name="dimensions">An span of integers that represent the size of each dimension of the DenseTensor to create.</param>
/// <param name="reverseStride">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.</param>
/// <param name="dimensions">
/// An span of integers that represent the size of each dimension of the DenseTensor to create.
/// </param>
/// <param name="reverseStride">
/// 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.
/// </param>
public DenseTensor(ReadOnlySpan<int> dimensions, bool reverseStride = false) : base(dimensions, reverseStride)
{
memory = new T[Length];
}
/// <summary>
/// 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.
/// </summary>
/// <param name="memory"></param>
/// <param name="dimensions">An span of integers that represent the size of each dimension of the DenseTensor to create.</param>
/// <param name="reverseStride">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.</param>
public DenseTensor(Memory<T> memory, ReadOnlySpan<int> dimensions, bool reverseStride = false) : base(dimensions, reverseStride)
/// <param name="dimensions">
/// An span of integers that represent the size of each dimension of the DenseTensor to create.</param>
/// <param name="reverseStride">
/// 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.
/// </param>
public DenseTensor(Memory<T> memory, ReadOnlySpan<int> 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<T> Buffer => memory;
/// <summary>
/// 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
/// </summary>
/// <param name="index">An integer index computed as a dot-product of indices.</param>
/// <returns>The value at the specified position in this Tensor.</returns>
@ -106,8 +123,8 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
}
/// <summary>
/// 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
/// </summary>
/// <param name="index">An integer index computed as a dot-product of indices.</param>
/// <param name="value">The new value to set at the specified position in this Tensor.</param>
@ -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
/// <returns>A shallow copy of this tensor.</returns>
public override Tensor<T> Clone()
{
return new DenseTensor<T>(Buffer.ToArray(), dimensions, IsReversedStride);
// create copy
return new DenseTensor<T>(new Memory<T>(memory.ToArray()), dimensions, IsReversedStride);
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TResult">Type contained in the returned Tensor.</typeparam>
/// <param name="dimensions">An span of integers that represent the size of each dimension of the DenseTensor to create.</param>
/// <param name="dimensions">
/// An span of integers that represent the size of each dimension of the DenseTensor to create.</param>
/// <returns>A new tensor with the same layout as this tensor but different type and dimensions.</returns>
public override Tensor<TResult> CloneEmpty<TResult>(ReadOnlySpan<int> dimensions)
{
@ -182,7 +204,8 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
/// <summary>
/// Reshapes the current tensor to new dimensions, using the same backing storage.
/// </summary>
/// <param name="dimensions">An span of integers that represent the size of each dimension of the DenseTensor to create.</param>
/// <param name="dimensions">
/// An span of integers that represent the size of each dimension of the DenseTensor to create.</param>
/// <returns>A new tensor that reinterprets backing Buffer of this tensor with different dimensions.</returns>
public override Tensor<T> Reshape(ReadOnlySpan<int> 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<T>(Buffer, dimensions, IsReversedStride);

View file

@ -429,12 +429,16 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">type contained within the Tensor. Typically a value type such as int, double, float, etc.</typeparam>
/// <typeparam name="T">
/// type contained within the Tensor. Typically a value type such as int, double, float, etc.</typeparam>
/// <param name="diagonal">Tensor representing the diagonal to build the new tensor from.</param>
/// <param name="offset">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.</param>
/// <returns>A new tensor of the same layout and order as <paramref name="diagonal"/> of one higher rank, with the values of <paramref name="diagonal"/> along the specified diagonal and zeros elsewhere.</returns>
/// <param name="offset">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.</param>
/// <returns>A new tensor of the same layout and order as <paramref name="diagonal"/> of one higher rank,
/// with the values of <paramref name="diagonal"/> along the specified diagonal and zeros elsewhere.</returns>
public static Tensor<T> CreateFromDiagonal<T>(Tensor<T> diagonal, int offset)
{
if (diagonal.Rank < 1)
@ -678,10 +682,16 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
}
/// <summary>
/// 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).
/// </summary>
/// <param name="fromArray">Array from which to derive dimensions.</param>
/// <param name="reverseStride">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.</param>
/// <param name="reverseStride">
/// 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.</param>
protected Tensor(Array fromArray, bool reverseStride) : base(typeof(T))
{
if (fromArray == null)

View file

@ -77,6 +77,7 @@
<None Include="InferenceTest.cs"/>
<None Include="OrtIoBindingAllocationTest.cs" Condition=" '$(EnableDefaultCompileItems)' == 'true' " />
<None Include="Tensors\TensorTests.cs" Condition=" '$(EnableDefaultCompileItems)' == 'true' " />
<None Include="Tensors\ArrayTensorExtensionTests.cs" Condition=" '$(EnableDefaultCompileItems)' == 'true' " />
</ItemGroup>
<ItemGroup>
<None Update="Tensors\TensorArithmetic.tt">

View file

@ -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<int> expected, DenseTensor<int> 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<int>(), 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<int>(), 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<int>(), 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<int>(), 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<int>(), 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<int>();
var expectedDims = new int[] { 1, 1, 2, 2, 2 };
Assert.Equal(tensor.Length, array.Length);
Assert.Equal(expectedDims, tensor.Dimensions.ToArray());
CheckValues(array.Cast<int>(), tensor);
}
}
}

View file

@ -89,6 +89,9 @@
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\Tensors\TensorTests.cs">
<Link>TensorTests.cs</Link>
</Compile>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\Tensors\ArrayTensorExtensionsTests.cs">
<Link>ArrayTensorExtensionsTests.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>