[C#] Rename unreleased API, add utilities (#16806)

### Description
1. rename OrtValue.FillStringTensorElement to StringTensorSetElementAt .
To the API user I think we're conceptually setting the string at an
offset in the tensor with is roughly equivalent to `List<string> list
... list[index] = "value"`.
2. While working on new inference examples, I noticed that I am still
inclined to use `DenseTensor` for N-D indexing. Added `GetStrides()` and
`GetIndex()` from strides for long dims, so the user can obtain strides
and translate N-D indices into a flat index to operate directly on the
native `OrtValue` buffers. Expose these functions to the user.
3. Make sure we generate docs for C# public static  functions.
This commit is contained in:
Dmitri Smirnov 2023-08-02 10:06:42 -07:00 committed by GitHub
parent f4faceab28
commit bd4d011142
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 153 additions and 45 deletions

View file

@ -1,8 +1,8 @@
using System;
using Microsoft.ML.OnnxRuntime.Tensors;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
namespace Microsoft.ML.OnnxRuntime.InferenceSample
{
@ -30,7 +30,7 @@ namespace Microsoft.ML.OnnxRuntime.InferenceSample
// We create an OrtValue in this case over the buffer of potentially different shapes.
// It is Okay as long as the specified shape does not exceed the actual length of the buffer
var shape = Array.ConvertAll<int, long>(inputMeta[name].Dimensions, Convert.ToInt64);
Debug.Assert(shape.Aggregate(1L, (a, v) => a * v) <= inputData.LongLength);
Debug.Assert(ShapeUtils.GetSizeForShape(shape) <= inputData.LongLength);
var ortValue = OrtValue.CreateTensorValueFromMemory(inputData, shape);
_inputData.Add(ortValue);

View file

@ -342,7 +342,7 @@ namespace Microsoft.ML.OnnxRuntime
"Strings are not supported by this API");
}
var shapeSize = ArrayUtilities.GetSizeForShape(shape);
var shapeSize = ShapeUtils.GetSizeForShape(shape);
var requiredBufferSize = shapeSize * typeInfo.TypeSize;
if (requiredBufferSize > sizeInBytes)
{

View file

@ -5,9 +5,7 @@ using Microsoft.ML.OnnxRuntime.Tensors;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
@ -506,7 +504,7 @@ namespace Microsoft.ML.OnnxRuntime
"Cannot map managed strings buffer to native OrtValue. Use string specific interfaces");
}
var shapeSize = ArrayUtilities.GetSizeForShape(shape);
var shapeSize = ShapeUtils.GetSizeForShape(shape);
var requiredBufferSizeInBytes = shapeSize * typeInfo.TypeSize;
// We allow creating a tensor over part of the buffer
@ -552,7 +550,7 @@ namespace Microsoft.ML.OnnxRuntime
"Cannot map managed strings buffer to native OrtValue. Use string specific interfaces.");
}
var shapeSize = ArrayUtilities.GetSizeForShape(shape);
var shapeSize = ShapeUtils.GetSizeForShape(shape);
// We allow creating a tensor over part of the buffer
if (shapeSize > memory.Length)
{
@ -778,7 +776,7 @@ namespace Microsoft.ML.OnnxRuntime
/// <param name="str">ReadOnlySpan over chars</param>
/// <param name="index">index of the string element within the tensor
/// must be within bounds of [0, N)</param>
public void FillStringTensorElement(ReadOnlySpan<char> str, int index)
public void StringTensorSetElementAt(ReadOnlySpan<char> str, int index)
{
unsafe
{
@ -805,9 +803,9 @@ namespace Microsoft.ML.OnnxRuntime
/// <param name="rom">ReadOnlyMemory instance over an array of chars</param>
/// <param name="index">index of the string element within the tensor
/// must be within bounds of [0, N)</param>
public void FillStringTensorElement(ReadOnlyMemory<char> rom, int index)
public void StringTensorSetElementAt(ReadOnlyMemory<char> rom, int index)
{
FillStringTensorElement(rom.Span, index);
StringTensorSetElementAt(rom.Span, index);
}
/// <summary>
@ -818,7 +816,7 @@ namespace Microsoft.ML.OnnxRuntime
/// </summary>
/// <param name="utf8Bytes">read only span of bytes</param>
/// <param name="index">flat index of the element in the string tensor</param>
public void FillStringTensorElement(ReadOnlySpan<byte> utf8Bytes, int index)
public void StringTensorSetElementAt(ReadOnlySpan<byte> utf8Bytes, int index)
{
NativeApiStatus.VerifySuccess(NativeMethods.OrtGetResizedStringTensorElementBuffer(Handle,
(UIntPtr)index, (UIntPtr)utf8Bytes.Length, out IntPtr buffer));
@ -1111,7 +1109,7 @@ namespace Microsoft.ML.OnnxRuntime
int count = 0;
foreach (var key in keys)
{
ortValues[0].FillStringTensorElement(key.AsSpan(), count++);
ortValues[0].StringTensorSetElementAt(key.AsSpan(), count++);
}
ortValues[1] = CreateTensorValueFromMemory(values, shape);
@ -1163,7 +1161,7 @@ namespace Microsoft.ML.OnnxRuntime
int count = 0;
foreach (var value in values)
{
ortValues[1].FillStringTensorElement(value.AsSpan(), count++);
ortValues[1].StringTensorSetElementAt(value.AsSpan(), count++);
}
return CreateMap(ref ortValues[0], ref ortValues[1]);
}

View file

@ -0,0 +1,82 @@
using System.Diagnostics;
using System;
namespace Microsoft.ML.OnnxRuntime.Tensors
{
/// <summary>
/// This class contains utilities for useful calculations with shape.
/// </summary>
public static class ShapeUtils
{
/// <summary>
/// Returns a number of elements in the tensor from the given shape
/// </summary>
/// <param name="shape"></param>
/// <returns>size</returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static long GetSizeForShape(ReadOnlySpan<long> shape)
{
long product = 1;
foreach (var dim in shape)
{
if (dim < 0)
{
throw new ArgumentOutOfRangeException($"Shape must not have negative elements: {dim}");
}
checked
{
product *= dim;
}
}
return product;
}
/// <summary>
/// Gets the set of strides that can be used to calculate the offset of n-dimensions in a 1-dimensional layout
/// </summary>
/// <param name="dimensions"></param>
/// <returns>an array of strides</returns>
public static long[] GetStrides(ReadOnlySpan<long> dimensions)
{
long[] strides = new long[dimensions.Length];
if (dimensions.Length == 0)
{
return strides;
}
long stride = 1;
for (int i = strides.Length - 1; i >= 0; i--)
{
strides[i] = stride;
if (dimensions[i] < 0)
{
throw new ArgumentException($"Dimension {i} is negative");
}
stride *= dimensions[i];
}
return strides;
}
/// <summary>
/// Calculates the 1-d index for n-d indices in layout specified by strides.
/// </summary>
/// <param name="strides">pre-calculated strides</param>
/// <param name="indices">Indices. Must have the same length as strides</param>
/// <param name="startFromDimension"></param>
/// <returns>A 1-d index into the tensor buffer</returns>
public static long GetIndex(ReadOnlySpan<long> strides, ReadOnlySpan<long> indices, int startFromDimension = 0)
{
Debug.Assert(strides.Length == indices.Length);
long index = 0;
for (int i = startFromDimension; i < indices.Length; i++)
{
index += strides[i] * indices[i];
}
return index;
}
}
}

View file

@ -11,8 +11,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System;
using System.Diagnostics;
namespace Microsoft.ML.OnnxRuntime.Tensors
{
@ -20,20 +20,6 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
{
public const int StackallocMax = 16;
public static long GetSizeForShape(long[] shape)
{
long product = 1;
foreach (var dim in shape)
{
if (dim < 0)
{
throw new ArgumentOutOfRangeException("Shape must not have negative elements:" + dim);
}
product *= dim;
}
return product;
}
public static long GetProduct(ReadOnlySpan<int> dimensions, int startIndex = 0)
{
long product = 1;
@ -162,7 +148,7 @@ namespace Microsoft.ML.OnnxRuntime.Tensors
}
/// <summary>
/// Calculates the n-d indices from the 1-d index in a layout specificed by strides
/// Calculates the n-d indices from the 1-d index in a layout specified by strides
/// </summary>
/// <param name="strides"></param>
/// <param name="reverseStride"></param>

View file

@ -363,7 +363,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
Assert.Equal(typeof(float), inputMeta[inputName].ElementType);
Assert.True(inputMeta[inputName].IsTensor);
var longShape = Array.ConvertAll<int, long>(inputMeta[inputName].Dimensions, Convert.ToInt64);
var byteSize = longShape.Aggregate(1L, (a, b) => a * b) * sizeof(float);
var byteSize = ShapeUtils.GetSizeForShape(longShape);
pinnedInputs.Add(FixedBufferOnnxValue.CreateFromMemory<float>(memInfo, inputData,
TensorElementType.Float, longShape, byteSize));
@ -375,7 +375,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
Assert.Equal(typeof(float), outputMeta[outputName].ElementType);
Assert.True(outputMeta[outputName].IsTensor);
longShape = Array.ConvertAll<int, long>(outputMeta[outputName].Dimensions, Convert.ToInt64);
byteSize = longShape.Aggregate(1L, (a, b) => a * b) * sizeof(float);
byteSize = ShapeUtils.GetSizeForShape(longShape);
float[] outputBuffer = new float[expectedOutput.Length];
pinnedOutputs.Add(FixedBufferOnnxValue.CreateFromMemory<float>(memInfo, outputBuffer,
TensorElementType.Float, longShape, byteSize));

View file

@ -51,10 +51,10 @@ namespace Microsoft.ML.OnnxRuntime.Tests
_inputShape = Array.ConvertAll<int, long>(inputMeta[_inputName].Dimensions, Convert.ToInt64);
_outputShape = Array.ConvertAll<int, long>(outputMeta[_outputName].Dimensions, Convert.ToInt64);
var inputShapeSize = ArrayUtilities.GetSizeForShape(_inputShape);
var inputShapeSize = ShapeUtils.GetSizeForShape(_inputShape);
Assert.Equal(inputShapeSize, _inputData.Length);
var outputShapeSize = ArrayUtilities.GetSizeForShape(_outputShape);
var outputShapeSize = ShapeUtils.GetSizeForShape(_outputShape);
Assert.Equal(outputShapeSize, _outputData.Length);
_inputSizeInBytes = inputShapeSize * sizeof(float);
@ -297,7 +297,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
long[] inputOutputShape = { 3, 2 };
float[] input = { 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F };
var inputOutputShapeSize = ArrayUtilities.GetSizeForShape(inputOutputShape);
var inputOutputShapeSize = ShapeUtils.GetSizeForShape(inputOutputShape);
Assert.Equal(inputOutputShapeSize, input.LongLength);
var memInput = new Memory<float>(input);

View file

@ -1,6 +1,5 @@
using Microsoft.ML.OnnxRuntime.Tensors;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Xunit;
@ -22,7 +21,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
string[] strsRom = { "HelloR", "OrtR", "WorldR" };
string[] strs = { "Hello", "Ort", "World" };
long[] shape = { 1, 1, 3 };
var elementsNum = ArrayUtilities.GetSizeForShape(shape);
var elementsNum = ShapeUtils.GetSizeForShape(shape);
Assert.Equal(elementsNum, strs.Length);
Assert.Equal(elementsNum, strsRom.Length);
@ -65,13 +64,13 @@ namespace Microsoft.ML.OnnxRuntime.Tests
for (int i = 0; i < elementsNum; ++i)
{
// First populate via ROM
strTensor.FillStringTensorElement(strsRom[i].AsMemory(), i);
strTensor.StringTensorSetElementAt(strsRom[i].AsMemory(), i);
Assert.Equal(strsRom[i], strTensor.GetStringElement(i));
Assert.Equal(strsRom[i], strTensor.GetStringElementAsMemory(i).ToString());
Assert.Equal(Encoding.UTF8.GetBytes(strsRom[i]), strTensor.GetStringElementAsSpan(i).ToArray());
// Fill via Span
strTensor.FillStringTensorElement(strs[i].AsSpan(), i);
strTensor.StringTensorSetElementAt(strs[i].AsSpan(), i);
Assert.Equal(strs[i], strTensor.GetStringElement(i));
Assert.Equal(strs[i], strTensor.GetStringElementAsMemory(i).ToString());
Assert.Equal(Encoding.UTF8.GetBytes(strs[i]), strTensor.GetStringElementAsSpan(i).ToArray());
@ -127,7 +126,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
Assert.NotNull(dataTypeInfo);
Assert.Equal(dataType, dataTypeInfo.ElementType);
var elementsNum = ArrayUtilities.GetSizeForShape(shape);
var elementsNum = ShapeUtils.GetSizeForShape(shape);
Assert.True(tensor.IsTensor);
Assert.False(tensor.IsSparseTensor);
@ -162,7 +161,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
int[] data = { 1, 2, 3 };
var mem = new Memory<int>(data);
long[] shape = { 1, 1, 3 };
var elementsNum = ArrayUtilities.GetSizeForShape(shape);
var elementsNum = ShapeUtils.GetSizeForShape(shape);
Assert.Equal(elementsNum, data.Length);
@ -200,7 +199,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
data[2] = 3;
long[] shape = { 1, 1, 3 };
var elementsNum = ArrayUtilities.GetSizeForShape(shape);
var elementsNum = ShapeUtils.GetSizeForShape(shape);
Assert.Equal(elementsNum, Elements);
using (var tensor = OrtValue.CreateTensorValueWithData(OrtMemoryInfo.DefaultInstance, TensorElementType.Int32,

View file

@ -101,5 +101,48 @@ namespace Microsoft.ML.OnnxRuntime.Tests.ArrayTensorExtensions
Assert.Equal(expectedDims, tensor.Dimensions.ToArray());
CheckValues(array.Cast<int>(), tensor);
}
[Fact]
public void TestLongStrides()
{
long[] emptyStrides = ShapeUtils.GetStrides(Array.Empty<long>());
Assert.Empty(emptyStrides);
long[] negativeDims = { 2, -3, 4, 5 };
Assert.Throws<ArgumentException>(() => ShapeUtils.GetStrides(negativeDims));
ReadOnlySpan<long> goodDims = stackalloc long[] { 2, 3, 4, 5 };
long[] expectedStrides = { 60, 20, 5, 1 };
Assert.Equal(expectedStrides, ShapeUtils.GetStrides(goodDims));
}
[Fact]
public void TestLongGetIndex()
{
ReadOnlySpan<long> dims = stackalloc long[] { 2, 3, 4, 5 };
long size = ShapeUtils.GetSizeForShape(dims);
Assert.Equal(120, size);
ReadOnlySpan<long> strides = ShapeUtils.GetStrides(dims);
static void IncDims(ReadOnlySpan<long> dims, Span<long> indices)
{
for (int i = dims.Length - 1; i >= 0; i--)
{
indices[i]++;
if (indices[i] < dims[i])
break;
indices[i] = 0;
}
}
Span<long> indices = stackalloc long[] { 0, 0, 0, 0 };
for (long i = 0; i < size; i++)
{
long index = ShapeUtils.GetIndex(strides, indices);
Assert.Equal(i, index);
IncDims(dims, indices);
}
}
}
}

View file

@ -542,7 +542,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
Assert.NotNull(typeInfo);
// ArrayUtilities not accessible in all builds
var shapeSize = shape.Aggregate(1L, (a, v) => a * v);
var shapeSize = ShapeUtils.GetSizeForShape(shape);
var inferredSize = rawData.Length / typeInfo.TypeSize;
Assert.Equal(shapeSize, inferredSize);
Assert.Equal(0, rawData.Length % typeInfo.TypeSize);
@ -588,7 +588,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
{
for (int i = 0; i < strings.Count; ++i)
{
ortValue.FillStringTensorElement(strings[i].Span, i);
ortValue.StringTensorSetElementAt(strings[i].Span, i);
}
return ortValue;
}

View file

@ -524,7 +524,7 @@ EXTRACT_PACKAGE = NO
# included in the documentation.
# The default value is: NO.
EXTRACT_STATIC = NO
EXTRACT_STATIC = YES
# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
# locally in source files will be included in the documentation. If set to NO,