跳到主要内容

数据截至 (上游 commit 118e812b316f)

tinygrad — 架构与原理

30 秒导读: tinygrad 是一个小到能读完的深度学习框架(本 commit 全包约 2.4 万行 Python)。它对外是 PyTorch 风格的 eager Tensor API,对内是一台完整的编译器:你写的每个算子只是在建一张 UOp 图,.realize() 时调度器把图切成若干 GPU kernel,再逐 kernel 生成 PTX/Metal/LLVM 等源码并编译执行。它站在 micrograd(教学玩具)与 PyTorch(工业巨兽)之间——能真的训练 LLaMA,也真的能从第一个文件读到最后一个。


1. 这是什么(零基础也能懂)

一句话定义

tinygrad 是一个带自动微分的懒求值张量库 + 面向多种加速器(NVIDIA/AMD/Metal/CPU/WebGPU…)的 kernel 编译器,外加跑真实训练所需的 nn/optim/数据集。

它要解决谁的什么问题

设想你想搞清楚「一个深度学习框架到底由什么组成」:

  • PyTorch 有数百万行 C++/CUDA,读不完;
  • micrograd 只有一百多行,但只支持标量,离真实框架太远;
  • TVM/XLA 只是编译器,不管前端;JAX 的 IR 又藏得很深。

tinygrad 的回答是:用行数预算把「完整」压缩到「可读」sz.py 是它自带的行数统计脚本,仓库文化就是给框架本体设行数上限——所以它被迫把每个机制都做到本质。

它能做什么

能力具体支持
Tensor + autogradeager API、广播、符号化 shape(Variable)、符号化反向传播(tinygrad/tensor.pytinygrad/mixin/)
编译器UOp IR、图重写、kernel 融合、BEAM 自动调优、TensorCore(tinygrad/uop/tinygrad/schedule/tinygrad/codegen/)
后端NV(自研用户态驱动)、AMD、CUDA、Metal、CPU(clang/LLVM)、OpenCL、WebGPU、Qualcomm DSP(tinygrad/runtime/)
训练栈nn.Linear/Conv2d/BatchNorm、SGD/Adam、fused optimizer、MNIST 数据集(tinygrad/nn/)
JIT/图执行@TinyJit 捕获-重放、设备图(类 CUDA Graph)(tinygrad/engine/jit.py)
多设备Tensor.shard 张量并行、allreduce(tinygrad/schedule/multi.pytinygrad/schedule/allreduce.py)
模型examples 里有 GPT-2、LLaMA、Stable Diffusion、Mixtral、olmoe 等(examples/)

用起来什么样

README 里的官方最小示例——一个手写 matmul,靠「懒」融成一个 kernel:

from tinygrad import Tensor

N = 1024
a, b = Tensor.empty(N, N), Tensor.empty(N, N)
# reshape + broadcast 乘 + sum,三次调用都不计算,只是在建图
c = (a.reshape(N, 1, N) * b.T.reshape(1, N, N)).sum(axis=2)
c.realize() # 到这一行才调度、编译、执行;DEBUG=4 能看到生成的 kernel 源码

训练体验则是 PyTorch 味(摘自 examples/beautiful_mnist.py:18-29,它把 __call__ 标成 @function、把训练步标成 @TinyJit):

class Model:
@function # 整个前向变成一个可参数化的 CALL
def __call__(self, x:Tensor) -> Tensor: return x.sequential(self.layers)

@TinyJit # 第二次调用起直接重放 kernel 序列
@Context(TRAINING=1)
def train_step(self, X_train:Tensor, Y_train:Tensor) -> Tensor:
opt.zero_grad()
loss = self(X_train[samples]).sparse_categorical_crossentropy(Y_train[samples]).backward()
return loss.realize(*opt.schedule_step())

一句话直觉

把 tinygrad 想成「张量界的 Lisp」: 一切(张量、算子、梯度、kernel、甚至模式规则本身)都是同一种 S-表达式式的 UOp 树;框架做的每一件事——建图、求导、融合、优化、生成代码——都是对这棵树做模式匹配重写


