Add MAUI test app that can be used to test model loading and performance (#16658)

### Description
<!-- Describe your changes. -->
MAUI test app with tooling to add model and generated or provided input
test data.

The app will load the model and validate the output. It can also run a
specified number of iterations to provide basic performance information.

<img width="401" alt="image"
src="https://github.com/microsoft/onnxruntime/assets/979079/daf3af13-fb22-4cbb-9159-486b483a7485">

### Motivation and Context
<!-- - Why is this change required? What problem does it solve?
- If it fixes an open issue, please link to the issue here. -->
Primarily to make it easier to test an arbitrary model on iOS. A MAUI
app allows testing on all platforms.

---------

Co-authored-by: Edward Chen <18449977+edgchen1@users.noreply.github.com>
This commit is contained in:
Scott McKay 2023-07-18 08:21:18 +10:00 committed by GitHub
parent a45b834722
commit ad90352a68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 61316 additions and 262 deletions

View file

@ -1,15 +1,22 @@
---
# clang-format settings for the C# code
BasedOnStyle: Microsoft
BasedOnStyle: Microsoft
# Setting ColumnLimit to 0 so developer choices about where to break lines are maintained.
# Developers are responsible for adhering to the 120 character maximum.
ColumnLimit: 0
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: true
BeforeWhile: true
AfterCaseLabel: true
BeforeWhile: true
SplitEmptyFunction: false
SplitEmptyRecord: false
# unfortunately there's no config option for handling the 'get' or 'set' of properties
# unfortunately there's no config option for handling the 'get' or 'set' of properties
IndentCaseLabels: true
KeepEmptyLinesAtTheStartOfBlocks: false
SpacesInContainerLiterals: false
NamespaceIndentation: All
SpacesInContainerLiterals: false
SortIncludes: CaseSensitive
SortUsingDeclarations: LexicographicNumeric

View file

@ -0,0 +1,80 @@
using Microsoft.ML.OnnxRuntime.Tensors;
using System;
using System.Collections.Generic;
namespace Microsoft.ML.OnnxRuntime.Tests
{
internal class FloatComparer : IEqualityComparer<float>
{
private float atol = 1e-3f;
private float rtol = 1.7e-2f;
public bool Equals(float x, float y)
{
return Math.Abs(x - y) <= (atol + rtol * Math.Abs(y));
}
public int GetHashCode(float x)
{
return x.GetHashCode();
}
}
internal class DoubleComparer : IEqualityComparer<double>
{
private double atol = 1e-3;
private double rtol = 1.7e-2;
public bool Equals(double x, double y)
{
return Math.Abs(x - y) <= (atol + rtol * Math.Abs(y));
}
public int GetHashCode(double x)
{
return x.GetHashCode();
}
}
internal class ExactComparer<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
return x.Equals(y);
}
public int GetHashCode(T x)
{
return x.GetHashCode();
}
}
/// <summary>
/// Use it to compare Float16
/// </summary>
internal class Float16Comparer : IEqualityComparer<Float16>
{
public ushort tolerance = 0;
public bool Equals(Float16 x, Float16 y)
{
return Math.Abs(x.value - y.value) <= (tolerance + y.value);
}
public int GetHashCode(Float16 x)
{
return x.GetHashCode();
}
}
/// <summary>
/// Use it to compare Bloat16
/// </summary>
internal class BFloat16Comparer : IEqualityComparer<BFloat16>
{
public ushort tolerance = 0;
public bool Equals(BFloat16 x, BFloat16 y)
{
return Math.Abs(x.value - y.value) <= (tolerance + y.value);
}
public int GetHashCode(BFloat16 x)
{
return x.GetHashCode();
}
}
}

View file

@ -1993,7 +1993,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
#endif
var session = (deviceId.HasValue)
? new InferenceSession(model, option)
: new InferenceSession(model);
: new InferenceSession(model);
float[] inputData = TestDataLoader.LoadTensorFromEmbeddedResource("bench.in");
float[] expectedOutput = TestDataLoader.LoadTensorFromEmbeddedResource("bench.expected_out");
var inputMeta = session.InputMetadata;

View file

