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.
import sys
import clika_runtime as crt
model = crt.io.OnnxModel.open(sys.argv[1] if len(sys.argv) > 1 else "model.onnx")
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)}")
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.
import numpy as np
import clika_runtime as crt
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())
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.