Recursive AI

Getting started with OneModel

All onemodel docs

Install the OneModel preview for .NET 10, run an ONNX model with one call, read its inputs and outputs, and see which kernels it picked. No native binaries, no ONNX Runtime.

Install

OneModel targets net10.0, so you need the .NET 10 SDK. It is a preview: this guide uses 0.3.0-preview.1, and the API may change before 1.0.

Terminal
dotnet new console -o OneModelStart
cd OneModelStart
dotnet add package RecursiveAI.OneModel --version 0.3.0-preview.1

The package has no dependency beyond the .NET base class library and contains no native code. It works under NativeAOT and trimming; for a NativeAOT build, see publishing with the right instruction set.

The sample models

The guide and the cookbook use three small models made with the onnx Python package (pip install onnx numpy), so the outputs below can be reproduced exactly. Any float32 ONNX model with one input and one output works the same way. models.py:

PYTHON
import numpy as np, onnx
from onnx import helper as h, TensorProto as T, numpy_helper as nh
 
rng = np.random.default_rng(7)
 
def w(name, *shape):
    return nh.from_array(rng.standard_normal(shape).astype(np.float32) * 0.5, name)
 
# classifier.onnx: 4 features -> 8 hidden (Relu) -> 3 classes (Softmax)
g = h.make_graph(
    [h.make_node("Gemm", ["x", "W1", "b1"], ["h"], transB=1),
     h.make_node("Relu", ["h"], ["hr"]),
     h.make_node("Gemm", ["hr", "W2", "b2"], ["logits"], transB=1),
     h.make_node("Softmax", ["logits"], ["probs"], axis=1)],
    "classifier",
    [h.make_tensor_value_info("x", T.FLOAT, ["batch", 4])],
    [h.make_tensor_value_info("probs", T.FLOAT, ["batch", 3])],
    [w("W1", 8, 4), w("b1", 8), w("W2", 3, 8), w("b2", 3)])
m = h.make_model(g, opset_imports=[h.make_opsetid("", 17)], ir_version=8)
onnx.checker.check_model(m)
onnx.save(m, "classifier.onnx")
 
# scorer.onnx: two named inputs, two outputs
g = h.make_graph(
    [h.make_node("MatMul", ["features", "W"], ["proj"]),
     h.make_node("Add", ["proj", "bias"], ["score"]),
     h.make_node("ReduceMax", ["score"], ["best"], axes=[1], keepdims=0)],
    "scorer",
    [h.make_tensor_value_info("features", T.FLOAT, ["batch", 3]),
     h.make_tensor_value_info("bias", T.FLOAT, [1, 2])],
    [h.make_tensor_value_info("score", T.FLOAT, ["batch", 2]),
     h.make_tensor_value_info("best", T.FLOAT, ["batch"])],
    [nh.from_array(np.array([[1, 0], [0, 1], [1, 1]], np.float32), "W")])
m = h.make_model(g, opset_imports=[h.make_opsetid("", 13)], ir_version=8)
onnx.checker.check_model(m)
onnx.save(m, "scorer.onnx")
 
# wide.onnx: a larger float32 model for half-precision weight storage (256 -> 512 -> 64)
g = h.make_graph(
    [h.make_node("MatMul", ["x", "W1"], ["h"]),
     h.make_node("Relu", ["h"], ["hr"]),
     h.make_node("MatMul", ["hr", "W2"], ["y"])],
    "wide",
    [h.make_tensor_value_info("x", T.FLOAT, [1, 256])],
    [h.make_tensor_value_info("y", T.FLOAT, [1, 64])],
    [nh.from_array((rng.standard_normal((256, 512)) * 0.05).astype(np.float32), "W1"),
     nh.from_array((rng.standard_normal((512, 64)) * 0.05).astype(np.float32), "W2")])
m = h.make_model(g, opset_imports=[h.make_opsetid("", 17)], ir_version=8)
onnx.checker.check_model(m)
onnx.save(m, "wide.onnx")
print("ok")

classifier.onnx takes four features and returns three class probabilities. Run python3 models.py and copy classifier.onnx next to Program.cs.

Run a model

Model.Load reads the file and builds a session; Run takes the input values in row-major order and returns the output as a new array. Names and shapes come from the model. Here the model declares its input as [batch, 4], so four values are one row:

C#
using RecursiveAI.OneModel;
 
using var model = Model.Load("classifier.onnx");
 
float[] features = [5.1f, 3.5f, 1.4f, 0.2f];
float[] probs = model.Run(features);
 
Console.WriteLine(string.Join(", ", probs.Select(p => p.ToString("F4"))));
Console.WriteLine($"Kernels: {model.Session.KernelPath}");
foreach (var input in model.Session.Inputs)
    Console.WriteLine($"Input {input.Name}: {input.DType}, rank {input.Rank}");
Text
0.2323, 0.2813, 0.4864
Kernels: Avx512
Input x: F32, rank 2

KernelPath says which kernels the session chose from the hardware at start-up: Avx512, Avx2, Neon, Portable128 or Scalar. On another machine it may differ; in the default fast mode the results then agree to within rounding, and in exact mode they are the same bits.

What Run accepts

  • float[], long[] (token ids) and int[] inputs, with an optional shape: model.Run(values, [2, 4]) for two rows.
  • A span form, Run<TIn, TOut>(input, shape, output), that writes into your buffer and allocates nothing once the model has run once with that shape.
  • One input and one output. For several of either, named tensors, or options such as exact mode, use model.Session or Session.Load: see Named inputs and outputs.

Run may be called from any number of threads at once. Fast-mode results don’t depend on the thread count.

When a model doesn’t load

A model that uses an operator OneModel doesn’t implement fails in Model.Load with a SessionException that lists every missing operator at once, not the first one it meets: see When a model won’t load. Which operators pass the ONNX backend tests is in conformance.

Next