Rust in Everything
基于 tenferro-runtime 0.8.0

怎么读这一卷

主线位置:三条主线和 einsum 线的总览。本页把每条线拆成几步,标出每一步落在哪个函数上、哪一页讲它;再逐条核实 README 里「写代码之前必须知道」的五条约定;最后讲 Cargo feature 怎么决定编进来哪个 CPU provider 和哪个 GPU 后端,以及为什么有几个 feature 必须在所有 crate 上一致。「一次 matmul」和「一次 grad」用实验把直接计算主线和 traced 主线各走一遍。

flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| use1["五条约定 再到 01"]
  q -->|读懂实现| all[00 到 11 按顺序]
  q -->|只关心求导| ad["一次 grad 再到 04 05"]
  q -->|只关心 einsum| es["06 einsum 再到 11"]

锁定版本

项本卷说明
tenferro-rs 工作区0.8.0,tag v0.8.0,commit a9f494cc(2026-10-30)[workspace.package] version = "0.8.0",rust-version = "1.96",edition 2021;16 个 crate 全部发布,同一个版本号
上一版0.7.1(8a1839fe,2026-09-23)本地仓库的 tag 从 v0.1.0(2026-06-23)到 v0.8.0 共 9 个
tcontract、tcontract-exec0.9.0(9f793ede)根 Cargo.toml 写 tcontract-exec = "0.9.0";tcontract 由 cpueinsum 带进来
tcontract-kernel0.3.0(3239c86d)由 tcontract 带进来
cpueinsum0.9.0(124aa12d)tenferro-cpu、tenferro-einsum 的直接依赖
tlinalg0.8.0(53d1f0d9)tenferro-linalg 的直接依赖
threadteam0.2.0(3f95f617)tenferro-cpu 再导出 threadteam、Pool、PoolBuilder
strided-rsstrided-kernel / -fused / -basic / -nn 0.9.0(6bdf8e75);strided-view 0.7.1、strided-perm 0.9.1(7053f9d1);strided-traits 0.7.0根 Cargo.toml 是普通的 caret 要求(strided-view = "0.7.0"、strided-perm = "0.9.0"),注释说同一兼容线内 Cargo 会统一成一个版本;仓库的 Cargo.lock 锁的是 0.7.0 / 0.9.0,新项目解析到当天晚一个提交发布的 0.7.1 / 0.9.1
computegraph、omeco0.1.0(594cdcec)、0.2.6(6c83996f)和 0.7.1 时相同;computegraph 现在是 "0.1.0",不再有 git rev
t4a-faer0.27.0(81f3b1d3)faer = { package = "t4a-faer", version = "0.27.0", ... }:依赖键还叫 faer,指向的是 tensor4all 的分叉,上游 faer 的类型对不上
实验依赖里的 rayon / rustfft / elem-simd-math1.12.0 / 6.4.1 / 0.1.2锁文件里实际解析到的版本;rayon 由 threadteam、t4a-faer、gemm-common 和 omeco 依赖,tenferro 自己不直接依赖它

源码链接一律指向 tag 和上面的 commit:https://github.com/tensor4all/tenferro-rs/blob/v0.8.0/...。crates.io 上的 tenferro-*-0.8.0 包和 tag 是同一份代码(包里的 .cargo_vcs_info.json 写的就是 a9f494cc)。仓库的合并多是 squash,提交标题末尾的 (#NNNN) 就是 PR 号,本卷的「冷知识」都从这里查。仓库的 docs/ 下有大量上游设计文档(architecture/、design/、spec/、guides/),本卷把它们当资料读,引用之前在源码或实验里核实;0.8.0 的不少文档还留着 WebGPU、CubeCL 和 HostTensor 的说法,对不上的地方会写出来。

实测平台是 M2 Max、macOS 27、rustc 1.99.0,只有 CPU。CUDA 后端和 tenferro-xla 只读源码。

三条主线和 einsum 线

每页开头都会标出本页在哪条线上。

直接计算主线:一次 a.matmul(&b, &mut engine)

