tract

September 4, 2026 · View on GitHub

tract is Sonos' neural-network inference engine written in Rust. It reads ONNX, NNEF, TensorFlow Lite, and TensorFlow models, optimises them, and runs them on CPU (x86/ARM), GPU (Metal, CUDA), embedded targets, and WASM.

This is the operational quick reference: where things live, how to build and test, and the abstractions you need to work with the graph. It is descriptive — the rules you must follow when contributing are in AGENTS.md (also readable as CLAUDE.md), and take precedence over anything here.

For conceptual background not derivable from the source, see the rest of this directory — README.md indexes it.


Crate map

CratePurposeDepends on
dataTensor, DType, TractResult, low-level storage(none)
linalgMicro-kernel dispatch (BLAS-style, hand-rolled SIMD)data
coreTypedModel, op trait, passes, rewriter, TypedModelPatchlinalg, data
hirUntyped inference graph (pre-type-analysis)core
nnefNNEF load/save, tract-OPL extensions, NNEF ser/de for core opscore
onnx-opltract-OPL extensions for ONNX-specific opsnnef, extra
onnxONNX importernnef, hir, onnx-opl, extra
tfliteTensorFlow Lite importercore
tensorflowTensorFlow importerhir, pulse
pulse-oplStreaming op primitivesnnef
pulseStreaming / causal inferencepulse-opl, transformers
transformersTransformer-specific ops (RmsNorm, Silu, GeluApproximate, ...)nnef¹
gpuShared GPU abstractionscore, pulse-opl, transformers
metalApple Metal backendgpu, core, pulse-opl, transformers
cudaNVIDIA CUDA backendgpu, core, pulse-opl, transformers
extraMiscellaneous ops not yet in corennef, pulse
cli / libclitract command-line toolmost of the above
api/rsHigh-level stable public Rust APInnef, onnx, pulse, transformers, metal, cuda, ...
api/ffiC FFI over api/rsapi/rs

¹ transformers has no direct tract-core dep; import core types via tract_nnef::tract_core.


Build and test

# build everything
cargo build --workspace

# test a single crate
cargo test -p tract-core

# test the whole workspace
cargo test --workspace

# format (always repo-wide, never per-crate; rust-toolchain.toml pins stable, so bare cargo fmt is correct)
cargo fmt --all

# lint
cargo clippy --workspace

The harness/ directory contains integration tests that run against real models. .travis/native.sh runs the full native Linux CI suite; running it locally requires libssl-dev (needed by the tflite step).

Synthetic NNEF tests under harness/nnef-test-cases/ are driven by a runme.sh that calls the tract CLI with --assert-output-bundle against a reference io.npz. Add new cases there rather than as Rust integration tests. If the assertion you need isn't expressible through the CLI, extend the CLI.

Model inspection

# human-readable graph dump
tract model.nnef.tgz dump

# machine-readable — pipe to jq or python
tract model.nnef.tgz dump --audit-json | jq '.nodes[] | select(.op_name == "Conv")'

Reach for --audit-json when scripting; the plain dump output is meant for humans and is awkward to parse.

Hardware and kernel benchmarking

tract hwbench <M,K,N[,dt]>... reports the machine (cores, cache/memory bandwidth) and times every matmul micro-kernel at each shape, marking the dispatcher's pick with <-- (--json to parse, --assert to gate on it) — the tool for diagnosing or calibrating kernel selection. See doc/kernel-notes.md and doc/cli-recipe.md.

test-rt

test-rt is the cross-backend test framework. It separates test suites from runtimes: a suite (e.g. suite-unit, suite-onnx) defines a set of Test-trait objects; a runner crate (e.g. test-unit-core, test-metal, test-cuda) picks a Runtime implementation and runs a subset of those suites against it, with its own ignore list.

Layout:

CrateRole
test-rt/infraTest, TestSuite, Runtime traits and test-runner harness
test-rt/suite-unitUnit tests for core ops (conv, einsum, matmul, ...)
test-rt/suite-onnxONNX backend test suite
test-rt/test-unit-coreRuns suite-unit on the default CPU runtime
test-rt/test-onnx-coreRuns suite-onnx on the default CPU runtime
test-rt/test-metalRuns unit + onnx suites on the Metal backend
test-rt/test-cudaRuns unit + onnx suites on the CUDA backend
test-rt/test-f16Runs f16-specific cases
test-rt/test-tfliteRuns suite against the TFLite runtime
test-rt/test-nnef-cycleVerifies NNEF round-trip for all suite cases

