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 as CpuBackend
  • 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 values

If 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), or input_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) or input_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]);