@ -15,10 +15,13 @@ namespace Microsoft.ML.OnnxRuntime.Tests
public class DisposableListTest<T> : List<T>, IDisposableReadOnlyCollection<T>
where T : IDisposable
{
public DisposableListTest() { }
public DisposableListTest(int count) : base(count) { }
public DisposableListTest()
{}
public DisposableListTest(int count)
: base(count)
{}
#region IDisposable Support
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
@ -51,7 +54,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#endregion
}
internal struct DisposableTestPair<TValue> : IDisposable
@ -90,7 +93,6 @@ namespace Microsoft.ML.OnnxRuntime.Tests
return model;
}
internal static float[] LoadTensorFromEmbeddedResource(string path)
{
var tensorData = new List<float>();
@ -99,7 +101,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
var resourceName = assembly.GetManifestResourceNames().Single(p => p.EndsWith("." + path));
using (StreamReader inputFile = new StreamReader(assembly.GetManifestResourceStream(resourceName)))
{
inputFile.ReadLine(); //skip the input name
inputFile.ReadLine(); // skip the input name
string[] dataStr = inputFile.ReadLine().Split(new char[] { ',', '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < dataStr.Length; i++)
{
@ -115,14 +117,14 @@ namespace Microsoft.ML.OnnxRuntime.Tests
if (nodeMeta.OnnxValueType != OnnxValueType.ONNX_TYPE_TENSOR)
{
throw new InvalidDataException($"Metadata for: '{nodeName}' has a type: '{nodeMeta.OnnxValueType}'" +
$" but loading as tensor: '{tensor.Name}'");
$" but loading as tensor: '{tensor.Name}'");
}
var protoDt = (Tensors.TensorElementType)tensor.DataType;
var metaElementType = nodeMeta.ElementDataType;
if (!((protoDt == metaElementType) ||
(protoDt == TensorElementType.UInt16 &&
(metaElementType == TensorElementType.BFloat16 || metaElementType == TensorElementType.Float16))))
(protoDt == TensorElementType.UInt16 &&
(metaElementType == TensorElementType.BFloat16 || metaElementType == TensorElementType.Float16))))
throw new InvalidDataException($"For node: '{nodeName}' metadata expects: '{metaElementType}' but loaded loaded tensor type: '{protoDt}'");
// Tensors within Sequences may have no dimensions as the standard allows
@ -130,7 +132,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
if (nodeMeta.Dimensions.Length > 0 && nodeMeta.Dimensions.Length != tensor.Dims.Count)
{
throw new InvalidDataException($"node: '{nodeName}' nodeMeta.Dim.Length: {nodeMeta.Dimensions.Length} " +
$"is expected to be equal to tensor.Dims.Count {tensor.Dims.Count}");
$"is expected to be equal to tensor.Dims.Count {tensor.Dims.Count}");
}
var intDims = new int[tensor.Dims.Count];
@ -143,7 +145,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
{
if ((nodeMeta.Dimensions[i] != -1) && (nodeMeta.Dimensions[i] != tensor.Dims[i]))
throw new InvalidDataException($"Node: '{nodeName}' dimension at idx {i} is {nodeMeta.Dimensions}[{i}] " +
$"is expected to either be -1 or {tensor.Dims[i]}");
$"is expected to either be -1 or {tensor.Dims[i]}");
}
// element type for Float16 and BFloat16 in the loaded tensor would always be uint16, so
@ -155,7 +157,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
}
internal static NamedOnnxValue CreateNamedOnnxValueFromTensorRawData(string nodeName, ReadOnlySpan<byte> rawData,
TensorElementType elementType, int[] intDims)
TensorElementType elementType, int[] intDims)
{
switch (elementType)
{
@ -209,33 +211,33 @@ namespace Microsoft.ML.OnnxRuntime.Tests
internal static NamedOnnxValue LoadOnnxValueFromFilePb(string fullFilename, string nodeName, NodeMetadata nodeMeta)
{
// No sparse tensor support yet
//Set buffer size to 4MB
// Set buffer size to 4MB
const int readBufferSize = 4194304;
using (var file = new FileStream(fullFilename, FileMode.Open, FileAccess.Read, FileShare.Read, readBufferSize))
{
switch (nodeMeta.OnnxValueType)
{
case OnnxValueType.ONNX_TYPE_TENSOR:
{
var tensor = Onnx.TensorProto.Parser.ParseFrom(file);
return LoadTensorPb(tensor, nodeName, nodeMeta);
}
{
var tensor = Onnx.TensorProto.Parser.ParseFrom(file);
return LoadTensorPb(tensor, nodeName, nodeMeta);
}
case OnnxValueType.ONNX_TYPE_SEQUENCE:
{
var sequence = Onnx.SequenceProto.Parser.ParseFrom(file);
return CreateNamedOnnxValueFromSequence(sequence, nodeName, nodeMeta);
}
{
var sequence = Onnx.SequenceProto.Parser.ParseFrom(file);
return CreateNamedOnnxValueFromSequence(sequence, nodeName, nodeMeta);
}
case OnnxValueType.ONNX_TYPE_MAP:
{
throw new NotImplementedException(
"Map test data format requires clarification: https://github.com/onnx/onnx/issues/5072");
}
{
throw new NotImplementedException(
"Map test data format requires clarification: https://github.com/onnx/onnx/issues/5072");
}
case OnnxValueType.ONNX_TYPE_OPTIONAL:
{
var opt = Onnx.OptionalProto.Parser.ParseFrom(file);
return CreateNamedOnnxValueFromOptional(opt, nodeName, nodeMeta);
}
{
var opt = Onnx.OptionalProto.Parser.ParseFrom(file);
return CreateNamedOnnxValueFromOptional(opt, nodeName, nodeMeta);
}
default:
throw new NotImplementedException($"Unable to load value type: {nodeMeta.OnnxValueType} not implemented");
}
@ -245,33 +247,33 @@ namespace Microsoft.ML.OnnxRuntime.Tests
internal static DisposableTestPair<OrtValue> LoadOrtValueFromFilePb(string fullFilename, string nodeName, NodeMetadata nodeMeta)
{
// No sparse tensor support yet
//Set buffer size to 4MB
// Set buffer size to 4MB
const int readBufferSize = 4194304;
using (var file = new FileStream(fullFilename, FileMode.Open, FileAccess.Read, FileShare.Read, readBufferSize))
{
switch (nodeMeta.OnnxValueType)
{
case OnnxValueType.ONNX_TYPE_TENSOR:
{
var tensor = Onnx.TensorProto.Parser.ParseFrom(file);
return new DisposableTestPair<OrtValue>(nodeName, LoadOrValueTensorPb(tensor, nodeName, nodeMeta));
}
{
var tensor = Onnx.TensorProto.Parser.ParseFrom(file);
return new DisposableTestPair<OrtValue>(nodeName, LoadOrValueTensorPb(tensor, nodeName, nodeMeta));
}
case OnnxValueType.ONNX_TYPE_SEQUENCE:
{
var sequence = Onnx.SequenceProto.Parser.ParseFrom(file);
return new DisposableTestPair<OrtValue>(nodeName, CreateOrtValueFromSequence(sequence, nodeName, nodeMeta));
}
{
var sequence = Onnx.SequenceProto.Parser.ParseFrom(file);
return new DisposableTestPair<OrtValue>(nodeName, CreateOrtValueFromSequence(sequence, nodeName, nodeMeta));
}
case OnnxValueType.ONNX_TYPE_MAP:
{
throw new NotImplementedException(
"Map test data format requires clarification: https://github.com/onnx/onnx/issues/5072");
}
{
throw new NotImplementedException(
"Map test data format requires clarification: https://github.com/onnx/onnx/issues/5072");
}
case OnnxValueType.ONNX_TYPE_OPTIONAL:
{
var opt = Onnx.OptionalProto.Parser.ParseFrom(file);
return new DisposableTestPair<OrtValue>(nodeName, CreateOrtValueFromOptional(opt, nodeName, nodeMeta));
}
{
var opt = Onnx.OptionalProto.Parser.ParseFrom(file);
return new DisposableTestPair<OrtValue>(nodeName, CreateOrtValueFromOptional(opt, nodeName, nodeMeta));
}
default:
throw new NotImplementedException($"Unable to load value type: {nodeMeta.OnnxValueType} not implemented");
}
@ -279,14 +281,14 @@ namespace Microsoft.ML.OnnxRuntime.Tests
}
private static void SequenceCheckMatchOnnxType(string nodeName, SequenceMetadata meta,
OnnxValueType onnxType)
OnnxValueType onnxType)
{
if (meta.ElementMeta.OnnxValueType == onnxType)
return;
throw new InvalidDataException($"Sequence node: '{nodeName}' " +
$"has element type: '{onnxType}'" +
$" expected: '{meta.ElementMeta.OnnxValueType}'");
$"has element type: '{onnxType}'" +
$" expected: '{meta.ElementMeta.OnnxValueType}'");
}
private static string MakeSequenceElementName(string nodeName, string seqName, int seqNum)
@ -307,55 +309,54 @@ namespace Microsoft.ML.OnnxRuntime.Tests
switch (seqElemType)
{
case Onnx.SequenceProto.Types.DataType.Tensor:
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_TENSOR);
var sequenceOfTensors = new List<NamedOnnxValue>(sequence.TensorValues.Count);
foreach (var tensor in sequence.TensorValues)
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_TENSOR);
var sequenceOfTensors = new List<NamedOnnxValue>(sequence.TensorValues.Count);
foreach (var tensor in sequence.TensorValues)
{
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
var namedOnnxValue = LoadTensorPb(tensor, elemName, elemMeta);
sequenceOfTensors.Add(namedOnnxValue);
}
return NamedOnnxValue.CreateFromSequence(nodeName, sequenceOfTensors);
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
var namedOnnxValue = LoadTensorPb(tensor, elemName, elemMeta);
sequenceOfTensors.Add(namedOnnxValue);
}
return NamedOnnxValue.CreateFromSequence(nodeName, sequenceOfTensors);
}
case Onnx.SequenceProto.Types.DataType.Sequence:
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_SEQUENCE);
var seqOfSequences = new List<NamedOnnxValue>(sequence.SequenceValues.Count);
foreach (var s in sequence.SequenceValues)
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_SEQUENCE);
var seqOfSequences = new List<NamedOnnxValue>(sequence.SequenceValues.Count);
foreach (var s in sequence.SequenceValues)
{
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
seqOfSequences.Add(CreateNamedOnnxValueFromSequence(s, elemName, elemMeta));
}
return NamedOnnxValue.CreateFromSequence(nodeName, seqOfSequences);
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
seqOfSequences.Add(CreateNamedOnnxValueFromSequence(s, elemName, elemMeta));
}
return NamedOnnxValue.CreateFromSequence(nodeName, seqOfSequences);
}
case Onnx.SequenceProto.Types.DataType.Map:
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_MAP);
var seqOfMaps = new List<NamedOnnxValue>(sequence.MapValues.Count);
foreach (var m in sequence.MapValues)
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_MAP);
var seqOfMaps = new List<NamedOnnxValue>(sequence.MapValues.Count);
foreach (var m in sequence.MapValues)
{
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
seqOfMaps.Add(CreateNamedOnnxValueFromMap(m, elemName, elemMeta));
}
return NamedOnnxValue.CreateFromSequence(nodeName, seqOfMaps);
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
seqOfMaps.Add(CreateNamedOnnxValueFromMap(m, elemName, elemMeta));
}
return NamedOnnxValue.CreateFromSequence(nodeName, seqOfMaps);
}
case Onnx.SequenceProto.Types.DataType.Optional:
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_OPTIONAL);
var seqOfOpts = new List<NamedOnnxValue>(sequence.OptionalValues.Count);
foreach (var opt in sequence.OptionalValues)
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_OPTIONAL);
var seqOfOpts = new List<NamedOnnxValue>(sequence.OptionalValues.Count);
foreach (var opt in sequence.OptionalValues)
{
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
seqOfOpts.Add(CreateNamedOnnxValueFromOptional(opt, elemName, elemMeta));
}
return NamedOnnxValue.CreateFromSequence(nodeName, seqOfOpts);
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
seqOfOpts.Add(CreateNamedOnnxValueFromOptional(opt, elemName, elemMeta));
}
return NamedOnnxValue.CreateFromSequence(nodeName, seqOfOpts);
}
default:
throw new NotImplementedException($"Sequence test data loading does not support element type: " +
$"'{seqElemType}'");
$"'{seqElemType}'");
}
}
internal static NamedOnnxValue CreateNamedOnnxValueFromMap(Onnx.MapProto map, string nodeName, NodeMetadata nodeMetadata)
{
@ -369,20 +370,20 @@ namespace Microsoft.ML.OnnxRuntime.Tests
switch ((Onnx.OptionalProto.Types.DataType)optional.ElemType)
{
case Onnx.OptionalProto.Types.DataType.Tensor:
{
var tensor = optional.TensorValue;
return LoadTensorPb(tensor, nodeName, meta);
}
{
var tensor = optional.TensorValue;
return LoadTensorPb(tensor, nodeName, meta);
}
case Onnx.OptionalProto.Types.DataType.Sequence:
{
var sequence = optional.SequenceValue;
return CreateNamedOnnxValueFromSequence(sequence, nodeName, meta);
}
{
var sequence = optional.SequenceValue;
return CreateNamedOnnxValueFromSequence(sequence, nodeName, meta);
}
case Onnx.OptionalProto.Types.DataType.Map:
{
var map = optional.MapValue;
return CreateNamedOnnxValueFromMap(map, nodeName, meta);
}
{
var map = optional.MapValue;
return CreateNamedOnnxValueFromMap(map, nodeName, meta);
}
case Onnx.OptionalProto.Types.DataType.Optional:
throw new NotImplementedException($"Unable to load '{nodeName}' optional contained within optional");
default:
@ -395,7 +396,8 @@ namespace Microsoft.ML.OnnxRuntime.Tests
}
internal static NamedOnnxValue CreateNamedOnnxValueFromRawData<T>(string name, ReadOnlySpan<byte> rawData,
int[] dimensions) where T : struct
int[] dimensions)
where T : struct
{
var typedSrcSpan = MemoryMarshal.Cast<byte, T>(rawData);
var dt = new DenseTensor<T>(typedSrcSpan.ToArray(), dimensions);
@ -407,14 +409,14 @@ namespace Microsoft.ML.OnnxRuntime.Tests
if (nodeMeta.OnnxValueType != OnnxValueType.ONNX_TYPE_TENSOR)
{
throw new InvalidDataException($"Metadata for: '{nodeName}' has a type: '{nodeMeta.OnnxValueType}'" +
$" but loading as tensor: {tensor.Name}");
$" but loading as tensor: {tensor.Name}");
}
var protoDt = (Tensors.TensorElementType)tensor.DataType;
var metaElementType = nodeMeta.ElementDataType;
if (!((protoDt == metaElementType) ||
(protoDt == TensorElementType.UInt16 &&
(metaElementType == TensorElementType.BFloat16 || metaElementType == TensorElementType.Float16))))
(protoDt == TensorElementType.UInt16 &&
(metaElementType == TensorElementType.BFloat16 || metaElementType == TensorElementType.Float16))))
throw new InvalidDataException($"For node: '{nodeName}' metadata expects: '{metaElementType}' but loaded loaded tensor type: '{protoDt}'");
// Tensors within Sequences may have no dimensions as the standard allows
@ -422,7 +424,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
if (nodeMeta.Dimensions.Length > 0 && nodeMeta.Dimensions.Length != tensor.Dims.Count)
{
throw new InvalidDataException($"node: '{nodeName}' nodeMeta.Dim.Length: {nodeMeta.Dimensions.Length} " +
$"is expected to be equal to tensor.Dims.Count {tensor.Dims.Count}");
$"is expected to be equal to tensor.Dims.Count {tensor.Dims.Count}");
}
var shape = tensor.Dims.ToArray();
@ -431,7 +433,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
{
if ((nodeMeta.Dimensions[i] != -1) && (nodeMeta.Dimensions[i] != shape[i]))
throw new InvalidDataException($"Node: '{nodeName}' dimension at idx {i} is {nodeMeta.Dimensions}[{i}] " +
$"is expected to either be -1 or {shape[i]}");
$"is expected to either be -1 or {shape[i]}");
}
// element type for Float16 and BFloat16 in the loaded tensor would always be uint16, so
@ -452,56 +454,55 @@ namespace Microsoft.ML.OnnxRuntime.Tests
switch (seqElemType)
{
case Onnx.SequenceProto.Types.DataType.Tensor:
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_TENSOR);
using (var sequenceOfTensors = new DisposableListTest<OrtValue>(sequence.TensorValues.Count))
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_TENSOR);
using (var sequenceOfTensors = new DisposableListTest<OrtValue>(sequence.TensorValues.Count))
foreach (var tensor in sequence.TensorValues)
{
foreach (var tensor in sequence.TensorValues)
{
var element = LoadOrValueTensorPb(tensor, sequence.Name, elemMeta);
sequenceOfTensors.Add(element);
}
return OrtValue.CreateSequence(sequenceOfTensors);
var element = LoadOrValueTensorPb(tensor, sequence.Name, elemMeta);
sequenceOfTensors.Add(element);
}
return OrtValue.CreateSequence(sequenceOfTensors);
}
}
case Onnx.SequenceProto.Types.DataType.Sequence: // Sequence of sequences
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_SEQUENCE);
using (var seqOfSequences = new DisposableListTest<OrtValue>(sequence.TensorValues.Count))
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_SEQUENCE);
using (var seqOfSequences = new DisposableListTest<OrtValue>(sequence.TensorValues.Count))
foreach (var s in sequence.SequenceValues)
{
foreach (var s in sequence.SequenceValues)
{
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
var ortValue = CreateOrtValueFromSequence(s, elemName, elemMeta);
seqOfSequences.Add(ortValue);
}
return OrtValue.CreateSequence(seqOfSequences);
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
var ortValue = CreateOrtValueFromSequence(s, elemName, elemMeta);
seqOfSequences.Add(ortValue);
}
return OrtValue.CreateSequence(seqOfSequences);
}
}
case Onnx.SequenceProto.Types.DataType.Map:
{
throw new NotImplementedException(
"Test data format for maps is under investigation");
}
{
throw new NotImplementedException(
"Test data format for maps is under investigation");
}
case Onnx.SequenceProto.Types.DataType.Optional:
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_OPTIONAL);
using (var seqOfSequences = new DisposableListTest<OrtValue>(sequence.TensorValues.Count))
{
SequenceCheckMatchOnnxType(nodeName, sequenceMeta, OnnxValueType.ONNX_TYPE_OPTIONAL);
using (var seqOfSequences = new DisposableListTest<OrtValue>(sequence.TensorValues.Count))
foreach (var opt in sequence.OptionalValues)
{
foreach (var opt in sequence.OptionalValues)
{
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
var ortValue = CreateOrtValueFromOptional(opt, elemName, elemMeta);
seqOfSequences.Add(ortValue);
}
return OrtValue.CreateSequence(seqOfSequences);
var elemName = MakeSequenceElementName(nodeName, sequence.Name, seqNum++);
var ortValue = CreateOrtValueFromOptional(opt, elemName, elemMeta);
seqOfSequences.Add(ortValue);
}
return OrtValue.CreateSequence(seqOfSequences);
}
}
default:
throw new NotImplementedException($"Sequence test data loading does not support element type: " +
$"'{seqElemType}'");
$"'{seqElemType}'");
}
}
internal static OrtValue CreateOrtValueFromOptional(Onnx.OptionalProto optional, string nodeName, NodeMetadata nodeMetadata)
@ -510,21 +511,20 @@ namespace Microsoft.ML.OnnxRuntime.Tests
switch ((Onnx.OptionalProto.Types.DataType)optional.ElemType)
{
case Onnx.OptionalProto.Types.DataType.Tensor:
{
var tensor = optional.TensorValue;
return LoadOrValueTensorPb(tensor, nodeName, meta);
}
{
var tensor = optional.TensorValue;
return LoadOrValueTensorPb(tensor, nodeName, meta);
}
case Onnx.OptionalProto.Types.DataType.Sequence:
{
var sequence = optional.SequenceValue;
return CreateOrtValueFromSequence(sequence, nodeName, meta);
}
{
var sequence = optional.SequenceValue;
return CreateOrtValueFromSequence(sequence, nodeName, meta);
}
case Onnx.OptionalProto.Types.DataType.Map:
{
throw new NotImplementedException(
"Test data format for maps is under investigation");
}
{
throw new NotImplementedException(
"Test data format for maps is under investigation");
}
case Onnx.OptionalProto.Types.DataType.Optional:
throw new NotImplementedException($"Unable to load '{nodeName}' optional contained within optional");
default:
@ -566,7 +566,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
}
internal static NamedOnnxValue CreateNamedOnnxValueFromStringTensor(IList<Google.Protobuf.ByteString> strings,
string nodeName, int[] dimensions)
string nodeName, int[] dimensions)
{
string[] strArray = new string[strings.Count];
for (int i = 0; i < strings.Count; ++i)
@ -582,7 +582,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
return NamedOnnxValue.CreateFromTensor<string>(nodeName, dt);
}
internal static OrtValue CreateOrtValueFromStringTensor(IList<Google.Protobuf.ByteString> strings,
long[] shape)
long[] shape)
{
var ortValue = OrtValue.CreateTensorWithEmptyStrings(OrtAllocator.DefaultInstance, shape);
try
@ -608,7 +608,7 @@ namespace Microsoft.ML.OnnxRuntime.Tests
using (var inputFile = new System.IO.StreamReader(filename))
{
if (skipheader)
inputFile.ReadLine(); //skip the input name
inputFile.ReadLine(); // skip the input name
string[] dataStr = inputFile.ReadLine().Split(new char[] { ',', '[', ']', ' ' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < dataStr.Length; i++)
{
@ -619,78 +619,4 @@ namespace Microsoft.ML.OnnxRuntime.Tests
return tensorData.ToArray();
}
}
internal class FloatComparer : IEqualityComparer<float>
{
private float atol = 1e-3f;
private float rtol = 1.7e-2f;
public bool Equals(float x, float y)
{
return Math.Abs(x - y) <= (atol + rtol * Math.Abs(y));
}
public int GetHashCode(float x)
{
return x.GetHashCode();
}
}
internal class DoubleComparer : IEqualityComparer<double>
{
private double atol = 1e-3;
private double rtol = 1.7e-2;
public bool Equals(double x, double y)
{
return Math.Abs(x - y) <= (atol + rtol * Math.Abs(y));
}
public int GetHashCode(double x)
{
return x.GetHashCode();
}
}
class ExactComparer<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
return x.Equals(y);
}
public int GetHashCode(T x)
{
return x.GetHashCode();
}
}
/// <summary>
/// Use it to compare Float16
/// </summary>
internal class Float16Comparer : IEqualityComparer<Float16>
{
public ushort tolerance = 0;
public bool Equals(Float16 x, Float16 y)
{
return Math.Abs(x.value - y.value) <= (tolerance + y.value);
}
public int GetHashCode(Float16 x)
{
return x.GetHashCode();
}
}
/// <summary>
/// Use it to compare Bloat16
/// </summary>
internal class BFloat16Comparer : IEqualityComparer<BFloat16>
{
public ushort tolerance = 0;
public bool Equals(BFloat16 x, BFloat16 y)
{
return Math.Abs(x.value - y.value) <= (tolerance + y.value);
}
public int GetHashCode(BFloat16 x)
{
return x.GetHashCode();
}
}
}
}

