C#-Tutorial: Grundlagen

Erfahren Sie, wie Sie mit der C#-API mit der Inferenz beginnen.

OrtValue API

Die neue auf OrtValue basierende API ist der empfohlene Ansatz. Die OrtValue-API erzeugt weniger Garbage und ist performanter. Einige Szenarien zeigen eine 4-fache Leistungsverbesserung gegenüber der vorherigen API und deutlich weniger Garbage.

OrtValue ist ein universeller Container, der verschiedene ONNX-Typen wie Tensoren, Maps und Sequenzen aufnehmen kann. Er existierte schon immer in der onnxruntime-Bibliothek, wurde aber in der C#-API nicht verfügbar gemacht.

Die auf OrtValue basierende API bietet einen einheitlichen Zugriff auf Daten über ReadOnlySpan<T> und Span<T>-Strukturen, unabhängig von ihrem Speicherort, verwaltet oder nicht verwaltet.

Beachten Sie, dass die folgenden Klassen NamedOnnxValue, DisposableNamedOnnxValue, FixedBufferOnnxValue in Zukunft veraltet sein werden. Sie werden für neuen Code nicht empfohlen.

Datenform

Die Klasse DenseTensor kann für den mehrdimensionalen Zugriff auf die Daten verwendet werden, da die neue Span-basierte API nur einen eindimensionalen Index bietet. Es wurde jedoch eine langsame Leistung bei der Verwendung des mehrdimensionalen Zugriffs der Klasse DenseTensor gemeldet. Man kann dann ein OrtValue auf Basis der Tensordaten erstellen.

Die Klasse ShapeUtils bietet Hilfe bei der Handhabung mehrdimensionaler Indizes für OrtValues.

Wenn die Ausgabeformen bekannt sind, kann ein OrtValue auf Basis der verwalteten oder nicht verwalteten Allokationen vorab zugewiesen und diese OrtValues als Ausgaben bereitgestellt werden. Aus diesem Grund verringert sich der Bedarf an IOBinding erheblich.

Datentypen

OrtValues können direkt auf verwalteten unmanaged struct-basierten blittable Typen-Arrays erstellt werden. Die onnxruntime C#-API ermöglicht die Verwendung von verwalteten Puffern für Eingaben oder Ausgaben.

Zeichendaten werden in C# als UTF-16-Zeichenfolgenobjekte dargestellt. Sie müssen immer noch kopiert und in UTF-8 in den nativen Speicher konvertiert werden. Diese Konvertierung ist jedoch jetzt optimierter und erfolgt in einem einzigen Durchgang ohne zwischengeschaltete Byte-Arrays.

Dasselbe gilt für Zeichenfolgen-OrtValue-Tensoren, die als Ausgaben zurückgegeben werden. Die zeichenbasierte API arbeitet jetzt mit Span<char>, ReadOnlySpan<char> und ReadOnlyMemory<char>-Objekten. Dies erhöht die Flexibilität der API und vermeidet unnötige Kopien.

Datenlebenszyklus

Abgesehen von einigen der oben genannten veralteten API-Klassen sind fast alle C#-API-Klassen IDisposable. Das bedeutet, sie müssen nach Gebrauch entsorgt werden, sonst kommt es zu Speicherlecks. Da OrtValues zum Speichern von Tensordaten verwendet werden, können die Leckgrößen riesig sein. Sie sammeln sich wahrscheinlich mit jedem Run-Aufruf an, da jeder Inferenzaufruf Eingabe-OrtValues benötigt und Ausgabe-OrtValues zurückgibt. Warten Sie nicht auf Finalizer, deren Ausführung nicht garantiert ist, und wenn sie doch stattfinden, dann ist es zu spät.

Dies umfasst SessionOptions, RunOptions, InferenceSession, OrtValue. Run()-Aufrufe geben IDisposableCollection zurück, die es ermöglicht, alle enthaltenen Objekte in einer Anweisung oder einem using zu entsorgen. Dies liegt daran, dass diese Objekte native Ressourcen besitzen, oft ein natives Objekt.

