编程 tinygrad 深度解剖:一套算子、ShapeTracker 与惰性图,凭什么用几千行代码把 PyTorch 的活干了

2026-07-24 04:46:04 +0800 CST views 6

tinygrad 深度解剖:一套算子、ShapeTracker 与惰性图,凭什么用几千行代码把 PyTorch 的活干了

「PyTorch 有 200 万行,tinygrad 目标是 1 万行以内还能跑通 LLaMA。」

第一次读到 tinygrad 这句 slogan 的时候,我是不信的。深度学习框架这东西,在我印象里就是"重"的代名词——CUDA kernel、cuDNN、autograd 引擎、算子库、分发调度……随便哪一块拎出来都是几十万行的工程量。一个号称"介于 PyTorch 和 micrograd 之间"的框架,凭什么用几千行代码同时支持 CPU、CUDA、Metal、AMD、WebGPU 这么多后端,还能真的训练模型?

带着这个怀疑,我把 tinygrad 的源码翻了个底朝天。翻完之后我的结论是:tinygrad 不是"精简版 PyTorch",它是一套彻底不同的抽象哲学。它把整个深度学习计算世界压缩成了极少数几类原子操作,然后靠一个惰性计算图 + 编译器把这些原子重新组装成高性能 kernel。这篇文章我想带你把这套哲学从头拆一遍——它到底怎么做到的,代码长什么样,性能从哪来,以及它对我们理解现代 ML 框架有什么启发。

这不是一篇 API 教程。如果你想学 Tensor.matmul 怎么调,看官方文档五分钟就够了。这篇文章想讲的是底层机制:一个 a + b 从 Python 层写下去,到最后变成一段可以在 GPU 上跑的代码,中间到底发生了什么。


一、先说清楚:tinygrad 解决的是什么问题

要理解 tinygrad 的设计,得先理解它在跟谁较劲。

传统框架(PyTorch/TensorFlow)的算子模型是**"胖算子":每一个高层操作——卷积、LayerNorm、Softmax、Attention——背后都对应一个(或多个)手写的、高度优化的 kernel。为什么快?因为 NVIDIA 的工程师用汇编级的功夫把 cuDNN 的卷积调到了极致。为什么重?因为每支持一种新硬件、每加一种新算子、每来一种新数据类型,你都得重新写一遍 kernel。算子的数量是乘法级增长**的:算子种类 × 硬件后端 × 数据类型 × 数据布局。这就是为什么 PyTorch 的 ATen 有几千个 kernel。

tinygrad 的赌注恰恰相反:"瘦算子" + 编译器。它认为所有的深度学习计算,本质上都可以被分解成极少数几类原子操作。你不需要为卷积写一个 kernel,你只需要把卷积表达成这些原子操作的组合,然后让一个编译器把这堆原子重新融合(fuse)成高效的 kernel。

这套思路的核心算子只有几类(tinygrad 里叫 Ops,历史上也被称作 LazyOp):

  • 一元运算(UnaryOps)EXP2LOG2SINSQRTRECIPNEG 等。逐元素、一个输入。
  • 二元运算(BinaryOps)ADDMULMAXCMPLTMOD 等。逐元素、两个输入。
  • 规约运算(ReduceOps)SUMMAX。沿某些轴把维度"压扁"。
  • 移动运算(MovementOps)RESHAPEPERMUTEEXPANDPADSHRINKFLIP不碰数据本身,只改变对数据的"看法"。这是 tinygrad 最精妙的一块,后面详说。
  • 三元/条件(TernaryOps)WHERE(也就是 select)、MULACC(乘加,用于 matmul)。
  • 加载/存储 & Buffer 管理:负责跟真实内存打交道。

就这么点东西。你可能已经开始怀疑了:Softmax 怎么办?Conv2d 怎么办?matmul 怎么办?

答案是:全都用上面这些算子拼出来。这不是理论上的可能性,而是 tinygrad 实实在在的实现方式。我们一个个看。


二、惰性求值:tensor 不是"值",而是"计算配方"

理解 tinygrad 的第一个关键认知转变是:在 tinygrad 里,一个 Tensor 大部分时候并不持有任何真实数据。它持有的是"如何计算出这份数据"的配方。

看这段代码:

from tinygrad import Tensor

a = Tensor([1.0, 2.0, 3.0])
b = Tensor([4.0, 5.0, 6.0])
c = a + b
d = c * 2
# 到这里为止,GPU / CPU 上一次计算都没发生
print(d.numpy())  # 只有到这一刻,才真正开始算