View file

@ -119,30 +119,8 @@
<!-- NOTE: The xUnit framework doesn't pickup the tests defined within the referenced Microsoft.ML.OnnxRuntime.Tests.Common project -->
<ItemGroup>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\OrtFloat16Tests.cs">
<Link>OrtFloat16Tests.cs</Link>
</Compile>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\OrtEnvTests.cs">
<Link>OrtEnvTests.cs</Link>
</Compile>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\InferenceTest.cs">
<Link>InferenceTest.cs</Link>
</Compile>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\OrtIoBindingAllocationTest.cs">
<Link>OrtIoBindingAllocationTest.cs</Link>
</Compile>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\Tensors\TensorTests.cs">
<Link>TensorTests.cs</Link>
</Compile>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\OrtValueTests.cs">
<Link>OrtValueTests.cs</Link>
</Compile>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\Tensors\ArrayTensorExtensionsTests.cs">
<Link>ArrayTensorExtensionsTests.cs</Link>
</Compile>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\TrainingTest.cs">
<Link>TrainingTest.cs</Link>
</Compile>
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\**\*Test.cs" />
<Compile Include="..\Microsoft.ML.OnnxRuntime.Tests.Common\**\*Tests.cs" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MauiModelTester"
x:Class="MauiModelTester.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View file

