tenferro 解决什么问题
主线位置:三条主线的外面。本页回答 tenferro 为什么要有四种张量、三种执行方式,以及它和数组库、深度学习框架各差在哪;三条主线都从这张图出发。
flowchart TB user["用户代码"] --> typed["TypedTensor"] user --> dyn["Tensor"] user --> eager["EagerTensor"] user --> traced["TracedTensor"] typed --> eng["借出的 engine"] dyn --> eng eager --> ses["with_eager_session"] ses --> eng traced --> comp["GraphCompiler"] comp --> rt["Runtime prepare"] rt -->|run| eng eng --> cpu["CpuEngine"] eng --> gpu["CudaEngine"]
tenferro 怎么描述自己
README 的粗体第一句:「A Rust-native tensor computation stack with opt-in autodiff for scientific workloads.」接着给出位置:「tenferro-rs sits between low-level array crates and full deep-learning frameworks for Rust scientific code that needs column-major storage, LAPACK/Fortran/Julia-friendly layouts, dynamic-shape traced programs, explicit device control, and operation families whose AD rules can live outside the core tensor type.」
它为什么存在,README 的「Project」一节说得很直接:「Moving tensor-network engines from Julia to Rust exposed a missing layer: a scientific-computing tensor stack between ndarray, faer, and the deep-learning frameworks — column-major, dynamic-shape, autodiff-capable, and extensible.」它是 tensor4all 这套科学计算栈的引擎,在 tensor4all-rs 里自己用。README 同时提醒:「tenferro-rs is a pre-1.0 experimental research platform; public APIs may change substantially, including across 0.x releases.」从 v0.1.0(2026-06-23)到 v0.8.0(2026-10-30)四个多月发了 9 个版本,0.8.0 把执行模型整个换了一遍;本卷只对 0.8.0 负责。
README 的介绍段落说直接 API「run through a chosen CPU, CUDA, or experimental WebGPU backend」,「Which API Should I Use?」表里也还有「CUDA or experimental WebGPU execution」一行。tag 上的代码已经没有 WebGPU:webgpu feature 和整条 WebGPU / Metal 路径在 0.8.0 删掉了(见怎么读这一卷的 feature 一节)。GPU 只剩 CUDA。
「opt-in autodiff」在 crate 上看得见:只做直接计算,依赖 tenferro-runtime 和 tenferro-cpu 就够;要求导再加 tenferro-ad;einsum、线性代数、FFT 的 AD 规则在各自 crate 的 autodiff feature 后面(见怎么读这一卷的 feature 表)。
「Is tenferro for You?」四条落在哪
| README 的说法 | 落在哪 | 讲它的页 |
|---|---|---|
| 「Shapes are only known at runtime」:traced 程序编译一次,秩、截断阈值、迭代次数在执行时才定 | TracedTensor 的形状提示可以是符号维度(sym_shape),设计见 dynamic-symbolic-shapes.md。用 from_vec_col_major 建的输入是具体形状,形状会进程序:同一个 y = sum(x*x) 的梯度程序,n = 3 和 n = 1000000 编出来的 semantic_fingerprint 不同([145, 199, 23, ...] 和 [17, 151, 37, ...]) | TracedTensor、编译 |
「You want autodiff in Rust without a Python runtime」:eager 的 backward(),eager 和 traced 都有 VJP / JVP,traced 上能组合出 HVP | tenferro-ad:EagerTensor::backward、TracedTensorAdExt | 05 自动微分 |
| 「Your data lives in the column-major world」:和 Fortran、Julia、MATLAB、LAPACK 一样的存储,行主序数据用步长视图桥接 | from_vec_col_major;视图在 strided-rs;Tensor::from_vec_row_major 重排一次 | 列主序与视图 |
| 「You need operations the core does not ship」:运算族和它的 AD 规则在核心之外,外部 crate 可以加,甚至换一种代数(热带半环的例子) | tenferro-internal-extension-macros 的注册宏;einsum 的 semantic_ad_rules 就是这样挂上去的;ext/tropical | 扩展算子、semantic AD 与扩展 |
README 还说它刻意建在现成的数值生态上,而不是重写一遍:「faer for dense linear algebra, a native CUDA backend for GPU kernels, omeco for contraction ordering」,加上 num-traits / num-complex 做泛型数值。实际上 faer 是 tensor4all 的分叉 t4a-faer,而且 tenferro 和 faer 之间还隔着 tcontract、cpueinsum、tlinalg 这几层单独发布的 provider。这几个库各管什么见源码地图。
四种张量 API
README 的「Which API Should I Use?」一张表,上游的 core-concepts.md 把它拆成「The Three Axes」:数据层(值是什么)、执行模型(什么时候算)、engine(在哪算、谁持有设备,CpuEngine 或 CudaEngine)。四种张量是前两根轴的组合:
| 类型 | dtype | 什么时候算 | 自动微分 | 定义在 |
|---|---|---|---|---|
TypedTensor<T, R, D> | 编译期的 T;R 是秩,默认 DynRank;D 是表示(Host、Gpu,默认 Dynamic) | 立即,在借来的 engine 上 | 无 | tenferro-tensor |
Tensor | 运行期的 DType | 立即,在借来的 engine 上 | 无 | tenferro-tensor |
EagerTensor | 运行期 | 立即,在 with_eager_session 借来的 engine 上 | backward(&mut engine);EagerRuntime 上的 grad / vjp / jvp | tenferro-ad |
TracedTensor | 运行期 | 编译、prepare、run 时 | 图变换 grad / vjp / jvp,可以嵌套 | tenferro-runtime,AD 在 tenferro-ad |
为什么不是一种:
TypedTensor和Tensor是同一种存储的两个视角,From<TypedTensor<f64>> for Tensor等转换是现成的。前者让编译器检查标量类型,as_slice()直接给&[f64];后者在 dtype 要到运行期才知道时用,比如读一个文件、或者在 CPU / GPU 之间分派。0.8.0 删掉了单独的HostTensor,主机上的张量就是TypedTensor<T, DynRank, Host>,第三个类型参数D在数据模型里讲。EagerTensor给想要 PyTorch 式写法的人:每个运算立即出数,标量损失上backward()。它和直接运算的差别不在算法,在于它带着运行时和记录。0.8.0 的EagerRuntime不再持有后端,运算写在runtime.with_eager_session(&mut engine, |s| s.mul(&x, &x))的闭包里。TracedTensor给 JAX 式的工作流:先记图,求导是对图的变换,产物还是图,可以再求导(HVP),编译、准备一次,反复执行。代价是要先编译,要有一个注册了 engine 描述的Runtime。
上游的建议是「Use the smallest layer that matches the job. AD is optional, and many projects should never leave the concrete tensor APIs.」值可以在几层之间直接流动:tenferro_ad::Tensor 就是 tenferro_runtime::Tensor,variable_from 收的就是它。四种张量最后都落在同一种 engine 上:直接运算把 &mut engine 传给每个调用,eager 把它交给 with_eager_session,traced 把它交给 runtime.run。engine 持有什么,见怎么读这一卷的第 3 条约定。
同一个错误在四层里表现不一样。f64 和 f32 混在一起算(four_apis,release,CpuEngine::serial()):
let a = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0])?;
let b = Tensor::from_vec_col_major(vec![2], vec![1.0_f32, 2.0])?;
a.add(&b, &mut engine); // Tensor
rt.with_eager_session(&mut engine, |s| s.mul(&ea, &eb))?; // EagerTensor
let tc = (&ta * &tb)?; // TracedTensor
Tensor f64 + f32: Err(Validation { op: "add", source: DTypeMismatch { expected: F64, actual: F32 } })
Tensor matmul f64 x f32: Err(Validation { op: "dot_general", source: DTypeMismatch { expected: F64, actual: F32 } })
display: dot_general: dtype mismatch: expected F64, actual F32
Tensor as_slice::<f32> on f64: Err(Validation { op: "Tensor::as_slice", source: DTypeMismatch { expected: F32, actual: F64 } })
Eager f64 * f32: dtype F64 value [1.0, 4.0]
Traced f64 * f32: Ok(F64)
traced op: Mul
traced op: Convert { from: F32, to: F64 }
Traced grad: Ok(F64)
Traced run: dtype F64 value [5.0]
TypedTensor<f64> 和 TypedTensor<f32> 根本加不到一起,编译器就拦住了;Tensor 在运行期报 DTypeMismatch;EagerTensor 和 TracedTensor 却把 f32 提升成 f64 算完了,traced 图里多了一个 Convert { from: F32, to: F64 }。
问: 同样是运行期 dtype,为什么 Tensor 拒绝 f64 + f32,EagerTensor 和 TracedTensor 却自动提升?
答: 直接运算是一层很薄的东西:Tensor::add 把两个输入原样交给 engine,CPU 内核要求两边同 dtype,不同就报错,不替你做任何隐式转换。eager 和 traced 多了一层「语义」:上游设计文档 eager-semantic-promotion-recording.md 写着「Eager primal execution promotes compatible mixed-dtype inputs immediately before backend dispatch」,又说「Normal traced construction already inserts explicit Convert operations before promoting binary operations」。显式的 Convert 是给求导用的:反向时梯度要沿着它转回 f32,AD 规则看到的每个二元运算都是同 dtype 的。上面的输出里 traced 图的那条 Convert 就是它。提升按什么规则走(哪些组合允许),见 tensor-operations.md 的「dtype-promotion lattice」和本卷的数据模型。
和别的库比
只写能核实的事实:README 原文、各库在本机缓存里的源码、本卷和 burn 卷、candle 卷里的实测。
| tenferro 0.8.0 | ndarray 0.16 | t4a-faer 0.27 | burn 0.22.1 | candle 0.11.0 | |
|---|---|---|---|---|---|
| 自我定位 | 「tensor computation stack with opt-in autodiff for scientific workloads」 | N 维数组 | 稠密线性代数(「Temporary tensor4all fork of faer」) | 「both a tensor library and a deep learning framework」 | 「make serverless inference possible」 |
| 默认内存顺序 | 列主序,from_vec_col_major | 行主序(from_shape_vec 的步长 [3, 1]),可选 Fortran 顺序 | 列主序矩阵(行步长 1) | 本卷不核实 | 行主序(见 candle 卷布局与视图) |
| 自动微分 | eager 和 traced 两套,可选 | 无 | 无 | Autodiff<B> 装饰器 | 内建在 Tensor 里 |
| 选后端 | 调用方持有的 engine,&mut 借给每个运算;traced 由 Runtime::prepare 按 engine.target() 准备 | 只有 CPU | 只有 CPU | Device 值,Dispatch 在运行期分派 | Device / Storage 三变体枚举 |
| GPU | CUDA(t4a-cuda-runtime / -kernels 的 kernel-catalogue engine) | 无 | 无 | cubecl 的各运行时 | 手写 .cu、.metal |
| einsum 和收缩规划 | tenferro-einsum,计算在 cpueinsum,退火规划用 omeco | 无(另有 crate) | 无 | 本卷不核实 | 本卷不核实 |
layouts 实验(release)里三者对同一个 2×3 矩阵,faer 用的是 tenferro_cpu::faer 再导出的 t4a-faer:
ndarray from_shape_vec: strides [3, 1], a[0,1] = 2
faer Mat: row_stride 1, col_stride 8
tenferro from_vec_col_major: data [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
faer 的列步长是 8 而不是 2:Mat 按 SIMD 对齐给每一列留了空位。tenferro 的张量是紧凑的列主序,交给 faer 时按视图传步长,不用拷贝(见一次 matmul)。
几处说明:
- ndarray、faer:README 的「Where else to look」说,没有 autodiff 和 traced 图的一般 N 维数组用 ndarray,纯稠密线性代数直接用 faer。faer 本身就在 tenferro 里面:CPU 上的 GEMM 经 tcontract、分解经 tlinalg 最终都交给它,tenferro 在外面加的是张量形状、engine、线程池和 AD。
- burn、candle:README 说深度学习用它们。tenferro 不带神经网络层、优化器和模型格式,0.8.0 倒是加了
sigmoid、gelu、softmax、layer_norm等运算(见激活、softmax 与归一化);它带了深度学习框架通常不带的东西:列主序、SVD / QR 等分解的 AD 规则、einsum 的收缩规划、可以从外部加的运算族。burn 那一侧见 burn 解决什么问题,candle 见 candle 解决什么问题;三者的完整对照放在本卷的 08 设计取舍。 - JAX、PyTorch:README 说宿主语言是 Python 的话它们是自然的选择,tenferro 给想在 Rust 里原生用这种栈的项目。README 的致谢一节写明了借鉴:traced 图的编译和「trace-then-transform」的自动微分、
dot_general原语和 StableHLO 式的运算词汇、einsum 收缩路径的格式来自 JAX;eager 的backward()沿用 PyTorch 的 API 形状,PyTorch 手写的求导公式是 AD 规则的对照基准,tensor-ad-oracles的参考数据由 PyTorch 生成。JAX 和 PyTorch 自身的实现本卷不做断言。
tenferro-runtime 和 tenferro-cpu(默认 feature),做一次 2×2 的 f32 matmul,release 二进制多大?程序就是这些:let a = TypedTensor::<f32>::from_vec_col_major(vec![2, 2], vec![0.0, 2.0, 1.0, 3.0])?;
let mut engine = CpuEngine::serial();
let c = a.matmul(&a, &mut engine)?;
println!("{:?}", c.host_data()?);
candle 0.11.0 做同样的事是 0.92 MB(见 candle 解决什么问题)。先猜一个数,再看文末。
动脑筋答案
30.3 MB(30258240 字节),strip 之后 19.7 MB(19714976 字节),是 candle 的 30 多倍,也比 burn 0.22.1 只用 CPU 的 6.7 MB(见 burn 卷的设计取舍)大得多。intro-tiny,release,输出 [2.0, 6.0, 3.0, 11.0]。
size -m看到__text段 16.55 MB,__eh_frame1.49 MB,__gcc_except_tab0.73 MB:几乎都是代码和展开表,没有嵌入的数据。- 按符号名粗分(
nm -n相邻符号地址之差算作符号大小,每个符号算给它的修饰名里最先出现的 crate 名),strided_basic7.50 MB、rayon_core1.62 MB、threadteam1.44 MB、nano_gemm1.17 MB、tenferro_cpu1.07 MB,tcontract和tcontract_kernel各约 0.3 MB。rayon_core名下的多半是带着上层闭包的泛型实例。 - 也就是说,大小来自 CPU 一侧按 dtype、按布局、按线程方式展开的内核,而不是 GPU:这个程序只用了一个串行 engine,依赖图里没有任何 GPU 包,但 strided-rs 的并行遍历和 threadteam 的 team 代码照样编了进来。tenferro 的二进制大,是它为「什么 dtype、什么步长都能直接算」付的代价;这个取舍在 08 设计取舍 里和 candle 的手写内核、burn 的运行期编译放在一起比。
小结
- tenferro 是给科学计算的张量栈:列主序、调用方持有的 engine、动态形状的 traced 程序、AD 规则可以放在核心之外的运算族;自动微分可选。README 里的 WebGPU 说法已经过时,GPU 只有 CUDA。
- 四种张量是两根轴的组合:
TypedTensor/Tensor管静态或动态 dtype,直接 / eager / traced 管什么时候算、能不能求导;三种执行方式最后都借同一种 engine。 - 直接运算不做隐式转换,f64 + f32 报
DTypeMismatch;eager 和 traced 会提升,traced 图里插一个显式的Convert。 - 和 ndarray、faer 比,多了 AD、GPU、einsum 规划,而 faer(t4a-faer)本身就在里面;和 burn、candle 比,没有神经网络层,多了列主序、分解的 AD 规则和可扩展的运算族。
- 只做一次 CPU matmul 的二进制 30.3 MB,主要是 strided-rs、threadteam 和 GEMM 微内核展开出来的代码。