C# 教程: 基础

学习如何使用 C# API 进行推理入门。

OrtValue API

基于新的 OrtValue 的 API 是推荐的方法。OrtValue API 生成的垃圾更少,性能更高。在某些场景下,性能比旧 API 提升了 4 倍,并且显著减少了垃圾生成。

OrtValue 是一个通用容器,可以容纳不同的 ONNX 类型,例如张量、映射和序列。它一直存在于 onnxruntime 库中,但在 C# API 中没有公开。

基于 OrtValue 的 API 通过 ReadOnlySpan<T>Span<T> 结构提供统一的数据访问,无论数据位于托管内存还是非托管内存。

请注意,以下类 NamedOnnxValueDisposableNamedOnnxValueFixedBufferOnnxValue 将在将来弃用。不推荐在新代码中使用它们。

数据形状

由于新的基于 Span 的 API 只支持一维索引,因此可以使用 DenseTensor 类进行数据的多维访问。然而,一些用户报告使用 DenseTensor 类的多维访问时性能较慢。可以在张量数据之上创建 OrtValue。

ShapeUtils 类提供了一些帮助,用于处理 OrtValues 的多维索引。

如果已知输出形状,可以在托管或非托管内存分配之上预先分配 OrtValue,并将这些 OrtValue 用作输出。因此,对 IOBinding 的需求大大减少。

数据类型

OrtValues 可以直接在托管的 非托管 基于结构的可Blittable类型 数组之上创建。onnxruntime C# API 允许对输入或输出使用托管缓冲区。

字符串数据在 C# 中表示为 UTF-16 字符串对象。仍然需要将其复制并转换为 UTF-8 到本机内存。然而,现在的转换更加优化,并且可以在单次遍历中完成,无需中间字节数组。

这同样适用于作为输出返回的字符串 OrtValue 张量。基于字符的 API 现在在 Span<char>ReadOnlySpan<char>ReadOnlyMemory<char> 对象上操作。这增加了 API 的灵活性,并避免了不必要的复制。

数据生命周期

除了上面提到的部分已弃用的 API 类外,几乎所有的 C# API 类都是 IDisposable 的。这意味着使用后需要进行处置,否则会发生内存泄漏。由于 OrtValues 用于保存张量数据,泄漏的大小可能很大。每次 Run 调用都可能累积泄漏,因为每次推理调用都需要输入 OrtValues 并返回输出 OrtValues。不要指望终结器,因为它们不保证会运行,即使运行,也通常为时已晚。

这包括 SessionOptionsRunOptionsInferenceSessionOrtValue。Run() 调用返回 IDisposableCollection,允许在一个语句或 using 块中处置所有包含的对象。这是因为这些对象拥有本机资源,通常是本机对象。

如果不对在托管缓冲区之上创建的 OrtValue 进行处置,会导致该缓冲区在内存中被无限期地固定。这样的缓冲区无法进行垃圾回收或在内存中移动。

在本机 onnxruntime 内存之上创建的 OrtValue 也应该及时处置。否则,本机内存将不会被释放。由 Run() 返回的 OrtValue 通常持有本机内存。

GC 无法操作本机内存或任何其他本机资源。

using 语句或块是确保对象被处置的便捷方式。InferenceSession 可以是一个生命周期较长的对象,并且是另一个类的成员。它最终必须被处置。这意味着,包含该对象的类也必须实现 IDisposable 接口才能达到此目的。

OrtValue API 还提供了类似 Visitor 的 API,用于遍历 ONNX 映射和序列。这是访问 ONNX Runtime 数据的更有效方式。

运行模型的代码示例

要开始使用模型进行评分,请使用 InferenceSession 类创建一个会话,并将模型的 文件路径 作为参数传入。

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

创建会话后,您可以使用 InferenceSession 对象的 Run 方法运行推理。

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();

如果您有现有的代码使用 Tensor 类进行数据操作,仍然可以使用它。然后可以在 Tensor 缓冲区之上创建 OrtValue

// 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);

这里是一种填充字符串张量的方法。字符串不能直接映射,必须复制/转换为本机内存。为此,我们预先分配一个具有指定维度的空字符串的本机张量,然后按索引设置单个字符串。

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);
}

更多示例