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.
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:
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:
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}");
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) andint[]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.SessionorSession.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
- Exact mode: the same bits on every platform and thread count.
- The OneModel format: convert an ONNX file once and load it as memory-mapped weights.
- Half-precision weights: halve the memory of large weights.
- Sessions and plans: how a model is planned, cached and run.