CPU 后端
张量那一部分讲到 a.matmul(&b, &mut engine) 里的 a。这一部分讲 engine:它从哪来、持有什么,matmul 和 add 进了它之后在哪个线程上、用哪个库算,结果写进哪块内存。
0.8.0 的 CPU 后端分两层。tenferro 仓库里的四个 crate 管张量语义和资源:tenferro-cpu(engine、各运算族的入口、provider 选择;src/ 约 1.43 万行,不算测试文件)、tenferro-cpu-basic(缓冲池和整块覆盖守卫,1338 行)、tenferro-internal-cpu-kernels(普通逐元素运算的 dtype 分派,3740 行)、tenferro-cpu-fused(逐元素融合,426 行)。数值代码在单独发布的库里:线程来自 threadteam 0.2,二元收缩来自 cpueinsum 0.9 和 tcontract 0.9,线性代数来自 tlinalg 0.8(native)或 tlinalg-blas 0.5(BLAS),GEMM 和分解最终落到 t4a-faer 0.27 或系统的 BLAS/LAPACK,逐元素循环在 strided-rs 0.9 里。后端 trait Engine 定义在 tenferro-tensor/src/backend.rs。
- engine 与 threadteam 池:
CpuEngine是调用方持有、用&mut借出的值,持有缓冲池、计划缓存和一个 threadteam team;每个运算从它铸一个放置令牌,没有会话,也没有全进程仲裁器。本机复用缓存线程构造一个 8 线程 engine 只要几微秒;小运算在 team engine 上比串行 engine 多约 0.45 µs;四个线程各持一个 engine 并发跑,墙钟时间和一个线程差不多。 - provider:native 与 BLAS:
native(默认)和blas两个 feature 互斥;matmul 经 cpueinsum 的BinaryPlan落到 tcontract(native,多数时候是 faer)或 CBLAS(blas),线性代数落到 tlinalg 或 LAPACK,full_piv_lu两边都用 faer。本机 Accelerate 的 2048 f32 matmul 约 2.5 TFLOP/s,native 8 线程约 0.67 TFLOP/s。 - 逐元素、SIMD 与融合:一次
add先按DType、再按KernelDType两次分派;exp、tanh等走 elem-simd-math 的向量内核;并行与否由「串行时间是否达到 3 倍派发代价」决定;输出用完回到缓冲池;融合只在 traced 程序里发生,单线程不再变慢,5 个输入的区域不再报错。
flowchart TB user["调用方 &mut engine"] --> eng["CpuEngine"] rt["Runtime::run"] --> eng eng --> tok["threadteam 令牌"] eng --> pool["BufferPool"] tok --> dot["dot_general"] tok --> ew["逐元素"] tok --> fuse["融合区域"] dot --> nat["cpueinsum tcontract"] dot --> blas["CBLAS"] nat --> faer["t4a-faer"] ew --> strided["strided-kernel"] fuse --> sfused["strided-fused"]
第一页回答「engine 是什么、多贵、线程从哪来」,第二页回答「谁来算」,第三页回答「逐元素运算怎么走、结果写在哪」。traced 程序怎么切成区域、什么时候把中间结果还回缓冲池,在图与运行时;GPU 后端在 07 部分,本机只读源码。和 candle 的 CPU 后端对照:candle 的逐元素运算单线程、没有缓冲池(见 CPU 后端),tenferro 把线程池、缓冲池和计划缓存都收进一个由调用方持有的 engine。