@ -0,0 +1,11 @@
namespace MauiModelTester;
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="MauiModelTester.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MauiModelTester"
Shell.FlyoutBehavior="Disabled">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Shell>

View file

@ -0,0 +1,9 @@
namespace MauiModelTester;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiModelTester.MainPage" BackgroundColor="Black">
<ScrollView x:Name="MainScrollView" Margin="10">
<Grid RowSpacing="10" ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0"
Text="ONNX Runtime Mobile Model Tester"
FontSize="Header"
TextColor="DeepSkyBlue"
HorizontalOptions="Center" />
<Grid Grid.Row="1" ColumnSpacing="10">
<!-- Run params. EP and iterations currently-->
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Text="Execution Provider"
TextColor="White" VerticalOptions="Center"/>
<Picker Grid.Row="0" Grid.Column="1" x:Name="ExecutionProviderOptions"
TextColor="White" Margin="5,5,5,5"/>
<Label Grid.Row="1" Grid.Column="0" Text="Iterations" TextColor="White" VerticalOptions="Center"/>
<Entry Grid.Row="1" Grid.Column="1" x:Name="Iterations" Text="10" TextColor="White"/>
</Grid>
<Button Grid.Row="2"
x:Name="RunButton" Text="Run"
FontSize="Large"
TextColor="White"
BackgroundColor="DeepSkyBlue"
SemanticProperties.Hint="Runs the model the requested number of times."
Clicked="OnRunClicked"
HorizontalOptions="Center" />
<Label Grid.Row="3" Text="Performance test results" TextColor="White" FontAttributes="Bold" />
<ActivityIndicator Grid.Row="4" x:Name="BusyIndicator" IsRunning="False" Color="Orange" />
<ScrollView Grid.Row="4" x:Name="TestResultsView" Margin="10">
<StackLayout x:Name="TestResults">
<Label Text="Results will be displayed after the model is run." TextColor="GhostWhite" />
</StackLayout>
</ScrollView>
</Grid>
</ScrollView>
</ContentPage>

View file

