Skip to content

CLI reference

Hydronnx ships three user-facing CLIs. All are Rust binaries; all read .onnx and fail loudly on stderr with error: ... plus exit code 1 on any failure. The one exception is shared by all three: a command-line usage error (an unknown flag, or two flags that conflict) is reported by the argument parser in its own format with exit code 2.

All commands shown here are verified against the binaries. --help output is the source of truth; anything in this document that disagrees with --help is a doc bug (please file an issue).

Inspect an ONNX model file: print the model header, input and output tensors, node list, weight inventory, and the non-differentiable-operator report.

Usage: chelis-hydronnx-inspect <PATH>
Arguments:
<PATH> Path to the .onnx file
Options:
-h, --help Print help

A single text report on stdout, organized in sections. Run against the worked example (examples/tabular_classifier/iris_classifier.onnx) the report is:

model: iris_classifier
producer: gen_tabular_model.py
ir_version: 8
opset_version: 17
file_size: 606 bytes
inputs (1):
features : f32 [1, 4]
outputs (1):
probabilities : f32 [1, 3]
nodes (4):
[gemm1] Gemm (features, W1, B1)
[relu1] Relu (h1_pre)
[gemm2] Gemm (h1, W2, B2)
[softmax] Softmax (logits)
weights (4):
W1 : f32 [4, 6] (96 bytes)
B1 : f32 [6] (24 bytes)
W2 : f32 [6, 3] (72 bytes)
B2 : f32 [3] (12 bytes)
non-differentiable operators (0):
none - this model contains no mathematically non-differentiable operators.
NOTE: this section reports only *mathematical* non-differentiability and does not
guarantee `grad` will succeed. Hydronnx verifies weighted Gemm AD, but
other operator surfaces still need an explicit AD gate.

