C# 教程: 基础
学习如何使用 C# API 进行推理入门。
OrtValue API
基于新的 OrtValue
的 API 是推荐的方法。OrtValue
API 生成的垃圾更少,性能更高。在某些场景下,性能比旧 API 提升了 4 倍,并且显著减少了垃圾生成。
OrtValue 是一个通用容器,可以容纳不同的 ONNX 类型,例如张量、映射和序列。它一直存在于 onnxruntime 库中,但在 C# API 中没有公开。
基于 OrtValue
的 API 通过 ReadOnlySpan<T>
和 Span<T>
结构提供统一的数据访问,无论数据位于托管内存还是非托管内存。
请注意,以下类 NamedOnnxValue
、DisposableNamedOnnxValue
、FixedBufferOnnxValue
将在将来弃用。不推荐在新代码中使用它们。
数据形状
由于新的基于 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。不要指望终结器,因为它们不保证会运行,即使运行,也通常为时已晚。
这包括 SessionOptions
、RunOptions
、InferenceSession
、OrtValue
。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);
}