@ -0,0 +1,174 @@
using System.Diagnostics;
namespace MauiModelTester;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
// See:
// ONNX Runtime Execution Providers: https://onnxruntime.ai/docs/execution-providers/
// Core ML: https://developer.apple.com/documentation/coreml
// NNAPI: https://developer.android.com/ndk/guides/neuralnetworks
ExecutionProviderOptions.Items.Add(nameof(ExecutionProviders.CPU));
if (DeviceInfo.Platform == DevicePlatform.Android)
{
ExecutionProviderOptions.Items.Add(nameof(ExecutionProviders.NNAPI));
}
if (DeviceInfo.Platform == DevicePlatform.iOS)
{
ExecutionProviderOptions.Items.Add(nameof(ExecutionProviders.CoreML));
}
// XNNPACK provides optimized CPU execution on ARM64 and ARM platforms for models using float
var arch = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture;
if (arch == System.Runtime.InteropServices.Architecture.Arm64 ||
arch == System.Runtime.InteropServices.Architecture.Arm)
{
ExecutionProviderOptions.Items.Add(nameof(ExecutionProviders.XNNPACK));
}
ExecutionProviderOptions.SelectedIndex = 0; // default to CPU
ExecutionProviderOptions.SelectedIndexChanged += ExecutionProviderOptions_SelectedIndexChanged;
_currentExecutionProvider = ExecutionProviders.CPU;
// start creating session in background.
CreateInferenceSession();
}
private async Task CreateInferenceSession()
{
// wait if we're already creating an inference session.
if (_inferenceSessionCreationTask != null)
{
await _inferenceSessionCreationTask.ConfigureAwait(false);
_inferenceSessionCreationTask = null;
}
_inferenceSessionCreationTask = CreateInferenceSessionImpl();
}
private async Task CreateInferenceSessionImpl()
{
var executionProvider = ExecutionProviderOptions.SelectedItem switch {
nameof(ExecutionProviders.NNAPI) => ExecutionProviders.NNAPI,
nameof(ExecutionProviders.CoreML) => ExecutionProviders.CoreML,
nameof(ExecutionProviders.XNNPACK) => ExecutionProviders.XNNPACK,
_ => ExecutionProviders.CPU
};
if (_inferenceSession == null || executionProvider != _currentExecutionProvider)
{
_currentExecutionProvider = executionProvider;
// re/create an inference session with the execution provider.
// this is an expensive operation as we have to reload the model, and should be avoided in production apps.
_inferenceSession = new OrtInferenceSession(_currentExecutionProvider);
await _inferenceSession.Create();
// Display the results which at this point will have the model load time and the warmup Run() time.
ShowResults();
}
}
private void ExecutionProviderOptions_SelectedIndexChanged(object sender, EventArgs e)
{
// update in background
UpdateExecutionProvider();
}
private void OnRunClicked(object sender, EventArgs e)
{
// run in background
RunAsync();
}
private async Task UpdateExecutionProvider()
{
try
{
await SetBusy(true);
await CreateInferenceSession();
await SetBusy(false);
}
catch (Exception ex)
{
await SetBusy(false);
MainThread.BeginInvokeOnMainThread(() => DisplayAlert("Error", ex.Message, "OK"));
}
}
private async Task RunAsync()
{
try
{
await SetBusy(true);
await ClearResult();
var iterationsStr = Iterations.Text;
int iterations = iterationsStr == string.Empty ? 10 : int.Parse(iterationsStr);
// create inference session if it doesn't exist or EP has changed
await CreateInferenceSession();
await Task.Run(() => _inferenceSession.Run(iterations));
await SetBusy(false);
ShowResults();
}
catch (Exception ex)
{
await SetBusy(false);
MainThread.BeginInvokeOnMainThread(() => DisplayAlert("Error", ex.Message, "OK"));
}
}
private async Task SetBusy(bool busy)
{
await MainThread.InvokeOnMainThreadAsync(() =>
{
// disable controls that would create a new session or another
// Run call until we're done with the current Run.
ExecutionProviderOptions.IsEnabled = !busy;
RunButton.IsEnabled = !busy;
BusyIndicator.IsRunning = busy;
BusyIndicator.IsVisible = busy;
});
}
private async Task ClearResult()
{
await MainThread.InvokeOnMainThreadAsync(() =>
{ TestResults.Clear(); });
}
private void ShowResults()
{
var createResults = () =>
{
var stats = _inferenceSession.PerfStats;
var label = new Label { TextColor = Colors.GhostWhite };
label.Text = $"Model load time: {stats.LoadTime.TotalMilliseconds:F4} ms\n";
label.Text += $"Warmup run time: {stats.WarmupTime.TotalMilliseconds:F4} ms\n\n";
label.Text += string.Join('\n', stats.GetRunStatsReport(true));
TestResults.Add(label);
Debug.WriteLine(label.Text);
};
MainThread.BeginInvokeOnMainThread(createResults);
}
private ExecutionProviders _currentExecutionProvider;
private OrtInferenceSession _inferenceSession;
private Task _inferenceSessionCreationTask;
}

View file

@ -0,0 +1,69 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0-android;net6.0-ios</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net6.0-windows10.0.19041.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>MauiModelTester</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Display name -->
<ApplicationTitle>MauiModelTester</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.microsoft.OnnxRuntime.MauiModelTester</ApplicationId>
<ApplicationIdGuid>24eecf93-5fe6-4ea3-a3dd-baf27df7656a</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">12.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageIcon>onnxruntime_icon.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\onnxruntime_icon.png" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\onnxruntime_logo.png" Resize="true" Color="#C76006" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\test\Microsoft.ML.OnnxRuntime.Tests.Common\EqualityComparers.cs" />
<Compile Include="..\Microsoft.ML.OnnxRuntime.PerfTool\OnnxMl.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.16.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.16.0-dev-20230707-1222-2a11f29eaa" />
<PackageReference Include="Microsoft.ML.OnnxRuntime.Extensions" Version="0.8.0" />
</ItemGroup>
<!-- Project loaded by MauiModelTester.sln. Use the nightly package. -->
<ItemGroup Condition="'$(SolutionName)'=='MauiModelTester'">
<PackageReference Include="Microsoft.ML.OnnxRuntime.Managed" Version="1.16.0-dev-20230707-1224-2a11f29eaa" />
</ItemGroup>
<!-- Project loaded by the ORT C# solution in /csharp. Use the local build of the C# wrapper -->
<ItemGroup Condition="'$(SolutionName)'!='MauiModelTester'">
<ProjectReference Include="..\..\src\Microsoft.ML.OnnxRuntime\Microsoft.ML.OnnxRuntime.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,27 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31611.283
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MauiModelTester", "MauiModelTester.csproj", "{80517A36-E015-4282-89A7-245B25A963B7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{80517A36-E015-4282-89A7-245B25A963B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80517A36-E015-4282-89A7-245B25A963B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80517A36-E015-4282-89A7-245B25A963B7}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{80517A36-E015-4282-89A7-245B25A963B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80517A36-E015-4282-89A7-245B25A963B7}.Release|Any CPU.Build.0 = Release|Any CPU
{80517A36-E015-4282-89A7-245B25A963B7}.Release|Any CPU.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {61F7FB11-1E47-470C-91E2-47F8143E1572}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,24 @@
using Microsoft.Extensions.Logging;
namespace MauiModelTester;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>().ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
#if DEBUG
// Add the extension debug logger so Debug.WriteLine output shows up in the Output window when running in VS
builder.Logging.AddDebug();
System.Diagnostics.Debug.WriteLine("Debug output enabled.");
#endif
return builder.Build();
}
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
<packageSources>
<clear />
<add key="NuGet Official" value="https://api.nuget.org/v3/index.json" />
<add key="ORT-Nightly" value="https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/ORT-Nightly/nuget/v3/index.json" />
</packageSources>
<disabledPackageSources>
<clear />
</disabledPackageSources>
</configuration>

View file

@ -0,0 +1,115 @@
using Microsoft.ML.OnnxRuntime;
using System.Diagnostics;
namespace MauiModelTester
{
public enum ExecutionProviders
{
CPU, // CPU execution provider is always available by default
NNAPI, // NNAPI is available on Android
CoreML, // CoreML is available on iOS/macOS
XNNPACK // XNNPACK is available on ARM/ARM64 platforms and benefits 32-bit float models
}
// An inference session runs an ONNX model
internal class OrtInferenceSession
{
public OrtInferenceSession(ExecutionProviders provider = ExecutionProviders.CPU)
{
_sessionOptions = new SessionOptions();
switch (_executionProvider)
{
case ExecutionProviders.CPU:
break;
case ExecutionProviders.NNAPI:
_sessionOptions.AppendExecutionProvider_Nnapi();
break;
case ExecutionProviders.CoreML:
_sessionOptions.AppendExecutionProvider_CoreML();
break;
case ExecutionProviders.XNNPACK:
_sessionOptions.AppendExecutionProvider("XNNPACK");
break;
}
// enable pre/post processing custom operators from onnxruntime-extensions
_sessionOptions.RegisterOrtExtensions();
_perfStats = new PerfStats();
}
// async task to create the inference session which is an expensive operation.
public async Task Create()
{
// create the InferenceSession. this is an expensive operation so only do this when necessary.
// the InferenceSession supports multiple calls to Run, including concurrent calls.
var modelBytes = await Utils.LoadResource("test_data/model.onnx");
var stopwatch = new Stopwatch();
stopwatch.Start();
_inferenceSession = new InferenceSession(modelBytes, _sessionOptions);
stopwatch.Stop();
_perfStats.LoadTime = stopwatch.Elapsed;
(_inputs, _expectedOutputs) = await Utils.LoadTestData();
// warmup
Run(1, true);
}
public void Run(int iterations = 1, bool isWarmup = false)
{
// do all setup outside of the timing
var runOptions = new RunOptions();
var outputNames = _inferenceSession.OutputNames;
_perfStats.ClearRunTimes();
// var stopwatch = new Stopwatch();
for (var i = 0; i < iterations; i++)
{
// stopwatch.Restart();
var stopwatch = new Stopwatch();
stopwatch.Start();
using IDisposableReadOnlyCollection<OrtValue> results =
_inferenceSession.Run(runOptions, _inputs, outputNames);
stopwatch.Stop();
if (isWarmup)
{
_perfStats.WarmupTime = stopwatch.Elapsed;
// validate the expected output on the first Run only
if (_expectedOutputs.Count > 0)
{
// create dictionary of output name to results
var actual = outputNames.Zip(results).ToDictionary(x => x.First, x => x.Second);
foreach (var expectedOutput in _expectedOutputs)
{
var outputName = expectedOutput.Key;
Utils.TensorComparer.VerifyTensorResults(outputName, expectedOutput.Value,
actual[outputName]);
}
}
}
else
{
_perfStats.AddRunTime(stopwatch.Elapsed);
}
}
}
public PerfStats PerfStats => _perfStats;
private SessionOptions _sessionOptions;
private InferenceSession _inferenceSession;
private ExecutionProviders _executionProvider = ExecutionProviders.CPU;
private Dictionary<string, OrtValue> _inputs;
private Dictionary<string, OrtValue> _expectedOutputs;
private PerfStats _perfStats;
}
}