2. 顶层全景(它大概怎么转)

2.1 主线结构

怎么读这张图:自上而下是一次 .realize() 的数据流;每个方框是框架的一层,标注了对应的源码目录。

Tensor API(mixin 算子,PyTorch 味)
│ 每个算子只建图,不算

UOp 懒计算图 ──── realize() ────► callify:整张图包成一个 CALL
│ (输入 buffer 换成 PARAM)
▼ schedule/(rangeify:融合 + 切 kernel + 依赖排序)
LINEAR:一串 kernel CALL
│ 一个 CALL = 一个 kernel = SINK(AST) + buffer 列表
▼ codegen/(逐 kernel:优化 → lowering → 渲染 → 编译)
PROGRAM(源码 SOURCE + 二进制 BINARY)

▼ engine/realize.py run_linear:逐个 CALL 派发
设备执行层(runtime/ops_*.py:GPU/CPU/…)

2.2 部件职责

部件干什么在哪个文件
Tensor用户 API,只是 uop/grad/is_param 三个字段的薄壳tinygrad/tensor.py:270
mixin 算子库elementwise/movement/reduce/创建等全部算子,实现为建 UOptinygrad/mixin/op.pytinygrad/mixin/elementwise.py:12
UOp / Ops统一 IR:一个 op 码 + dtype + src 子节点 + argtinygrad/uop/ops.py:238tinygrad/uop/__init__.py:13
PatternMatcher / UPat声明式重写规则与模式语言,全框架的驱动方式tinygrad/uop/ops.py:1474:1326
compute_gradient符号化 autograd:对 UOp 图逐节点套梯度规则tinygrad/mixin/gradient.py:100
transform_to_call「callify」:把张量图包成一个参数化 CALLtinygrad/tensor.py:220
run_rangeify / get_kernel_graph融合决策:movement op 变索引算术,决定哪些中间结果落地成 buffer,切成 kerneltinygrad/schedule/indexing.py:188tinygrad/schedule/rangeify.py:378
create_schedule按 RAW/WAR 依赖把 kernel 拓扑排序成 LINEARtinygrad/schedule/__init__.py:28
full_rewrite_to_sink单 kernel 的约二十步 lowering(展开 reduce、加 local 内存、加 GPU 维度、反量化分解…)tinygrad/codegen/__init__.py:286
Scheduler / beam_searchkernel 级优化:轴类型变换(UPCAST/LOCAL/UNROLL/TC)与自动搜索tinygrad/codegen/opt/postrange.py:14tinygrad/codegen/opt/search.py:111
Renderer 家族把线性化后的 UOp 渲染成 C 系源码 / PTX / LLVM IR / WGSL / NIRtinygrad/renderer/cstyle.py:120tinygrad/renderer/ptx.py:137
Device / Buffer / Allocator / Compiled设备发现、显存分配与缓存、编译与加载tinygrad/device.py:15:102:250:343
run_linear / pm_exec执行引擎:编译 LINEAR 并逐 CALL 派发tinygrad/engine/realize.py:320:299
TinyJit / GraphRunner捕获 kernel 序列,之后按 key 重放;可拼成设备图tinygrad/engine/jit.py:214:89
Optimizer参数分组、fused optimizer(所有参数拼成一根向量一次更新)tinygrad/nn/optim.py:7