flowchart TB
  mk["from_vec_col_major"] --> eng["CpuEngine serial 或 new"]
  eng --> mm["matmul 变 dot_general"]
  mm --> ce["cpueinsum 二元计划"]
  ce --> tc["tcontract 选策略"]
  tc --> faer["faer matmul"]
  faer --> read["host_data 借出"]
步骤源码讲它的页
1. TypedTensor::from_vec_col_major 检查形状和长度,把 Vec 当成列主序收下,不重排tenferro-tensor/src/types.rs数据模型、列主序与视图
2. 调用方建 engine:CpuEngine::serial 没有线程池;CpuEngine::new 收下调用方用 Pool::builder 建好的 threadteam 池,两个以上 worker 时把池换成一个预留好的 team。tenferro 自己不建线程,也不读 RAYON_NUM_THREADStenferro-cpu/src/engine.rs、contraction.rs,threadteamCpuEngine 与线程池
3. TypedTensorSessionOpsExt 的 matmul 用 matmul_config_for_shapes 算出 DotGeneralConfig,调 engine.dot_general_read:matmul 只是 dot_general 的一个特例。trait 名和参数名还叫 session,类型已经是 &mut dyn Enginetenferro-runtime/src/typed_tensor.rs一次 matmul
4. CpuEngine::dot_general_read → execute_dot_allocated → contraction::dot_fresh:在调用线程上分配未初始化的输出,从 engine 的计划缓存取一个二元收缩计划exec_session.rs、contraction.rs一次 matmul、CpuEngine 与线程池
5. cpueinsum 的 BinaryPlan::execute_uninit_slices 交给 tcontract;tcontract 的 Plan::build 在建计划时选策略,一个不用拷贝就能并成一次 GEMM 的 f64 收缩选 faer;FaerPlan::run 经 with_faer_par 拿到 Par::Team 或 Par::Seq,调 faer 的 matmul_with_conjcpueinsum、tcontract、tcontract-execprovider
6. host_data 借出主机上的 &[T],不拷贝tenferro-tensor/src/types.rs存储与所有权

