Skip to content

Loading a model

Hydronnx's user-facing workflow is three steps: inspect, emit, use. You point one CLI at your .onnx to confirm Hydronnx can handle it, point the other CLI at the same file to generate a Chelis module, then import that module like any other Chelis code.

This document walks through the workflow concretely against the worked example at examples/tabular_classifier/. Every command and every expected line is verified against the CLIs.

+--------------------------+
model.onnx | chelis-hydronnx-inspect | inventory + readiness report
+--------------------------+
|
+--------------------------+
model.onnx | chelis-hydronnx-emit | Hydronnx.<Name>.ch
+--------------------------+
|
+--------------------------+
.ch | chelis check / test / | type-checked Chelis program
| prove |
+--------------------------+

The two CLIs are independent. inspect is read-only, handy for "is this model in scope, what is its surface, will grad work", but you can skip straight to emit if you already know the answers. emit re-runs the parse internally; it does not depend on inspect.

chelis-hydronnx-inspect reads the .onnx, validates the opset and dtype set, and prints a human-readable inventory.

Terminal window
chelis-hydronnx-inspect examples/tabular_classifier/iris_classifier.onnx

The full output is shown in the CLI reference. What to look at, in order:

  1. The header. opset_version: 17 is supported (Hydronnx accepts opsets 11 through 23). producer: gen_tabular_model.py is informational.
  2. inputs / outputs. The example shows features : f32 [1, 4] mapping to probabilities : f32 [1, 3]. The dtypes (f32, f64, i32, i64, the supported set) and the static shapes ([1, 4]) tell you what type the emitted forward will have.
  3. nodes. Four nodes: Gemm, Relu, Gemm, Softmax, all in the emit set. A node carrying a deferred operator (Round, MultiHeadAttention, ScatterND, or ScatterElements) would appear here, the inspect would still succeed, and emit would reject. Conv, ConvTranspose, MaxPool, AveragePool, Cos, Tan, Floor, and Ceil emit. The deferral list is in Supported ONNX surface.
  4. weights. Four initializers. The example is small enough (a few hundred bytes of .onnx) that the emitted weight literals stay tiny. A multi-megabyte model is out of scope for the inline-emit path; see Weights and large models.
  5. non-differentiable operators. The detector's per-node verdict. A non-empty list here is a strong signal that chelis grad will fail; an empty list (the example's case) is not a guarantee. See Limitations and scope.

If inspect exits non-zero, the model is out of scope, and emit will produce the same error. Common cases are catalogued in Error handling.

chelis-hydronnx-emit turns the same .onnx into a .ch Chelis module.

Terminal window
chelis-hydronnx-emit \
examples/tabular_classifier/iris_classifier.onnx \
-o examples/tabular_classifier/src/irisclassifier.ch

Output:

wrote examples/tabular_classifier/src/irisclassifier.ch

What emit produced:

module Hydronnx.IrisClassifier
export (forward)
-- hydronnx - generated Chelis module. Do not edit by hand.
-- ... provenance metadata ...
def w1() = reshape(to_tensor([...]), [cast(4, int64), cast(6, int64)])
def b1() = to_tensor([...])
def w2() = ...
def b2() = ...
sig forward: tensor[1, 4, f32] -> tensor[1, 3, f32]
def forward(features) = {
h1_pre = add(matmul(features, w1()), expand(b1(), cast(0, int32), cast(1, int32)))
h1 = relu(h1_pre)
-- ... ...
probabilities
}

The two lines that matter for downstream code are the module declaration and the sig:

  • The module is Hydronnx.IrisClassifier. Your own Chelis files import Hydronnx.IrisClassifier (forward).
  • The signature is tensor[1, 4, f32] -> tensor[1, 3, f32], the same shapes and dtypes the inspect step reported. From this point on, forward is a typed Chelis function. Pass it a tensor[1, 5, f32] and chelis check rejects the call at compile time.

The emit is deterministic. Re-running on the same .onnx regenerates the file byte-for-byte, modulo the -- emitted: timestamp and -- source: path lines. The worked example's acceptance test (tests/h5_example.rs) checks exactly that, so if emit ever drifted, the test would catch it.

The CLI's --out / -o, --module-name, --precision, and --batch-dim flags are documented in the CLI reference. A reminder of the warn-only ones:

  • --precision f64 and --batch-dim N are plumbed but inert. They print a loud warning: to stderr and proceed; the emitted .ch is what you would have gotten without the flag. Do not use them expecting effect.

If your ONNX model declares a symbolic batch dim (an ONNX dim_param), that dim survives into the emitted sig. For example, tests/fixtures/e2e/symbolic_batch.onnx (a single Relu over a [batch, 5] input) emits:

sig forward: tensor[batch, 5, f32] -> tensor[batch, 5, f32]
def forward(x: tensor[batch, 5, f32]) = {
y = relu(x)
y
}

A caller is then either also generic over batch, and unifies, or provides a concrete batch value at the call site. Rank-2 MatMul and default Gemm preserve a symbolic graph-input batch dim, including the common rank-1 Gemm bias broadcast. Runtime-dim reshape supports ONNX 0 dims copied from symbolic graph inputs; -1 inference with symbolic dims rejects. See Limitations and scope.

The emitted module is ordinary Chelis. The worked example's src/predict.ch is the user-written code:

module Hydronnx.Predict
import Hydronnx.IrisClassifier (forward)
export (feature_scale, normalize, top_class, classify)
def feature_scale() = reshape(
to_tensor([cast(0.1, f32), cast(0.1, f32), cast(0.1, f32), cast(0.1, f32)]),
[cast(1, int64), cast(4, int64)],
)
sig normalize: tensor[1, 4, f32] -> tensor[1, 4, f32]
def normalize(raw) = mul(raw, feature_scale())
sig top_class: tensor[1, 3, f32] -> tensor[1, int64]
def top_class(probs) = argmax_reduce(probs, cast(1, int32))
sig classify: tensor[1, 4, f32] -> tensor[1, int64]
def classify(raw) = top_class(forward(normalize(raw)))

Three things to notice:

  • forward is imported and called like any other function. The composition top_class(forward(normalize(raw))) is type-checked end to end. If the intermediate shapes did not line up, chelis check would reject this line at compile time. There is no runtime shape surprise.
  • feature_scale() is hand-written: nothing in the Hydronnx workflow forces a particular pre-process. The emitted forward is a function, not a pipeline.
  • top_class ends in argmax_reduce. The composed classify therefore has an int64 output and is no longer mathematically differentiable. That is not Hydronnx's choice; it is the post-process the example author picked. See Composition with Chelis for more on building pipelines, and Properties and verification for attaching @property blocks to the emitted forward.

The example is a complete reef project. From its directory:

Terminal window
cd examples/tabular_classifier
chelis test

Both example tests pass, producing 2 passed, 0 failed. That confirms the inspect, emit, use loop runs end to end on a real model.

If any step exits non-zero, see Error handling for the full error catalogue: which HydronnxError variants exist, what each one's message looks like, what tends to cause it, and how to resolve it.

The most common cases:

  • Unsupported operator. Your model contains one of the deferred operators. Hydronnx prints which operator and where. The deferral is documented in Supported ONNX surface.
  • Unsupported opset. Your model is older than opset 11 or newer than 23. Re-export at a supported opset (most exporters take an opset_version argument).
  • Unsupported dtype. Your model uses an ONNX dtype outside the supported set (f32, f64, i32, i64, bool). Cast at export time.

A model that inspects clean but does not emit cleanly is rare. Almost always it is a parsed-but-unsupported operator that the inspect path tolerates and the emit path rejects.