2.3 主线走一遍:(a @ b).sum().realize() 发生了什么

  1. 建图(不算)。 a @ bTensor.matmul → dot(tinygrad/mixin/op.py:391:367):reshape/转置/乘/sum 全经 Tensor._apply_uop(tinygrad/tensor.py:330)拼成一张 UOp 图。此刻没有任何计算。
  2. callify。 .realize()(tinygrad/tensor.py:416)先调 transform_to_call(tinygrad/tensor.py:220):给图里要落地的值分配输出 buffer,把输入 buffer 换成 PARAM,整张图变成一个 CALL
  3. 调度。 create_linear_with_vars(tinygrad/schedule/__init__.py:181)对每个图跑 prepare_rangeifyget_kernel_graph(tinygrad/schedule/rangeify.py:378)→ create_schedule(tinygrad/schedule/__init__.py:28):reshape/广播等 movement op 被消成索引算术(RANGE),matmul 与 sum 融合,切成一个(或几个)kernel,按依赖排序成 LINEAR
  4. 编译。 run_linear(tinygrad/engine/realize.py:320)先 lower_and_compile(tinygrad/engine/realize.py:268):每个 kernel 的 SINK 经 to_program(tinygrad/codegen/__init__.py:506)走完 lowering、轴优化(可能 BEAM 搜索)、渲染,得到带源码和二进制的 PROGRAM;多个 kernel 用进程池并行编译。
  5. 执行。 pm_exec(tinygrad/engine/realize.py:299)按 CALL 的 ast 类型派发:PROGRAMexec_kernel(绑定 buffer、发射 global/local 尺寸),跨设备 COPYexec_copy
  6. 回填。 输出 buffer 的 UOp 写回所有存活 Tensor(_apply_map_to_tensors,tinygrad/tensor.py:244),之后 c.numpy() 就能直接读数。

这条线最值得记住的一点: 除了第 5 步,一切都是纯 Python 的图变换;设备只在最后被碰到。这就是 tinygrad「前端、调度器、lowering、执行」四段论(它自己的 docs/developer/developer.md 也这么分)。


3. 阅读地图(建议顺序)

五章由浅入深。时间有限读 01 → 03 → 04 就能抓住 tinygrad 的全部要害:它怎么表示计算、怎么把计算切成 kernel、怎么把 kernel 变成机器码。

顺序章节讲什么适合谁
101-tensor-and-autograd.mdTensor 薄壳、算子建图、符号化 autograd、@function所有人必读,框架的「形状」
202-uop-and-rewrites.mdUOp/Ops、interning、UPat 模式语言、graph_rewrite 引擎想读懂任何一处源码的人(重写无处不在)
303-schedule-kernels.mdcallify、rangeify 融合、切 kernel、依赖排序、内存规划关心「融合是怎么决定的」的人
404-codegen-renderer.mdlowering 流水线、轴优化/BEAM/TensorCore、渲染器关心性能与 GPU 代码生成的人
505-device-runtime-jit.mdDevice/Buffer/Allocator、后端、TinyJit、多设备要接新硬件或部署的人

4. 巧妙之处(可借鉴的技术)

  • 自我设限的行数预算。 仓库自带行数统计 sz.py,本 commit 的 tinygrad/ 包合计约 2.4 万行。预算逼着作者把每个概念做到最小:autograd 没有 Function 子类体系,渲染器没有 visitor 类体系,全没了。
  • 一切皆图重写。 建图、求导、融合、lowering、渲染,全用同一个 PatternMatcher + graph_rewrite(tinygrad/uop/ops.py:1474:1753)。学会一套机制,读懂整个框架。
  • UOp 全局 interning。 UOpMetaClass.ucache(tinygrad/uop/ops.py:199-200)保证「同构子图即同一对象」:相等比较是指针比较,缓存键是 O(1) 的;UOp.key(tinygrad/uop/ops.py:269-270)再给出内容哈希,调度缓存(tinygrad/schedule/__init__.py:118)和编译缓存(tinygrad/codegen/__init__.py:505)都建在它的上面。
  • 符号化 autograd。 梯度不是预先写死的 backward 函数,而是对 UOp 图现场套 pm_gradient 规则生成的另一张 UOp 图(tinygrad/mixin/gradient.py:55)——反向图自动享受和前向完全相同的融合、优化与编译。这是 JAX 式「IR 上的 AD」。
  • movement op 永不搬数据。 reshape/permute/expand/pad/shrink/flip 只是给「坐标」做变换;rangeify 时它们被消成对 RANGE 变量的算术(apply_movement_op,tinygrad/schedule/indexing.py:169),连 reshape 合并都成了符号化简问题。
  • 优化 = 改轴的类型标签。 GPU 优化不改写代码,而是把某个 RANGE 的 AxisType 从 WEAK 改成 GLOBAL/LOCAL/UPCAST/UNROLL/GROUP_REDUCE(Scheduler.shift_to,tinygrad/codegen/opt/postrange.py:90)。BEAM 搜索的空间就是「轴类型赋值」的空间。
  • 权重文件即 Tensor。 Tensor(pathlib.Path) 直接建一个 DISK tensor(tinygrad/tensor.py:286),加载模型就是 mmap + 按需拷贝,不需要单独的「反序列化」层。
  • 可视化内建。 VIZ=1 会把每一步 graph_rewrite 的前后对比存下来,用 tinygrad/viz/serve.py 起服务逐步回放——调试编译器的一等工具。