To add a new op test: add a case to the relevant suite-* crate. The runner crates pick it up automatically; add an ignore entry in the runner only if the backend genuinely cannot support the case.


Core abstractions

Client code (applications, examples, language bindings) should use api/rs only. The internal crates (core, nnef, onnx, ...) are not stable API surface. When asked "is X part of the public API?", check api/rs/src/lib.rs — that is the authoritative surface, not the internal crate's pub items.

key principle

tract avoid specializing for one model or an application. Model-wide behaviours or optimisations should emerge from op-scoped manipulation composing together. There are pragmatic compromises: sometimes introducing "big" primitives than could be implemented with atomic operators (Convolution, Attention, RmsNorm, ...) is unlocking optimisations.

TypedModel / TypedNode / TypedFact

The main IR. A TypedModel is a DAG of TypedNodes. Each node holds a boxed Op and a list of TypedFact outputs (element type + symbolic shape).

Op trait (core/src/ops/mod.rs)

Every op implements Op and usually TypedOp. Key methods:

  • eval -- eager execution
  • output_facts -- shape/type inference
  • declutter -- return a TypedModelPatch to simplify the op or replace it
  • codegen -- return a patch targeting a specific backend/platform

Model rewriting

TypedModelPatch is the preferred way to modify a model. Direct mutation of TypedModel nodes or edges is an exception reserved for construction and well-understood bulk transforms -- in almost every other case, implement rule functions that return TypedModelPatches, and wrap them in a Rewriter to define a ModelTransform.

TypedModelPatch (core/src/model/patch.rs)

Surgical edits to a model. Build a patch, then patch.apply(model).

Rewriter (core/src/model/rewriter.rs)

Per-op declutter rules collected into a typed dispatch table:

Rewriter::default()
    .with_rule_for::<MyOp>("rule-name", |ctx, model, node, name, op| {
        rule_if!(some_condition);
        rule_if_some!(x = maybe_value);
        // build and return a TypedModelPatch
        Ok(Some(patch))
    })

Use Rewriter for rules that fire on a single op type. The three guard macros (defined in core/src/transform.rs) provide early-return ergonomics inside a TractResult<Option<TypedModelPatch>> body:

MacroExits when
rule_if!(cond)cond is false
rule_if_let!(pat = expr)pattern does not match
rule_if_some!(pat = expr)value is None

ModelTransform (core/src/transform.rs)

Whole-model passes (e.g. float precision translation, block-quant folding). Implement the ModelTransform trait when you need to walk the entire graph rather than react to individual op types.

When to use which

SituationTool
Simplify / fuse one op typeOp::declutter + TypedModelPatch
Cross-op pattern (N ops -> M ops)Rewriter rule
Whole-model structural changeModelTransform
Backend lowering for one opOp::codegen + TypedModelPatch

Op detection via declutter

Tract detects and fuses transformer ops during the declutter pass, not in a separate recognition pass:

  • RmsNorm -- detected in Reduce::declutter
  • Silu -- detected in Sigmoid::declutter (via element_wise! ; declutter: param)
  • GeluApproximate -- detected in Pow::declutter (chained declutter_pow)

Streaming and pulsification

Streaming inference converts a TypedModel into a PulsedModel (pulse/src/model.rs) that processes a fixed-size chunk along one axis at each step.

  • Streaming axis and pulse size. Each op pulsifies through an impl in pulse/src/ops/. The streaming axis is tracked node-by-node; an op that reorders or merges axes must declare what the streaming axis becomes on its output, otherwise pulsification fails or produces wrong delays.
  • Delay. Ops that need past context (conv, attention windows, banded masks) insert a Delay op from pulse-opl/src/delay.rs to buffer earlier pulses. The accumulated output delay is exposed as pulse.delay and used by the CLI assertion path to skip warmup before comparing against a batch reference.
  • ChangeAxes. core/src/optim/change_axes.rs rewrites axis layout during optimisation. Any change-axes interaction has to preserve the streaming axis identity, so new ops that move axes around need a change_axes impl that agrees with their pulsification.

The streaming model has subtle invariants -- don't touch pulse / pulse-opl casually.


NNEF serialisation

NNEF ser/de for core ops lives in nnef/src/ops/core/. Ops are registered with a primary tract_core_* name and backward-compatible tract_transformers_* aliases where needed.

Re-export shims in transformers/src/ops/mod.rs keep downstream crates (cuda, metal, gpu, test-rt) working without a direct tract_core dep.