PyTorch 全貌一图:从 Python 到硬件的完整架构解析

PyTorch 全貌一图:从 Python 到硬件的完整架构解析 图片 1

你已经打过类似的话一千遍了。这个系列的存在,是为了让你在结尾时知道这些台词的所有内容。所有这些:他们接触的Python、他们停留的C++、记录的图、选择的内核、使用的内存,以及运行的两个时钟。

这些词在楼下都有明确的含义。

这是第0部分,地图。首先我们快速地穿过所有层次。然后我们划定领地。然后是十二个想法,让代码库的其余部分变得可预测。然后是这个系列的运作方式,以及如何阅读它。

这里没有任何故事能完整呈现。这里的一切都有其位置,每个完整故事都有编号部分等待着。

开始前有个承诺。本系列中的每个测量数字都来自一个你可以自己运行的小脚本,数字出现处就链接了。我是在一台装有 Torch 2.11.0 的 Apple M3 Max 笔记本上测量的 [1].你的数字会有所不同。他们制作的图案 不会。

衰落

PyTorch很有深度。键盘和芯片之间有八个层级。我称它们为地板,这个仪表显示了所有地板。它贯穿整个系列,所以你总能感受到自己有多深。

学习建筑的最快方法是不停地穿过一次。那就是这个部分。

第一层:蟒蛇

第2层(共8层)蟒蛇完整故事:第五部分,机械

torch.randn看起来像是Python函数。问问Python到底是什么:

>>> type(torch.randn)

Python 只给用编译代码写成的函数提供该类型。编译代码指的是:在你安装之前就已经翻译成机器指令的代码,这样里面就没有Python正文可读,也没有调试器可以停下来的那一行。

那么,这些机器指令到底藏在哪里?在共享图书馆里。共享库是程序运行时加载的编译代码文件。它们就放在你光盘的焊炬包里,你可以看它们 (证明):

torch._C -> _C.cpython-312-darwin.so  (49 KB, the loader)
libtorch_cpu.dylib        206.5 MB   (tensors and kernels)
libtorch_python.dylib      28.5 MB   (the python side of the border)

p0_the_library.py校样,随时可阅读或运行

"""Proof: where the compiled part of pytorch actually lives.

torch._C is a thin compiled stub; the weight of the framework is in
the shared libraries next to it. Prints the files and their sizes.
"""
import glob
import os
import torch

stub = torch._C.__file__
print(f"torch {torch.__version__}")
print(f"torch._C -> {os.path.basename(stub)}  "
      f"({os.path.getsize(stub)/1024:.0f} KB stub)")
libdir = os.path.join(os.path.dirname(stub), "lib")
for lib in ["libtorch_cpu.dylib", "libtorch_python.dylib"]:
    p = os.path.join(libdir, lib)
    if os.path.exists(p):
        print(f"{lib:24s} {os.path.getsize(p)/1024/1024:6.1f} MB")

下载并运行

先看一下尺寸,然后再看:

你从 Python 看到的 PyTorch 部分是一个 49 KB 的文件,唯一的任务是加载另外两个文件。真实代码是235MB编译代码。import torch把它带入你的创作过程,之后再打电话torch.randn意味着跳进那具身体。

今天我们只需要知道这些文件的存在。

这是代码库中第一个真正的惊喜:你整天写的Python只是它最小的一层。

边界

第3层,共8层 边界完整故事:第五部分,机械

呼叫立即离开Python。它落在哪里?

在一个名为THPVariable_randn,就在最上一层那个28.5MB的图书馆里。还有一个奇怪的事实:这个函数在PyTorch仓库里并不存在。克隆仓库,搜索名字,却什么都没找到。

程序在构建过程中会写入该函数,同时还会有成千上万个同类程序。下面的想法4解释了原因,第五部分展示了负责写作的程序。

跨越这条边界需要时间。仅要看成本,只需计时最小的运算,几乎没有算术能掩盖它(证明):

add, 1 element   :    0.538 microseconds per call
add, 4M elements :  337.264 microseconds per call

p2_dispatch_cost.py校样,随时可阅读或运行

"""Proof: the fixed cost of one eager op, and why size hides it.

Times the same `a + b` at two sizes. The one-element add is nearly
pure machinery (dispatch, wrapping, allocation); the 4M-element add is
nearly pure arithmetic. CPU, single process.
"""
import time
import torch

