Core Concepts
Three tensor layers
tenferro provides three tensor types for different use cases:
| Layer | Type | Use case | Requires Engine? |
|---|---|---|---|
TypedTensor<T> |
Statically typed | Direct computation, compile-time dtype safety | No (needs CpuBackend) |
Tensor |
Dynamic dtype enum | Mixed-dtype workflows, FFI | No (needs CpuBackend) |
TracedTensor |
Lazy graph handle | Automatic differentiation, graph optimization | Yes |
Choose the simplest layer that meets your needs:
- No AD needed ->
Tensor+CpuBackend - AD (grad/vjp/jvp) needed ->
TracedTensor+Engine - Compile-time dtype safety ->
TypedTensor<T>+CpuBackend
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::{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_tensor::{Tensor, TensorBackend, cpu::CpuBackend};
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.
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.
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 + CpuBackend |
| PyTorch (eager) | Eager with autograd | TracedTensor + Engine |
JAX (jit) |
Staged/lazy computation | TracedTensor + Engine |
Minimal examples
Eager (no AD)
use tenferro_tensor::{Tensor, TensorBackend, cpu::CpuBackend};
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]);