第 2 步和第 4 步要先记住:engine 是调用方手里的一个普通值,每个运算以 &mut 借它,运算从调用线程开始执行;只有 provider 决定把活分给多个线程时,才用到 engine 里预留的 team(一次 matmul 里 lldb 停在 thread #1,也就是主线程)。

traced 主线:一次 y.grad(&x) 再执行

flowchart TB
  tt["TracedTensor"] --> ops["运算只记图"]
  ops --> gr["grad 变换出新图"]
  gr --> cp["GraphCompiler compile"]
  cp --> rb["Runtime builder 注册"]
  rb --> pr["prepare 按 target"]
  pr --> run["run 借 engine"]
  run --> out["into_tensor"]
步骤源码讲它的页
1. TracedTensor 是图里的一个值:一个 Arc<Graph<StdTensorOp>> 加上值的编号、dtype、秩和形状提示;from_vec_col_major 建一个带数据的输入tenferro-runtime/src/traced.rsTracedTensor
2. impl Mul for &TracedTensor、reduce_sum 不算任何东西,只建一个新的图片段,父片段用 Arc 指回去;形状和 dtype 在这里检查,所以返回 Resulttraced.rs、computegraph 的 Graphcomputegraph、TracedTensor
3. TracedTensorAdExt::grad(tenferro-ad)按 AD 规则生成一张新图,结果还是一个 TracedTensor;vjp、jvp 同理tenferro-ad/src/traced.rstraced AD、primitive AD
4. GraphCompiler::compile 把各个片段解析、物化并去重,冻结成 CompiledGraph,里面是与后端无关的 SemanticProgramtenferro-runtime/src/graph/、program/编译
5. Runtime::builder,register_engine 收下 tenferro_cpu::runtime_engine_registration 给出的描述,build 出一个不可变的 Runtime。Runtime 不存任何 engineruntime/snapshot.rs、tenferro-cpu/src/runtime_adapter.rs执行与缓存
6. Runtime::prepare 按输入签名和 ExecutionEngine::target 给出的目标描述准备程序;Runtime::run 借调用方的 engine 执行,返回 Vec<Output>;Output::into_tensor 转成 Tensorruntime/snapshot.rs、runtime/engine.rs执行与缓存

和 JAX 一样,求导是对图的变换,产物还是图;要数值就得编译、准备、执行。各步的耗时见一次 grad。

eager 主线:一次 loss.backward(&mut engine)

flowchart TB
  er["EagerRuntime new"] --> var["variable_from"]
  var --> ses["with_eager_session"]
  ses --> op["运算立即执行并记录"]
  op --> bw["backward 借 engine"]
  bw --> g["grad 累加进槽"]
  g --> cl["clear_grad"]
步骤源码讲它的页
1. EagerRuntime::new 返回 Arc<EagerRuntime>,文档写着「The context stores no engine: every operation runs on the engine the caller passes in.」tenferro-ad/src/eager.rseager AD
2. variable_from 把一个 Tensor 包成需要梯度的 EagerTensoreager.rseager AD
3. with_eager_session 把运行时和借来的 engine 配成一个 EagerSession 交给闭包;EagerSession::mul 立即执行,输入里有被追踪的变量时同时把这一步记下来eager.rseager AD、primitive AD
4. backward 只接受标量,也要借一个 engineeager.rseager AD
5. grad 读梯度槽;多次 backward 会累加,clear_grad 清掉。不想动槽时用函数式的 EagerRuntime::gradeager.rseager AD

einsum 线

步骤源码讲它的页
1. parse_einsum_notation 解析成 EinsumNotation,... 在知道输入的秩之后展开成整数标签;带括号的写法走 NestedEinsum::parsetenferro-einsum/src/subscripts.rs、syntax/einsum
2. 两个输入、能写成一次 dot_general 的式子由 try_binary_dot_shortcut 直接发出,不建收缩树eager.rs、binary_dot.rseinsum
3. 三个及以上输入时 ContractionTree::optimize_with_options 定顺序:默认的 ContractionOptimizerOptions 是 0 次退火,走 tenferro 自己的确定性贪心;只有开了退火才调 omeco 的 TreeSAplanning/tree.rs、omecoeinsum
4. 执行:CPU 上三个以上 F32 / F64 / C32 / C64 输入的整个收缩交给 cpueinsum,tenferro 只给出两两配对的顺序;其余情况按树逐步两两收缩,binary_contract 每步发一个 engine 上的 dot_general_read(或外积),最后一次转置排出输出的轴序;具体张量的入口是 eager_einsum_subscripts_on_session,traced 下用 TraceContextEinsumExt 记图eager.rs、traced.rs、extension.rseinsum、扩展算子
5. AD:einsum 的规则不在核心里,由 semantic_ad_rules 注册extension.rssemantic AD 与扩展

README 的五条约定,逐条核实

README 在 Quickstart 之前列了五条「Conventions you must know before writing code」,0.8.0 的文字和 0.7.1 一字未改。下面每条都配一个程序。conventions 和 intro-chain 两个实验;conventions 是 release,intro-chain 是编不过的 debug 构建。

1. 列主序

原文:「Dense buffers are column-major: the leftmost dimension varies fastest. Row-major data passed to from_vec_col_major is silently reinterpreted as column-major — permuted/wrong values, never rejected.」

把 [[1,2,3],[4,5,6]] 按行写成 vec![1,2,3,4,5,6] 交给 from_vec_col_major(vec![2, 3], ..),再乘一个全 1 的列向量求行和:

let mut engine = CpuEngine::serial();
let row_major = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0];
let a = TypedTensor::<f64>::from_vec_col_major(vec![2, 3], row_major.clone())?;
let ones = TypedTensor::<f64>::from_vec_col_major(vec![3, 1], vec![1.0; 3])?;
let sums = a.matmul(&ones, &mut engine)?;
let bridged = Tensor::from_vec_row_major(vec![2, 3], row_major)?;
1. row sums (want [6, 15]): [9.0, 12.0]
1. reordered input:          [6.0, 15.0]
1. Tensor::from_vec_row_major data: [1.0, 4.0, 2.0, 5.0, 3.0, 6.0]

