tcontract is a CPU library for dense tensor contraction in Rust, with a C ABI. It
computes D = alpha * op(A) * op(B) + beta * op(C) for strided operands of
f32, f64, c32 and c64, including batch indices, repeated indices
(diagonals), reductions and conjugation, without transposing or copying the
operands: a packed, block-scatter driver (TBLIS-style) packs general strides
straight into microkernel panels, with faer as a strategy for problems that fuse
into one copy-free batched GEMM. Threads and scratch memory come from an
explicit execution context the host passes in; there is no ambient pool and no
environment variable that changes behavior.
The C ABI is the standard TAPP tensor
product interface, declared by the pinned upstream headers, plus DLPack
operands and a few named extensions. It builds into one shared library,
libtcontract_capi.
AI-assisted contributions are welcome; see AGENTS.md and REPOSITORY_RULES.md.
| Crate | Owns |
|---|---|
tcontract-exec |
Execution context: serial, or the host's borrowed/owned/shared-Arc Rayon pool (or created and joined through the C API); width chosen from work; reusable, wrapper-owned scratch. |
tcontract-kernel |
The kernel layer: packed formats, kernel-family descriptors and their registry, resolution with frozen blocking, packing and write-back, cache blocking, and every microkernel family (scalar, AVX2, AVX-512 and NEON register-tile kernels, portable reference kernels, native complex kernels). |
tcontract |
Binary contraction over one validated Problem (from DotGeneral or from labels): Plan<T>, PlanConfig, the packed, faer and elementwise strategies chosen by the planner, contract_batched, the labels-based unary update (unary), shared errors, and the backend trait. |
tcontract-capi |
libtcontract_capi (cdylib, staticlib, rlib): the TAPP C ABI over tcontract, DLPack operands, executors. The only crate that produces a library for C. |
tcontract-testkit |
Test support: an independent label oracle, seeded fixtures, a naive second backend, downstream-kernel fixtures. Not a production fallback. |
tcontract-bench (benchmarks/) |
Benchmark harness (tcbench, C and Rust ABI rows); not part of the library. |
strided-rs is an external dependency pinned to a published release; Cargo.toml
carries the exact requirement, so this document does not repeat the number.
Consumers must use the same source revision to share StridedView types.
Arrows mean "depends on" and are drawn from cargo tree (normal and build
dependencies; tcontract-testkit is a dev-dependency of tcontract and
tcontract-capi, which creates no cycle because it is only linked into tests).
flowchart TB
CAPI["<b>tcontract-capi</b><br/>libtcontract_capi: TAPP, DLPack, executors"]
CT["<b>tcontract</b><br/>Problem, Plan, strategies"]
GK["<b>tcontract-kernel</b><br/>families, registry, packing, microkernels"]
EX["<b>tcontract-exec</b><br/>execution context"]
FA["faer"]
ST["strided-rs (external)"]
TK["tcontract-testkit"]
CAPI --> CT
CAPI --> EX
CT --> GK
CT --> EX
CT --> FA
CT --> ST
TK --> CT
TK --> GK
TK --> EX
classDef own fill:#e8f2ff,stroke:#2563a6,color:#132f50
classDef ext fill:#edf0f4,stroke:#536477,color:#233244
class CAPI,CT,GK,EX,TK own
class FA,ST ext
Not in scope: N-ary einsum and contraction ordering (they sit above, calling
tcontract per binary step), dense linear algebra, BLAS entry points
(GEMM is a contraction), AD and GPU backends.
crates/tcontract/examples/readme.rs (compiled and run in CI; a test
checks that this text is that file):
//! The minimal example of the repository README (a test checks that the README
//! shows this file verbatim): `D = A * B` through `DotGeneral`, `Plan`, execute.
use strided_view::{StridedView, StridedViewMut};
use tcontract::api::{DType, DotGeneral, LayoutSpec, OperandSpec, Problem};
use tcontract::{Plan, PlanConfig, Result};
use tcontract_exec::Exec;
fn main() -> Result<()> {
// D[i, k] = sum_j A[i, j] * B[j, k], all 2 x 2 and column-major.
let layout = |dims: &[usize], strides: &[isize]| -> Result<OperandSpec> {
Ok(OperandSpec::new(LayoutSpec::new(dims, strides, 0)?))
};
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,
)?;
// Planning chooses the strategy and kernel once; the plan is reusable.
let plan = Plan::<f64>::new(&problem, &PlanConfig::default())?;
println!("strategy: {:?}", plan.report().algorithm);
let a = [1.0, 2.0, 3.0, 4.0];
let b = [0.0, 1.0, 1.0, 0.0]; // swaps the columns of A
let mut d = [0.0; 4];
let av = StridedView::new(&a, &[2, 2], &[1, 2], 0).map_err(tcontract::Error::backend)?;
let bv = StridedView::new(&b, &[2, 2], &[1, 2], 0).map_err(tcontract::Error::backend)?;
let mut dv =
StridedViewMut::new(&mut d, &[2, 2], &[1, 2], 0).map_err(tcontract::Error::backend)?;
plan.execute_into(&Exec::serial(), 1.0, &av, &bv, &mut dv)?;
assert_eq!(d, [3.0, 4.0, 1.0, 2.0]);
println!("D = {d:?}");
Ok(())
}crates/tcontract-capi/tests/c/standard_consumer.c: includes only the pinned
upstream header <tapp.h>, compiles as C and as C++, and links -ltcontract_capi
(compiled and run in CI, with the same verbatim check):
/* A consumer that includes only the pinned upstream TAPP headers (no tcontract
header) and performs a serial contraction D = A * B through libtcontract_capi:
the standard ABI is enough for serial use. */
#include <stdio.h>
#include <tapp.h>
#define CHECK(cond, msg) do { if (!(cond)) { fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); return 1; } } while (0)
int main(void) {
TAPP_handle handle;
TAPP_executor exec;
CHECK(TAPP_check_success(TAPP_create_handle(&handle)), "create handle");
CHECK(TAPP_check_success(TAPP_create_executor(&exec)), "create executor");
/* A: 2x3, B: 3x2, D: 2x2, all column-major (strides in elements). */
int64_t ea[2] = {2, 3}, sa[2] = {1, 2};
int64_t eb[2] = {3, 2}, sb[2] = {1, 3};
int64_t ed[2] = {2, 2}, sd[2] = {1, 2};
TAPP_tensor_info ia, ib, id;
CHECK(TAPP_check_success(TAPP_create_tensor_info(&ia, TAPP_F64, 2, ea, sa)), "info A");
CHECK(TAPP_check_success(TAPP_create_tensor_info(&ib, TAPP_F64, 2, eb, sb)), "info B");
CHECK(TAPP_check_success(TAPP_create_tensor_info(&id, TAPP_F64, 2, ed, sd)), "info D");
int64_t la[2] = {'i', 'k'}, lb[2] = {'k', 'j'}, ld[2] = {'i', 'j'};
TAPP_tensor_product plan;
CHECK(TAPP_check_success(TAPP_create_tensor_product(
&plan, handle, TAPP_IDENTITY, ia, la, TAPP_IDENTITY, ib, lb,
TAPP_IDENTITY, id, ld, TAPP_IDENTITY, id, ld, TAPP_DEFAULT_PREC)),
"create product");
double a[6] = {1, 2, 3, 4, 5, 6}; /* A[i + 2k] */
double b[6] = {1, 2, 3, 4, 5, 6}; /* B[k + 3j] */
double d[4] = {-1, -1, -1, -1};
double alpha = 1.0, beta = 0.0;
TAPP_status status = -1;
CHECK(TAPP_check_success(TAPP_execute_product(plan, exec, &status, &alpha, a, b, &beta, TAPP_IN_PLACE, d)),
"execute");
CHECK(status == 0, "status written");
/* D[i,j] = sum_k A[i,k] B[k,j] */
CHECK(d[0] == 22 && d[1] == 28 && d[2] == 49 && d[3] == 64, "values");
/* The default executor 0 is the same serial executor. */
d[0] = d[1] = d[2] = d[3] = -1;
CHECK(TAPP_check_success(TAPP_execute_product(plan, 0, NULL, &alpha, a, b, &beta, TAPP_IN_PLACE, d)),
"execute on executor 0");
CHECK(d[0] == 22 && d[3] == 64, "values on executor 0");
CHECK(TAPP_check_success(TAPP_destroy_status(status)), "destroy status");
CHECK(TAPP_check_success(TAPP_destroy_tensor_product(plan)), "destroy product");
CHECK(TAPP_check_success(TAPP_destroy_tensor_info(ia)), "destroy A");
CHECK(TAPP_check_success(TAPP_destroy_tensor_info(ib)), "destroy B");
CHECK(TAPP_check_success(TAPP_destroy_tensor_info(id)), "destroy D");
CHECK(TAPP_check_success(TAPP_destroy_executor(exec)), "destroy executor");
CHECK(TAPP_check_success(TAPP_destroy_handle(handle)), "destroy handle");
printf("standard_consumer ok\n");
return 0;
}Build the library with cargo build --release -p tcontract-capi, install it with
crates/tcontract-capi/install.sh --prefix=DIR (headers, library, tcontract_capi.pc),
and see examples/c-consumer for a CMake
project that consumes it three ways. The extension headers are
<tcontract/tcontract.h> (DLPack operands, status codes, tcontract_abi_version) and
<tcontract/tapp_ext.h> (Rayon executors: one tcontract creates and joins for a
host without a pool, or a tcontract_pool the C host owns and lends to an
executor). Executor handles are valid for every plan of the library; handles of
another TAPP provider cannot be mixed in.
The planner picks the strategy (plan.report().algorithm says which) and, for
the packed driver, the highest-priority available kernel family. A caller can
override it through PlanConfig: kernel (KernelChoice::Id, exact, no
silent fallback), isa (KernelForce), method (complex method), blocking,
partition and the other fields. Family ids have the form
{isa}.{dtype}.{scheme}.{MR}x{NR}, for example avx2.f64.real.8x6,
avx512.c32.planar.32x6 or ref.f64.real.4x4, where isa is ref, avx2,
avx512 or neon; tcontract_kernel::list_kernels lists what this CPU offers
and the x86-64 manifest is pinned by a snapshot test. Downstream crates can
supply their own kernels through a caller-scoped catalog
(Plan::new_with_selector). The full old-to-new id table is in the
migration guide.
Rust 1.89 or newer. CI runs on Linux (x86-64) and macOS (arm64); the aarch64
kernels are also checked with cargo check --target aarch64-apple-darwin.
cargo build -p tcontract-capi # libtcontract_capi; the C tests link it
cargo test --workspace
cargo run --release -p tcontract --example readme
python3 scripts/check-agent-skills.pyThe full local gate is in AGENTS.md.
Phase 1 is done: the contraction stack above, the TAPP C ABI, and the
benchmark harness. faer is an internal strategy, not API. Phase 2 optimizes the
packed driver until it can replace faer, leaving one route for contractions
with a K role that differs only by kernel family (all-batch problems stay
delegated to strided-rs); hadamard.json must not
regress. There is no stable API or ABI yet and no crate has been published.
Performance claims need recorded measurements (PERFORMANCE_TIPS.md,
docs/experiments.md). The source integration that
produced this layout is described in docs/migration-2026-10.md
and issue #37.
The packed driver, its planning and the microkernels originate in tensorprimitives-rs by Lukas Devos, imported with history and authorship; the faer strategy follows the permute-plus-batched-GEMM contraction of tenferro-rs. The old tree is archived in docs/archive/tensorprimitives. Licenses, imported notices (TAPP headers BSD-3-Clause, DLPack Apache-2.0) and the per-project record are in docs/provenance.md. The code is licensed MIT OR Apache-2.0 (LICENSE-MIT, LICENSE-APACHE).
Design documents: design principles, architecture, research map, decision log.
tcontract builds on faer by Sarah Quiñones El Kazdadi. If you use tcontract in published work, please also cite the faer paper:
S. Q. El Kazdadi, "faer: A linear algebra library for the Rust programming language", Journal of Open Source Software 11(123), 6099 (2026), doi:10.21105/joss.06099.
@article{Kazdadi2026,
doi = {10.21105/joss.06099},
url = {https://doi.org/10.21105/joss.06099},
year = {2026},
publisher = {The Open Journal},
volume = {11},
number = {123},
pages = {6099},
author = {Kazdadi, Sarah Quiñones El},
title = {faer: A linear algebra library for the Rust programming language},
journal = {Journal of Open Source Software}
}