View file

@ -0,0 +1,71 @@
namespace MauiModelTester
{
internal class PerfStats
{
internal PerfStats()
{
_runTimes = new List<double>();
}
internal TimeSpan LoadTime { get; set; }
internal TimeSpan WarmupTime { get; set; }
/// <summary>
/// Add TimeSpan for one call to Run.
/// </summary>
/// <param name="runTime">Elapsed time</param>
internal void AddRunTime(TimeSpan runTime)
{
_runTimes.Add(runTime.TotalMilliseconds);
}
internal void ClearRunTimes()
{
_runTimes.Clear();
}
internal List<string> GetRunStatsReport(bool outputRunTimes = false)
{
List<string> lines = new List<string>();
if (_runTimes.Count > 0)
{
// we want unsorted run times if we need to investigate any unexpected latency as that gives a clearer
// picture of when in the iterations the latency occurred.
List<string> runTimesOutput = null;
if (outputRunTimes)
{
runTimesOutput = new List<string>();
runTimesOutput.Add("\nRun times (ms):");
runTimesOutput.Add(string.Join(", ", _runTimes.Select(x => x.ToString("F2"))));
}
_runTimes.Sort();
var totalRunTime = _runTimes.Sum();
lines.Add($"Total time for {_runTimes.Count} iterations: {totalRunTime:F2} ms\n");
lines.Add($"Average run time: {totalRunTime / _runTimes.Count:F2} ms");
lines.Add($"Minimum run time: {_runTimes.Min():F2} ms");
lines.Add($"Maximum run time: {_runTimes.Max():F2} ms\n");
lines.Add($"50th Percentile run time: {_runTimes[(int)(_runTimes.Count * 0.5)]:F2} ms");
lines.Add($"90th Percentile run time: {_runTimes[(int)(_runTimes.Count * 0.9)]:F2} ms");
if (_runTimes.Count >= 100)
{
lines.Add($"95th Percentile run time: {_runTimes[(int)(_runTimes.Count * 0.95)]:F2} ms");
lines.Add($"99th Percentile run time: {_runTimes[(int)(_runTimes.Count * 0.99)]:F2} ms");
}
if (outputRunTimes)
{
lines.AddRange(runTimesOutput);
}
}
return lines;
}
private List<double> _runTimes;
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="false" android:icon="@mipmap/onnxruntime_icon" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View file

@ -0,0 +1,10 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
namespace MauiModelTester;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}

View file

@ -0,0 +1,15 @@
using Android.App;
using Android.Runtime;
namespace MauiModelTester;
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>

View file

@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="MauiModelTester.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:MauiModelTester.WinUI">
</maui:MauiWinUIApplication>

View file

@ -0,0 +1,24 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace MauiModelTester.WinUI;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
<mp:PhoneIdentity PhoneProductId="796F758E-3487-477A-A1A3-9F8F898A776C" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View file

@ -0,0 +1,9 @@
using Foundation;
namespace MauiModelTester;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View file

@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;
namespace MauiModelTester;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View file

@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "MsixPackage",
"nativeDebugging": false
}
}
}

View file