Das Nicht-Entsorgen eines OrtValue, der auf einem verwalteten Puffer basierte, würde dazu führen, dass dieser Puffer auf unbestimmte Zeit im Speicher fixiert bleibt. Ein solcher Puffer kann nicht vom Garbage Collector gesammelt oder im Speicher verschoben werden.

OrtValues, die auf dem nativen onnxruntime-Speicher basierten, sollten ebenfalls umgehend entsorgt werden. Andernfalls wird der native Speicher nicht freigegeben. Von Run() zurückgegebene OrtValues enthalten normalerweise nativen Speicher.

Der GC kann nicht auf nativem Speicher oder anderen nativen Ressourcen operieren.

Die using-Anweisung oder ein Block ist eine praktische Möglichkeit, sicherzustellen, dass die Objekte entsorgt werden. InferenceSession kann ein langlebiges Objekt und ein Mitglied einer anderen Klasse sein. Es muss letztendlich entsorgt werden. Das bedeutet, dass die enthaltende Klasse ebenfalls disposable gemacht werden müsste, um dies zu erreichen.

Die OrtValue API bietet auch eine besucherähnliche API, um ONNX-Maps und -Sequenzen zu durchlaufen. Dies ist eine effizientere Methode, um auf ONNX Runtime-Daten zuzugreifen.

Codebeispiel zum Ausführen eines Modells

Um mit der Bewertung des Modells zu beginnen, erstellen Sie eine Sitzung mit der Klasse InferenceSession und übergeben Sie den Dateipfad zum Modell als Parameter.

using var session = new InferenceSession("model.onnx");

Sobald eine Sitzung erstellt ist, können Sie die Inferenz mit der Methode Run des InferenceSession-Objekts ausführen.

float[] sourceData;  // assume your data is loaded into a flat float array
long[] dimensions;    // and the dimensions of the input is stored here

// Create a OrtValue on top of the sourceData array
using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(sourceData, dimensions);

var inputs = new Dictionary<string, OrtValue> {
    { "name1",  inputOrtValue }
};


using var runOptions = new RunOptions();

// Pass inputs and request the first output
// Note that the output is a disposable collection that holds OrtValues
using var output = session.Run(runOptions, inputs, session.OutputNames[0]);

var output_0 = output[0];

// Assuming the output contains a tensor of float data, you can access it as follows
// Returns Span<float> which points directly to native memory.
var outputData = output_0.GetTensorDataAsSpan<float>();

// If you are interested in more information about output, request its type and shape
// Assuming it is a tensor
// This is not disposable, will be GCed
// There you can request Shape, ElementDataType, etc
var tensorTypeAndShape = output_0.GetTensorTypeAndShape();

Sie können die Klasse Tensor weiterhin für die Datenmanipulation verwenden, wenn Sie vorhandenen Code haben, der dies tut. Erstellen Sie dann ein OrtValue auf dem Tensor-Puffer.

// Create and manipulate the data using tensor interface
DenseTensor<float> t1 = new DenseTensor<float>(sourceData, dimensions);

// One minor inconvenience is that Tensor class operates on `int` dimensions and indices.
// OrtValue dimensions are `long`. This is required, because `OrtValue` talks directly to
// Ort API and the library uses long dimensions.

// Convert dims to long[]
var shape = Array.Convert<int,long>(dimensions, Convert.ToInt64);

using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(OrtMemoryInfo.DefaultInstance,
    t1.Buffer, shape);

Hier ist eine Möglichkeit, einen Zeichenfolgen-Tensor zu befüllen. Zeichenfolgen können nicht zugeordnet werden und müssen in den nativen Speicher kopiert/konvertiert werden. Zu diesem Zweck weisen wir einen nativen Tensor mit leeren Zeichenfolgen und den angegebenen Dimensionen vorab zu und setzen dann einzelne Zeichenfolgen nach Index.

string[] strs = { "Hello", "Ort", "World" };
long[] shape = { 1, 1, 3 };
var elementsNum = ShapeUtils.GetSizeForShape(shape);

using var strTensor = OrtValue.CreateTensorWithEmptyStrings(OrtAllocator.DefaultInstance, shape);

for (long i = 0; i < elementsNum; ++i)
{
    strTensor.StringTensorSetElementAt(strs[i].AsSpan(), i);
}

Weitere Beispiele