结论:tinygrad 把张量图变成可执行的 LINEAR 调用序列,分四步:Tensor.realize() 挑出需要存储的输出 → 图转 CALL → prepare_rangeify()/get_kernel_graph() 定物化边界与内核图 → create_schedule() 按依赖排序。排序必须同时满足 RAW(生产者先于消费者)与 WAR(读旧状态者先于覆盖者),否则可变张量会读到错值。要注意两点边界:调度缓存命中只表示跳过了图准备与排序,并不代表内核已编译或设备内存已分配;内存规划按「首次/末次出现」预测生存期并以 256 字节块放进 arena,是对调度图的推演,不是对设备完成时刻的测量。
从 Tensor.realize() 到内核图:把四步串起来
入口是 Tensor.realize(),它只挑 needs_storage() 为真的输出,再经 linear_with_vars() 调用 transform_to_call()。后者收集待实现的存储、给图加 tag、把已实现输入替换为 PARAM,生成外层 SINK.call(...) 以及原图到新缓冲的映射。调度不只是创建一张临时执行列表——_apply_map_to_tensors() 会更新作用域内引用旧节点的 Tensor,使它们指向新的 UOp。
随后 lower_sink_to_linear() 只处理没有 KernelInfo 且输出已绑定的 SINK 调用;未绑定的 value call 留给前置准备阶段内联。它按 function.key 查进程内的 schedule_cache,未命中时依次执行准备、内核图生成与排序三步。下表把这一段的职责摊开,最后一列是容易被前端忽略的契约。
| 阶段 / 关键函数 | 代码行为 | 硬件或插件契约 |
|---|---|---|
prepare_rangeify()、resolve_function() |
先处理多设备图、移动算子与最早的重写;未绑定输出函数可按参数 slot 内联,检查尺寸和 dtype。 | 动态形状、数据类型与多设备分片不是可忽略的装载细节。 |
run_rangeify() |
建消费者关系,给各节点传播/新建 range;多消费者不兼容或归约、广播边界可触发物化。 | 「一个图节点 = 一个内核」不成立,实际 fusion 与中间缓冲须从生成的内核图核对。 |
get_kernel_graph()、split_store() |
清理符号和临时物化,限制缓冲,转 STORE,把可分割存储封成带 KernelInfo 的 SINK.call。 |
这里只输出候选内核图;具体 ISA、GPU 源码与二进制在后续 codegen 阶段产生。 |
create_schedule() |
从 AFTER 状态构建生产者→消费者的 RAW 与读旧状态→覆盖写入的 WAR 边,按入度排序为 LINEAR;剩余边构成环时抛错。 |
可变 Tensor 的旧值和新值有顺序语义;只按数据流拓扑排序会漏掉覆盖风险。 |
resolve_linear_call() |
把参数 slot 绑定到外层缓冲/符号变量;嵌套 LINEAR 的位置参数覆盖外层同名绑定,未传的新变量可继承外层。 |
多层预编译函数与插件调用必须保持参数作用域及 dtype/尺寸一致。 |
pm_copy_from_store、assert_all_same_devices() |
特定跨设备纯复制模式可转 COPY;普通 SINK 若含多个设备的缓冲会报错。 |
算法插件不能隐式假定计算内核可直接跨设备读写;跨设备传输是独立调度节点。 |
依赖排序:RAW 与 WAR 缺一不可
create_schedule() 从每个 AFTER 提取待执行内核及其前置状态:读某状态的内核依赖写出该状态的内核,这是 RAW;而后续覆盖同一旧状态的写入还必须排在既有读者之后,这是 WAR。排序阶段生成的是 k.body.call(*buf_uops) 调用,不是立即执行。如果依赖图有环,函数抛 RuntimeError,不会返回一个「尽力而为」的顺序。
之后 create_linear_with_vars() 解析嵌套调用与 bound Variables,仅保留调度实际用到的变量值,并对同名值冲突报错。这里有一个分叉:处于 JIT 捕获且 CAPTURING 开启时,它把 LINEAR 加入捕获器并返回空列表;只有非捕获路径才会调用内存规划。也就是说,「用 JIT 跑一遍」和「真正规划内存并执行」不是同一条路径。
COPY 与内存规划:lane 分离与 256 字节 arena
memory_plan_rewrite() 按 LINEAR 中内部缓冲的首次/末次出现估算生存期,以 256 字节块把可规划缓冲放进同设备、同 lane 的 arena。它排除传入的 held buffers 与 DISK/CL/WEBGPU 缓冲,把 COPY 缓冲和计算缓冲放在不同 lane,并为 COPY 延长释放时点,以减少因复用引入的依赖。
这一层的措辞很重要:它按调度图预测生存期,不是对设备驱动的实际完成时刻做测量。因此 arena 复用依赖调度顺序与同步是否被正确尊重——不要把内部视图当成独立分配,也不要把它当成可以长期持有的外部指针。
| 边界 | 源码语义 | 误读风险 |
|---|---|---|
| 缓存命中 | SCACHE 控制是否读写 schedule_cache,命中即跳过图准备与调度 |
命中不代表内核已编译或设备内存已分配 |
| 图校验 | SPEC 只在未命中分支验证 Tensor 图 |
缓存命中本身不重复该检查 |
| COPY 延长 | 为 COPY 缓冲延长释放时点以降低复用依赖 | 这是减少依赖的手段,不等于已同步设备完成 |
| 可变 Tensor | 旧值/新值有顺序语义,WAR 边防止读者被覆盖 | 只按数据流拓扑排序会漏掉覆盖风险 |
多设备不是「把同一张量复制到所有设备」
alu_multi() 尝试在分片上逐设备计算;copy_multi() 可把分片拼接回单设备,或经 allreduce 汇聚到多设备;handle_allreduce() 按设备数、元素数、具体形状以及 RING/ALL2ALL 配置,选择朴素、ring 或 all-to-all 路径。这带来一个明确的插件契约:新硬件若只实现单设备复制,并不能宣称支持这些分片与聚合语义。跨设备传输是独立调度节点,普通计算内核不能假定可以隐式跨设备读写。
验证边界:缓存用例通过,视图与多张量用例未跑
本机执行 python3 -m unittest test.null.test_schedule_cache -v,两个缓存/变量绑定用例通过。但尝试导入 test.null.test_schedule.TestContiguous 时,依赖模块因环境缺少 NumPy 而失败,视图用例并未运行;test/null/test_multitensor.py 也未运行。因此还须以实际模型产物比对调度内核数、COPY 次数、中间峰值内存与跨设备同步。硬件插件另需验证缓冲别名、重连后视图失效、设备完成信号与最坏时延——调度成功不等于车上可用。
常见问题
调度缓存命中,是否说明内核已经编译好了?
不是。缓存命中只表示跳过了图准备与调度两步,源文档明确它并不意味后续内核已编译或设备内存已分配。此外 SPEC 只在未命中分支验证 Tensor 图,缓存命中本身不重复该检查。
为什么调度要做 WAR 依赖,而不只做数据流的 RAW?
因为可变 Tensor 的旧值和新值有顺序语义。create_schedule() 除了 RAW,还让后续覆盖同一旧状态的写入排在既有读者之后;只按数据流拓扑排序会漏掉覆盖风险。依赖图有环时函数直接抛 RuntimeError,不返回「尽力而为」的顺序。
内存规划里的 256 字节块是实际分配单位吗?
它是内存规划阶段用于把可规划缓冲放进同设备、同 lane arena 的起始粒度。规划按 LINEAR 中内部缓冲的首次/末次出现估算生存期,是对调度图的预测,不是对设备驱动实际完成时刻的测量,因此 arena 复用依赖调度顺序与同步是否被正确尊重。
硬件只实现单设备复制,能声称支持多设备分片吗?
不能。alu_multi() 做分片逐设备计算、copy_multi() 做拼接或 allreduce、handle_allreduce() 按设备数与形状选朴素/ring/all-to-all 路径。源文档明确:新硬件若只实现单设备复制,并不能宣称支持这些分片和聚合语义。
延伸阅读
- tinygrad 执行引擎与设备后端 —— 承接调度之后的设备提交阶段。
- tinygrad GPU 内存分配器与页表归属 —— 解释调度层 arena 之外的真正分配归属。
- tinygrad 代码生成:expand/reduce 与去向量化的屏障契约 —— 承接内核图之后的 codegen 阶段。
- tinygrad 的 JIT 捕获与回放 —— 对照捕获路径与非捕获路径为何走不同分支。
- VisionIPC 缓冲契约 —— 缓冲所有权与生存期在另一条链路上的同类问题。
参考资料
- 项目 02 ——
openpilot-research/TINYGRAD_TENSOR_SCHEDULING_MEMORY_DEEP_DIVE.md(取用了张量图到 LINEAR 的四步、RAW/WAR 依赖、COPY 与 256 字节内存规划、多设备边界与验证结果)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复