Rust in Everything
基于 tenferro-runtime 0.8.0

源码地图

主线位置:三条主线经过的代码分布在哪些 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-core2040850layout.rs(686)、lib.rs、scalar_set.rs、rank.rs秩与布局元数据、dtype 标签、标量 trait 和提升规则数据模型
tenferro-core-ops1169042catalog.rs(1149)核心 primitive 的目录(名字、元数据)computegraph
tenferro-tensor26273101862886types.rs(11680)、backend.rs(3864)、storage/TypedTensor<T, R, D>、Tensor、视图、存储、Engine 等后端 trait02 张量
tenferro-internal-ops12240651620ad/registry.rs(1763)、ad/structural.rs、ad/context.rs图运算的词汇表 StdTensorOp,primitive 的 AD 规则primitive AD
tenferro-runtime496101769211380runtime/(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-basic1338754238buffer_pool.rs(788)共享的缓冲池、整块覆盖守卫逐元素
tenferro-internal-cpu-kernels374021560elementwise.rs(2802)普通逐元素内核的 dtype 分派逐元素
tenferro-cpu-fused426769103lib.rs运行期 DAG 的融合逐元素,交给 strided-fused逐元素
tenferro-cpu14228147972581indexing.rs(2565)、structural.rs(1763)、exec_session.rs(1072)、engine.rs(654)CpuEngine、把运算路由到 provider、索引和结构运算、给运行时的 engine 注册03 CPU 后端
tenferro-gpu1553619749089cuda/(15127,structural.rs 7634)CUDA engine 和显式传输;几乎全部只在 cuda 下编译CUDA
tenferro-ad16275525316361eager.rs(5754)、semantic_transform.rs(2397)、semantic_extension.rs、traced.rsEagerRuntime / EagerTensor,traced 的 grad / vjp / jvp,semantic 变换05 自动微分
tenferro-einsum1250469892266concrete.rs(2396)、eager.rs(1420)、extension.rs、planning/下标、收缩规划、三种入口、AD 规则einsum
tenferro-linalg39551777723283gpu/(9860)、cpu/(8779)、tensor_ext.rs(3070)分解、求解及其 AD 规则;CPU 内核在 tlinalg线性代数
tenferro-fft675325815184lib.rs(2274)、cpu.rs、cuda/FFT(CPU 用 rustfft、realfft)FFT
tenferro-xla35262971235lowering/program.rs(1016)、pjrt/StableHLO lowering、运行期加载 PJRT 插件;只读源码XLA
tenferro-internal-extension-macros5221620lib.rs注册扩展算子的过程宏扩展算子
合计2057317798874668

另有 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 on tenferro-ad only when the crate's autodiff feature 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 到了发布清单里都成了空列表(见怎么读这一卷)。

📝NOTE
冷知识:CPU 后端依赖运行时,是从 v0.3.0 开始的

这条朝上的边来自 #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.03177(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.010486、2514、9821cpueinsum;tenferro-linalg(tcontract-exec)tcontract:一个二元收缩问题,建计划时在打包驱动、faer、逐元素三种策略里选一种;tcontract-exec:每次调用的执行包(threadteam 令牌、工作区、批处理策略)和它的 faer 适配;tcontract-kernel:GEMM 微内核族、打包、分块
tlinalg-rs:tlinalg 0.8.07308(tlinalg-blas 另 5095)tenferro-linalg基于 faer、不依赖张量类型的线性代数内核,自带借用式输入输出、工作区和并行度
threadteam-rs:threadteam 0.2.06256tenferro-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 467tenferro-cpu-basic、-cpu-fused、-internal-cpu-kernels、-cpu、-gpu、-linalg;tcontract任意步长的视图和 CPU 上的遍历:逐元素、归约、拷贝、置换、融合;strided-nn 是新加的 softmax 融合内核
computegraph 0.1.01017tenferro-runtime、-internal-ops、-ad、-einsum、-linalg、-fft、-xla和具体运算无关的计算图:建图、解析、物化、SSA 编译、求值
omeco 0.2.613099tenferro-einsum张量网络的收缩顺序优化(贪心、TreeSA)和切片
t4a-faer 0.27.0121250(cargo 缓存里 src/ 的全部 .rs)tenferro-cpu、tenferro-linalg、tcontract、tlinalg稠密线性代数;只读 cargo 缓存
t4a-cuda-runtime、t4a-cuda-kernels 0.1.05854、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 over Operation: 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-process HashMap order」。讲它的页: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 个包。

💬 关联讨论

发起讨论
加载中...