Core Concepts
Four tensor layers
tenferro provides four tensor types for different use cases:
| Layer | Type | Use case | Requires Engine? |
|---|---|---|---|
TypedTensor<T> |
Statically typed | Direct computation, compile-time dtype safety | No (needs a backend) |
Tensor |
Dynamic dtype enum | Mixed-dtype workflows, FFI | No (needs a backend) |
EagerTensor |
Eager AD handle | PyTorch-style scalar-loss backward | No (needs EagerContext) |
TracedTensor |
Lazy graph handle | Transform AD, graph reuse, graph optimization | Yes |
Choose the simplest layer that meets your needs:
- Compile-time dtype safety ->
TypedTensor<T>+ a backend such asCpuBackend - No AD needed ->
Tensor+ a backend - PyTorch-style scalar-loss backward ->
EagerTensor<B>+EagerContext<B> - Transform AD or graph reuse ->
TracedTensor+Engine<B>
TypedTensor: statically typed storage
TypedTensor<T> stores concrete tensor data with a compile-time scalar type. Use it when you want dtype safety in Rust code and you do not want runtime dtype dispatch.
use tenferro::{Tensor, TypedTensor};
// Column-major buffer: columns are [1, 2], [3, 4], [5, 6].
let typed = TypedTensor::<f64>::from_vec(vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
assert_eq!(typed.as_slice(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
// Wrap a typed tensor in the matching dynamic enum when you need `Tensor`.
let dynamic = Tensor::F64(typed.clone());
assert_eq!(dynamic.shape(), &[2, 3]);Tensor: eager computation
Tensor holds concrete data and supports immediate computation. Every operation takes a backend context (&mut impl TensorBackend) and returns results immediately.
use tenferro::{CpuBackend, Tensor, TensorBackend};
let mut ctx = CpuBackend::new();
// Column-major buffer: columns are [1, 2], [3, 4], [5, 6].
let a = Tensor::from_vec(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
let b = Tensor::from_vec(vec![3, 2], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
let c = a.matmul(&b, &mut ctx).unwrap();
assert_eq!(c.shape(), &[2, 2]);
let (_u, s, _vt) = a.svd(&mut ctx).unwrap();
assert_eq!(s.shape(), &[2]); // min(2, 3) singular valuesIf you have used NumPy or PyTorch in eager mode, this is the familiar pattern.
EagerTensor: PyTorch-style scalar-loss backward
EagerTensor wraps immediate tensor values in an EagerContext that records operations for scalar-loss reverse-mode autodiff. Use it when you want the familiar PyTorch pattern of computing a loss and calling .backward() on it.
TracedTensor: lazy computation with AD
TracedTensor is the value you pass around in user code. You create it from a shape and flat data, then combine it with operations such as +, *, reshape, transpose, einsum, or svd.
In PyTorch terms, a TracedTensor feels like a tensor object you can keep composing. In JAX terms, it is closer to building up a staged computation, except tenferro keeps the staging model explicit instead of hiding it behind jit.
Concrete vs symbolic shapes
Each TracedTensor carries its shape in one of two modes:
- Concrete: shape is known at graph-build time. Use
from_vec(shape, data),from_tensor_concrete_shape(tensor), orinput_concrete_shape(dtype, shape)for a placeholder with a fixed shape. - Symbolic: shape is only known at eval time. Use
from_tensor_symbolic_shape(tensor)orinput_symbolic_shape(dtype, rank)when the shape varies across calls (dynamic batch sizes, polymorphic graphs).
The choice affects how N-ary einsum is lowered. All-concrete inputs let tenferro optimize the contraction path at build time and decompose into binary DotGeneral ops. Any symbolic input keeps a single NaryEinsum op in the graph and defers path optimization to eval time (cached per shape). See the einsum guide for examples.
Engine: the executor
Engine owns the backend that actually runs your computation. It also keeps reusable execution state, so the normal pattern is to build one engine and reuse it across many evaluations.
If you think in PyTorch, Engine is the closest thing to the runtime that turns your tensor program into real values. If you think in JAX, it plays the role of the execution context you pass to traced work, but as an explicit Rust value.
eval(): get results
Operations on TracedTensor are lazy. Nothing is materialized until you call .eval(&mut engine), which returns a concrete Tensor.
Input data -> TracedTensor -> operations -> .eval(&mut engine) -> Tensor result
Execution model comparison
| Library | Mental model | tenferro equivalent |
|---|---|---|
| NumPy | Eager, no AD | Tensor + a backend |
| PyTorch (eager) | Eager with autograd | EagerTensor<B> + EagerContext<B> |
JAX (jit) |
Staged/lazy computation | TracedTensor + Engine<B> |
Minimal examples
Eager (no AD)
use tenferro::{CpuBackend, Tensor, TensorBackend};
let mut ctx = CpuBackend::new();
let a = Tensor::from_vec(vec![2], vec![1.0_f64, 2.0]);
let b = Tensor::from_vec(vec![2], vec![3.0_f64, 4.0]);
let sum = a.add(&b, &mut ctx).unwrap();
assert_eq!(sum.as_slice::<f64>().unwrap(), &[4.0, 6.0]);Lazy with AD
use tenferro::{CpuBackend, Engine, TracedTensor};
let a = TracedTensor::from_vec(vec![2], vec![1.0_f64, 2.0]);
let b = TracedTensor::from_vec(vec![2], vec![3.0_f64, 4.0]);
let mut sum = &a + &b;
let mut engine = Engine::new(CpuBackend::new());
let result = sum.eval(&mut engine).unwrap();
assert_eq!(result.as_slice::<f64>().unwrap(), &[4.0, 6.0]);