Section by section:

  • Header lines. model: (ONNX model_name, (unnamed) if blank), producer: (ONNX producer_name), ir_version: (the model's IR version), opset_version: (the ai.onnx opset the model was exported at), file_size: (in bytes, read from the filesystem).
  • inputs (N):. One line per graph input: <name> : <dtype> <shape>. Symbolic dims (ONNX dim_param) appear by name ([batch, 5]); static dims by value ([1, 4]); the rare ONNX "dynamic" dim with neither value nor name renders as ?.
  • outputs (N):. Same shape as inputs.
  • nodes (N):. One line per node: [<name>] <op_type> (<inputs>). An unnamed node prints as [(unnamed)].
  • weights (N):. One line per initializer: <name> : <dtype> <shape> (<bytes>). The byte count is numel * dtype-bytes (4 for f32 / i32, 8 for f64 / i64, 1 for bool).
  • non-differentiable operators (N):. The detector's per-node report. See Non-differentiable operators below.

The detector reports operators whose gradient is undefined or zero-almost-everywhere by their own ONNX semantics: ArgMax / ArgMin (integer-index output), Floor / Ceil / Round / Sign (piecewise-constant), the comparison ops Equal / Greater / Less / GreaterOrEqual / LessOrEqual and the logical ops And / Or / Not / Xor (Boolean output), Scatter / ScatterElements (replace, not accumulate), and a Cast whose target is an integer dtype.

A model with one or more of these prints them in graph order. For tests/fixtures/per_op/reduce/ArgMax.onnx:

non-differentiable operators (1):
[ArgMax] ArgMax - integer-index output - zero gradient a.e., undefined on ties

The NOTE block is always printed, even when the count is 0. It is honest about the scope: a zero count is not a guarantee that chelis grad will succeed. Hydronnx gates weighted Gemm AD, but broader operator surfaces still need explicit AD coverage. See Limitations and scope.

chelis-hydronnx-inspect exits non-zero on any parse failure. The error text is the Display of the underlying HydronnxError (see Error handling):

$ chelis-hydronnx-inspect /tmp/missing.onnx
error: file not found: /tmp/missing.onnx

Emit a Chelis .ch Surf module from an ONNX model file.

This is the build-time code generator behind the model-load surface. The model load is sketched as a runtime function, but Chelis is ahead-of-time compiled, so the actual implementation is this CLI: you run it once per model, ahead of time, and check the generated .ch in (or regenerate it as part of your build).

Usage: chelis-hydronnx-emit [OPTIONS] <PATH>
Arguments:
<PATH>
Path to the input .onnx file
Options:
-o, --out <OUT>
Path to write the generated .ch file. `-` writes to stdout. When
omitted, the file is written next to the input with a name
derived from the emitted module (e.g. `tabularmlp.ch`)
--module-name <NAME>
Override the emitted module-name segment (the part after
`Hydronnx.`). Use this when the model's own name trips the chelis
`module-pascal-components` lint
--precision <PREC>
Target weight precision (`f32` or `f64`). Weights are emitted at
their native ONNX dtype; `--precision f64` is inert and prints a
loud `warning:` to stderr before proceeding
--batch-dim <N>
Override the model's batch dimension. The signature is derived
mechanically from the ONNX graph; this flag is inert and prints a
loud `warning:` to stderr before proceeding
-h, --help
Print help (see a summary with '-h')
  • -o <path>: write to <path> literally.
  • -o -: write to stdout (useful for piping).
  • no -o: write next to the input .onnx, with a filename derived from the emitted module's last component, lowercased, no underscores. A model whose emitted module is Hydronnx.IrisClassifier lands at irisclassifier.ch next to the source .onnx. Reef matches the module ladder to file paths literally, so this default keeps the emitted file reef-importable with no further work.

A successful run prints wrote <path> on stdout and exits 0 (or, with -o -, prints the .ch itself with no wrote ... line).

By default the emitted module is Hydronnx.<PascalCase of model name>. For example, an ONNX model_name = "iris_classifier" becomes module Hydronnx.IrisClassifier. If the derived name trips the chelis module-pascal-components lint (for example a single-word lowercase model name like "resnet50" whose PascalCase form has a digit run), pass --module-name <Name> to override the last segment. The Hydronnx. prefix is fixed.

--precision and --batch-dim: warn-only no-ops

Section titled “--precision and --batch-dim: warn-only no-ops”

Both flags are plumbed into EmitOpts but inert. Weights are emitted at the model's native ONNX dtype, and the batch dimension is read from the ONNX graph directly. Passing either flag in a way the emit cannot honor prints a loud warning: to stderr and proceeds (exit 0). The flags exist so the surface anticipates a later extension; the no-op is honest, not silent.

Passing --precision f64 prints a warning: line to stderr noting that the flag has no effect and that weights are emitted at their native ONNX dtype, then proceeds to wrote <path>. Passing --batch-dim N prints a warning: line noting that the flag has no effect and the model's declared batch dimension is used as-is, then proceeds. In both cases the exit code is 0.

--precision f32 is not warned on (the native dtype for the fixtures already is f32, so the flag is honored for that value).

After parsing, chelis-hydronnx-emit runs the same detector that chelis-hydronnx-inspect reports, and if the model contains any mathematically non-differentiable operator, prints a loud warning: block to stderr and proceeds. The emitted forward is still a valid Chelis function; the warning is about grad, not about emit.

Example (verified against tests/fixtures/per_op/reduce/ArgMax.onnx):

warning: this model contains 1 mathematically non-differentiable operator(s) - automatic differentiation (`grad`) on the emitted `forward` will not work. chelis's AD does not name the operator; Hydronnx does:
[ArgMax] ArgMax - integer-index output - zero gradient a.e., undefined on ties
note: removing these operators does not by itself make the model AD-ready; chelis AD support is operator-dependent. Hydronnx gates the weighted Gemm path, but broader models need their own AD parity gate.

When the count is 0 the warning is not emitted: chelis-hydronnx-emit does not echo the inspect CLI's NOTE block. Silence on emit is not a claim of AD-readiness; see the warning's note: line above and the AD section of Limitations and scope.

Every emitted module is a self-contained Chelis file beginning with a provenance comment block. The shape (taken from examples/tabular_classifier/src/irisclassifier.ch):

module Hydronnx.IrisClassifier
export (forward)
-- hydronnx - generated Chelis module. Do not edit by hand.
-- module: Hydronnx.IrisClassifier
-- source: examples/tabular_classifier/iris_classifier.onnx
-- model: iris_classifier
-- opset: 17 (ONNX IR version 8)
-- producer: gen_tabular_model.py
-- emitted: 2026-05-22T21:28:03Z
-- operators (4):
-- Gemm x2
-- Relu x1
-- Softmax x1
-- weights (4):
-- W1 : f32 [4, 6]
-- B1 : f32 [6]
-- W2 : f32 [6, 3]
-- B2 : f32 [3]
def w1() = reshape(to_tensor([cast(0.1523..., f32), ...]), [cast(4, int64), cast(6, int64)])
def b1() = to_tensor([cast(-0.0428..., f32), ...])
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
}

Key shape details:

  • The module exports exactly one function: forward. Its sig carries the ONNX input and output tensor types verbatim: same dims, same dtype. Static ONNX dims become literal integers; an ONNX dim_param (symbolic dim) becomes a Chelis dim variable. Run chelis-hydronnx-emit on tests/fixtures/e2e/symbolic_batch.onnx to see this in action; the emitted sig is sig forward: tensor[batch, 5, f32] -> tensor[batch, 5, f32].
  • Weights are emitted as zero-arg defs (def w1() = ...). They are baked into the source, not loaded from a file at runtime. Each call site invokes them as w1(). This is the ahead-of-time-friendly form; large weight tensors make this scaling-limited, see Weights and large models.
  • The emitted body uses chelis Surf builtins (matmul, relu, softmax, expand, cast, and so on). There is no Hydronnx runtime to depend on.