前面四行 Python,d 并没有被计算出来。tinygrad 在内部构建了一张计算图(历史上是 LazyBuffer,现在统一到 UOp 图):

d = MUL(ADD(a, b), 2)

只有当你调用 .numpy().item().realize() 这类"逼它交出真实数据"的操作时,tinygrad 才会触发所谓的 realize(物化):把这张图交给调度器和编译器,生成 kernel,真正跑一遍,产出内存里的数字。

这为什么重要?因为惰性求值给了框架"全局视野"。当你一次性把 ADDMUL 都攒在图里,编译器就能看到:"哦,这两个逐元素操作可以合并进同一个 kernel,一次遍历内存就把加法和乘法都做了,不用来回读写两次显存。" 这就是 kernel fusion(算子融合),是 tinygrad 性能的最大来源之一。

对比一下 eager 模式(PyTorch 默认):a + b 立刻算出一个中间张量写回显存,* 2 再读回来算一遍再写回去。两次 kernel 启动、两次显存往返。对于访存密集(memory-bound)的逐元素操作,这个开销是致命的。tinygrad 的惰性图天然地把这些操作 fuse 成一个 kernel。

我们可以用环境变量把这个过程"照出来":

DEBUG=4 python -c "
from tinygrad import Tensor
a = Tensor.rand(1024, 1024)
b = Tensor.rand(1024, 1024)
(a + b + 1).realize()
"

DEBUG=4 会把 tinygrad 生成的 kernel 源码直接打印出来。你会看到,a+b+1 这三个操作被融合进了一个 kernel,里面是一条 out[i] = a[i] + b[i] + 1.0 之类的语句——一次读,一次写,中间的临时结果全在寄存器里流转,根本不落显存。这就是编译器的价值。


三、ShapeTracker:零拷贝的"视图魔术"

如果说惰性图是 tinygrad 的骨架,那 ShapeTracker 就是它最反直觉、也最优雅的一块肌肉。

先抛个问题:reshapepermute(转置)、expand(广播)、pad(补零)这些操作,在 PyTorch 里有的是零拷贝(view),有的要复制内存(比如 contiguous 之后的 permute)。tinygrad 的野心是——所有的 movement 操作全部零拷贝。它是怎么做到的?

答案是 ShapeTracker:它把"数据在内存里怎么放"和"你想怎么看这份数据"彻底解耦。真实数据永远是内存里一段扁平的一维 buffer,ShapeTracker 维护的是一组 View,每个 View 记录 shape(形状)、strides(步长)、offset(偏移)、以及可选的 mask(用于 pad/shrink)。

要访问逻辑坐标 (i, j, k) 处的元素,ShapeTracker 会算出它在物理一维 buffer 里的真实索引:

物理索引 = offset + i*stride_0 + j*stride_1 + k*stride_2

这套 strides 的玩法其实 NumPy 老早就有了。tinygrad 的突破在于:它把任意复杂的 movement 操作链,也表达成对 View 的变换,并且能把多个 View 合并(或在不能合并时叠加)成一个可计算的索引表达式。

我们直观感受一下。转置(permute)一个矩阵,物理内存一个字节都不动,只是把两个维度的 stride 交换:

from tinygrad import Tensor

x = Tensor.rand(3, 4)
y = x.permute(1, 0)   # 逻辑上是 (4, 3)
# 物理 buffer 没变,只是 ShapeTracker 里 shape 变 (4,3),strides 交换了

expand(广播)更妙——把某个维度的 stride 设成 0。stride=0 意味着"不管这个维度的下标怎么变,物理索引都不动",于是同一份数据被"看成"了重复很多份,全程零拷贝零内存放大:

a = Tensor([1.0, 2.0, 3.0])          # shape (3,)
b = a.reshape(3, 1).expand(3, 4)     # 逻辑上 (3,4),每行重复4次
# b 底层还是那3个数,第二维 stride=0

pad(补零)则靠 mask:在 View 上标记哪些逻辑坐标是"有效数据",哪些是"越界的应该返回 0"。访问时先查 mask,越界就直接产出 0,不需要真的分配一块补了零的大内存。

这套机制真正的杀伤力体现在卷积上。tinygrad 没有卷积 kernel。它的 Conv2d 是这么实现的:用一连串 movement 操作(reshape + pad + expand + permute + shrink)把输入图像"变形"成一个巨大的、带重叠的滑动窗口视图,然后对这个视图做一次 MUL + SUM 规约——卷积就变成了逐元素乘 + 规约。而这一串 movement 全是 ShapeTracker 上的 stride 变换,零内存拷贝。最后编译器把整条链融合成一个 kernel。

