Rust in Everything
基于 tenferro-runtime 0.8.0

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 上能组合出 HVPtenferro-ad:EagerTensor::backward、TracedTensorAdExt05 自动微分
「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 / jvptenferro-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 }。

📝NOTE
没有蠢问题

问: 同样是运行期 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.0ndarray 0.16t4a-faer 0.27burn 0.22.1candle 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只有 CPUDevice 值,Dispatch 在运行期分派Device / Storage 三变体枚举
GPUCUDA(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 自身的实现本卷不做断言。
❗IMPORTANT
动脑筋:只依赖 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_frame 1.49 MB,__gcc_except_tab 0.73 MB:几乎都是代码和展开表,没有嵌入的数据。
  • 按符号名粗分(nm -n 相邻符号地址之差算作符号大小,每个符号算给它的修饰名里最先出现的 crate 名),strided_basic 7.50 MB、rayon_core 1.62 MB、threadteam 1.44 MB、nano_gemm 1.17 MB、tenferro_cpu 1.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 微内核展开出来的代码。

💬 关联讨论

发起讨论
加载中...