@ -0,0 +1,80 @@
# MAUI Model Tester
## Usage
Run create_test_data.py to specify
- path to the model you wish to use
- symbolic dimension values to use if needed
- any specific input if the randomly generated input will not be good enough
- you can create specific input with /tools/python/onnx_test_data_utils.py
- see the comments in create_test_data.py for more details
- expected output if saving the output from running the model locally is not good enough
- the model will be executed when creating the test data to validate the input
This will copy the model to Resources\Raw\test_data\model.onnx and the test data files to
Resources\Raw\test_data\test_data_set_0
The MAUI application will read the model and test data from those locations and should need no other changes to be able
to execute the model.
NOTE: The project uses builds from the nightly feed to keep things simple.
If it was part of the main ONNX Runtime C# solution we'd have to
- add the ORT nightly feed to the top level nuget.config
- this potentially adds confusion about where nuget packages come from in unit tests
- keep updating the referenced nightly packages so they remain valid so the complete solution builds in the CI
## Testing C# or native code changes
If you have new code to test the easiest way is to run the nuget packaging pipeline on
https://aiinfra.visualstudio.com/Lotus/_build against your branch. Download the native and managed nuget packages from
the CI artifacts and update the nuget.config to point to the directory they are in.
This can be used to test both native and C# code changes.
If you wish to test local changes to the C# code, you can create a local nuget package and add the directory it's in to
nuget.config. With the current setup you'd first need to build ORT with the `--build_csharp` param to update the C#
Directory.build.props and create the native library for the current platform, as the current packaging infrastructure
creates the native and managed packages at the same time and requires a native library to exist.
Alternatively, you can open /csharp/OnnxRuntime.CSharp.sln and temporarily add
/csharp/tools/MauiModelTester/MauiModelTester.csproj to it. The csproj should automatically adjust to use a project
reference to /csharp/src/Microsoft.ML.OnnxRuntime/Microsoft.ML.OnnxRuntime.csproj instead of a package reference to
the Microsoft.ML.OnnxRuntime.Managed nuget package. Note that you must still run build.bat/build.sh with
`--build_csharp` to generate /csharp/Directory.Build.props, but can run with the `--update` parameter so no native build
is done. Most likely you should grab the latest nightly native package from the packaging pipeline so the native code is
compatible.
## Local build setup
The following commands _should_ install the necessary workloads to create the managed package including mobile targets,
build the managed library and create the native (local build only) and managed packages. The packages will be in the
ORT build output directory (e.g. build/Windows/Debug/Debug). The native package will only contain the runtime for the
current platform (e.g. Windows 64-bit if you're building on Windows) so can't be used for testing other platforms. Use
the native package from the nightly feed or a packaging CI.
```
dotnet workload install ios android macos
dotnet workload restore .\src\Microsoft.ML.OnnxRuntime\Microsoft.ML.OnnxRuntime.csproj -p:SelectedTargets=All
msbuild -t:restore .\src\Microsoft.ML.OnnxRuntime\Microsoft.ML.OnnxRuntime.csproj -p:SelectedTargets=All
msbuild -t:build .\src\Microsoft.ML.OnnxRuntime\Microsoft.ML.OnnxRuntime.csproj -p:SelectedTargets=All
msbuild .\OnnxRuntime.CSharp.proj -t:CreatePackage -p:OrtPackageId=Microsoft.ML.OnnxRuntime -p:Configuration=Debug -p:Platform="Any CPU"
```
## Example test data creation
Example commands to create test data
### PyTorch mobilenet V3 with a symbolic dimension called 'batch' that we set to 1.
https://pytorch.org/vision/main/models/mobilenetv3.html exported with torch.onnx.export with the batch dimension
being dynamic. We provide a value of 1 for the batch size to use in our testing.
`create_test_data.py -s batch=1 -m pytorch_mobilenet_v3.onnx`
### SuperResolution with pre-processing in the model.
As the model has pre-processing in it from onnxruntime-extensions, we want to provide the raw bytes from a jpeg or png.
Convert image to protobuf file with name of 'image' to match the model input. The output filename doesn't matter.
`python ..\..\..\tools\python\onnx_test_data_utils.py --action image_to_pb --raw --input lion.png --output superres_input.pb --name image`
Create test data using this input.
`create_test_data.py --input_data superres_input.pb --model_path RealESRGAN_with_pre_post_processing.onnx`

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

@ -0,0 +1,15 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories). Deployment of the asset to your application
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
These files will be deployed with you package and will be accessible using Essentials:
async Task LoadMauiAsset()
{
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
using var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
pytorch_mobilenet_v3.onnx

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Color x:Key="Primary">#ed7a0e</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="Tertiary">#C76006</Color>
<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
<Color x:Key="Yellow100Accent">#F7B548</Color>
<Color x:Key="Yellow200Accent">#FFD590</Color>
<Color x:Key="Yellow300Accent">#FFE5B9</Color>
<Color x:Key="Cyan100Accent">#28C2D1</Color>
<Color x:Key="Cyan200Accent">#7BDDEF</Color>
<Color x:Key="Cyan300Accent">#C3F2F4</Color>
<Color x:Key="Blue100Accent">#3E8EED</Color>
<Color x:Key="Blue200Accent">#72ACF1</Color>
<Color x:Key="Blue300Accent">#A7CBF6</Color>
</ResourceDictionary>

View file

@ -0,0 +1,405 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
</Style>
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
</Style>
<Style TargetType="BoxView">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Primary}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Frame">
<Setter Property="HasShadow" Value="False" />
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value="0.5" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ListView">
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Shadow">
<Setter Property="Radius" Value="15" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Offset" Value="10,10" />
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.ForegroundColor" Value="{OnPlatform WinUI={StaticResource Primary}, Default={StaticResource White}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,247 @@
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using Microsoft.ML.OnnxRuntime.Tests;
using System.Reflection;
namespace MauiModelTester
{
internal class Utils
{
internal static async Task<byte[]> LoadResource(string name)
{
using Stream fileStream = await FileSystem.Current.OpenAppPackageFileAsync(name);
using MemoryStream memoryStream = new MemoryStream();
fileStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
internal static async Task<(Dictionary<string, OrtValue>, Dictionary<string, OrtValue>)> LoadTestData()
{
var loadData = async (string prefix) =>
{
var data = new Dictionary<string, OrtValue>();
int idx = 0;
do
{
var filename = "test_data/test_data_set_0/" + prefix + idx + ".pb";
var exists = await FileSystem.Current.AppPackageFileExistsAsync(filename);
if (!exists)
{
// we expect sequentially named files for all inputs so as soon as one is missing we're done
break;
}
var tensorProtoData = await LoadResource(filename);
// get name and tensor data and create OrtValue
Onnx.TensorProto tensorProto = null;
tensorProto = Onnx.TensorProto.Parser.ParseFrom(tensorProtoData);
var ortValue = CreateOrtValueFromTensorProto(tensorProto);
data[tensorProto.Name] = ortValue;
idx++;
}
while (true);
return data;
};
var inputData = await loadData("input_");
var outputData = await loadData("output_");
return (inputData, outputData);
}
internal static OrtValue CreateOrtValueFromTensorProto(Onnx.TensorProto tensorProto)
{
Type tensorElementType = GetElementType((TensorElementType)tensorProto.DataType);
OrtValue ortValue = null;
// special case for strings
if (tensorElementType == typeof(string))
{
var numElements = tensorProto.Dims.Aggregate(1L, (x, y) => x * y);
ortValue = OrtValue.CreateTensorWithEmptyStrings(OrtAllocator.DefaultInstance,
tensorProto.Dims.ToArray());
int idx = 0;
foreach (var str in tensorProto.StringData)
{
ortValue.FillStringTensorElement(str.Span, idx++);
}
}
else
{
// use reflection to call generic method
var func = typeof(Utils)
.GetMethod(nameof(TensorProtoToOrtValue), BindingFlags.Static | BindingFlags.NonPublic)
.MakeGenericMethod(tensorElementType);
ortValue = (OrtValue)func.Invoke(null, new[] { tensorProto });
}
return ortValue;
}
internal static Type GetElementType(TensorElementType elemType)
{
switch (elemType)
{
case TensorElementType.Float:
return typeof(float);
case TensorElementType.Double:
return typeof(double);
case TensorElementType.Int16:
return typeof(short);
case TensorElementType.UInt16:
return typeof(ushort);
case TensorElementType.Int32:
return typeof(int);
case TensorElementType.UInt32:
return typeof(uint);
case TensorElementType.Int64:
return typeof(long);
case TensorElementType.UInt64:
return typeof(ulong);
case TensorElementType.UInt8:
return typeof(byte);
case TensorElementType.Int8:
return typeof(sbyte);
case TensorElementType.String:
return typeof(byte);
case TensorElementType.Bool:
return typeof(bool);
default:
throw new ArgumentException("Unexpected element type of " + elemType);
}
}
static OrtValue TensorProtoToOrtValue<T>(Onnx.TensorProto tensorProto)
{
unsafe
{
var elementSize = sizeof(T);
T[] data = new T[tensorProto.RawData.Length / elementSize];
fixed(byte *bytes = tensorProto.RawData.Span) fixed(void *target = data)
{
Buffer.MemoryCopy(bytes, target, tensorProto.RawData.Length, tensorProto.RawData.Length);
}
return OrtValue.CreateTensorValueFromMemory(data, tensorProto.Dims.ToArray());
}
}
internal class TensorComparer
{
// we need to use a delegate in the checker func to handle string as well as numeric types
private delegate ReadOnlySpan<T> GetDataFn<T>(OrtValue ortValue);
private static ReadOnlySpan<T> GetData<T>(OrtValue ortValue)
where T : struct
{
return ortValue.GetTensorDataAsSpan<T>();
}
private static ReadOnlySpan<string> GetStringData(OrtValue ortValue)
{
return ortValue.GetStringTensorAsArray();
}
private static void CheckEqual<T>(string name, OrtValue expected, OrtValue actual,
IEqualityComparer<T> comparer, GetDataFn<T> getDataFn)
{
var expectedTypeAndShape = expected.GetTypeInfo().TensorTypeAndShapeInfo;
var actualTypeAndShape = actual.GetTypeInfo().TensorTypeAndShapeInfo;
if (expectedTypeAndShape.ElementCount != actualTypeAndShape.ElementCount)
{
throw new ArithmeticException(
$"Element count mismatch for {name}. " +
$"Expected:{expectedTypeAndShape.ElementCount} Actual:{actualTypeAndShape.ElementCount}");
}
var expectedData = getDataFn(expected);
var actualData = getDataFn(actual);
List<string> mismatches = new List<string>();
for (int i = 0; i < expectedData.Length; i++)
{
if (!comparer.Equals(expectedData[i], actualData[i]))
{
mismatches.Add($"[{i}] {expectedData[i]} != {actualData[i]}");
}
}
if (mismatches.Count > 0)
{
throw new ArithmeticException(
$"Result mismatch for {name}. Mismatched entries:{string.Join(',', mismatches)}");
}
}
private static void CheckEqual<T>(string name, OrtValue expected, OrtValue actual,
IEqualityComparer<T> comparer)
where T : struct
{
CheckEqual(name, expected, actual, comparer, GetData<T>);
}
internal static void VerifyTensorResults(string name, OrtValue expected, OrtValue actual)
{
var tensorElementType = expected.GetTypeInfo().TensorTypeAndShapeInfo.ElementDataType;
switch (tensorElementType)
{
case TensorElementType.Float:
CheckEqual(name, expected, actual, new FloatComparer());
break;
case TensorElementType.Double:
CheckEqual(name, expected, actual, new DoubleComparer());
break;
case TensorElementType.Int32:
CheckEqual(name, expected, actual, new ExactComparer<int>());
break;
case TensorElementType.UInt32:
CheckEqual(name, expected, actual, new ExactComparer<uint>());
break;
case TensorElementType.Int16:
CheckEqual(name, expected, actual, new ExactComparer<short>());
break;
case TensorElementType.UInt16:
CheckEqual(name, expected, actual, new ExactComparer<ushort>());
break;
case TensorElementType.Int64:
CheckEqual(name, expected, actual, new ExactComparer<long>());
break;
case TensorElementType.UInt64:
CheckEqual(name, expected, actual, new ExactComparer<ulong>());
break;
case TensorElementType.UInt8:
CheckEqual(name, expected, actual, new ExactComparer<byte>());
break;
case TensorElementType.Int8:
CheckEqual(name, expected, actual, new ExactComparer<sbyte>());
break;
case TensorElementType.Bool:
CheckEqual(name, expected, actual, new ExactComparer<bool>());
break;
case TensorElementType.Float16:
CheckEqual(name, expected, actual, new Float16Comparer { tolerance = 2 });
break;
case TensorElementType.BFloat16:
CheckEqual(name, expected, actual, new BFloat16Comparer { tolerance = 2 });
break;
case TensorElementType.String:
CheckEqual<string>(name, expected, actual, new ExactComparer<string>(), GetStringData);
break;
default:
throw new ArgumentException($"Unexpected data type of {tensorElementType}");
}
}
}
}
}

View file

@ -0,0 +1,159 @@
import argparse
import shutil
import sys
from pathlib import Path
from typing import Dict, List, Optional
import numpy as np
# set to the directory the ONNX Runtime repo is in
# `git checkout https://github.com/microsoft/onnxruntime.git` if needed.
ORT_ROOT_DIR = Path(__file__).parents[3]
SOLUTION_DIR = Path(__file__).parent
# add path for test data/dir generation utils
sys.path.append(str(ORT_ROOT_DIR / "tools" / "python"))
def parse_args():
parser = argparse.ArgumentParser(
description="""Setup the model and test data for usage with the MAUI model tester app.
Input data will be randomly generated as needed.
The model will be run locally and the output saved as expected output.
Explicit input data or expected output data can be specified by providing .pb files with the input/output name
and tensor. These can be created with /tools/python/onnx_test_data_utils.py.
See https://github.com/microsoft/onnxruntime/blob/main/tools/python/PythonTools.md#creating-a-test-directory-for-a-model # noqa
for info on creating specific input or expected output""",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--symbolic_dims",
"-s",
help="Symbolic dimension values if the model inputs have symbolic dimensions and the input data is being "
"generated. Format is `name=value {name2=value2 ...}.",
type=str,
nargs="+",
required=False,
)
parser.add_argument(
"--input_data",
"-i",
help="Input data pb files created with onnx_test_data_utils.py. Multiple can be specified.",
type=Path,
nargs="+",
required=False,
)
parser.add_argument(
"--output_data",
"-o",
help="Expected output data pb files created with onnx_test_data_utils.py. Multiple can be specified.",
type=Path,
nargs="+",
required=False,
)
parser.add_argument(
"--model_path",
"-m",
help="Path to ONNX model to use. Model will be copied into the test app",
type=Path,
Required=True,
)
args = parser.parse_args()
args.model_path.resolve(strict=True)
# convert symbolic dims to dictionary
symbolic_dims = None
if args.symbolic_dims:
symbolic_dims = {}
for value in args.symbolic_dims:
pieces = value.split("=")
assert len(pieces) == 2
name = pieces[0].strip()
dim_value = int(pieces[1].strip())
symbolic_dims[name] = dim_value
args.symbolic_dims = symbolic_dims
return args
def create_existing_data_map(pb_files: List[Path]):
import onnx_test_data_utils as data_utils
data_map = {}
for file in pb_files:
file.resolve(strict=True)
name, data = data_utils.read_tensorproto_pb_file(str(file))
data_map[name] = data
return data_map
def add_model_and_test_data_to_app(
model_path: Path,
symbolic_dims: Optional[Dict[str, int]] = None,
input_map: Optional[Dict[str, np.ndarray]] = None,
output_map: Optional[Dict[str, np.ndarray]] = None,
):
import ort_test_dir_utils as utils
output_path = SOLUTION_DIR / "Resources" / "Raw"
test_name = "test_data"
test_path = output_path / test_name
# remove existing data
if test_path.exists():
shutil.rmtree(test_path)
# If you want to directly create input data without using onnx_test_data_utils you can edit the input map here
# if not input_map:
# input_map = {}
#
# input_map['Input3'] = np.random.rand(1, 1, 28, 28).astype(np.float32)
utils.create_test_dir(
str(model_path),
str(output_path),
test_name,
# Explicit input data. Any missing required inputs will have data generated for them.
name_input_map=input_map,
# Optional map for any symbolic values.
symbolic_dim_values_map=symbolic_dims,
# Expected output can be provided if you want to validate model output against this.
name_output_map=output_map,
)
# create_test_dir will copy the model to the output directory.
# rename the copied model to the generic name the app expects.
copied_model = output_path / test_name / model_path.name
copied_model.rename(copied_model.with_name("model.onnx"))
# add a text file with the original model path just so there's some info on where it came from
with open(test_path / "model_info.txt", "w") as model_info_file:
model_info_file.write(str(model_path))
def create_test_data():
args = parse_args()
input_map = None
output_map = None
if args.input_data:
input_map = create_existing_data_map(args.input_data)
if args.output_data:
output_map = create_existing_data_map(args.output_data)
add_model_and_test_data_to_app(args.model_path, args.symbolic_dims, input_map, output_map)
if __name__ == "__main__":
create_test_data()

View file

@ -129,6 +129,12 @@ def get_arg_parser():
parser.add_argument("--output", help="Filename to serialize the TensorProto to.")
image_to_pb_group = parser.add_argument_group("image_to_pb", "image_to_pb specific options")
image_to_pb_group.add_argument(
"--raw",
action="store_true",
help="Save raw image bytes for usage with a model that has the DecodeImage custom operator "
"from onnxruntime-extensions.",
)
image_to_pb_group.add_argument(
"--resize",
default=None,
@ -191,7 +197,11 @@ if __name__ == "__main__":
print("Missing argument. Need input, output, name to be specified.", file=sys.stderr)
sys.exit(-1)
img_np = image_to_numpy(args.input, args.resize, args.channels_last, args.add_batch_dim)
if args.raw:
img_np = np.fromfile(args.input, np.ubyte)
else:
img_np = image_to_numpy(args.input, args.resize, args.channels_last, args.add_batch_dim)
numpy_to_pb(args.name, img_np, args.output)
elif args.action == "random_to_pb":
if not args.output or not args.shape or not args.datatype or not args.name:

View file

@ -5,7 +5,7 @@ import shutil
import numpy as np
import onnx
import onnx_test_data_utils
from onnx import numpy_helper
from onnx import TensorProto, numpy_helper
import onnxruntime as ort
@ -49,7 +49,7 @@ def _create_missing_input_data(model_inputs, name_input_map, symbolic_dim_values
dims.append(dim.dim_value)
elif dim_type == "dim_param":
if dim.dim_param not in symbolic_dim_values_map:
raise ValueError(f"Value for symbolic dim {dim.dim_param} was not provided.")
raise ValueError(f"Value for symbolic dim '{dim.dim_param}' was not provided.")
dims.append(symbolic_dim_values_map[dim.dim_param])
else:
@ -57,9 +57,16 @@ def _create_missing_input_data(model_inputs, name_input_map, symbolic_dim_values
# shape for the input name instead.
raise ValueError("Unsupported model. Unknown dim with no value or symbolic name.")
np_type = onnx.mapping.TENSOR_TYPE_TO_NP_TYPE[input.type.tensor_type.elem_type]
# create random data. give it range -10 to 10 so if we convert to an integer type it's not all 0s and 1s
data = (np.random.standard_normal(dims) * 10).astype(np_type)
onnx_type = input.type.tensor_type.elem_type
# create random data.
data = np.random.random_sample(dims)
# use range of [0, 1) for floating point data
# use range of [0, 256) for other data types
if onnx_type not in [TensorProto.FLOAT, TensorProto.BFLOAT16, TensorProto.DOUBLE, TensorProto.FLOAT16]:
data *= 256
np_type = onnx.mapping.TENSOR_TYPE_TO_NP_TYPE[onnx_type]
data = data.astype(np_type)
name_input_map[input.name] = data
@ -139,7 +146,20 @@ def create_test_dir(
# save expected output data if provided. run model to create if not.
if not name_output_map:
output_names = [o.name for o in model_outputs]
sess = ort.InferenceSession(test_model_filename)
so = ort.SessionOptions()
# try and enable onnxruntime-extensions if present
try:
import onnxruntime_extensions
so.register_custom_ops_library(onnxruntime_extensions.get_library_path())
except ImportError:
# ignore if onnxruntime_extensions is not available.
# if the model uses custom ops from there it will fail to load.
pass
sess = ort.InferenceSession(test_model_filename, so)
outputs = sess.run(output_names, name_input_map)
name_output_map = {}
for name, data in zip(output_names, outputs):