Guide B
The stack for backend developers
What happens below the tensor network algorithms: a tensor runtime with automatic differentiation, and underneath it small, tensor-free libraries for strided contraction and batched linear algebra that you can use on their own, from Rust or from C. If you maintain a quantum chemistry or many-body code and need fast contractions without transposes, the middle of this page is for you.
Status as of 2026-10-13. Several crates described here are not on crates.io yet; this page says so where it matters.
Who owns what
The backend is split along one line: a host that knows about tensors, and providers that only see borrowed strided memory. Most design decisions follow from keeping that line clean.
| Layer | Owns | Leaves to others |
|---|---|---|
| tenferro-rs (host) | tensor types, allocation, dtype dispatch, device placement, AD rules, graph tracing, gauge conventions, error wrapping | the inner loops of contraction and factorization |
| cpueinsum | executing an N-ary einsum in a given order: which labels each intermediate keeps, one reusable scratch buffer | choosing the order, parsing "ij,jk->ik", GPUs |
| tcontract | one binary contraction over arbitrary strides: planning, packing, microkernels, threading inside the call | N-ary einsum, ordering, linear algebra, AD |
| tlinalg, tlinalg-blas | batched factorizations and solves, the batch loop and its lane fan-out | tensors, allocation policy, dtype and device dispatch |
| strided-rs | the strided view types and traits everyone exchanges; permutation and elementwise kernels | everything above |
Design principles
- Explicit execution, no ambient state. Threads and scratch memory come from a context the caller passes in. No library starts a global pool, and no environment variable changes results. Two consequences: the same call is reproducible regardless of what else runs in the process, and an outer scheduler (MPI, hataori, a Julia session) can partition the machine without fighting the libraries.
- Tensor-free provider interfaces. Providers take borrowed strided views plus a few scalars and flags. They can be tested and benchmarked on plain buffers, and a different host (a quantum chemistry code, another tensor library) can call them without adopting tenferro.
- Plan once, execute many times. Contractions and einsums have a plan object that fixes the strategy, kernel and blocking for given shapes and strides. Tensor network sweeps and iterative solvers repeat the same shapes thousands of times, so per-call overhead matters as much as peak throughput.
- Measured, not assumed. Performance claims live in benchmark repositories keyed by commit and hardware, with comparisons against PyTorch and JAX (for tenferro) and vendor LAPACK (for tlinalg).
tenferro: the host
tenferro-rs is a Rust tensor runtime for scientific code: typed and dynamic dense tensors, einsum, linear algebra, FFT, and automatic differentiation, on CPU, CUDA (through a CubeCL fork) and experimental WebGPU, with an XLA path. tensor4all-rs uses it for every dense operation.
- Tensor types.
TypedTensor<T>and dynamicTensorfor direct compute;EagerTensoradds PyTorch-stylebackward();TracedTensorrecords a graph (in computegraph) for JAX-style VJP, JVP and composed higher-order derivatives such as Hessian vector products. - Conventions. Storage is column-major, matching LAPACK, Fortran and Julia. There is no facade crate: depend on
tenferro-runtime, a backend such astenferro-cpu, and the operation crates you use. Operations take an explicit backend; create it once and reuse it, since it carries the buffer pool. - Providers. On the CPU,
tenferro-einsumandtenferro-cpuexecute through cpueinsum (and so tcontract), andtenferro-linalgadapts tlinalg and tlinalg-blas to an interface tenferro defines. The providers do not depend on tenferro.
The tenferro documentation has the quickstarts. Version 0.7.1 is on crates.io; the main branch already builds on the unpublished providers described below.
cpueinsum: N-ary einsum in a given order
cpueinsum runs an einsum over integer labels with a contraction order supplied by the caller in SSA form. For each binary step it plans a tcontract call. On top it decides which labels every intermediate must keep (those in the output or needed later; everything else is summed as early as possible) and places all intermediates in one reusable scratch buffer.
It intentionally does not search for contraction orders and does not parse string notation. Those belong to the caller (tenferro, or your own code with an order from opt_einsum, cotengra or a hand-derived factorization).
d[i, l] = Σ a[i, j] b[j, k] c[k, l], contracting (a b) first. From the cpueinsum README:
use cpueinsum::strided_view::{StridedView, StridedViewMut};
use cpueinsum::{EinsumPlan, EinsumSpec, Exec, Layout, Scratch};
// Inputs are operands 0, 1, 2; step 0 makes operand 3.
let spec = EinsumSpec::new(&[&[0, 1], &[1, 2], &[2, 3]], &[0, 3], &[[0, 1], [3, 2]])?;
let cm = Layout::new(&[2, 2], &[1, 2])?;
let plan = EinsumPlan::<f64>::new(&spec, &[cm, cm, cm], cm)?;
let mut scratch = Scratch::new();
// ... build StridedViews over your buffers, then:
plan.execute_into(&Exec::serial(), &views, &mut out, &mut scratch)?;
Threading is explicit: Exec::rayon(&Pool::borrow(&pool)) runs on a Rayon pool you own. For workloads made of many small heterogeneous contractions, BinaryPlan and GroupedPlan prepare jobs once and run a whole group under one scheduler.
cpueinsum links no BLAS. The separate cpueinsum-blas crate can take over binary steps large enough to profit from a vendor GEMM (at least 215 multiply accumulates per GEMM) and leaves smaller steps on tcontract. It depends on cblas-sys only, so the final binary chooses the BLAS provider.
tcontract: binary contraction without transposes
tcontract computes
D = alpha * op(A) * op(B) + beta * op(C)
for strided operands of f32, f64, c32 and c64, with contracted, batch and free indices, repeated indices (diagonals), reductions and conjugation. Operands are never transposed or copied into a canonical layout first. It was previously named tprims-rs and includes kernels contributed from Lukas Devos's tensorprimitives work.
How a contraction runs
- Describe the problem. A
Problemis built either from aDotGeneral(contracting and batch axes, as in XLA) or from index labels, together with the layouts (dims and strides) of A, B and D. - Plan once.
Plan<T>::newvalidates the problem and chooses a strategy and a kernel. The plan is reusable for every call with the same layouts. - Execute.
execute_intotakes an execution context (Exec::serial()or a Rayon pool the host owns) and the views.contract_batchedruns many independent problems of one shape.
| Strategy | Used when | How |
|---|---|---|
| packed | general strides and index orders | a TBLIS-style block-scatter driver packs strided operands straight into microkernel panels, with cache blocking |
| faer | the problem fuses into one batched GEMM without copies | calls faer's matrix multiply directly on the views |
| elementwise | every index is a batch index | a single elementwise pass |
Microkernel families cover AVX2, AVX-512 and NEON register tiles, native complex kernels, and portable reference kernels; the planner resolves the family for the target and freezes the blocking in the plan.
D = A B with column-major 2 × 2 operands. Abridged from crates/tcontract/examples/readme.rs, which CI compiles and runs:
use strided_view::{StridedView, StridedViewMut};
use tcontract::api::{DType, DotGeneral, LayoutSpec, OperandSpec, Problem};
use tcontract::{Plan, PlanConfig};
use tcontract_exec::Exec;
let layout = |dims: &[usize], strides: &[isize]| {
Ok(OperandSpec::new(LayoutSpec::new(dims, strides, 0)?))
};
// contract axis 1 of A with axis 0 of B; no batch axes
let dot = DotGeneral::new(&[1], &[0], &[], &[]);
let problem = Problem::from_dot_general(
DType::F64,
layout(&[2, 2], &[1, 2])?,
layout(&[2, 2], &[1, 2])?,
layout(&[2, 2], &[1, 2])?,
&dot,
)?;
let plan = Plan::<f64>::new(&problem, &PlanConfig::default())?;
println!("strategy: {:?}", plan.report().algorithm);
plan.execute_into(&Exec::serial(), 1.0, &av, &bv, &mut dv)?;
The C ABI
tcontract builds into one shared library, libtcontract_capi, that implements TAPP, the community standard C interface for tensor products, declared by the pinned upstream headers. A C or C++ program that includes only <tapp.h> can create tensor descriptors, plan and execute a contraction serially. On top of the standard, operands can be passed as DLPack tensors, and a few named extensions expose executors for multithreaded runs. Because the ABI is a standard, a code written against TAPP can switch between implementations without source changes.
tlinalg: batched linear algebra
tlinalg-rs holds the factorizations that tensor algorithms reduce to once a tensor is unfolded into matrices. It is the layer tenferro's CPU linear algebra has been extracted into.
The batched interface
Every entry point is batched in the style of PyTorch: one call takes a strided descriptor of rank 2 + B with dimensions [rows, cols, b...] and processes all B batch items. The library owns the loop over the batch, so it can decide whether to run many small matrices in parallel or give all threads to one large matrix. Outputs are compact and column-major. When items fail, the reported error is deterministic: the failing item with the lowest index.
| Family | Includes |
|---|---|
| SVD | thin, full, values only |
| QR | QR, column-pivoted QR (rank revealing), compact Householder form |
| LU and solves | packed LU, factor once and solve, full-pivot LU, triangular solve |
| Cholesky | factorization and solves |
| Eigensolvers | Hermitian (eigh, eigvalsh) and general (eig, eigvals) |
Two sibling providers
tlinalg
Pure Rust, on tensor4all's fork of faer. Takes a Parallel token describing the threads it may use; with the default Auto policy the library decides between fanning out over batch items (for small matrices, up to 64 rows or columns) and letting faer parallelize inside one item.
tlinalg-blas
The same batched signatures on LAPACK and BLAS. The batch loop is serial and the vendor library owns its threading. Scratch comes from a host workspace queried once per call and reused for every item. The vendor is chosen by the final binary.
Neither provider depends on the other, and they share no trait crate. tenferro defines the interface it needs and adapts each provider to it. A parity suite runs every family through both providers for all four element types and several batch shapes (including transposed and broadcast batch strides) and compares the results in a gauge-aware way.
strided-rs: the shared vocabulary
strided-rs defines StridedView, StridedViewMut and the raw descriptors that every CPU layer passes across crate boundaries, plus fast permutation and elementwise kernels. Arbitrary strides are allowed, including negative and zero (broadcast).
Because these types cross crate boundaries, every crate in a build must resolve to the same strided-rs release, currently 0.4.8; two copies would produce two incompatible view types. To make this cheap for downstream users, the plan is to version the core types and traits separately from the permutation engine and stabilize them toward 1.0 (strided-rs#296). A longer term direction is to make strided-rs embeddable from other languages through its own C API (strided-rs#234).
The faer fork
The pure Rust dense kernels come from tensor4all/faer, a fork of faer developed independently and published as the t4a-faer crates. It is where workload specific changes are tried quickly: strided and batched entry points, uninitialized output destinations, caller-owned workspaces, an explicit thread policy, and numerical fixes such as the unpivoted QR fix that tlinalg relies on. Many of these are not in upstream faer. Every change is recorded in PATCHSET.md; the fork keeps upstream's license and attribution, and consumers pin a revision or a published t4a-faer version.
Execution contexts and hataori
Each layer has its own explicit context, and the contexts nest:
- tcontract and cpueinsum take an
Exec: serial, or a borrowed, owned or shared Rayon pool. Width is chosen from the amount of work, so a small contraction does not wake every thread. - tlinalg takes a
Paralleltoken with a thread budget; tlinalg-blas leaves threading to the vendor. - tenferro carries these inside its backend object, and tensor4all-rs wraps that in a
CpuExecutionContext.
Something has to own the processes and threads at the top. That is hataori: map and a dynamically scheduled pmap over serial, Rayon, MPI and hybrid execution, inspired by Distributed.jl. hataori splits the machine into ranks and thread domains and passes each task the pool it may use, which flows down through the contexts above. MPI can be linked at build time or loaded at run time (through rsmpi-rt), so a Rust extension can share the MPI library of a Julia or Python host. The synchronous API is usable now; an asynchronous runtime with futures, remote objects and migration is in development in the same repository.
For quantum chemistry and other host codes
Coupled cluster, integral transformations, density fitting and many-body perturbation theory spend their time in contractions of tensors whose index order rarely matches what GEMM wants. The usual workaround (permute, call GEMM, permute back) costs memory traffic and extra buffers. tcontract is built to avoid exactly that, in the same spirit as TBLIS, and is designed to be called from code that has its own tensor containers.
- From C, C++ or Fortran: link
libtcontract_capiand program against the TAPP headers. Your arrays stay where they are; descriptors carry the strides. Fortran codes can call the C ABI throughiso_c_binding, and column-major layouts are first class. - From Python: DLPack operands let arrays exported by NumPy, PyTorch, JAX or CuPy style libraries be described without copying, given a thin binding layer.
- From Rust: depend on
tcontractdirectly for binary contractions, oncpueinsumif you already have a contraction order for a multi-tensor expression, and ontlinalgfor batched factorizations of the resulting matrices. None of them require tenferro. - Threads: pass your own pool. Nothing in these libraries competes with OpenMP or MPI in your code behind your back.
None of tcontract, cpueinsum and tlinalg is on crates.io yet, and their interfaces may still change. Pin a git revision. If you are evaluating them for a production code, opening an issue with your typical tensor shapes is the most useful thing you can do: it feeds directly into the benchmark campaigns below.
Benchmarks
| Repository | What it measures |
|---|---|
| tenferro-benchmark | tenferro against PyTorch and JAX on CPU and GPU, per workload class |
| tcontract-benchmark | binary contractions across shapes and strides, keyed by commit and hardware profile |
| tlinalg-benchmark | batched factorizations, faer provider against LAPACK, keyed by commit and hardware profile |
| tensor4all-benchmark | tensor network level algorithms: QTT elementwise products, MPO MPO contraction |
Status and contributing
| Component | State |
|---|---|
| tenferro-rs | crates.io 0.7.1 main depends on the unpublished providers |
| cpueinsum, tcontract | git only early, interfaces settling |
| tlinalg, tlinalg-blas | git only extraction from tenferro in progress |
| strided-rs | crates.io 0.4.8 core versioning split planned |
| faer fork | crates.io t4a-faer 0.24.4 independent of upstream, no compatibility promise |
| computegraph | crates.io 0.1.0 |
| hataori | git only synchronous API usable, runtime in development |
All repositories accept issues and pull requests, including AI assisted contributions; each one documents its rules in AGENTS.md and its contributing guide. Good first contributions are benchmark cases from real applications, kernels for hardware we have not tuned for, and bindings for other languages.