形状和长度对得上,构造就成功,没有任何报错;张量里装的是 [[1,3,5],[2,4,6]]。按列写成 vec![1,4,2,5,3,6] 才是想要的结果。0.8.0 给动态 dtype 的 Tensor 加了 from_vec_row_major,它把行主序数据重排一次再存成列主序(输出第三行);TypedTensor 上没有这个构造函数。为什么选列主序、行主序数据怎么不拷贝地桥接,见列主序与视图。

2. 没有门面 crate

原文:「cargo add tenferro fails by design; depend on the crates you need」。tag 上工作区的 members 是 16 个 crates/tenferro-*、docs/tutorial-code 和 ext/ 下的四个示例 crate(bf16-proof、df64-proof、scalar-consumer-algorithm、scalar-consumer-application),没有名叫 tenferro 的包。本卷离线写作,cargo add tenferro 在 crates.io 上会怎样没有实测;本机 crates.io 索引缓存里有 16 个 tenferro-* 名字,没有 tenferro。

📝NOTE
冷知识:门面 crate 是 v0.1.0 之前删掉的

#900「Refactor crate boundaries without root facade」(2026-05-26,首次发布于 v0.1.0)之前,工作区里有一个 tenferro 包,其他 crate 都叫 tenferro-internal-device、tenferro-internal-tensor、tenferro-internal-runtime 之类,ext/tropical 的注释写着外部扩展「depend only on the public tenferro facade where possible」。#900 的提交说明里有「refactor(api): remove root tenferro facade」「refactor(runtime): extract traced graph runtime」「refactor(ad): extract automatic differentiation crate」,同时加了一份设计文档 docs/plans/2026-05-25-no-facade-crate-boundaries-design.md。它给的理由正是 Cargo feature 的统一:「Cargo features are additive and feature-unified across the dependency graph. If one downstream dependency enables autodiff or cuda on a shared crate, every user of that crate in the final build sees the unified feature set.」所以要按 crate 选能力,并且「A facade tends to accumulate re-exports and convenience features, which reintroduces the same feature-unification and public-dependency pressure」。没有门面并没有让 feature 统一的问题消失:下文 native 和 blas 互斥的那一节,就是它在 0.8.0 咬人的地方。

3. 显式后端

原文:「Direct operations take an explicit backend argument; construct the backend/runtime once and reuse it — per-call construction discards the buffer pool.」

在 0.8.0 的代码里,这个「backend 参数」就是 engine:fn matmul(&self, rhs: &TypedTensor<T>, session: &mut dyn Engine)。参数名还叫 session,但已经没有会话可开了,CpuEngine 直接实现 Engine,&mut engine 传进去即可,只 use tenferro_runtime::prelude::* 就能调。

那么 engine 持有什么?CpuEngine 的字段是:执行资源(线程池或预留的 team、收缩用的工作区)、缓冲池 BufferPool、计划缓存 CpuPlanCache 的句柄、批处理策略、可选的分配域、运行期 engine id、扩展缓存。用同一个 engine 连做三次小 matmul 再看:

let c = a.matmul(&ones, &mut engine)?;
println!("3. pool before drop: {:?}", engine.buffer_pool_stats());
drop(c);
println!("3. pool after drop:  {:?}", engine.buffer_pool_stats());
println!("3. runtime cache:    {:?}", engine.runtime_cache_stats());
let fresh = CpuEngine::serial();
println!("3. fresh engine pool: {:?}", fresh.buffer_pool_stats());
3. pool before drop: BufferPoolStats { buffers: 0, capacity_bytes: 0 }
3. pool after drop:  BufferPoolStats { buffers: 1, capacity_bytes: 16 }
3. runtime cache:    CacheStats { entries: 2, retained_bytes: 717136, hits: 2, misses: 1, evictions: 0, clears: 0 }
3. fresh engine pool: BufferPoolStats { buffers: 0, capacity_bytes: 0 }

结果张量一 drop,它的 Vec 就经一个弱引用回到产生它的 engine 的缓冲池(存储里记着 Weak<dyn HostBufferRecycler<T>>,见存储与所有权)。计划缓存里第一次是 miss,之后两次是 hit。新建的 engine 两样都是空的。所以 README 这句话在 0.8.0 仍然成立,而且比字面更宽:每次新建 engine 丢掉的是缓冲池和计划缓存(计划缓存可以用 with_plan_cache 在几个 engine 之间共享);多线程 engine 还要重新预留一次 team。每次新建 engine 的代价实测见一次 matmul。

