C# 教程:基础

了解如何开始使用 C# API 进行推理。

OrtValue API

新的基于 OrtValue 的 API 是推荐的方法。OrtValue API 产生的垃圾更少,性能更高。在某些场景中,与之前的 API 相比,性能提高了 4 倍,并且垃圾显着减少。

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

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

请注意,以下类 NamedOnnxValueDisposableNamedOnnxValueFixedBufferOnnxValue 将在未来被弃用。不建议在新代码中使用它们。

数据形状

DenseTensor 类可用于对数据进行多维访问,因为新的基于 Span 的 API 仅具有一维索引。但是,有人报告说,使用 DenseTensor 类的多维访问时性能较慢。然后,可以在张量数据之上创建 OrtValue。

ShapeUtils 类提供了一些帮助来处理 OrtValue 的多维索引。

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

数据类型

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

字符串数据在 C# 中表示为 UTF-16 字符串对象。它仍然需要被复制并转换为 UTF-8 到本机内存。但是,现在的转换经过了优化,并且在单个过程中完成,而无需中间字节数组。

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

数据生命周期

除了上面一些已弃用的 API 类之外,几乎所有 C# API 类都是 IDisposable。这意味着它们在使用后需要被释放,否则会发生内存泄漏。由于 OrtValue 用于保存张量数据,因此泄漏的大小可能很大。它们可能会随着每次 Run 调用而累积,因为每次推理调用都需要输入 OrtValue 并返回输出 OrtValue。不要指望终结器,它们不保证会运行,即使运行,也为时已晚。

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

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

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

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

using 语句或块是确保对象被释放的便捷方法。InferenceSession 可以是一个长期存在的对象,并且是另一个类的成员。它最终必须被释放。这意味着,包含类也必须被设为可释放的才能实现这一点。

OrtValue API 还提供类似访问者的 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);
}

更多示例