def per_op_us(a, b, iters):
    # warmup
    for _ in range(2000):
        a + b
    t0 = time.perf_counter()
    for _ in range(iters):
        a + b
    return (time.perf_counter() - t0) / iters * 1e6

tiny = per_op_us(torch.ones(1), torch.ones(1), 200_000)
big_n = 4_000_000
big = per_op_us(torch.ones(big_n), torch.ones(big_n), 2_000)

print(f"torch {torch.__version__}, cpu")
print(f"add, 1 element   : {tiny:8.3f} us/op")
print(f"add, 4M elements : {big:8.3f} us/op")
print(f"machinery share of the tiny op: ~all of it")
print(f"ops/sec you can issue from python: {1e6/tiny:,.0f}")

# The sweep behind the toll meter: the same add at twelve sizes,
# 1 to 4M elements in powers of four. Every dot on the widget's
# axis is one line of this output.
import json
sweep = []
for k in range(12):
    n = 4 ** k
    iters = max(1_000, min(200_000, 40_000_000 // max(n, 1)))
    us = per_op_us(torch.ones(n), torch.ones(n), iters)
    sweep.append({"n": n, "us": round(us, 3)})
    print(f"add, {n:>9,} elements : {us:9.3f} us/op")
print("JSON_SWEEP=" + json.dumps(sweep))

下载并运行

单元素加法几乎不算数学。所以它 0.54 微秒几乎就是纯粹的交叉成本:离开 Python,检查参数,构建结果对象,返回。半微秒听起来几乎没有。

这意味着 Python 最多每秒能执行大约 190 万次操作,而单一训练步骤包含数千次操作。留着这个号码。它在第六个理念中回归。

调度员

第4层,共8层 调度员完整故事:第五部分,机械

在边境下,电话传到了PyTorch最奇怪的机器:调度员。调度器是路由器,负责决定每次操作中运行哪些代码以及按何种顺序运行。

看看它必须做出什么决定。你的三条线从没说过“记录渐变”。不if你代码中的语句会开启这个功能。然而,某个地方决定,这个矩阵乘法应该被记住backward().那个东西是调度员。

每个操作都经过固定的层叠。每一层都可以对调用采取行动、改变调用,或者让调用保持不变。PyTorch 中计算梯度的部分 Autograd 就是其中一层。

混合精度也是一个。这次运行中,只有自升机处于觉醒状态。

核心

第5层,共8层 内核完整故事:第8部分,内核与硬件

在堆栈底部,选择一个具体函数。“被选中”这个词最贴切。该火炬制造拥有3,677个注册操作名称(证明打印计数),而名称不是函数体。

行动经过addmm,矩阵乘法model(x)对于CPU和每种GPU类型、稠密度张量和稀疏张量,都有独立的主体。这样一个为一个设备和一个数据类型编写的身体称为内核。

调度员的最后一项任务是选择一个:

p4_micro_proofs.py校样,随时可阅读或运行

"""Micro-proofs quoted in Part 0: storage sharing, view errors,
mutation rewriting history, the no_grad layer, float32 absorption."""
import torch

print(f"torch {torch.__version__}\n")

# 1. a tensor is a window over storage
x = torch.arange(6, dtype=torch.float32)
v = x.view(2, 3)
print("same bytes under both:", x.data_ptr() == v.data_ptr())
print("v.stride():", v.stride(), " v.t().stride():", v.t().stride())
try:
    v.t().view(-1)
except RuntimeError as e:
    print("v.t().view(-1) ->", str(e).split(".")[0])

# 2. mutation rewrites the recorded program
a = torch.ones(3, requires_grad=True)
y = a * 2
print("\nbefore add_:", type(y.grad_fn).__name__)
y.add_(1)
print("after  add_:", type(y.grad_fn).__name__)

# 3. no_grad removes one dispatcher layer
with torch.no_grad():
    z = a * 2
print("\ninside no_grad, grad_fn:", z.grad_fn)

# 4. float32 absorbs small numbers
t = torch.tensor(1e8)
print("\n(1e8 + 1) - 1e8 in float32 =", ((t + 1) - t).item())

# 5. the size of the operation list (idea 3)
print("\nregistered operation names:",
      len(torch._C._dispatch_get_all_op_names()))

下载并运行

完整的操作列表存储在仓库中的一个文件中:native_functions.yaml[2]. 它的兄弟姐妹 derivatives.yaml[3] 列出每个运算的导数。其他一切都是从中生长出来的 这两个文件。仓库里没有其他文件能告诉你类似情况 每行。

楼下一层是Memory。torch.randn(64, 128)需要32,768字节:每个数字64行乘以128个数字乘以4个字节。在CPU上,这属于普通分配。在显卡上则不是。

在那里,PyTorch 运行自己的配置器,一个内存管理器,会向 GPU 驱动请求一次大块数据,然后重复使用,因为每次请求驱动速度较慢。

这个分配器决定内存耗尽时的错误含义。第四部分对此进行了探讨。

两座钟

第7层,共8层,队列完整故事:第四部分,见PyTorch

故事在这里分为两部分。以下是PyTorch中最有用的性能事实。

在GPU上,你的Python线不会完成这些工作。它请求工作,请求会立即返回。GPU在自己的时钟上完成工作,而Python则继续运行。我是在这台机器的显卡上测量的 (证明):

time to request 50 matrix multiplications :   1.58 ms
time until the work was actually done     :  73.83 ms
python was free during                    :  72.25 ms  (98%)

p3_two_timelines.py校样,随时可阅读或运行

"""Proof: the CPU runs ahead of the GPU.

Queues 50 large matmuls on the MPS device and measures two times:
how long Python took to *ask* for the work, and how long the work
actually took. The difference is the gap the chapter draws.
"""
import time
import torch

assert torch.backends.mps.is_available(), "needs an Apple-silicon GPU"
dev = torch.device("mps")

a = torch.randn(2048, 2048, device=dev)
b = torch.randn(2048, 2048, device=dev)
for _ in range(5):          # warmup
    (a @ b)
torch.mps.synchronize()

t0 = time.perf_counter()
for _ in range(50):
    c = a @ b
t_queue = time.perf_counter() - t0
torch.mps.synchronize()
t_done = time.perf_counter() - t0

print(f"torch {torch.__version__}, mps")
print(f"time to queue 50 matmuls : {t_queue*1e3:8.2f} ms")
print(f"time until work finished : {t_done*1e3:8.2f} ms")
print(f"python was free for      : {(t_done-t_queue)*1e3:8.2f} ms ({(t_done-t_queue)/t_done:.0%} of the wall time)")

# The three ways to read the loss, measured, for the two-clocks
# widget: never, once at the end, after every step.
import json
def run_mode(mode, iters=50):
    for _ in range(5):
        (a @ b)
    torch.mps.synchronize()
    t0 = time.perf_counter()
    t_free = 0.0
    for i in range(iters):
        c = a @ b
        if mode == "every":
            c[0, 0].item()
    t_q = time.perf_counter() - t0
    if mode == "once":
        c[0, 0].item()
    torch.mps.synchronize()
    total = time.perf_counter() - t0
    return {"mode": mode, "queue_ms": round(t_q * 1e3, 2),
            "total_ms": round(total * 1e3, 2),
            "free_ms": round((total - t_q) * 1e3, 2)}

modes = [run_mode(m) for m in ("never", "once", "every")]
for m in modes:
    print(f"read {m['mode']:>5}: total {m['total_ms']:8.2f} ms, "
          f"python busy {m['queue_ms']:8.2f} ms")
print("JSON_MODES=" + json.dumps(modes))

下载并运行

Python 要求在两毫秒内完成全部五十次乘法,然后自由等待,GPU 计算了另外七十二毫秒。CPU 上则没有这种分割;计算是在你的电话线返回之前完成的。

在任何加速器上,分频是程序的正常状态。

这就是为什么热心的PyTorch足够快,值得使用:Python领先,GPU从不等待它。这也是为什么简单的时序代码会给出错误答案,以及为什么loss.item()在训练环内可以减缓整个步骤。

.item()需要实际数字。这个数字位于GPU尚未完成的工作队列末尾,因此Python必须停下来等待整个队列:

互动

该工具需要JavaScript;静态画作代替。

互动 1.两个钟,测量过。选择循环读取损失的频率;这些通道显示了谁等待,以及等待多久。

第四部分正是在这个图的基础上教授诚实的测量。

转弯

第4层,共8层,Autograd 层层故事:第二部分,Autograd

第三行:loss.backward().到目前为止,没有任何解释这条防线是如何运作的。前向计算结束。PyTorch 是如何知道区分什么的?

它知道是因为前传还有第二个任务。每当操作经过调度器的自积层时,都会写入一条小记录:运行了哪些操作,以及记录的步骤产生了输入。

指向记录的记录构成一个图,这里的“记录”总是指这个记录结构。到时候loss存在,其图也存在 (证明):

loss.grad_fn     = SumBackward0
SumBackward0
  AddmmBackward0
    AccumulateGrad

p1_graph_chain.py校样,随时可阅读或运行

"""Proof: loss.backward() walks a graph that forward quietly recorded.

Builds the chapter's three-line program and prints the autograd graph
that exists before backward is ever called.
"""
import torch
import torch.nn as nn

torch.manual_seed(0)
model = nn.Sequential(nn.Linear(128, 256), nn.ReLU(), nn.Linear(256, 10))

x = torch.randn(64, 128)
loss = model(x).sum()

print(f"torch {torch.__version__}")
print(f"x.grad_fn        = {x.grad_fn}")
print(f"loss.grad_fn     = {type(loss.grad_fn).__name__}")

node, depth = loss.grad_fn, 0
while node is not None and depth < 10:
    print("  " * depth + type(node).__name__)
    nexts = [n for n, _ in node.next_functions if n is not None]
    node = nexts[0] if nexts else None
    depth += 1

下载并运行

backward()什么都没发明。它会将图从损失走回你的输入,运行每个记录的导数,并存储结果于.grad.徒步结束于AccumulateGrad,存储的记录。

而且它没有从别处开始:x.grad_fn是None, 因为x是直接创造的,而不是计算出来的。

这里有一个问题应该让你感到困扰。衍生品需要价值。矩阵乘法的导数需要被乘积的矩阵,前向通路是长的。写出导数,需求显现:

那他们在哪里?他们在前传时被救下,旁边还有纪录:

所以前传默默决定了记忆训练的花费。第二部分展示了具体的存档规则。第四部分展示了如何观看这一切的发生。还有一种叫做激活检查点的方法,用内存换取额外的计算量;它在第二部分有自己的章节。

从这部分带出一句话:倒转只能走前进写的文字。听起来很小。在第9部分中,它成为决定集群中哪些GPU必须相互通信的规则。

这就是整个堕落:一个名字、一个边界、一堆层、一个选定的核心、一个记忆守护者、两个时钟,以及一个倒着走的图表。现在说说领地,正式。

领地

PyTorch 是分层构建的,每一层只与其邻居通信。下面的每个盒子至少是这个系列的一个部分。

同样的区域,也就是仓库中的文件夹。如果你打开代码库,这就是防止你迷路的地图:

关于这张地图的三个事实,帮你节省了数周时间。首先:torch/是纯Python,你现在可以读取其中的每个文件。第二:aten/以及c10/是C++;张量、核和调度器都存在于此,且torch/csrc/是连接两种语言的唯一桥梁。

第三:torchgen/是边界层的程序,即在构建过程中编写代码的程序。你读取的仓库就是输入。你运行的库就是输出。这就是为什么在仓库中搜索THPVariable_randn什么都没发现:

核心之上是生态系统。它看起来无尽,但形状很简单:每个库都会在一个特定且可命名的位置连接到PyTorch。了解那些依附的地方,也了解生态系统。

从中间往外读图片。transformers构建其模型为nn.Module如果你理解第三部分,可以阅读其来源。deepspeed取代了分布式引擎,因此其归属为第9部分。

vllm以及sglang保留模型权重,并替换它们周围的运行时间。而且trl以及peft完全不要直接触碰PyTorch;他们在基础上不断发展transformers.

整个生态系统可以放进一张表格里。第二栏标注了PyTorch仓库中每个家族所附带的位置:

附加于火炬侧谁他们留下什么,带来什么
嗯。模块torch/nn/变压器、扩散器、TIMM模型是模块;Torch在操控他们
训练循环torch/autograd/ torch/optim/闪电,加速火炬停止发动机;他们开车
分布式引擎torch/distributed/深速换了引擎,带来了ZeRO
热切的运行时间torch/nn/ torch/library.pyvllm、sglang、TensorRT-LLM保留权重,替换运行时,每个都用自己的内核 CSRC/ 替换
操作列表aten/ torch/library.py闪光注意,火炬视野行动名单上新增了名字
两层同时torch/autograd/+ 变换器懒惰通过变压器训练,带来自己的特里同核
只有权重没有;权重文件TEI、llama.cpp、MLX离开火炬,保留了配重;llama.cpp将它们重新编码为GGUF

桌子的两排值得拍照。第一个是急切运行时间,也就是服务引擎所替代的部分:

第二个是Triton,贯穿整个表的内核语言:PyTorch的编译器编写它,库也带来了自己的:

往下读表,每行保存下来的PyTorch内容都更少。最后一行只保留权重文件。这就是生态系统的静默法则:权重会比运行时间更持久。第10部分逐一介绍这些连接点。

十二个理念

PyTorch的大部分内容并不是成千上万个独立的决策。这是一组小小的理念,却无处不在地应用。这十二个让代码库的其他部分在你读之前就变得可预测。

每个故事后来都会以完整的章节或部分形式回归。每个人现在都有可查的证据;小证明共享一个文字(证明).

1. 张量是存储上的窗口

完整故事:第一部分,张量

张量不包含数字。它包含了查找地点的描述:指向一个平面内存块的指针、每个维度的大小以及步幅。步幅是指在该平面上移动以到达下一维度元素的步数。

两个张量可能看起来完全不同,但读取相同的字节:

>>> x = torch.arange(6.); v = x.view(2, 3)
>>> x.data_ptr() == v.data_ptr()   # same address in memory
True
>>> v.stride(), v.t().stride()     # transpose swapped the strides
((3, 1), (1, 3))

移调没有移动数据。描述中交换了两个数字。有些描述根本无法写下来,这正是原因v.t().view(-1)在reshape而是默默地复制数据。第一部分以这个谜题开场,并彻底解开了它。

互动

该工具需要JavaScript;静态画作代替。

互动 2.六个数字,一个存储。形状和步伐决定每个单元读取哪个槽位;只有当游走顺序匹配时,View(-1) 才存在。

2. Autograd 记录你从未编写过的程序

完整报道:第二部分,Autograd

在你的代码中,y是一个名字,你可以自由覆盖它。第二条线会破坏第一条线的价值线。图无法承受这一点:倒推需要每一步。所以 autograd 每次更改写一条记录,且不会被覆盖。

观看纪录的变化y要做:

>>> a = torch.ones(3, requires_grad=True)
>>> y = a * 2
>>> type(y.grad_fn).__name__
'MulBackward0'
>>> y.add_(1)                      # change y in place
>>> type(y.grad_fn).__name__
'AddBackward0'
>>> y[0] = 9                       # overwrite one slot
>>> type(y.grad_fn).__name__
'CopySlices'

三份账单,三张唱片,一条链条。y.grad_fn始终保存最新的记录,每条记录都指向前一个,因此整个历史记录始终可访问。那段历史就是你从未写过的程序。

在各种现场变动下保持游戏正确运作的机制非常有深度,这是第二部分中最精彩的章节之一。

3. 一个操作列表是整个接口

完整故事:第五部分,机械

这3,677个注册名称是PyTorch的真实接口。每个后端都实现了它的一部分。编译器会重写由他们组成的程序:torch.compile读取你的程序变成的列表,返回一个更短的列表,矩阵乘法和加法可以融合成一个addmm,一连串小的元素操作链变成一个生成的核。

导出格式存储它们:torch.export将列表写入磁盘,作为这些名称的图,ONNX 文件也是同样的概念,每个名字都转化为 ONNX 的词汇表。

量化取代了它们:float32 matmul 被替换成 int8 体,列表位置相同,运算方式不同。当你遇到新的PyTorch技术时,首先要问一个问题:它对运营有什么影响?

答案通常能解释整个设计。

4. PyTorch 大部分代码自行编写

完整故事:第五部分,机械

native_functions.yaml宣告每一个操作。derivatives.yaml声明每个导数。在构建时,torchgen/读取并写入 Python 绑定、Autograd 记录类和调度表。

这就是为什么在仓库里搜索你刚刚调用的函数时,什么都找不到:你搜索的是构建的输入,函数就在输出中。做PyTorch的人会先读yaml。

第五部分结束后,你也会加入。

5. 特征是带有开关的层

完整故事:第五部分,机械

PyTorch 的功能能干净利落地组合,因为每个功能都是调度器中的一层,监控同一个操作流:

>>> with torch.no_grad():
...     z = a * 2
>>> z.grad_fn is None              # nothing was recorded
True

no_grad编辑了没有功能。它设置了一个标志,将操作发送到自动梯度层之外,所以什么都不会被记录。混合精度、追踪和虚拟地图的工作原理相同,这也是为什么它们可以在彼此未知的情况下组合使用。

第五部分则是国旗下的机械设备。

6. 每个操作首先支付固定成本

完整报道:第七部分,编译者

在所有操作中,交叉和布线前都只有半微秒的计算。那是边界地板上的测量值。说实话,这个数字解释了原因torch.compile存在,为什么会有融合优化器?

为什么任何慢模型的第一个问题是:它是否受限于计算,还是受限于执行大量小操作的成本?

互动

该工具需要JavaScript;静态画作代替。

互动 3.同样的叠加,在十二个测量尺寸下。这些点在作者的机器上测量;在曲线出现之前插上你的旗帜。

7. 记忆力,而非速度,才是训练跑的致命因素

完整报道:第四部分,见PyTorch

慢速的程序还是会结束。程序在 GPU 内存耗尽后会死机CUDA out of memory这是PyTorch中最常见的死亡事件。前向传递保存了后向(如上图转弯)的值。

分配者保留并重复利用区块。他们共同决定你能训练多大的模型。本系列在第四部分将记忆作为一流主题。

8. Python 是它获胜的原因,以及它所付出的代价

完整报道:第七部分,编译者

PyTorch 之所以赢,是因为你用普通的 Python 写它,配合普通的调试器和打印语句。价格是《理念6》的边境成本,每次行动都要支付。该框架的历史是一系列试图保留前者同时减少后者。TorchScript 试图用自己的语言替代 Python;目前处于维护状态[4].当前编译器监控你的 Python 运行并翻译它能翻译的内容,而且它正在取得优势。该 需要记住的模式:在PyTorch内部,赌注与Python有关 总是迷失。

9. 前进决定后退必须做什么

完整报道:第九部分,已分发

倒退只能走前写的。在一台机器上,这听起来像是个细节。在大规模上,这成为了主流:在分布式训练中,前向传递时张量在GPU间的分配方式决定了哪些GPU在后向传递中必须交换数据。

一个想法是,从笔记本电脑到集群。它是第九部分的主干。

10. 共享字节加上原地写入会带来最棘手的问题

完整报道:第二部分,Autograd

想法1允许多个张量读取相同的字节。第二个想法允许你在原地更改这些字节。结合起来:一次写入可以同时改变多个张量的含义,任何记录程序的系统(自升、编译器、导出)都必须注意到并保持正确。

当PyTorch的一个角落看起来异常复杂时,可以问问共享字节加上原地写入会对它造成什么影响。这通常是答案。

11. 代码保留其历史

完整故事:第五部分,机械

该仓库保存了各个时代的遗迹:2016年的原始C代码、2018年的Caffe2合并、2019年的TorchScript,以及自2023年以来不断增长的编译器区。

当档案看起来奇怪时,通常解释是历史上的:这里最早有更古老的生物居住。第五部分讲述了这段历史,并解释了现在的情况。

12. 浮点是一种合同;读一读

完整报道:第四部分,见PyTorch

>>> t = torch.tensor(1e8)
>>> ((t + 1) - t).item()
0.0

float32 的数字大约有 7 位小数精度,所以将 1 比 1 亿数加起来并不会改变 [5].这不是 bug;它是数字格式在兑现承诺的效果。添加 训练中使用的更快、更不精确的格式,以及 有些GPU内核在不同运行中以不同顺序求和, “为什么我的损失在两次运行之间会发生变化”变成了一个精确的问题 答案。第四部分逐条解读本合同条款。

本系列的绘画风格

你刚才看到的每一个静态人物都是真实的Excalidraw场景,场景文件随系列一起附带;你可以打开任何图纸并进行编辑。你能操作的四个仪器都遵循相同的语言,其中的每个数字都来自证明脚本。

它们都说着一种视觉语言,所以到了第二部分你会不假思索地阅读它们:

橙色总是标记主体:唯一在动的东西。墨水就是结构。灰色代表语境。虚线表示已记录、暗示或沉睡。深度计标记了地板。而且机器人每个部分最多只出现一次,因为到处都是吉祥物就不再好笑了。

如何阅读这篇文章

十二部分。每页都是一页长页,就像这页一样。而这个系列每个部分背后有一个低调的目标:到最后,你应该足够熟悉这台机器,自己组装一个小型PyTorch。

每一幅展示机械装置的图纸、每一个模型旁边的公式、每一个校样脚本,都是其中的一部分。

部分门后是什么
0. 地图你来了
1. 张量存储、阶梯、视图、数据类型、广播
2. 自学院图、原地写入、检查点、倒退
3. 每日火炬nn,optim,数据加载,混合精度,内部视图
4. 见到PyTorch分析器、内存、浮点数、诚实测量
5. 机械调度员、ATEN、Torchgen、历史
6. 扩展PyTorch子类、自定义操作、新后端
7. 编译者发电机、AOT自衰、电感、动态形状
8. 内核与硬件显卡型号、Triton、Cutlass,快速意味着什么
9. 分布式集体、DDP、FSDP、Dtensor、并行培训
10. 寄送它导出、量化、执行器、生态系统
11. 参与PyTorch开发贡献者的野外指南

你不必从头到尾读。三条阅读线贯穿各部分,如同车站线:

互动

该工具需要JavaScript;静态画作代替。

互动 4.选一条线。《Grey》从前到后,处处停顿;橙色适合大多数读者;绿色追逐速度;蓝色是新贡献者的。点表示该条线就停在那里。

每个章节内的每个部分都遵循相同的七步,因此节奏很快就变得熟悉:

而且方法要明确说明,因为你应该知道你信任的是什么。每个机制主张都会在发布前与源代码进行核对或脚本展示。这些脚本被链接在原位,并钉顶到一个火炬版本。

当PyTorch移动且某个申诉变得陈旧时,章节会被更正,并会在页面上注明更正。一部关于无法承认漂移的内部成员的系列,一年内就会出错。

你现在能说什么

用这份清单来测试自己。读完一次后,你应该能用自己的话说:

  • 什么type(torch.randn)返回,以及编译后的正体实际上存在于你的磁盘上
  • 调度员是什么,以及怎么做no_grad无需编辑任何函数即可停止 Autograd
  • 什么是内核,以及是什么决定了哪个内核运行
  • 为什么CPU和GPU运行在两个时钟上,以及为什么简单的时序代码会说谎
  • 图是什么,谁写的,以及为什么倒向永远不能做正向的事,这些都没写下来
  • 以及十二个思想,每个都用一句话表达

如果其中一个模糊,则返回其底层;每条只有一分钟长。这正是这个页面的意义所在。

你自己试试

五个证明脚本就是练习题。每一个:先预测输出,然后运行,再解释差异。

  1. p0_the_library.py:你自己的Torch安装中编译的库有多大?
  2. p1_graph_chain.py:运行完一个两层模型后,还剩下什么图表?
  3. p2_dispatch_cost.py:你的机器每次操作固定成本是多少?
  4. p3_two_timelines.py: Python 完成请求后,你的 GPU 还能工作多久?
  5. p4_micro_proofs.py:十二个想法,浓缩成五个小实验。

选一扇门

第一部分是张量。它以第一个想法中的谜题开场,现在你亲眼看到了错误:

>>> v.t().view(-1)
RuntimeError: view size is not compatible with input tensor's
size and stride ...
>>> v.t().reshape(-1)   # this one works. why?
tensor([0., 3., 1., 4., 2., 5.])

同样的张量。同样的要求。一行拒绝,另一行悄悄复制数据。这两句话的区别就是这个系列的前半部分。

楼下见。

参考文献

[1] 哈利利,五个校样脚本,分别在Apple M3 Max、Torch 2.11.0、CPU和苹果GPU上测量, 2026.如上所述;重新运行它们来检查我。

[2] PyTorch 来源,native_functions.yaml,钉在v2.11.0标签上。https://github.com/pytorch/pytorch/blob/v2.11.0/aten/src/ATen/native/native_functions.yaml

[3] PyTorch 来源,Derivatives.yaml,钉在v2.11.0标签上。https://github.com/pytorch/pytorch/blob/v2.11.0/tools/autograd/derivatives.yaml

[4] PyTorch 文档,火炬脚本,表示处于维护状态。https://docs.pytorch.org/docs/stable/jit.html

[5] IEEE,754单精度:24位二进制精度,约7位小数位。

以下三件值得阅读的好书:杨爱德的PyTorch 内部结构讨论,该映射对C++侧的深度;该PyTorch 开发者播客同一作者的短篇剧集;以及仓库自己的CONTRIBUTING.md,用维护者的话描述了文件夹布局。

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论