The emit is deterministic: the same .onnx produces the same .ch byte-for-byte, modulo the -- emitted: timestamp and -- source: path lines. The acceptance test (tests/h5_example.rs) pins this.

Like chelis-hydronnx-inspect, chelis-hydronnx-emit exits non-zero on any failure, with error: ... on stderr. See Error handling for the full catalogue. A common example:

$ chelis-hydronnx-emit tests/fixtures/per_op/elementwise/Round.onnx -o -
error: unsupported operator 'Round' at node index 0: tests/fixtures/per_op/elementwise/Round.onnx

Execute an ONNX model with its real weights through the translate and chelis-ir eval path (Path A of Weights and large models). translate_model separates structure (a chelis-ir Dag) from weights (in-memory tensors of any size), and the interpreter runs the DAG with the weights handed in at eval time, so there is no inline-source weight ceiling. A real ResNet18 reproduces ONNX Runtime's logits to about 1e-6 this way.

This is the runtime product, not the typed-source product: nothing here is chelis checked or provable. Verify the architecture with chelis-hydronnx-emit --weights placeholder; run the numbers here. The typed-source path with real weights is gated on Chelis typed external constants.

Usage: chelis-hydronnx-run [OPTIONS] <PATH>
Arguments:
<PATH> Path to the input .onnx file
Options:
--inputs <FILE.npz> Read graph inputs from a .npz file, each array keyed by its
raw ONNX graph-input name. Inputs with an initializer
default are optional: omit to keep the default, include to
override
--fill <MODE> Synthesize every graph input that has no initializer
default (static input shapes only)
[possible values: zeros, ones, deterministic]
--weights <FILE.hnw> Override initializer values from a .hnw sidecar weight
archive (checksums verified on read)
--export-weights <FILE.hnw> Write the model's initializers to a .hnw sidecar weight
archive. With no --inputs/--fill, exports and exits without
running
--json Print full output tensors as one JSON object on stdout
instead of the per-output summary
-h, --help Print help

Exactly one input source is required to run: --inputs (concrete tensors, works with symbolic batch dims) or --fill (static shapes only; deterministic is the reproducible (i % 257)/257 - 0.5 pattern the ResNet18 eval gate uses). --export-weights alone exports the archive and exits (export needs only the parser, so it also works on models the translator rejects).

A graph input that is also backed by an initializer is the ONNX default-value pattern, and the initializer is authoritative: --fill never synthesizes over it, and under --inputs the array is optional. Absent keeps the default, present overrides it (reported on stderr).

$ chelis-hydronnx-run model.onnx --inputs input.npz
ran `tabular_mlp` (25 dag nodes) with real weights
output `y`: shape [4, 1]
min 0.500076 max 0.500693 mean 0.500384 argmax(flat) 3
first 4 = [0.5000757012052212, ...]
$ chelis-hydronnx-run resnet18.onnx --fill deterministic
ran `main_graph` (2750 dag nodes) with real weights
output `191`: shape [1, 1000]
min -3.367538 max 6.307741 mean 0.000010 argmax(flat) 807
...

The --json form prints one object: {"model": "...", "outputs": [{"name": "...", "shape": [...], "data": [...]}]} with full f64 tensor values (the interpreter's storage precision; inputs and weights are promoted to f64 on load; npz dtypes are not cross-checked against the model's declared input dtypes, and i64 magnitudes above 2^53 lose precision in the promotion). JSON has no NaN/Infinity literals, so non-finite values are encoded as the strings "NaN" / "Infinity" / "-Infinity" (with a count warned on stderr). The default summary reports statistics over the finite values with the non-finite count alongside.

--export-weights model.hnw writes every initializer to a .hnw archive at its native dtype; --weights model.hnw then runs with the archive's values instead of the ONNX-embedded ones (every archive tensor must name a model constant and match its shape; the override count is reported on stderr). The round-trip is bit-identical and is gated by tests/run_cli.rs. Until Chelis has typed external constants, this is a validation surface for the archive core: the emitted .ch cannot reference an archive.

Exit 1 with error: ... on stderr: parse and translate failures (an unsupported operator stops translation), a missing npz array (the message lists the arrays present), a shape that contradicts a statically declared input dim, --fill on a symbolic input, an archive tensor that names no model initializer or whose shape disagrees with the model's, a corrupt archive (checksum mismatch), or --export-weights on a model with a non-finite float initializer (for example a -inf attention-mask constant; the archive format rejects non-finite floats by design, naming the tensor and element).