这就是"瘦算子"哲学的威力:卷积不是一个需要单独实现的东西,它是几个原子算子在 ShapeTracker 加持下的组合结果。你为一个后端实现了 MULSUM 和索引计算,你就免费得到了这个后端上的卷积。


四、从图到代码:UOp、调度器与 pattern matcher

前面讲的都是"用户视角"的抽象。现在下沉到编译器内部,看看那张惰性图是怎么变成真实代码的。

tinygrad 近年来做了一次重要的架构统一:把过去分层的 LazyBuffer / LazyOp / UOp 统一成了单一的 UOp 图。UOp(micro-operation)是整个系统的中枢数据结构,从最上层的 tensor 运算到最底层的 ALU 指令、循环、内存加载,全部用同一种 UOp 节点表达。一个 UOp 长这样(概念上):

class UOp:
    op: Ops          # 这个节点是什么操作:ADD / LOAD / RANGE / ...
    dtype: DType     # 数据类型
    src: tuple[UOp]  # 它的输入(也是 UOp)
    arg: Any         # 附带参数,比如常量值、轴信息、ShapeTracker

整个计算就是一张由 UOp 组成的 DAG(有向无环图)。编译流程大致分这么几步:

1) Schedule(调度):调度器遍历这张大图,决定"哪些操作应该被切进同一个 kernel"。核心决策就是 fusion 边界——逐元素操作尽量往前融合,规约操作(reduce)通常会成为一个 kernel 的边界,因为规约需要跨元素同步。调度器输出的是一组 ScheduleItem,每个对应一个待生成的 kernel,以及它读写哪些 buffer。

2) Lowering(下降):把每个 kernel 对应的 UOp 子图,从"数学表达"下降到"带循环和索引的命令式表达"。这里会把逻辑上的多维张量运算,展开成显式的 RANGE(循环)、LOAD/STORE(访存,索引由 ShapeTracker 算出)、以及 ALU 运算节点。

3) Rewrite(重写/优化):这是 tinygrad 最有代码美学的一块——基于 pattern matcher 的图重写

tinygrad 的优化不是一堆写死的 if-else,而是一套声明式的重写规则集(PatternMatcher)。每条规则是一个 (模式, 重写函数) 对:匹配到符合某个结构的 UOp 子图,就把它替换成更优的形式。比如常量折叠、代数化简、消除冗余的 movement:

# 概念示意(并非逐字源码):x * 1 => x ;  x + 0 => x
from tinygrad.ops import PatternMatcher, UPat, Ops

simplify = PatternMatcher([
    # 乘以 1 直接消掉
    (UPat(Ops.MUL, src=(UPat.var("x"), UPat.cvar(1))), lambda x: x),
    # 加 0 直接消掉
    (UPat(Ops.ADD, src=(UPat.var("x"), UPat.cvar(0))), lambda x: x),
])

UPat 是"UOp 模式",描述你想匹配的子图结构;后面的 lambda 收到匹配出的变量,返回替换后的新 UOp(返回 None 表示不改)。整个优化器就是把成百上千条这样的规则反复应用到图上,直到不动点(没有规则再能改动图)。

这套设计的好处极其明显:加优化 = 加规则。你想支持一个新的代数化简、一个新硬件的特殊指令融合,不需要动核心调度逻辑,只要往 PatternMatcher 里塞一条新规则。规则本身是纯函数、可组合、可单独测试。这跟传统编译器里散落各处的 peephole 优化比起来,工程上干净太多了。

4) Codegen(代码生成):优化后的 UOp 图已经是"命令式"的了——有循环、有加载、有算术、有存储。最后一步是把它渲染(render)成目标后端的源代码字符串。


五、多后端:一套 IR,N 个 renderer

tinygrad 支持这么多后端(CPU/CLANG、OpenCL/GPU、METAL、CUDA、AMD、NV、WEBGPU 等),而代码量还极小,秘密就在这套统一 IR + 可插拔 renderer/runtime 的架构。