4. einsum 方言

原文:「Equations need the explicit arrow ("ij,jk->ik"). NumPy-style ... ellipsis is supported in flat notation and through EinsumNotation; parenthesized ellipsis remains unsupported.」

let no_arrow = [&x, &y].einsum("ij,jk", &mut engine);
let ell = [&bx, &by].einsum("...ij,...jk->...ik", &mut engine);
println!("{:?}", NestedEinsum::parse("(...ij,jk),kl->...il").map(|n| n.count_leaves()));
4. "ij,jk": Err(InvalidSubscripts { message: "einsum notation must contain exactly one '->', got: ij,jk" })
4. "ij,jk->ik": Ok([2, 4])
4. "...ij,...jk->...ik" on [5,2,3]x[5,3,4]: Ok([5, 2, 4])
4. "ij...,jk...->ik..." on [2,3,5]x[3,4,5]: Ok([2, 4, 5])
4. NestedEinsum "(ij,jk),kl->il": Ok(3)
4. NestedEinsum "(...ij,jk),kl->...il": Err(InvalidSubscripts { message: "ellipsis with parenthesized contraction order is not supported" })
4. flat einsum "(ij,jk)->ik": Err(InvalidSubscripts { message: "EinsumNotation::parse does not accept parentheses; use NestedEinsum::parse for parenthesized contraction order" })

不写箭头是一个类型化的错误,不会像 NumPy 那样按字母顺序推出输出(检查在 split_and_validate_notation)。省略号放在前面、后面都行。README 说的「parenthesized」是另一种记法:用括号指定收缩顺序("(ij,jk),kl->il",来自 Julia 的 OMEinsum),它不进普通的 einsum,要用 NestedEinsum::parse,而且不能和 ... 一起用。几条报错的原文和 0.7.1 相同。

5. 运算返回 Result

原文:「Traced operators return Result; propagate with ?.」

let sum = &a + &b + &c; // a、b、c 都是 TracedTensor
error[E0369]: cannot add `&TracedTensor` to `Result<TracedTensor, tenferro_runtime::Error>`
   --> src/main.rs:7:21
    |
  7 |   let sum = &a + &b + &c;
    |             ------- ^ -- &TracedTensor
    |             |
    |             Result<TracedTensor, tenferro_runtime::Error>
    |
note: `Result<TracedTensor, tenferro_runtime::Error>` does not implement `std::ops::Add<&TracedTensor>`
...

&a + &b 的类型是 Result<TracedTensor, Error>,而 Result 没有实现 Add,所以每一步都得写 ?:(&(&a + &b)? + &c)?,或者用方法链 a.add(&b)?.add(&c)?。candle 在这里多做了一步,给 Result<Tensor> 也实现了运算符,一条链只要一个 ?(见 candle 卷的怎么读这一卷)。不只 traced:直接运算、eager 运算也都返回 Result。

feature 各打开什么

工作区里没有一个「总开关」:每个 crate 各有一组 feature,大多是往下转发给 tenferro-cpu。按功能归类:

