Run an ONNX model
You have a model as an .onnx file and want ClikaRT to run it. Three types carry the whole story. io::OnnxModel is the format-level object, the ONNX graph as data: open it, inspect it, edit it, save it. compile() is the one crossing into the executable world, where every node parses into its runtime operator, weights bind, and shapes resolve. The result is a ModelGraph, the executable graph, with one run.
The programs below first open an existing file and read its contract, then build a tiny model from scratch so the page runs with no file on disk, then compile and run it. Any .onnx file works in the first section; nothing depends on the architecture.
Open a model and read its IO contract
Opening is cheap and does not compile anything: you get the graph as data. The IO contract (names, dtypes, dims, with dynamic dims reading as named placeholders or -1) is what you need to prepare feeds; print it before anything else when a model is new to you.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdio>
#include <string>
#include "ClikaRT/clika_rt.h"
using ClikaRT::io::OnnxModel;
using ClikaRT::unwrap;
int main(int argc, char** argv) {
const std::string path = argc > 1 ? argv[1] : "model.onnx";
OnnxModel model = unwrap(OnnxModel::open(path));
std::printf("nodes: %zu initializers: %zu opset: %lld\n",
model.num_nodes(), model.num_initializers(),
(long long)model.opset());
for (const auto& s : unwrap(model.inputs()))
std::printf(" input %s\n", s.name.c_str());
for (const auto& s : unwrap(model.outputs()))
std::printf(" output %s\n", s.name.c_str());
return 0;
}
The ONNX member family rides the C ABI (clika_rt_onnx_model and its members in clika_rt_core.h); the bootstrap is the api-table handshake from Call ClikaRT from C. The C++ tab shows the calls this family mirrors one to one.
model = crt.io.OnnxModel.open(sys.argv[1] if len(sys.argv) > 1 else tiny_model_path())
print(f"nodes: {model.num_nodes} initializers: {model.num_initializers} "
f"opset: {model.opset}")
for name, dtype, dims in model.inputs():
print(f" input {name} {list(dims)}") # a dynamic dim reads as -1
for name, dtype, dims in model.outputs():
print(f" output {name} {list(dims)}")
The program opens the path on its command line; with none, tiny_model_path() builds the MLP of the next section into a temporary file, so the page runs with no file on disk.
import io.clika.runtime.ClikaRt
import io.clika.runtime.OnnxModel
fun main(args: Array<String>) {
ClikaRt.load()
OnnxModel.open(if (args.isNotEmpty()) args[0] else "model.onnx").use { model ->
println("nodes: ${model.numNodes()} opset: ${model.opset()}")
}
}
package main
import (
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
// The format-level object: the graph as data, before any runtime work.
model := must(api.OnnxModelOpen("model.onnx"))
fmt.Printf("nodes %d, opset %d\n",
api.OnnxModelNumNodes(model), api.OnnxModelOpset(model))
// compile() is the verdict: every node parses into its runtime operator,
// weights bind, shapes resolve; a typed error names the first offender.
graph, err := model.Compile()
if err != nil {
log.Fatalf("compile refused: %v", err)
}
fmt.Printf("inputs %v\n", must(graph.InputNames()))
fmt.Printf("outputs %v\n", must(graph.OutputNames()))
}
use clika_rt::{Api, Result};
fn main() -> Result<()> {
// The path to libClikaRT.so: argv or an env var, your call (the crate
// loads the runtime at run time, exactly like the C ABI underneath).
let api = Api::load(std::env::args().nth(1).expect("lib path").as_ref())?;
let model = api.f_onnx_model_open("model.onnx")?;
println!("nodes: {}", api.onnx_model_num_nodes(&model));
Ok(())
}
Build a graph from scratch
When there is no file yet (a test, a fixture, a tool that emits ONNX), the same object builds a graph node by node: declare inputs, add initializers (weights enter as ordinary tensors), add nodes by operator type, name the outputs. The tiny MLP here is y = relu(X W + B) with a dynamic batch dimension.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <string>
#include <vector>
#include "ClikaRT/clika_rt.h"
using ClikaRT::DataType;
using ClikaRT::Tensor;
using ClikaRT::spec::Dim;
using ClikaRT::io::OnnxModel;
using ClikaRT::unwrap;
OnnxModel build_tiny_mlp() {
Dim batch; // one Dim object: every use is the SAME dynamic dimension
OnnxModel model = unwrap(OnnxModel::create("tiny_mlp", /*opset_version=*/21));
unwrap(model.add_input("X", DataType::Float32, {batch, 4}));
const std::string w = unwrap(model.add_initializer(
Tensor::full({4, 3}, 0.5, DataType::Float32)));
const std::string b = unwrap(model.add_initializer(
Tensor::full({3}, 0.25, DataType::Float32)));
const std::vector<std::string> mm = unwrap(model.add_node("MatMul", {"X", w}, 1));
const std::vector<std::string> sum = unwrap(model.add_node("Add", {mm[0], b}, 1));
const std::vector<std::string> y = unwrap(model.add_node("Relu", {sum[0]}, 1));
unwrap(model.add_output(y[0], DataType::Float32, {batch, 3}));
return model;
}
Graph authoring mirrors the same member family (clika_rt_onnx_model create/add_input/add_initializer/add_node); the C++ tab is the shape to follow.
def build_tiny_mlp() -> "crt.io.OnnxModel":
model = crt.io.OnnxModel.create("tiny_mlp", opset_version=21)
model.add_input("X", crt.float32, [-1, 4]) # any value <= 0 is dynamic
w = model.add_initializer(crt.tensor(np.full((4, 3), 0.5, dtype=np.float32)))
b = model.add_initializer(crt.tensor(np.full((3,), 0.25, dtype=np.float32)))
(mm,) = model.add_node("MatMul", ["X", w])
(summed,) = model.add_node("Add", [mm, b])
(out,) = model.add_node("Relu", [summed])
model.add_output(out, crt.float32, [-1, 3])
return model
The Kotlin binding consumes graphs (OnnxModel.open); authoring rides the C ABI's onnx family for now.
The Go wrappers cover opening, compiling and running; the graph-authoring members (add_input, add_node, add_output) are not generated in the module yet and ride the C ABI's onnx family for now. The C++ tab is the shape to follow.
use clika_rt::onnx::dim;
use clika_rt::{sys, Api, Result};
const F32: sys::clika_rt_data_type = sys::clika_rt_data_type_CLIKA_RT_DATA_TYPE_FLOAT32;
fn build_tiny_mlp(api: &Api) -> Result<clika_rt::OnnxModel<'_>> {
let model = api.f_onnx_model_create("tiny_mlp", 21)?;
api.f_onnx_model_add_input_name(&model, "X", F32, &[dim(-1), dim(4)])?;
let w = api.string_list_get_at(
&api.f_onnx_model_add_initializer(&model, &api.tensor_full(&[4, 3], 0.5, F32)?)?, 0);
let b = api.string_list_get_at(
&api.f_onnx_model_add_initializer(&model, &api.tensor_full(&[3], 0.25, F32)?)?, 0);
// add_node returns the runtime-named outputs, threaded into the next node.
Ok(model) // MatMul/Add/Relu nodes follow the same add_node shape
}
save(path) writes the graph; open(path) round-trips it structure-intact, and optimize() runs the rewrite pipeline to a fixed point (a minimal graph survives unchanged).
Compile and run
compile() can fail like any load of real weights and shapes, so it returns through the error contract; branch on the code name. The ModelGraph runs positionally (one tensor per input_names() entry, in that order) and, in Python, also by name.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdio>
#include <vector>
#include "ClikaRT/clika_rt.h"
using ClikaRT::DataType;
using ClikaRT::Tensor;
using ClikaRT::graph::ModelGraph;
using ClikaRT::unwrap;
int run(ClikaRT::io::OnnxModel& model) {
ClikaRT::Result<ModelGraph> compiled = CLIKART_TRY(model.compile());
if (!compiled.ok()) {
std::printf("compile: FAILED [%s]\n", compiled.code_name().c_str());
return 1;
}
ModelGraph graph = std::move(compiled.value());
const std::vector<float> x = {1.0F, 2.0F, 3.0F, 4.0F, -1.0F, 0.5F, 2.0F, -2.0F};
std::vector<Tensor> outputs = unwrap(graph.run(
{Tensor::from_data(x.data(), {2, 4}, DataType::Float32)}));
std::printf("y = %s\n", outputs[0].to_string().c_str());
return 0;
}
compile and run mirror the same members over clika_rt_onnx_model; the failure path hands back an error object whose code NAME is the stable channel, exactly as in Call ClikaRT from C.
graph = model.compile()
print(f"inputs {graph.input_names()} -> outputs {graph.output_names()}")
x = crt.tensor(np.array([[1, 2, 3, 4], [-1, 0.5, 2, -2]], dtype=np.float32))
(y,) = graph.run([x]) # positional: input_names() order
(y2,) = graph.run({"X": x}) # named: the same result
print(y.numpy())
codes = [n.op_code for n in graph.nodes()] # the compiled graph as data
assert crt.graph.OpCode.MatMul in codes
# The one-call loader: open and compile, then call the model with named inputs.
model.save("tiny_mlp.onnx")
loaded = crt.onnx.load("tiny_mlp.onnx", dynamic_axes={"X": {0: "batch"}}, device="cpu")
out = loaded(X=x) # a dict keyed by output name
assert list(out) == loaded.output_names
crt.onnx.load opens and compiles in one call: dynamic_axes names the axes that vary between calls, device places the weights, and the returned model takes its inputs by keyword and returns a dict keyed by output name. graph.nodes() reads the compiled graph as data, one crt.graph.OpCode per node; Trace eager code to graphs walks that surface.
model.compile().use { graph ->
println("inputs ${graph.inputNames()} -> outputs ${graph.outputNames()}")
val y = graph.run(listOf(x))[0]
println(y.summary())
}
package main
import (
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
model := must(api.OnnxModelOpen("tiny_mlp.onnx"))
graph := must(model.Compile())
// Positional run: one tensor per InputNames() entry, that order.
x := must(clikart.TensorOf(api, []float32{1, 2, 3, 4, 5, 6, 7, 8}, []int64{2, 4}))
outputs := must(graph.Run([]*clikart.Tensor{x}))
fmt.Printf("y = %s\n", outputs[0])
}
let graph = api.f_onnx_model_compile(&model)?;
let x = api.tensor_full(&[2, 4], 1.0, F32)?;
let outputs = api.f_model_graph_run_vector(&graph, &[&x])?;
println!("{}", api.tensor_to_string(&outputs.get(0)));
With the fixed all-half weights and the 0.25 bias above, the math fits in your head; the C++ program's run over the two-row input prints:
y = relu(X W + B):
[5.25, 5.25, 5.25]
[0, 0, 0]
The first row is (1+2+3+4) * 0.5 + 0.25 = 5.25 per output; the second row's pre-activation is negative in every column, so relu zeroes it.
Two pointers from here. A compiled ModelGraph is the same runnable type that tracing eager code produces, so everything downstream of compile() is shared. And a model too big to build by hand arrives as a file: the IO-contract section works unchanged on a checkpoint you downloaded.