关键在于:后端无关的部分(惰性图、ShapeTracker、调度、绝大多数重写规则、lowering)是所有后端共享的。真正跟硬件相关的只有两件小事:

  • Renderer:把最终的 UOp 图翻译成这个后端的源码。CUDA 后端翻成 CUDA C,Metal 后端翻成 Metal Shading Language,CLANG 后端翻成普通 C,WebGPU 翻成 WGSL。因为 UOp 已经是很低层、很规整的形式(循环 + 索引 + ALU),renderer 基本就是个"字符串模板填空",每个后端可能就几百行。
  • Runtime:负责把 renderer 产出的源码在目标平台上编译成可执行 kernel(比如调 nvrtc 编译 CUDA、调 Metal 编译器),并管理该平台的显存分配、数据拷贝、kernel 启动。同样是一个薄薄的适配层。

于是加一个新后端的成本,被压缩成了"写一个 renderer + 写一个 runtime",而不是"重新实现整个框架"。这就是为什么一个几千行的项目能支持这么多硬件——因为99% 的复杂度是后端无关的,被复用了

想看看不同后端生成的代码差别?换个环境变量就行:

# 用 C 后端跑,并打印生成的 C kernel
CPU=1 DEBUG=4 python -c "from tinygrad import Tensor; (Tensor.rand(8,8)@Tensor.rand(8,8)).realize()"

# 有 Metal 的 Mac 上换 Metal 后端
METAL=1 DEBUG=4 python -c "from tinygrad import Tensor; (Tensor.rand(8,8)@Tensor.rand(8,8)).realize()"

你会看到同一个矩阵乘法,在两个后端下被渲染成了各自语言的、结构高度相似的 kernel(循环 + 累加)。上层图完全一样,只是最后落地的字符串不同。


六、autograd:反向传播只是"图的另一半"

深度学习框架的另一半灵魂是自动微分。tinygrad 的 autograd 同样贯彻了它的极简哲学——它是反向模式自动微分(reverse-mode autodiff),而且因为整个前向计算已经是一张由少数原子算子组成的图,反向传播就变得异常干净:你只需要为每一类原子算子定义它的梯度规则,剩下的靠链式法则自动组合。

你为 ADD 定义梯度(两路都是 1)、为 MUL 定义梯度(各自乘上对方)、为 EXP2/LOG2/SUM/MAX 等定义梯度,那么任何由它们拼出来的复杂函数——Softmax、CrossEntropy、Attention——的梯度就自动有了,因为它们本来就是这些原子的组合。你不需要为 Softmax 单独写反向 kernel。

用起来和 PyTorch 几乎一样:

from tinygrad import Tensor
from tinygrad.nn.optim import SGD

x = Tensor([[1.0, 2.0], [3.0, 4.0]])
w = Tensor.rand(2, 2, requires_grad=True)
b = Tensor.zeros(2, requires_grad=True)

opt = SGD([w, b], lr=0.01)

for step in range(100):
    out = (x @ w + b).relu()
    loss = out.sum()
    opt.zero_grad()
    loss.backward()     # 沿着惰性图反向铺一层梯度图
    opt.step()

print(loss.item())

值得强调的一点:loss.backward() 本身也是惰性的。它并没有立刻算出梯度数值,而是在原有的前向图上,往回追加了一张"梯度计算图"。真正的数值计算,依然要等到 opt.step() 里触发 realize 时,才由调度器和编译器统一处理。这意味着前向和反向可以被一起 fuse、一起优化——又是全局视野带来的红利。


七、TinyJit:把 Python 开销彻底榨干

到这里你可能有个担忧:每次 forward,tinygrad 都要在 Python 里重新构图、调度、(如果没缓存)重新编译,这开销不小啊?对于训练循环这种同一套计算跑成千上万遍的场景,岂不是很浪费?

tinygrad 的答案是 TinyJit。它的思路特别朴素但有效:训练循环里,第一次和第二次跑的时候,它记录下这一轮到底启动了哪些 kernel、用了哪些 buffer、参数怎么传;确认稳定之后,从第三次开始,它就跳过所有 Python 层的构图和调度,直接按记录好的顺序重放(replay)这串 kernel 启动。

from tinygrad import Tensor, TinyJit
from tinygrad.nn.optim import SGD

model = ...  # 你的模型
opt = SGD(model.parameters(), lr=1e-3)

@TinyJit
def train_step(x, y):
    opt.zero_grad()
    loss = model(x).sparse_categorical_crossentropy(y)
    loss.backward()
    opt.step()
    return loss

for x, y in dataloader:
    loss = train_step(x, y)   # 第3次起,Python 开销几乎归零