feature在哪些 crate 上打开什么本卷
native(默认)tenferro-cpu、-ad、-einsum、-linalg、-fft、-gpu、-runtimetenferro-cpu 的 t4a-faer 依赖(以 tenferro_cpu::faer 再导出);收缩走 cpueinsum / tcontract;tenferro-linalg 加上 tlinalg 和 tcontract-exec/faer实测
blas同上tenferro-cpu:cpueinsum-blas、cblas-sys、lapack;tenferro-einsum:cpueinsum-blas;tenferro-linalg:tlinalg-blas,同时仍编 faer 和 tlinalg(full_piv_lu 两个构建都用 faer 的完全主元分解)经 blas-accelerate 实测
blas-openblas / blas-accelerate / blas-mkl同上(tenferro-runtime 除外的转发链相同)tenferro-cpu 上是 provider-src 加对应的 blas-src、lapack-src 后端;provider-src 又打开 blasAccelerate 实测,另两个没试
provider-injecttenferro-cpu、tenferro-linalgcblas-inject、lapack-inject:运行期注入 BLAS / LAPACK 函数指针(tenferro_cpu::inject)不展开
autodifftenferro-einsum、-linalg、-fft(tenferro-internal-ops 默认开)依赖 tenferro-ad,编译各运算族的 AD 规则;einsum 的 eager 入口 EagerSessionEinsumExt 也在它后面实测
cudatenferro-gpu、-ad、-einsum、-linalg、-fft、-internal-opstenferro-gpu 的 t4a-cuda-runtime、t4a-cuda-kernels 和 cudarc(动态加载驱动);tenferro-gpu 的模块几乎全部只在它打开时编译只读源码
rocm同上在 tenferro-gpu 上是空列表 [],其他 crate 只是转发:占位—
pjrttenferro-xlalibloading:运行期加载 PJRT 插件只读源码
ledgertenferro-cpu、-einsum、-linalg测试用:threadteam/ledger,记下每个放置令牌的来源不展开

0.7.1 的 cpu-faer、cpu-blas 在 #2026 改名成 native、blas;webgpu 和 cubecl-interop 已经删掉。几条从源码里能直接看到的规矩,都在 tenferro-cpu 的 lib.rs 开头:

  • native 和 blas 互斥:compile_error!("native and blas are mutually exclusive; use --no-default-features for blas")。另外 provider-inject 要求 blas;三个 blas-* 最多开一个;provider-inject 不能和 blas-* 同时开。

  • 两个都不开也能编。 源码注释说「A build with neither backend compiles: the CPU numerical routes report a typed Unsupported instead.」只依赖 tenferro-runtime 和 default-features = false 的 tenferro-cpu(intro-nofaer,debug):

    provider: tenferro.cpu.none
    matmul: Err(Unsupported { op: "dot_general", message: "this build selects no CPU numerical backend; enable the `native` or `blas` feature" })
    add:    Ok([2.0, 4.0, 6.0, 8.0])
    exp:    Ok([2, 2])
    

    逐元素运算照常工作,收缩和线代在运行期才报 Unsupported。0.7.1 在这里是编译期的 compile_error!。

  • tenferro-runtime 上的 CPU feature 是空的。 仓库里它写着 native = ["tenferro-cpu/native"],但 tenferro-cpu 只是它的 dev-dependency(反过来 tenferro-cpu 依赖 tenferro-runtime,见源码地图)。发布出去的清单里,native、blas、blas-* 都成了 []。从 crates.io 依赖时,要选 provider 就在 tenferro-cpu 和其他转发它的 crate 上开。

native 和 blas:所有 crate 要一致

互斥加上 feature 统一,结果是一个不可加的组合:依赖图里只要有一个 crate 留着默认的 native,另一个 crate 打开 blas,tenferro-cpu 就同时看到两个。intro-trap 只写了两行依赖,tenferro-cpu 要 Accelerate,tenferro-einsum 用默认 feature(debug):

tenferro-cpu = { version = "=0.8.0", default-features = false, features = ["blas-accelerate"] }
tenferro-einsum = "=0.8.0"
error[E0428]: the name `Scalar` is defined multiple times
  --> .../tenferro-cpu-0.8.0/src/contraction.rs:74:1
...
error: native and blas are mutually exclusive; use --no-default-features for blas
  --> .../tenferro-cpu-0.8.0/src/lib.rs:68:1
...
error: could not compile `tenferro-cpu` (lib) due to 9 previous errors

cargo tree -e features -i tenferro-cpu 把来源说得很清楚:tenferro-cpu feature "native" 来自 tenferro-einsum feature "native",后者来自它的 default。规则是:用 BLAS 时,依赖图里每一个转发 provider feature 的 tenferro crate(tenferro-cpu、-ad、-einsum、-linalg、-fft,以及开了 cuda 时的 -gpu)都要 default-features = false 并开同一个 blas-*。本卷的 BLAS 实验就是这样写的。报错里除了那条 compile_error!,还有 8 条重复定义、参数个数不对之类的错,它们都是两套 provider 代码同时编译的连带结果,读报错时先找 mutually exclusive 那一条。

