Rust in Everything
基于 tenferro-runtime 0.8.0

图与运行时

CPU 后端那一部分讲的是「给我张量和一个引擎,我马上算」。这一部分讲 traced 主线的中段:TracedTensor 上的运算什么也不算,只记图;GraphCompiler 把图编成一个程序;Runtime::prepare 针对一组引擎目标把程序准备好,Runtime::run 在调用方借出的引擎上执行。求导(grad / vjp / jvp)是在图上做的变换,放在自动微分部分;这里只关心图怎么来、怎么编、怎么跑。

代码在 tenferro-runtime/src/:traced.rs(traced 张量)、graph/(GraphCompiler)、program/(语义程序)、compiler/(降级与优化 pass)、segment.rs(分段)、runtime/(引擎目标、注册、快照、准备、缓存、调度、执行)。图的数据结构来自单独的仓库 computegraph-rs(crate computegraph 0.1.0),算子词汇表来自 tenferro-core-ops 和 tenferro-internal-ops。按 tag 数,tenferro-runtime/src 去掉 tests/ 目录和 tests.rs、*_tests.rs 文件后约 5.0 万行,computegraph 的 src 约 1000 行。

  1. computegraph:外部 crate 只管「图」:GraphOperation 只描述输入输出个数,值的身份是结构性的 ValueKey,resolve → materialize_merge → compile 顺手做了公共子表达式消除;它不懂求导,只给 AD 留了一个 OperationRole 标签;tenferro 用宏生成的 StdTensorOp 接进来,Operand 是 (),从不调用它的 eval。
  2. TracedTensor:一个 traced 张量是「某张小图里的一个值」,每个运算新建一张只含一个节点的图,把操作数的图挂成父图;运算返回 Result;同一个越界的切片,形状已知时建图就报错,形状由输入说明给出时编译才报错,符号形状的叶子拖到运行时。
  3. 编译:GraphCompiler::compile 不碰后端,只产出语义程序和默认输入绑定;降级成执行 IR、跑优化 pass、折叠元数据视图、分段和规划逐元素区都在 Runtime::prepare 里;做了公共子表达式消除和死代码消除,没有常量折叠。
  4. 执行与缓存:Runtime 只保存配置快照和准备缓存,不持有引擎;prepare 每次都先降级再查缓存,键里有语义指纹、运行时纪元、引擎和输入特化;run 不查缓存,绑定引擎后按计划执行;实测一个 41 条命令的程序,prepare 命中缓存约 88 µs,run 约 22 µs。
flowchart TB
  tt["TracedTensor 运算"] --> g["computegraph Graph"]
  g --> gc["GraphCompiler compile"]
  gc --> cg["CompiledGraph"]
  rb["Runtime builder"] --> rt["Runtime 快照"]
  cg --> prep["prepare 降级 查缓存"]
  rt --> prep
  tgt["engine target"] --> prep
  prep --> pd["Prepared"]
  pd --> run["run 借用引擎"]
  eng["CpuEngine"] --> run
  run --> out["Output"]
  ad["grad vjp jvp"] -.->|变换图| tt

第一页是图的底座,第二页是往图里加节点的那一侧,后两页是把图变成结果的那一侧。图被求导变换之后再走一遍同样的编译和执行,见 traced AD;整条主线串起来走一遍见一次 grad。引擎本身(CpuEngine、线程池、缓冲池)见 CPU 引擎。

💬 关联讨论

发起讨论
加载中...