一个 @TinyJit 装饰器,就把"Python 解释器的构图开销"从热循环里彻底抽走了。这在小 batch、高频迭代的训练里,往往能带来数倍的吞吐提升。它的实现依然是那套 UOp/kernel 抽象的自然延伸——因为一轮计算已经被明确表达成"一串确定的 kernel 启动",重放它是一件很自然的事。

需要注意的是 JIT 的经典约束:被 JIT 的函数里,shape 必须固定(形状一变,之前记录的 kernel 就失效了),也不能有依赖数据值的 Python 控制流。这跟 JAX 的 jit 是同一类约束,理解了原理就不会踩坑。


八、上手实战:训练一个能跑的小网络

光讲原理容易飘,我们落一个能实际跑起来的完整例子——一个极简 MLP 在随机数据上过拟合,把前面讲的惰性图、autograd、优化器、JIT 全串起来:

from tinygrad import Tensor, TinyJit
from tinygrad.nn import Linear
from tinygrad.nn.optim import Adam

class MLP:
    def __init__(self, din, dh, dout):
        self.l1 = Linear(din, dh)
        self.l2 = Linear(dh, dout)
    def __call__(self, x: Tensor) -> Tensor:
        return self.l2(self.l1(x).relu())

def get_params(m):
    # tinygrad 里参数就是带 requires_grad 的 Tensor
    return [m.l1.weight, m.l1.bias, m.l2.weight, m.l2.bias]

model = MLP(16, 64, 4)
opt = Adam(get_params(model), lr=1e-3)

# 造一批固定的随机训练数据
X = Tensor.rand(128, 16)
Y = Tensor.rand(128, 4)

@TinyJit
def step(x, y):
    opt.zero_grad()
    pred = model(x)
    loss = ((pred - y) ** 2).mean()   # MSE,全用原子算子拼出来
    loss.backward()
    opt.step()
    return loss.realize()

for i in range(500):
    loss = step(X, Y)
    if i % 100 == 0:
        print(f"step {i:4d}  loss={loss.item():.4f}")

跑起来你会看到 loss 稳步下降。注意几个细节:

  • LinearAdamrelumean**2 这些"高层"东西,最终全部被拆解成前面那套原子算子,编译成 kernel。
  • 整个 stepTinyJit 包住,从第三步起 Python 开销归零。
  • loss 直到 .item() 才真正把数值取回主机。

想知道这一步到底生成了几个 kernel、每个 kernel 长什么样、访存多少?加 DEBUG=2(看 kernel 统计)或 DEBUG=4(看源码)跑一遍,你会对"你的一行 Python 到底变成了多少 GPU 工作"有非常具体的体感。这种"可观测性"是 tinygrad 作为学习工具的一大优势——它不像 PyTorch 那样把一切藏在 C++ 黑箱里。


九、性能:小框架凭什么不慢

有人会本能地觉得"这么小的框架肯定跑不快"。这个直觉在很多场景下是错的,原因我们前面其实已经铺垫完了,这里系统总结一下 tinygrad 的性能来源:

1) 激进的算子融合。 这是最大的红利。逐元素操作全部融合,减少 kernel 启动次数和显存往返。对访存密集的负载(大量 ML 负载都是访存瓶颈而非算力瓶颈),少一次显存往返就是实打实的加速。

2) 零拷贝的 movement。 reshape/permute/expand/pad 全部只改 ShapeTracker,不搬内存。传统框架里一个不小心的 contiguous() 就是一次全量拷贝,tinygrad 从设计上消灭了这类隐性开销。

3) 编译期特化。 因为 shape 在编译 kernel 时是已知常量,tinygrad 可以把循环边界、索引计算全部特化成常量,生成的 kernel 没有动态 shape 的分支和额外计算。这跟"胖算子"要写通用 kernel 应对各种 shape 是相反的取舍——tinygrad 用"每种 shape 编译一次"换"每个 kernel 都是最优特化版"。

4) 搜索式 kernel 优化(BEAM search)。 对于关键 kernel,tinygrad 能开启 BEAM 搜索:它会枚举一堆优化策略组合(循环分块 tiling、向量化、循环展开 unroll、GPU 的 local/upcast 等),实际编译、实际测速,选出在当前硬件上最快的那个。

# 用 beam search 自动调优 kernel(第一次会慢,因为在搜索,结果会缓存)
BEAM=2 python your_training_script.py

