Composition with Chelis
An emitted forward is an ordinary Chelis function. You import it, compose it
with hand-written pre- and post-processing, attach it to properties, and hand it
to other code: everything Chelis lets you do with a function works on forward.
This is the payoff for the type discipline. The emitted module's sig makes its
inputs and outputs first-class types the rest of your code can speak in.
This document walks through the composition pattern using the worked example's
Hydronnx.Predict module (src/predict.ch) as the concrete case, then notes
what does not compose.
The pattern: pre-process, forward, post-process
Section titled “The pattern: pre-process, forward, post-process”The classic shape. Real-world inference is almost never "feed raw data to the model, ship the raw output". There is a normalize step on the input and a decode step on the output. In Chelis you write each as its own function and chain them.
The example does it like this:
module Hydronnx.Predictimport Hydronnx.IrisClassifier (forward)export (feature_scale, normalize, top_class, classify)
-- A fixed per-feature scale vector matching the model's input shape.-- Real pipelines would use learned mean/std; this is a stand-in.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)],)
-- Pre-process: scale raw input element-wise. The output type-- (`tensor[1, 4, f32]`) matches `forward`'s declared input by construction,-- so feeding it in type-checks.sig normalize: tensor[1, 4, f32] -> tensor[1, 4, f32]def normalize(raw) = mul(raw, feature_scale())
-- Post-process: collapse the probability row to the predicted class index.sig top_class: tensor[1, 3, f32] -> tensor[1, int64]def top_class(probs) = argmax_reduce(probs, cast(1, int32))
-- The composed pipeline: a loaded ONNX model threaded through hand-written-- Chelis code, type-checked end to end.sig classify: tensor[1, 4, f32] -> tensor[1, int64]def classify(raw) = top_class(forward(normalize(raw)))Five things to note:
forwardis imported and called like any other function. The lineimport Hydronnx.IrisClassifier (forward)is conventional Chelis; there is no specialimport Hydronnx (load_model)runtime surface. The model load is a build-time concern: Chelis is ahead-of-time compiled, and the model load is implemented as thechelis-hydronnx-emitCLI.- Every intermediate value's shape is in its
sig.normalize's output type is exactlyforward's input type.forward's output type is exactlytop_class's input type. The compositiontop_class(forward(normalize(raw)))is therefore well-typed by the chain ofsigs. If any step's shape did not match,chelis checkwould reject this line at compile time. - Nothing in the pipeline is Hydronnx-specific.
mul,reshape,to_tensor,argmax_reduce,cast: all chelis builtins. The emitter writes nothing that the rest of Chelis cannot also write. - The emitted weights stay encapsulated.
forward's body callsw1(),b1(), and so on, but those are private toHydronnx.IrisClassifier(onlyforwardisexported). The caller does not see the weights. - Composition works in both directions. You can wrap
forwardin other functions you write (asclassifydoes), andforwardis callable from inside any other Chelis code you write. There is no "model" boundary;forwardis just a function.
How the example wires this together for chelis test
Section titled “How the example wires this together for chelis test”tests/example.ch in the worked example calls classify and checks the
predicted class:
import Hydronnx.Predict (classify, normalize)import Hydronnx.IrisClassifier (forward)
def sample_input() = reshape( to_tensor([cast(5.1, f32), cast(3.5, f32), cast(1.4, f32), cast(0.2, f32)]), [cast(1, int64), cast(4, int64)],)
test test_classify_predicts_top_class = { result = classify(sample_input()) -- assert result == [0] (the argmax of the softmax output)}This is fully ordinary Chelis. The acceptance test (tests/h5_example.rs) runs
chelis test in the example dir and verifies both example tests pass.
What composes
Section titled “What composes”The list of things that just work because forward is a function:
- Function calls. Pre-process, post-process, and pipelines as above.
@propertyattachment. Quantify overforward's input type, reduce the output to a Boolean, hand the file tochelis prove. See Properties and verification. The single-file constraint applies: the@propertymust live in the same.chasforward.- AD on gated differentiable surfaces.
grad(forward)works for the weight-free fixture and for Hydronnx's weighted tabular Gemm fixture, both checked against finite-difference references. The small CNN image-classifier surface is a documented chelis AD blocker, so broader weighted models need their exact emitted operator surface gated before relying on AD. - Multiple emitted modules in one program. Nothing stops you from emitting
two different
.onnxfiles into twoHydronnx.<Name>modules and composing both:importboth and call them in sequence. The module-name override (--module-name) is there in case two emitted modules collide. - Standard chelis tooling.
chelis check,chelis test,chelis prove,chelis fmt: all of these operate on the emitted file as on any other.
What does not compose
Section titled “What does not compose”- Ungated weighted
grad. The worked Gemm classifier is AD-gated, but that does not automatically extend to every weighted model. Composinggrad(forward)into a hand-written loss should be paired with a model-specific finite-difference or parity gate. See Limitations and scope. - Cross-module
chelis prove. A property in module A that calls into module B does not verify;chelis proveworks on a single file. The worked example sidesteps this by appending the property to the emitted module. - A composed pipeline ending in a non-differentiable op is not
differentiable. The example's
classifyends inargmax_reduce(mathematically piecewise-constant, zero gradient almost everywhere), so even ifforwardwere AD-ready,grad(classify)would not be meaningful. The Hydronnxinspectandemitdetector reportsArgMaxas non-differentiable; composition inherits that property. This is not a Hydronnx limitation; it is a property of the post-process the example picked. - Runtime reshape with symbolic dims. Rank-2
MatMuland defaultGemmcan preserve a symbolic graph-input batch dim, and Hydronnx can emit Reshape0dims copied from symbolic graph inputs.-1inference with symbolic dims rejects. See Limitations and scope. - Large emitted modules. Weights are baked into the source as literals, so
the
.chgrows with the parameter count. The example is well within scope; a multi-megabyte model is not. Hydronnx has an internal sidecar archive core, but there is no inline-replacing sidecar-backed emit path. See Weights and large models.
A heuristic for "should I compose this in Chelis or in the exporter"
Section titled “A heuristic for "should I compose this in Chelis or in the exporter"”Two equally valid paths exist for a pre- or post-process: build it into the ONNX
model itself (so the emitted forward does it), or write it as hand-written
Chelis around the emitted forward. The trade-offs:
| Concern | In the ONNX model | In hand-written Chelis |
|---|---|---|
| Performance | A single emitted function; chelis sees the whole graph | Two function calls; chelis sees them separately |
| Type clarity | Implicit; types are an internal detail | Explicit sigs; every shape is named |
| Verification | Property quantifies over the model's original input | Property can quantify over the raw input |
| Hydronnx coverage | Needs every op to be in the emit set | Pure Chelis; no Hydronnx scope question |
| Provenance | Lost in the ONNX file | Visible in source |
Often the second path is better. A pre-process that uses a non-emittable op (for
example Round for quantization or ScatterElements for index updates) is
impossible in-model but may be expressible as hand-written Chelis if the
primitive exists there. Cos, Tan, Floor, and Ceil emit, but Floor and
Ceil still make AD warnings relevant. Pick the path where everything you need
is available.