From bd4d0111420508ce3819f9293282deb77cbefc03 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 2 Aug 2023 10:06:42 -0700 Subject: [PATCH] [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 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. --- .../InferenceSampleApi.cs | 6 +- .../OrtAllocator.shared.cs | 2 +- .../OrtValue.shared.cs | 18 ++-- .../ShapeUtils.shared.cs | 82 +++++++++++++++++++ .../Tensors/ArrayUtilities.shared.cs | 18 +--- .../InferenceTest.cs | 4 +- .../OrtIoBindingAllocationTest.cs | 6 +- .../OrtValueTests.cs | 13 ++- .../Tensors/ArrayTensorExtensionsTests.cs | 43 ++++++++++ .../TestDataLoader.cs | 4 +- tools/ci_build/github/Doxyfile_csharp.cfg | 2 +- 11 files changed, 153 insertions(+), 45 deletions(-) create mode 100644 csharp/src/Microsoft.ML.OnnxRuntime/ShapeUtils.shared.cs diff --git a/csharp/sample/InferenceSample/Microsoft.ML.OnnxRuntime.InferenceSample/InferenceSampleApi.cs b/csharp/sample/InferenceSample/Microsoft.ML.OnnxRuntime.InferenceSample/InferenceSampleApi.cs index ab7b8c50e9..05baa872a0 100644 --- a/csharp/sample/InferenceSample/Microsoft.ML.OnnxRuntime.InferenceSample/InferenceSampleApi.cs +++ b/csharp/sample/InferenceSample/Microsoft.ML.OnnxRuntime.InferenceSample/InferenceSampleApi.cs @@ -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(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); diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/OrtAllocator.shared.cs b/csharp/src/Microsoft.ML.OnnxRuntime/OrtAllocator.shared.cs index 86df1e6abf..3f918fc2ad 100644 --- a/csharp/src/Microsoft.ML.OnnxRuntime/OrtAllocator.shared.cs +++ b/csharp/src/Microsoft.ML.OnnxRuntime/OrtAllocator.shared.cs @@ -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) { diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/OrtValue.shared.cs b/csharp/src/Microsoft.ML.OnnxRuntime/OrtValue.shared.cs index b3f3ee517d..b374371446 100644 --- a/csharp/src/Microsoft.ML.OnnxRuntime/OrtValue.shared.cs +++ b/csharp/src/Microsoft.ML.OnnxRuntime/OrtValue.shared.cs @@ -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 /// ReadOnlySpan over chars /// index of the string element within the tensor /// must be within bounds of [0, N) - public void FillStringTensorElement(ReadOnlySpan str, int index) + public void StringTensorSetElementAt(ReadOnlySpan str, int index) { unsafe { @@ -805,9 +803,9 @@ namespace Microsoft.ML.OnnxRuntime /// ReadOnlyMemory instance over an array of chars /// index of the string element within the tensor /// must be within bounds of [0, N) - public void FillStringTensorElement(ReadOnlyMemory rom, int index) + public void StringTensorSetElementAt(ReadOnlyMemory rom, int index) { - FillStringTensorElement(rom.Span, index); + StringTensorSetElementAt(rom.Span, index); } /// @@ -818,7 +816,7 @@ namespace Microsoft.ML.OnnxRuntime /// /// read only span of bytes /// flat index of the element in the string tensor - public void FillStringTensorElement(ReadOnlySpan utf8Bytes, int index) + public void StringTensorSetElementAt(ReadOnlySpan 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]); } diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/ShapeUtils.shared.cs b/csharp/src/Microsoft.ML.OnnxRuntime/ShapeUtils.shared.cs new file mode 100644 index 0000000000..f58dde8509 --- /dev/null +++ b/csharp/src/Microsoft.ML.OnnxRuntime/ShapeUtils.shared.cs @@ -0,0 +1,82 @@ +using System.Diagnostics; +using System; + +namespace Microsoft.ML.OnnxRuntime.Tensors +{ + /// + /// This class contains utilities for useful calculations with shape. + /// + public static class ShapeUtils + { + /// + /// Returns a number of elements in the tensor from the given shape + /// + /// + /// size + /// + public static long GetSizeForShape(ReadOnlySpan 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; + } + + /// + /// Gets the set of strides that can be used to calculate the offset of n-dimensions in a 1-dimensional layout + /// + /// + /// an array of strides + public static long[] GetStrides(ReadOnlySpan 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; + } + + /// + /// Calculates the 1-d index for n-d indices in layout specified by strides. + /// + /// pre-calculated strides + /// Indices. Must have the same length as strides + /// + /// A 1-d index into the tensor buffer + public static long GetIndex(ReadOnlySpan strides, ReadOnlySpan 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; + } + } +} \ No newline at end of file diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayUtilities.shared.cs b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayUtilities.shared.cs index 325acdf37c..025c1331ce 100644 --- a/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayUtilities.shared.cs +++ b/csharp/src/Microsoft.ML.OnnxRuntime/Tensors/ArrayUtilities.shared.cs @@ -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 dimensions, int startIndex = 0) { long product = 1; @@ -162,7 +148,7 @@ namespace Microsoft.ML.OnnxRuntime.Tensors } /// - /// 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 /// /// /// diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/InferenceTest.cs b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/InferenceTest.cs index 7dde5b004e..5afd5638e9 100644 --- a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/InferenceTest.cs +++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/InferenceTest.cs @@ -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(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(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(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(memInfo, outputBuffer, TensorElementType.Float, longShape, byteSize)); diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtIoBindingAllocationTest.cs b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtIoBindingAllocationTest.cs index dbade5673c..b07bcdeeb3 100644 --- a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtIoBindingAllocationTest.cs +++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtIoBindingAllocationTest.cs @@ -51,10 +51,10 @@ namespace Microsoft.ML.OnnxRuntime.Tests _inputShape = Array.ConvertAll(inputMeta[_inputName].Dimensions, Convert.ToInt64); _outputShape = Array.ConvertAll(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(input); diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtValueTests.cs b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtValueTests.cs index 1b621e2b8e..e028e88ee3 100644 --- a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtValueTests.cs +++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtValueTests.cs @@ -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(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, diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Tensors/ArrayTensorExtensionsTests.cs b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Tensors/ArrayTensorExtensionsTests.cs index 5e1e82f809..1dcf2b4c6f 100644 --- a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Tensors/ArrayTensorExtensionsTests.cs +++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/Tensors/ArrayTensorExtensionsTests.cs @@ -101,5 +101,48 @@ namespace Microsoft.ML.OnnxRuntime.Tests.ArrayTensorExtensions Assert.Equal(expectedDims, tensor.Dimensions.ToArray()); CheckValues(array.Cast(), tensor); } + + [Fact] + public void TestLongStrides() + { + long[] emptyStrides = ShapeUtils.GetStrides(Array.Empty()); + Assert.Empty(emptyStrides); + + long[] negativeDims = { 2, -3, 4, 5 }; + Assert.Throws(() => ShapeUtils.GetStrides(negativeDims)); + + ReadOnlySpan goodDims = stackalloc long[] { 2, 3, 4, 5 }; + long[] expectedStrides = { 60, 20, 5, 1 }; + Assert.Equal(expectedStrides, ShapeUtils.GetStrides(goodDims)); + } + + [Fact] + public void TestLongGetIndex() + { + ReadOnlySpan dims = stackalloc long[] { 2, 3, 4, 5 }; + long size = ShapeUtils.GetSizeForShape(dims); + Assert.Equal(120, size); + + ReadOnlySpan strides = ShapeUtils.GetStrides(dims); + + static void IncDims(ReadOnlySpan dims, Span indices) + { + for (int i = dims.Length - 1; i >= 0; i--) + { + indices[i]++; + if (indices[i] < dims[i]) + break; + indices[i] = 0; + } + } + + Span 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); + } + } } } diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/TestDataLoader.cs b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/TestDataLoader.cs index d9843f1788..9e5e2b6203 100644 --- a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/TestDataLoader.cs +++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/TestDataLoader.cs @@ -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; } diff --git a/tools/ci_build/github/Doxyfile_csharp.cfg b/tools/ci_build/github/Doxyfile_csharp.cfg index dccc15ed11..38e8a59736 100644 --- a/tools/ci_build/github/Doxyfile_csharp.cfg +++ b/tools/ci_build/github/Doxyfile_csharp.cfg @@ -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,