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
将在未来被弃用。不建议在新代码中使用它们。
数据形状
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。不要指望终结器,它们不保证会运行,即使运行,也为时已晚。
这包括 SessionOptions
、RunOptions
、InferenceSession
、OrtValue
。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);
}