GPU 一侧还留着 0.7.1 那种写法:tenferro-ad 的 EagerExtensionBackendKind 只剩 Cpu 和带 #[cfg(feature = "cuda")] 的 Cuda 两个变体,einsum、linalg、fft 在开了 autodiff 时对它 match,Cuda 那一支只在它们自己的 cuda 下编译。按源码看,依赖图里有谁打开了 tenferro-ad/cuda,所有开了 autodiff 的运算族 crate 也都得开 cuda,否则 match 不完整。本机跑不了 CUDA,但 cargo check 不需要 CUDA 工具链:只给 tenferro-fft 开 ["autodiff", "cuda"]、einsum 和 linalg 只开 autodiff 时,两个 crate 都报 error[E0004]: non-exhaustive patterns: `tenferro_ad::extension::EagerExtensionBackendKind::Cuda` not covered。

📝NOTE
冷知识:WebGPU 那个 feature 陷阱是随整条 WebGPU 路径一起删掉的

0.7.1 的同一个枚举还有一个 #[cfg(feature = "webgpu")] WebGpu 变体,只给一个 crate 开 webgpu,另外两个运算族 crate 就报 non-exhaustive patterns。#2291(2026-10-28 合并,首次发布于 v0.8.0)里的提交 d8ddd68d「feat(gpu)!: one CUDA path — the kernel-catalogue engine replaces CubeCL」写明了范围:「the CubeCL engine, its kernels and the WebGPU/Metal lane are gone」,删除清单里有「the WebGPU/Metal lane (webgpu, apple, its tests, tutorial binaries and examples) ... and the webgpu/cubecl-interop features」。理由是 CUDA 只留一条路:同一个提交说新的 kernel-catalogue engine 在 A100 上的每一项对照都比它替换掉的 CubeCL engine 快,结果逐位相同。陷阱的写法没有变,只是可选的 GPU 后端只剩 cuda 一个。CUDA 路径见 CUDA。

阅读路线

前置知识

  • Rust 的 trait、&mut 借用和 ?;Cargo feature 怎么统一(上面的 native / blas 一节就是它的后果)。
  • 线性代数:矩阵乘、SVD、QR;einsum 记法。
  • 自动微分的两个方向:向量-雅可比积(VJP,反向)和雅可比-向量积(JVP,前向)。知道 JAX 的 grad 是图变换、PyTorch 的 backward() 是在记录上反向走一遍,读 04、05 会快很多;burn 和 candle 的 eager 自动微分见 burn 卷的 04 自动微分 和 candle 卷的 backward 与 GradStore。
  • 线程池:tenferro 的 CPU 并行用 threadteam 的池和放置令牌(底下是每个池私有的 rayon 线程),不用 tokio;池由调用方建。

小结

  • 全卷对着 tenferro-rs 0.8.0(a9f494cc,2026-10-30)写;CPU 计算在单独发布的 tcontract 0.9、cpueinsum 0.9、tlinalg 0.8、threadteam 0.2、strided-rs 0.9 和 t4a-faer 0.27 上,computegraph、omeco 不变。
  • 直接计算主线:from_vec_col_major → 调用方建 CpuEngine → matmul 即 dot_general,在调用线程上执行 → cpueinsum → tcontract 选 faer → host_data。
  • traced 主线:运算只记图 → grad 生成新图 → compile 物化去重 → Runtime 注册描述 → prepare → run(&mut [&mut engine]) → into_tensor。
  • eager 主线:EagerRuntime(不持有 engine)→ variable_from → with_eager_session(&mut engine) 立即执行并记录 → backward(&mut engine) 累加进 grad() → clear_grad。
  • README 五条约定都成立:行主序数据静默算错(Tensor::from_vec_row_major 是新加的桥);没有 tenferro 包;新建 engine 丢掉缓冲池和计划缓存;einsum 必须写箭头;运算返回 Result,a + b + c 编不过。
  • native 和 blas 互斥,一个 crate 留着默认 native、另一个开 blas 就编不过;两个都不开能编,但收缩在运行期报 Unsupported;tenferro-runtime 上的 CPU feature 在发布版里是空的。

💬 关联讨论

发起讨论
加载中...