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,并提供这些 OrtValues 作为输出。因此,对 IOBinding 的需求大大减少。

数据类型

OrtValues 可以直接在托管的unmanaged 基于 struct 的可复制类型数组之上创建。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。不要指望终结器,它们不保证一定会运行,即使运行也为时已晚。

这包括 SessionOptions, RunOptions, InferenceSession, OrtValueRun() 调用返回 IDisposableCollection,允许通过一个语句或 using 关键字来释放所有包含的对象。这是因为这些对象拥有本地资源,通常是本地对象。

不释放基于托管缓冲区创建的 OrtValue 将导致该缓冲区无限期地固定在内存中。这样的缓冲区无法被垃圾回收或在内存中移动。

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

垃圾回收器不能操作本地内存或任何其他本地资源。

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

更多示例