源码地图
主线位置:三条主线经过的代码分布在哪些 crate、哪些仓库里。直接计算主线从
tenferro-tensor的TypedTensor经tenferro-runtime的扩展 trait 落到tenferro-cpu的CpuEngine,再下到 cpueinsum、tcontract 和 t4a-faer;traced 主线在tenferro-runtime(图、编译、prepare / run)和tenferro-ad(求导变换)之间来回,图本身是 computegraph 的;eager 主线在tenferro-ad;einsum 线在tenferro-einsum,计算交给 cpueinsum,开了退火时规划交给 omeco。
flowchart TB fam["einsum linalg fft"] -->|autodiff| ad["tenferro-ad"] fam --> cpu ad --> cpu["tenferro-cpu"] ad -.->|cuda| gpu["tenferro-gpu"] gpu --> cpu cpu --> rt["tenferro-runtime"] cpu --> kern["cpu-basic 等三个"] cpu --> ce["cpueinsum"] fam --> tl["tlinalg"] ce --> tc["tcontract"] tc --> tx["tcontract-exec"] tl --> tx tx --> tt["threadteam"] tx --> fa["t4a-faer"] kern --> sr["strided-rs"] xla["tenferro-xla"] --> rt rt --> iops["internal-ops"] rt --> cg["computegraph"] iops --> t["tenferro-tensor"] t --> tcore["tensor-core"]
箭头是「依赖于」,取自 cargo tree -p <crate> -e normal --depth 1,实验依赖是默认的 native 加上三个运算族的 autodiff;tenferro-gpu 那条虚线只在 cuda 下存在,本机不编译,取自各 Cargo.toml。图里省掉了几条不改变层次的边:tenferro-cpu 还直接依赖 t4a-faer、threadteam 和 strided-rs,tenferro-einsum 还依赖 omeco,tenferro-linalg 还依赖 tcontract-exec 和 t4a-faer,tenferro-tensor 还依赖 tenferro-core-ops。
行数是 git show v0.8.0:<path> | wc -l 数的行数(含注释和空行),只用来比较各块大小,脚本对每个 crate 的全部 .rs 文件分类求和。tenferro 的测试大多不在 tests/ 目录,而是散在 src/ 里:src/tests.rs、src/**/tests/*.rs、foo_tests.rs 这样的文件,由 #[cfg(test)] mod tests; 引进来。下文把 src/ 下路径里有 tests 目录、或文件名是 tests.rs / test_*.rs / *_tests.rs 的算作「src 里的测试」,其余算源码;写在源文件里的 #[cfg(test)] mod xxx { .. } 算进源码。
分层与依赖方向
| crate | 源码 | src 里的测试 | tests/ | 入口和大文件 | 做什么 | 讲它的页 |
|---|---|---|---|---|---|---|
tenferro-tensor-core | 2040 | 85 | 0 | layout.rs(686)、lib.rs、scalar_set.rs、rank.rs | 秩与布局元数据、dtype 标签、标量 trait 和提升规则 | 数据模型 |
tenferro-core-ops | 1169 | 0 | 42 | catalog.rs(1149) | 核心 primitive 的目录(名字、元数据) | computegraph |
tenferro-tensor | 26273 | 10186 | 2886 | types.rs(11680)、backend.rs(3864)、storage/ | TypedTensor<T, R, D>、Tensor、视图、存储、Engine 等后端 trait | 02 张量 |
tenferro-internal-ops | 12240 | 6516 | 20 | ad/registry.rs(1763)、ad/structural.rs、ad/context.rs | 图运算的词汇表 StdTensorOp,primitive 的 AD 规则 | primitive AD |
tenferro-runtime | 49610 | 17692 | 11380 | runtime/(19545:preparation.rs 2686、snapshot.rs 2625、execution.rs 2366)、traced.rs(3498)、program/、graph/ | 给 TypedTensor / Tensor 加直接运算的扩展 trait;TracedTensor、GraphCompiler、Runtime、prepare / run、缓存、扩展注册 | 04 图与运行时 |
tenferro-cpu-basic | 1338 | 754 | 238 | buffer_pool.rs(788) | 共享的缓冲池、整块覆盖守卫 | 逐元素 |
tenferro-internal-cpu-kernels | 3740 | 2156 | 0 | elementwise.rs(2802) | 普通逐元素内核的 dtype 分派 | 逐元素 |
tenferro-cpu-fused | 426 | 769 | 103 | lib.rs | 运行期 DAG 的融合逐元素,交给 strided-fused | 逐元素 |
tenferro-cpu | 14228 | 14797 | 2581 | indexing.rs(2565)、structural.rs(1763)、exec_session.rs(1072)、engine.rs(654) | CpuEngine、把运算路由到 provider、索引和结构运算、给运行时的 engine 注册 | 03 CPU 后端 |
tenferro-gpu | 15536 | 1974 | 9089 | cuda/(15127,structural.rs 7634) | CUDA engine 和显式传输;几乎全部只在 cuda 下编译 | CUDA |
tenferro-ad | 16275 | 5253 | 16361 | eager.rs(5754)、semantic_transform.rs(2397)、semantic_extension.rs、traced.rs | EagerRuntime / EagerTensor,traced 的 grad / vjp / jvp,semantic 变换 | 05 自动微分 |
tenferro-einsum | 12504 | 6989 | 2266 | concrete.rs(2396)、eager.rs(1420)、extension.rs、planning/ | 下标、收缩规划、三种入口、AD 规则 | einsum |
tenferro-linalg | 39551 | 7777 | 23283 | gpu/(9860)、cpu/(8779)、tensor_ext.rs(3070) | 分解、求解及其 AD 规则;CPU 内核在 tlinalg | 线性代数 |
tenferro-fft | 6753 | 2581 | 5184 | lib.rs(2274)、cpu.rs、cuda/ | FFT(CPU 用 rustfft、realfft) | FFT |
tenferro-xla | 3526 | 297 | 1235 | lowering/program.rs(1016)、pjrt/ | StableHLO lowering、运行期加载 PJRT 插件;只读源码 | XLA |
tenferro-internal-extension-macros | 522 | 162 | 0 | lib.rs | 注册扩展算子的过程宏 | 扩展算子 |
| 合计 | 205731 | 77988 | 74668 |
另有 benches/ 和 examples/ 合计 7426 行。测试和源码的比例大约 0.74:src 里的测试 7.8 万行,tests/ 目录 7.5 万行。
几处大小值得注意:
tenferro-cpu的源码只有 1.4 万行,src里的测试比源码还多。二元收缩、GEMM、线代内核都不在这里了:tenferro-cpu的Cargo.toml直接依赖的是 cpueinsum、t4a-faer 和 threadteam,engine 只负责把运算路由过去(见provider)。上游的cpu-kernel-boundaries.md记录了这条边界。tenferro-gpu只剩 CUDA。 1.55 万行里cuda/一个目录占 1.51 万,lib.rs里的模块几乎都包在#[cfg(feature = "cuda")]里;本机能编的那部分只有几十行。tenferro-linalg的 GPU 一半比 CPU 一半还大(gpu/9860 行,包括 cuSOLVER 的 FFI;cpu/8779 行),因为 CPU 一侧的分解算法在 tlinalg 里,这里只是适配。- 用户直接看到的
Tensor、TypedTensor定义在tenferro-tensor,但tenferro-runtime的lib.rs把它们再导出,tenferro_ad::Tensor也是同一个类型。所以 README 的例子只依赖tenferro-runtime和tenferro-cpu,从不直接写tenferro-tensor。
和上游架构文档对照
docs/architecture/tenferro-crates.md 用四层描述这个工作区:Layer 1 是 tenferro-tensor-core;Layer 2 是 tenferro-tensor、tenferro-cpu 和三个内部 CPU crate、tenferro-gpu;Layer 3 是 tenferro-runtime、tenferro-xla 和 einsum / linalg / fft;Layer 4 是 tenferro-ad。它的「Dependency Direction」一节列出的 tenferro 内部边,和 cargo tree 逐条对得上(包括 tenferro-ad -> tenferro-cpu)。仍然朝「上」的两条,文档自己都写了:
tenferro-cpu→tenferro-runtime:Layer 2 依赖 Layer 3。文档说「The CPU-to-runtime line records the implemented runtime registration adapters ... It is not a runtime-to-CPU dependency」,并说这条边「approved solely for engine-registration adapters」。tenferro-gpu也依赖tenferro-runtime,同理。按真实的依赖顺序排,是 tensor-core / core-ops → tensor → internal-ops → runtime → cpu → gpu → ad → einsum / linalg / fft。- einsum / linalg / fft →
tenferro-ad:Layer 3 依赖 Layer 4,只在开了autodiff时成立。依赖列表没有这条边,规则里写着「Operation-family AD support depends ontenferro-adonly when the crate'sautodifffeature is enabled」。
这份文档在 0.8.0 有几处过时:第 II 节说 tenferro-tensor 里有「the host container (HostTensor, ...)」,而 HostTensor 已经删掉,代码里只剩 ErasedHostTensor;它说 tenferro-linalg 的 tlinalg「pinned by git revision」,tag 上的根 Cargo.toml 写的是 tlinalg = "0.8.0";依赖列表只列 tenferro 内部的边,不列 cpueinsum、tlinalg、threadteam、t4a-faer,而 CPU 计算的大头在它们那里。反方向的 tenferro-runtime → tenferro-cpu 只是 dev-dependency;因为它,tenferro-runtime 在仓库里写的 native = ["tenferro-cpu/native"] 等 feature 到了发布清单里都成了空列表(见怎么读这一卷)。
这条朝上的边来自 #1453「Execution engine provider U7-U8 semantic AD restart」(2026-07-26,首次发布于 v0.3.0)。这是一次大合并:482 个文件,+85633 / −33601 行。它给 tenferro-cpu/Cargo.toml 加了一行 tenferro-runtime = { path = "../tenferro-runtime", ... },提交说明里对应的两条是「feat(cpu): adapt providers into runtime preparation」和「feat(cpu): expose runtime engine registration」,新文件就是今天的 runtime_adapter.rs。在这之前,tenferro-runtime 只在 dev-dependencies 里用 tenferro-cpu 跑测试,两个 crate 在正式依赖上互不相识。改成这样以后,运行时只认 EngineRegistration 这个与后端无关的描述,CPU 后端自己把自己翻译成注册项。0.8.0 里这个函数换了参数,收的是一个 &CpuEngine(runtime_engine_registration),运行时仍然不必知道有哪些后端,也不保存 engine。同一次合并还从工作区依赖里删掉了 tidu(下文 computegraph 一条提到它);它怎么把求导从规则表上换下来,见 primitive AD 的冷知识。
外部仓库各管什么
0.8.0 的 CPU 栈分在 tensor4all 的几个仓库里,各自发布到 crates.io。行数用同一个脚本在对应 commit 上数,只数 crate 的源码(不含 src 里的测试和 tests/):
| 仓库(本卷的版本) | 源码行数 | 谁直接用 | 管什么 |
|---|---|---|---|
| cpueinsum-rs:cpueinsum 0.9.0 | 3177(cpueinsum-blas 另 1594) | tenferro-cpu、tenferro-einsum | 整数标签的 einsum 和二元收缩计划 BinaryPlan,调用方给出收缩顺序;底下是 tcontract,BLAS 版在 cpueinsum-blas |
| tcontract-rs:tcontract 0.9.0、tcontract-exec 0.9.0、tcontract-kernel 0.3.0 | 10486、2514、9821 | cpueinsum;tenferro-linalg(tcontract-exec) | tcontract:一个二元收缩问题,建计划时在打包驱动、faer、逐元素三种策略里选一种;tcontract-exec:每次调用的执行包(threadteam 令牌、工作区、批处理策略)和它的 faer 适配;tcontract-kernel:GEMM 微内核族、打包、分块 |
| tlinalg-rs:tlinalg 0.8.0 | 7308(tlinalg-blas 另 5095) | tenferro-linalg | 基于 faer、不依赖张量类型的线性代数内核,自带借用式输入输出、工作区和并行度 |
| threadteam-rs:threadteam 0.2.0 | 6256 | tenferro-cpu(并再导出)、strided-rs、tcontract-exec、t4a-faer | 调用方建的 Pool、不可 Clone 的放置令牌 Exec、预留成员的 Team;每个池底下是一个私有的 rayon 池 |
strided-rs:strided-basic 0.9.0 等 | strided-basic 23273、strided-perm 4126、strided-view 3716、strided-kernel 3510、strided-fused 3222、strided-traits 489、strided-nn 467 | tenferro-cpu-basic、-cpu-fused、-internal-cpu-kernels、-cpu、-gpu、-linalg;tcontract | 任意步长的视图和 CPU 上的遍历:逐元素、归约、拷贝、置换、融合;strided-nn 是新加的 softmax 融合内核 |
| computegraph 0.1.0 | 1017 | tenferro-runtime、-internal-ops、-ad、-einsum、-linalg、-fft、-xla | 和具体运算无关的计算图:建图、解析、物化、SSA 编译、求值 |
| omeco 0.2.6 | 13099 | tenferro-einsum | 张量网络的收缩顺序优化(贪心、TreeSA)和切片 |
| t4a-faer 0.27.0 | 121250(cargo 缓存里 src/ 的全部 .rs) | tenferro-cpu、tenferro-linalg、tcontract、tlinalg | 稠密线性代数;只读 cargo 缓存 |
| t4a-cuda-runtime、t4a-cuda-kernels 0.1.0 | 5854、4652(同上) | tenferro-gpu(cuda) | CUDA 的流、内存池、传输、NVRTC 和内核启动;内核目录;只读 cargo 缓存 |
逐个说几句:
- cpueinsum 和 tcontract:一次 f64 matmul 在 CPU 上的路是
tenferro-cpu→ cpueinsum 的BinaryPlan→ tcontract 的Plan→ faer。tcontract 在建计划时选策略,plan/mod.rs的模块文档写了四条规则:显式要求打包驱动的走打包驱动;全是批维的走逐元素;能不拷贝地并成一次 strided batched GEMM 的走 faer,除非 GEMM 体积超过该 dtype 的FaerLimit(默认只给 c64 设了 2^17);其余走打包驱动(tcontract-kernel 的微内核)。一次 matmul 实际停在哪,见一次 matmul;三种策略的取舍见 provider。 - threadteam:包描述是「Scoped CPU thread-team placement token: a Pool and a non-Clone Exec with fork, for_ranges, external and broadcast; no Rayon type in the public API.」0.2.0 的
Pool底下是一个私有的 rayon 池;默认设置的池在 drop 时把线程留进一个按宽度分的进程级缓存,下一个同宽度的池直接拿走(mod cache)。这个缓存对每次新建 engine 的代价影响很大,实测见一次 matmul。讲它的页:CpuEngine 与线程池。 - strided-rs:README 说它受 Julia 的 Strided.jl、StridedViews.jl 和 OMEinsum.jl 启发。tenferro 自己的 CPU 内核 crate 都很薄(
tenferro-cpu-fused只有 426 行),遍历、分块、多线程都在 strided-rs 里:strided-basic一个就有 2.3 万行。tenferro 根Cargo.toml的注释说,strided 0.9 的每个并行入口都把一个 threadteam 的Exec令牌作为最后一个参数,并按令牌的派发代价决定并行宽度。讲它的页:列主序与视图、逐元素。 - computegraph:
lib.rs第一行是「AD-agnostic tensor computation graph engine.」,README 说它「Fully generic overOperation: GraphOperation; it never references specific primitive operation sets.」TracedTensor里装的就是Arc<Graph<StdTensorOp>>,GraphCompiler调它的materialize_merge把片段压平、去重。它的 README 画的三层栈是 computegraph ← tidu-rs(「AD graph transforms」)← tenferro-rs,但工作区依赖里早就没有 tidu,图上的 AD 变换在tenferro-ad里。讲它的页:computegraph。 - omeco:crate 文档说它移植自 Julia 的 OMEinsumContractionOrders.jl。0.8.0 里只有开了退火时
tenferro-einsum才调它的TreeSA;默认(0 次退火)走 tenferro 自己的确定性贪心,源码注释说原因是「omeco 0.2.6's greedy breaks ties in per-processHashMaporder」。讲它的页:einsum。 - t4a-faer:包描述「Temporary tensor4all fork of faer: an AI-driven experiment build for tenferro-rs.」0.27 的
Par只有Seq和Team(threadteam::TeamExec)两种,并行只能跑在 threadteam 的 team 上。根Cargo.toml的注释说tenferro-cpu把它再导出为tenferro_cpu::faer(只在native构建里有),下游直接调 faer 时要用这个再导出,上游faer的类型对不上。 - t4a-cuda-runtime、t4a-cuda-kernels:包描述分别是「Standalone CUDA execution runtime (streams, memory pools, transfers, NVRTC and kernel launch) for tensor4all」和「Runtime-independent CUDA kernels for tensor4all and external CUDA consumers」。它们取代了 0.7.1 的 t4a-cubecl。讲它的页:CUDA。
工作区之外还有几个直接依赖值得记住:elem-simd-math(f32 / f64 的 SIMD 数学函数,tenferro-cpu 的 simd_math.rs 把 exp、log、tanh、erf 等映射过去)、rustfft 和 realfft(tenferro-fft 的 CPU 实现)、lru(计划缓存)。
依赖树:只做一次 CPU matmul 会拉进什么
实验 crate intro-tiny 只依赖 tenferro-runtime 和 tenferro-cpu(默认 feature,即 native),用一个 CpuEngine::serial() 做 2×2 的 f32 matmul。cargo tree -e normal 按名字和版本去重之后,除了它自己有 85 个包:16 个 tenferro crate 里用到 10 个,cpueinsum、tcontract 三件、threadteam、strided-rs 的 7 个、t4a-faer 和它的一族(gemm、nano-gemm、pulp 等)、rayon、computegraph 都在。本卷的主实验 crate 另加了 tenferro-ad、三个开了 autodiff 的运算族和 ndarray、anyhow,依赖图是 117 个包,多出来的主要是 omeco 和它的 serde / rand、tlinalg、rustfft 一族;Accelerate 版本是 114 个。这个小程序的二进制有多大,见 tenferro 解决什么问题。
小结
- 16 个 crate,源码约 20.6 万行,
src里的测试 7.8 万行,tests/目录 7.5 万行;测试多数和源码放在同一个目录树里。 - 真实的依赖顺序:tensor-core / core-ops → tensor → internal-ops → runtime → cpu → gpu → ad → einsum / linalg / fft;
tenferro-cpu依赖tenferro-runtime(自 v0.3.0 的 #1453),运算族在开了autodiff时依赖tenferro-ad。上游架构文档的内部边和cargo tree一致,但还写着HostTensor和 git 锁定的 tlinalg。 - 最大的是
tenferro-runtime(5.0 万)和tenferro-linalg(4.0 万);tenferro-cpu只有 1.4 万,tenferro-gpu几乎全是cuda/。 - CPU 上的收缩在 cpueinsum / tcontract,线代在 tlinalg,线程在 threadteam,遍历和内核在 strided-rs,GEMM 和分解最终在 t4a-faer;图在 computegraph,退火规划在 omeco;tenferro 自己管数据模型、engine、编译、运行时和 AD。
- 只依赖
tenferro-runtime和tenferro-cpu就有 85 个包。