tinygrad 深度学习框架上手:看懂从张量到 GPU 的完整流水线,以及后端怎么扩展
【免费下载链接】tinygradYou like pytorch? You like micrograd? You love tinygrad! ❤️项目地址: https://gitcode.com/GitHub_Trending/tiny/tinygrad
tinygrad 是一个刻意保持小巧的端到端深度学习框架:它把 PyTorch 风格的张量 API、自动求导,和一条"能从张量一路编译到具体硬件"的流水线打包在一起。对熟悉 PyTorch 的人来说,它更像 micrograd 和 PyTorch 之间的中间地带——既够用,又小到你能把每一层都读明白。这篇文章带新手走查它的两条主线:一条是可拦截的张量流水线(lazy 操作 → 调度 → 融合 → 编译 → JIT),另一条是可扩展的多后端(同一套上层代码跑在 CPU、NVIDIA、AMD、Metal、WebGPU 上)。读完你会知道:怎么把一个小模型跑起来、怎么"拆开"看中间发生了什么、以及想接一块新硬件该动哪里。
tinygrad 是什么:一套能自己"从张量写到硬件"的深度学习栈
先说结论:tinygrad 不是一个只能调用的黑盒库,而是一条你可以逐层打开的链路。它解决的核心问题是"我想看清并改写张量到硬件之间发生的每一步"。它由四块组成:
- 带自动求导的张量库(
tinygrad/tensor.py),负责前向/反向的 API; - 一套 IR 加编译器(
tinygrad/codegen/、tinygrad/schedule/),负责把一堆小操作融合、降级成 kernel; - JIT 与图执行(
tinygrad/engine/jit.py),负责把多次调用捕获、打包、重放; nn / optim / datasets(tinygrad/nn/、extra/datasets),负责真实的训练流程。
它借鉴了 PyTorch 的易用性、JAX 的"基于 IR 的自动求导 + 函数级 JIT"、以及 TVM 的调度与代码生成,但刻意不做全功能,保留"够小、可改"这一点。
这张对比图能帮你建立预期:你写的训练循环会和 PyTorch 很像,但不同之处在于编译器与 IR 全程可见、可 hack。
📌 适合谁看:想理解深度学习框架"内部到底怎么把x.matmul(w)变成一块 GPU kernel"的人,或者想给自己的设备加一个后端的人。不需要你懂 JAX 或 TVM,只需要知道张量和反向传播是什么。
核心机制:张量流水线里每一层都能被拦截和改写
这一章是全文的重点。tinygrad 里"可拦截"这件事,对应的是:你的张量操作默认是惰性的(lazy),只有当它被"落地"(realize)时才真正执行,而落地之前要经过一连串可观察、可改写的层。
具体流程是这样的:
- 你写
z = y.matmul(x).sum(),此时只是在内存里搭一张"操作图",硬件上什么都没发生; - 调用
z.realize()(或z.numpy())时,图被交给调度器拆成一批 kernel(tinygrad/schedule/); - 编译器把这些 kernel 融合、降级成目标代码(
tinygrad/codegen/、tinygrad/renderer/); - 运行时代码把它交给具体设备执行(
tinygrad/runtime/ops_*.py)。
关键在于第 2 到第 4 步之间的每一层都是"可被拦截"的:你能让它停下来看,也能改写它。README 里给了一个最直观的演示——用DEBUG环境变量控制"打开到哪一层":
DEBUG=3 python3 -c "from tinygrad import Tensor; N = 1024; a, b = Tensor.empty(N, N), Tensor.empty(N, N); (a.reshape(N, 1, N) * b.T.reshape(1, N, N)).sum(axis=2).realize()"把DEBUG设成3,你会看到尽管写法是"分开的几步",惰性机制把它们融合进了一个 kernel;再把它改成4,直接能看到最终生成的代码。这就是 tinygrad 里"拦截并观察"的最小动作——不需要读几百行源码,一个环境变量就能让你停在流水线的某一层看个明白。
最小可用示例:用十几行代码跑通一个训练循环
把上面的机制落到代码上。下面是一个能跑的线性分类器,结构上你会觉得眼熟(因为它就该和 PyTorch 长得像):
from tinygrad import Tensor, nn, Context class LinearNet: def __init__(self): self.l1 = Tensor.kaiming_uniform(784, 128) self.l2 = Tensor.kaiming_uniform(128, 10) def __call__(self, x): return x.flatten(1).dot(self.l1).relu().dot(self.l2) model = LinearNet() optim = nn.optim.Adam([model.l1, model.l2], lr=0.001) x, y = Tensor.rand(4, 1, 28, 28), Tensor([2, 4, 3, 7]) # 实际项目换成真实数据加载 with Context(TRAINING=1): for i in range(10): optim.zero_grad() loss = model(x).sparse_categorical_crossentropy(y).backward() optim.step() print(i, loss.item())这段代码做了一件事:前向(model(x))→ 算损失 →backward()求梯度 →optim.step()更新参数,循环往复。几个值得注意的点:
- 模型不需要继承任何基类,
__call__里就是"对张量做一串操作"。函数式操作(如.relu())是方法而不是类,这让网络定义非常直接。 with Context(TRAINING=1)把训练态打开,退出上下文时会自动还原——类似 PyTorch 的model.train(),但走的是全局上下文而不是对象状态。optim.zero_grad()放在循环里,每轮先清零再更新,避免梯度累加。
完整版见examples/beautiful_mnist.py,在 MNIST 上大约 5 秒能到 98% 精度。它能跑真实模型这一点,下面这张图可以直接验证。
后端怎么扩展:一个 ops 接口对应多块硬件
"可扩展"在 tinygrad 里最实在的体现,是同一套上层张量代码,能落到不同的硬件上,而且加新硬件的门槛很低。
设备相关的代码都集中在tinygrad/runtime/下,每个ops_*.py就是一个后端实现:ops_cpu.py、ops_nv.py、ops_amd.py、ops_cuda.py、ops_metal.py、ops_cl.py、ops_webgpu.py、ops_qcom.py等。换句话说,"可拦截流水线"的最后一步(把代码交给设备执行)是被插件化拆开的——上层不知道下面到底是 CPU 还是某块 GPU。
默认跑哪个后端,可以直接问:
python3 -c "from tinygrad import Device; print(Device.DEFAULT)"想强制指定,用DEV环境变量(例如DEV=CPU);不同后端的接口选择也走这个变量。README 里给了一个很有用的数字:一个新加速设备只需要支持大约 25 个底层 op,就能接入 tinygrad。这是它"可扩展"承诺的量化表达——你不用重写整个框架,只补一块"接口实现"。
⚙️ 一个实用提醒:tinygrad 也提供Tensor.from_blob这类零拷贝互操作接口,可以和 PyTorch/OpenCL 的显存直接共享数据。用外部内存指针时要保证指针在整个张量生命周期内有效,否则会读到坏数据——这在docs/runtime.md里有专门说明。
进阶组合:用 TinyJit 和图执行把多次调用打包重放
当模型反复执行同样形状的前向时,tinygrad 可以用 JIT 把开销降下来。用法很简单——给前向包一个装饰器:
from tinygrad import TinyJit @TinyJit def jit(x): return net(x).realize() out = jit(batch) # 第一次调用会捕获,之后重放它背后的逻辑(在tinygrad/engine/jit.py):第一次调用时捕获kernel 序列,之后把输入参数化成槽位、重放整条流水线;如果某批 kernel 落在支持图执行的设备上,还会被合并成一次"图调用"来批量执行。
这里有个和参考架构里"性能监控"对应的思路——tinygrad 提供了几个开关帮你"量"和"看":
VIZ=1:可视化计算图,配合tinygrad/viz/观察流水线到底长什么样;BEAM:在编译阶段对 kernel 做更激进的搜索优化(JITBEAM可单独控制);tinygrad.helpers里的Timing:给一段代码计时,对比"JIT 前 / JIT 后"的加速。
注意 JIT 的边界:它目前不支持输入尺寸变化,也不支持 tinygrad 之外的操作,所以适合形状固定的推理循环,而不是随意切换 batch 形状的训练。
调试与验证:DEBUG、VIZ 和 process replay 怎么用
tinygrad 的验证方式很"工程化",核心是把"改动是否改变了行为"变成可执行的事:
- 逐层打开:
DEBUG=3/4看融合与生成代码,是"调试中间件"式的动作——停在流水线的某一层观察; - 看图:
VIZ=1看计算图,判断融合、重排是否符合预期; - process replay:
test/external/process_replay/会把你的分支生成的 kernel 和 master 对比。如果你做了一个"只改结构、不该改结果"的重构或加速,PR 标题里带上[pr],就能用这套机制确认行为没漂移。
这套机制的价值在于:你不需要"感觉它变快了",而是有工具证明它确实按预期变快了、且没改坏别的。
上面这张图来自test/models/efficientnet/,展示的是"用 tinygrad 跑一个真实视觉模型做分类"这类验证场景的典型产物——这也是test/目录里大量测试在做的事情。
📌 常见疑问,挑几个新手容易踩的:
- 为什么我
print(t)打印出来的不是我预期的值?因为操作是惰性的,print/.numpy()之前张量还没落地。想让它真正执行,用.realize()或.numpy()。 - 为什么 JIT 没加速?检查输入形状是否每次都一样、以及前向里是否混入了 tinygrad 管不到的操作(比如某些 Python 侧的控制流)。
- 加新后端要写多少?大约 25 个底层 op 起步,参考
tinygrad/runtime/ops_python.py(纯 Python 参考实现)最直观。 - 测试怎么跑?装好
.[testing]依赖后,python3 -m pytest test/跑全量,python3 test/backend/test_ops.py只跑 op 测试。
上手路径
给你一条最短路径,避免一上来就啃uop/:
- 先跑通
examples/beautiful_mnist.py,确认环境和默认设备都 OK(用print(Device.DEFAULT)看一眼); - 打开
DEBUG=3/VIZ=1重跑一次小模型,"看见"流水线在做什么; - 把前向套上
@TinyJit,用Timing对比前后耗时,体会"打包重放"的收益; - 想接硬件时,读
docs/runtime.md和tinygrad/runtime/ops_python.py,照着约 25 个 op 的接口补一个自己的ops_*.py。
相关入口都是项目内的真实路径:快速上手看docs/quickstart.md,设备与接口看docs/runtime.md,全部环境变量看docs/env_vars.md,更多模型实现看extra/models,想理解 op 语义看test/里的用例。需要 clone 源码时,仓库地址是https://gitcode.com/GitHub_Trending/tiny/tinygrad,本地python3 -m pip install -e .即可。
一句话收尾:tinygrad 的价值不在于"比谁快",而在于它把"张量 → 硬件"这条链路做成了可逐层拦截、可组合、可验证的结构——你既能当普通框架用它训练,也能把它当成一台透明的机器去拆。
【免费下载链接】tinygradYou like pytorch? You like micrograd? You love tinygrad! ❤️项目地址: https://gitcode.com/GitHub_Trending/tiny/tinygrad
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考