跳转到内容
查看源码

图与状态

Graph 运行时(entelechy-graph)按 KernelID 组合 Kernel,进行带缓存、脏标记的求值。Kernel 拥有计算,Graph 拥有关系。

一次求值(一帧)只运行“有东西在读”的节点。节点在以下任一项改变时重新运行:它的 Kernel、它的值、它的范围、它读取的时钟或输入、它读取的来源。

一个节点如果给出了与上次逐字相同的结果,对它的读者来说就是“未改变”——所以一个便宜的、已稳定的节点可以让后面昂贵的节点免于运行,处于不动点的状态不会推进。(比较只在 Host 内存中总是进行;在设备上只有当节点仅为它读取的状态而运行时才比较,因为在设备上比较需要等待。)

当消费者只在自己的元素处 sample 生产者时,两者会融合成一个 Kernel,中间结果不写出。entelechy graph 打印展平后的图、已发布 Kernel 及其代数、状态推进顺序和计划:

// plan: 2 group(s)
// gray + source: fused.gray@1 vec2(128, 128)
// blocks: image.pixelate@1
// gray runs apart: `blocks` reads it neither at its own element (`sample`) nor at one offset of a row

每个“分开运行”的生产者都会附上原因(它在休眠、它的工作有预算、按地址读取……)。--no-fusion 可以关闭融合以便对照;pin(协议)可以保持某个节点物化以便 read。

docs/DOCUMENTS.md 明确写明从不融合的:归约、scan、scatter、原生操作、两个节点之间的窄 Field(narrow Field)。

node life: life.step@1 {
cells <~ life.result from seed.result; // 读上一帧提交的值;第一帧读 seed
}
  • port <~ source from initial 读取上一次成功求值提交的值,不构成依赖;在第一次提交前读 initial。
  • 被 <~ 读取的节点持有状态。用 <- 读它的节点看到本次求值产生的新状态,用 <~ 读它的看到上一次留下的状态。所以多个节点必须一致的时刻(比如“战斗开始”)应该由一个节点决定,其他节点用 <- 从它的新状态读取。entelechy graph 会在计划中说明每个节点读取了谁的新状态、谁的已提交状态。
  • 只有整次求值成功时状态才提交;失败的一帧什么都不改变。

重新发布的 Kernel 改变了状态端口的布局时:

cells <~ life.result from seed.result migrate life.widen@1;

migrate 指定一个 map Kernel:输入是旧布局,输出新布局,在所有节点之前运行一次,求值报告为 migrated。没有 migrate 时状态从 initial 重新开始,求值报告 reset。迁移 Kernel 未发布、不是这样的 map、布局不对或失败,都会使求值失败,状态保持原样。

写法 作用
node name: graph "path.esn" { … } 把另一张图作为节点放置:展开后其节点以 name. 前缀加入;它的 input 由父图连线,param 由父图赋值
times = N; + carry port <- output; 节点或子图放置 N 次(name/1…name/N),从第二次起 carry 把上一次的输出喂给端口;name.port 指最后一次。N 可以是字面量或图参数——改参数会让运行中的场景改变结构
node zeros: Rows<Float> { domain = 65; } 填充节点:一个 Field,而不是 Kernel

examples/fluid 用 times 放置 Jacobi 压力求解;examples/mipmap 用 carry 生成各级 mip。

  • async = true;:节点在自己的线程上离帧运行。完成前读者拿到它上一次的输出,它通过 <~ 喂的状态保持不变;发现它完成的那次求值读取新输出、重跑读者并提交。一次只运行一个;开始时复制输入。回放的 settle 和协议 evaluate 的 settle: true 会等待所有运行完成。
  • awake = value(source, row);:该行为 0 时节点休眠,不运行,只被它读取的节点也不运行。一个“标题画面 + 角色创建 + 世界”合在一个场景里的游戏,可以让不显示的两部分休眠。
  • param 是图参数,调用者可以覆盖(--param name=value、回放 param、协议 param)。节点自己的值属于文档,只能通过 compose 修改。
  • require name: a <= b; 声明参数必须满足的条件(比较两个计数),加载时和每次求值前检查,不满足时求值在任何东西运行前停止,并说明两边各算出了什么。例如 physics 包:require cells: columns * rows <= 1 << bits;。

图计算结构,从不计算值。(docs/DECISIONS.md)

节点的值是写出来的,domain 和 times 可以由结构推出,但任何数值计算都属于 Kernel。这让图保持可读、可编辑,Agent 修改结构时也不会意外引入计算。