这相当于一个微型的自动 kernel 调优器(autotuner)。它不靠人肉写汇编,而是靠"生成候选 + 真机实测"来逼近最优。搜索结果会缓存,后续直接命中。这是"编译器路线"相比"手写 kernel 路线"的一个长期优势——优化是可以自动化、可迁移到新硬件的,而手写 kernel 每换一代硬件就得重来。

当然要诚实:在 NVIDIA 生态、大 batch、经过多年打磨的标准算子(比如大矩阵乘、标准卷积)上,cuDNN/cuBLAS 这类手工极致优化的库依然是很难被完全超越的天花板。tinygrad 的定位不是"在所有场景碾压 PyTorch",而是"用极小的代码量,在广泛的硬件上,拿到足够好、且随着 BEAM 搜索和规则积累不断逼近极致的性能"。这是两种工程哲学的取舍,不是简单的谁快谁慢。


十、这套设计对我们意味着什么

把 tinygrad 拆完,我最大的收获其实不是"又学了个框架",而是它展示了一种处理复杂度的思维方式,这套思路远不止适用于深度学习框架:

第一,用"少数原语 + 组合"对抗"大量特例"。 深度学习的算子看起来无穷无尽,但 tinygrad 证明了它们可以被压缩成极少数原子的组合。当你面对一个"看起来需要写一百个特例"的系统时,值得停下来想想:这一百个特例背后,是不是藏着五个正交的原语?找到那五个,用组合去覆盖那一百个,工程量和维护成本会是量级的差别。这就是"瘦算子"哲学的普适价值。

第二,把"策略"从"机制"里拆出来,用声明式规则表达优化。 tinygrad 的 PatternMatcher 是这条原则的教科书级示范:核心机制(图重写引擎)是稳定的,具体优化(一条条 rewrite 规则)是可插拔、可组合、可测试的。反观很多系统里,优化逻辑和主流程死死缠在一起,加一个优化要动核心代码、要担心破坏其它路径。把优化做成"数据"(规则)而不是"代码"(散落的 if),系统的可演进性天差地别。

第三,惰性 + 编译带来"全局视野",全局视野带来优化空间。 eager 执行简单直观,但每一步都是局部决策,框架看不到全局,也就没法做跨操作的融合优化。tinygrad 用惰性图攒够上下文,再交给编译器统一决策——这跟数据库的查询优化器、现代 JS 引擎的 JIT 是同一个道理。当你能"晚一点决策",你往往能"决策得更好"。

第四,抽象对了,复用就是免费的。 tinygrad 支持一堆后端却只有几千行,根本原因是它找对了"后端无关"和"后端相关"的分界线——把 99% 的复杂度放在共享层,把硬件相关的东西压缩成薄薄的 renderer + runtime。设计抽象层时,这条分界线画在哪,直接决定了你的系统是"加一个后端要重写一遍"还是"加一个后端只要填几百行"。


结语:小,是一种能力

tinygrad 常被当成"George Hotz 的又一个炫技项目",但我拆完源码后越来越觉得,它其实是一份很严肃的工程论证:深度学习框架不必然是几百万行的庞然大物。当你把抽象选对,复杂度是可以被压缩、被复用、被自动化的。

它不完美——极端场景下比不过 cuDNN,动态 shape 场景对 JIT 不友好,某些算子的融合还在持续打磨。但它提供了一个极其宝贵的东西:一个小到可以被一个人完整读懂的现代 ML 框架。如果你真的想搞明白"一个 tensor 运算从 Python 到 GPU 到底经历了什么",读 PyTorch 的 C++ 源码你会淹死在工程细节里,而读 tinygrad,你能在一个周末里把主干看穿。

从这个角度说,tinygrad 最大的价值也许不是它现在能跑多快,而是它让"理解深度学习框架的底层"这件事,第一次变得对普通工程师真正可及。

对我们这些天天用框架、却很少有机会看清框架内部的人来说,这就够有价值了。有空的话,pip install tinygrad,挂上 DEBUG=4,去看看你写的那行 a + b 到底变成了什么。那种"啊,原来是这样"的瞬间,比读十篇教程都值。

推荐文章

liunx服务器监控workerman进程守护
2024-11-18 13:28:44 +0800 CST
JavaScript 策略模式
2024-11-19 07:34:29 +0800 CST
网站日志分析脚本
2024-11-19 03:48:35 +0800 CST
7种Go语言生成唯一ID的实用方法
2024-11-19 05:22:50 +0800 CST
使用Ollama部署本地大模型
2024-11-19 10:00:55 +0800 CST
程序员茄子在线接单