5. 边界与局限

诚实清单,大部分能从代码或 README 直接读出:

  • API 不稳定。 框架处于快速重写期(本 commit 刚完成向 rangeify 调度器的迁移),内部接口几乎每个版本都变;源卡片也把它列为 gotcha。
  • 纯 Python 前端开销。 调度与重写都在 Python 里跑,小 kernel 密集时 CPU 端会成为瓶颈;缓解手段是调度缓存 SCACHE(tinygrad/helpers.py:282)、并行编译进程池(tinygrad/engine/worker.py)、@TinyJit 重放、@function(precompile=True)
  • 功能取舍。 README 自述「fewer functional transforms (no full vmap/pmap yet)」;没有类似 autograd 的高阶梯度生态。
  • 正确性靠 SPEC 而非类型系统。 UOp 图的合法性由 tinygrad/uop/spec.pySPEC 开关下运行时校验(tinygrad/uop/ops.py:206-214),不是静态保证。
  • 后端成熟度参差。 NV/AMD 是重点投入的自研用户态驱动;runtime/ 里其余后端(OpenCL、DSP、WEBGPU 等)能力不一,ops_null.py/ops_python.py 只是参照实现。
  • 动态 shape 是「够用」级别。Variable 符号化支持(如变长序列),JIT 重放时按 var_vals 重算发射尺寸(tinygrad/engine/jit.py:137-142),但不是 JAX 那样的一等抽象。
  • 行数预算的代价。 代码有时「dense and clever rather than obvious」(源卡片原话)——单行里塞多个语义,初读需要适应。

6. 横向对比

维度microgradtinygrad(本篇)pytorch(inferred:并行写作中)
计算单元标量 Value任意形状 Tensor(UOp 图)Tensor(ATen)
求值立即(eager)懒,.realize() 触发编译eager + 可选 torch.compile
autograd每节点存 _backward 闭包符号化:对 IR 套规则生成反向图每算子 C++ BackwardFunction
执行Python 循环融合成 kernel,编译到 GPU/CPU预编译算子库 + 图编译器(Inductor)
规模~150 行~2.4 万行数百万行

一句话:micrograd 教你「autograd 是什么」,tinygrad 教你「真实框架比它多出来的每一样东西——shape、广播、设备、融合、codegen——最少需要多少代码」。

7. 代码地图(入口级)

每章末尾有细粒度地图,这里只列从零开始读源码的六个入口:

主题文件路径符号名
Tensor 与建图tinygrad/tensor.pyTensorTensor._apply_uopTensor.realizetransform_to_call
autogradtinygrad/mixin/gradient.pypm_gradientcompute_gradient
IR 与重写引擎tinygrad/uop/ops.pyUOpOpsUPatPatternMatchergraph_rewrite
调度器tinygrad/schedule/__init__.pytinygrad/schedule/rangeify.pycreate_linear_with_varsget_kernel_graphcreate_schedule
编译器tinygrad/codegen/__init__.pyfull_rewrite_to_sinkto_programdo_linearize
设备与执行tinygrad/device.pytinygrad/engine/realize.pyDeviceBufferCompiledrun_linearpm_exec